{"id":18026840,"url":"https://github.com/yegor256/factbase","last_synced_at":"2026-02-15T07:17:03.212Z","repository":{"id":239143375,"uuid":"798641472","full_name":"yegor256/factbase","owner":"yegor256","description":"In-memory database of facts (records with attributes) with a predicative searching facility","archived":false,"fork":false,"pushed_at":"2026-02-11T05:42:26.000Z","size":13488,"stargazers_count":17,"open_issues_count":12,"forks_count":11,"subscribers_count":3,"default_branch":"master","last_synced_at":"2026-02-11T13:42:56.067Z","etag":null,"topics":["database","nosql","ruby","ruby-gem"],"latest_commit_sha":null,"homepage":"https://rubygems.org/gems/factbase","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yegor256.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-05-10T07:24:37.000Z","updated_at":"2026-02-11T05:33:29.000Z","dependencies_parsed_at":"2025-04-28T16:23:12.898Z","dependency_job_id":"7de0a61b-e101-4477-99bc-24f33b2bf1d2","html_url":"https://github.com/yegor256/factbase","commit_stats":null,"previous_names":["yegor256/factbase"],"tags_count":132,"template":false,"template_full_name":null,"purl":"pkg:github/yegor256/factbase","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yegor256%2Ffactbase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yegor256%2Ffactbase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yegor256%2Ffactbase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yegor256%2Ffactbase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yegor256","download_url":"https://codeload.github.com/yegor256/factbase/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yegor256%2Ffactbase/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29472879,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-15T06:58:05.414Z","status":"ssl_error","status_checked_at":"2026-02-15T06:58:05.085Z","response_time":118,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["database","nosql","ruby","ruby-gem"],"created_at":"2024-10-30T08:08:14.701Z","updated_at":"2026-02-15T07:17:03.205Z","avatar_url":"https://github.com/yegor256.png","language":"Ruby","readme":"# Single-Table NoSQL-ish In-Memory Database\n\n[![DevOps By Rultor.com](https://www.rultor.com/b/yegor256/factbase)](https://www.rultor.com/p/yegor256/factbase)\n[![We recommend RubyMine](https://www.elegantobjects.org/rubymine.svg)](https://www.jetbrains.com/ruby/)\n\n[![rake](https://github.com/yegor256/factbase/actions/workflows/rake.yml/badge.svg)](https://github.com/yegor256/factbase/actions/workflows/rake.yml)\n[![discipline](https://zerocracy.github.io/judges-action/zerocracy-badge.svg)](https://zerocracy.github.io/judges-action/zerocracy-vitals.html)\n[![PDD status](https://www.0pdd.com/svg?name=yegor256/factbase)](https://www.0pdd.com/p?name=yegor256/factbase)\n[![Gem Version](https://badge.fury.io/rb/factbase.svg)](https://badge.fury.io/rb/factbase)\n[![Test Coverage](https://img.shields.io/codecov/c/github/yegor256/factbase.svg)](https://codecov.io/github/yegor256/factbase?branch=master)\n[![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/yegor256/factbase/master/frames)\n[![Hits-of-Code](https://hitsofcode.com/github/yegor256/factbase)](https://hitsofcode.com/view/github/yegor256/factbase)\n[![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)\n[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase.svg?type=shield\u0026issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase?ref=badge_shield\u0026issueType=license)\n\nThis Ruby gem manages an in-memory database of facts.\nA fact is simply an associative array of properties and their values.\nThe values are either atomic literals or non-empty sets of literals.\nIt is possible to delete a fact, but impossible to delete a property\nfrom a fact.\n\nHere is how you use it (it's thread-safe, by the way):\n\n```ruby\nfb = Factbase.new\nf = fb.insert\nf.kind = 'book'\nf.title = 'Object Thinking'\nfb.query('(eq kind \"book\")').each do |f|\n  f.seen = Time.now\nend\nfb.insert\nfb.query('(not (exists seen))').each do |f|\n  f.title = 'Elegant Objects'\nend\n```\n\nYou can save the factbase to the disk and then load it back:\n\n```ruby\nfile = '/tmp/simple.fb'\nf1 = Factbase.new\nf = f1.insert\nf.foo = 42\nFile.binwrite(file, f1.export)\nf2 = Factbase.new\nf2.import(File.binread(file))\nassert(f2.query('(eq foo 42)').each.to_a.size == 1)\n```\n\nYou can check the presence of an attribute by name and then\nset it, also by name:\n\n```ruby\nn = 'foo'\nif f[n].nil?\n  f.send(\"#{n}=\", 'Hello, world!')\nend\n```\n\nYou can make a factbase log all operations:\n\n```ruby\nrequire 'loog'\nrequire 'factbase/logged'\nlog = Loog::VERBOSE\nfb = Factbase::Logged.new(Factbase.new, log)\nf = fb.insert\n```\n\nYou can also count the amount of changes made to a factbase:\n\n```ruby\nrequire 'loog'\nrequire 'factbase/tallied'\nlog = Loog::VERBOSE\nfb = Factbase::Tallied.new(Factbase.new, log)\nf = fb.insert\nchurn = fb.churn\nassert churn.inserted == 1\n```\n\nProperties are accumulative.\nSetting a property again adds a value instead of overwriting:\n\n```ruby\nf = fb.insert\nf.foo = 42\nf.foo = 43\nassert(f.foo == 42)\nassert(f['foo'] == [42, 43])\nfb.query('(eq foo 43)').each do |f|\n  assert(f.foo == 42)\n  assert(f['foo'].include?(43))\nend\n```\n\n## Terms\n\nThere are some boolean terms available in a query\n(they return either `true` or `false`):\n\n* `(always)` and `(never)` are `true` and `false`\n* `(nil v)` is `true` if `v` is `nil`\n* `(not b)` is the inverse of `b`\n* `(or b1 b2 ...)` is `true` if at least one argument is `true`\n* `(and b1 b2 ...)` — if all arguments are `true`\n* `(when b1 b2)` — if `b1` is `true` and `b2` is `true`\nor `b1` is `false`\n* `(exists p)` — if `p` property exists\n* `(absent p)` — if `p` property is absent\n* `(zero v)` — if any `v` equals to zero\n* `(eq v1 v2)` — if any `v1` equals to any `v2`\n* `(lt v1 v2)` — if any `v1` is less than any `v2`\n* `(gt v1 v2)` — if any `v1` is greater than any `v2`\n* `(many v)` — if `v` has many values\n* `(one v)` — if `v` has one value\n\nThere are string manipulators:\n\n* `(concat v1 v2 v3 ...)` — concatenates all `v`\n* `(sprintf v v1 v2 ...)` — creates a string by `v` format with params\n* `(matches v s)` — if any `v` matches the `s` regular expression\n\nThere are a few terms that return non-boolean values:\n\n* `(at i v)` is the `i`-th value of `v`\n* `(size v)` is the cardinality of `v` (zero if `v` is `nil`)\n* `(type v)` is the type of `v`\n(`\"String\"`, `\"Integer\"`, `\"Float\"`, `\"Time\"`, or `\"Array\"`)\n* `(either v1 v1)` is `v2` if `v1` is `nil`\n\nIt's possible to modify the facts retrieved, on fly:\n\n* `(as p v)` adds property `p` with the value `v`\n* `(join s t)` adds properties named by the `s` mask with the values retrieved\nby the `t` term, for example, `(join \"x\u003c=foo,y\u003c=bar\" (gt x 5))` will add\n`x` and `y` properties, setting them to values found in the `foo` and `bar`\nproperties in the facts that match `(gt x 5)`\n\nAlso, some simple arithmetic:\n\n* `(plus v1 v2)` is a sum of `∑v1` and `∑v2`\n* `(minus v1 v2)` is a deduction of `∑v2` from `∑v1`\n* `(times v1 v2)` is a multiplication of `∏v1` and `∏v2`\n* `(div v1 v2)` is a division of `∏v1` by `∏v2`\n\nIt's possible to add and deduct string values to time values, like\n`(plus t '2 days')` or `(minus t '14 hours')`.\n\nTypes may be converted:\n\n* `(to_int v)` is an integer of `v`\n* `(to_str v)` is a string of `v`\n* `(to_float v)` is a float of `v`\n\nOne term is for meta-programming:\n\n* `(defn f \"self.to_s\")` defines a new term using Ruby syntax and returns `true`\n* `(undef f)` undefines a term (nothing happens if it's not defined yet),\nreturns `true`\n\nThere are terms that are history of search aware:\n\n* `(prev p)` returns the value of `p` property in the previously seen fact\n* `(unique p1 p2 ...)` returns true if at least one property value\nhasn't been seen yet; returns false when all specified properties\nhave duplicate values in this particular combination\n\nThe `agg` term enables sub-queries by evaluating the first argument (term)\nover all available facts, passing the entire subset to the second argument,\nand then returning the result as an atomic value:\n\n* `(lt age (agg (eq gender 'F') (max age)))` selects all facts where\nthe `age` is smaller than the maximum `age` of all women\n* `(eq id (agg (always) (max id)))` selects the fact with the largest `id`\n* `(eq salary (agg (eq dept $dept) (avg salary)))` selects the facts\nwith the salary average in their departments\n\nThere are also terms that match the entire factbase\nand must be used primarily inside the `(agg ..)` term:\n\n* `(nth v p)` returns the `p` property of the _v_-th fact (must be\na positive integer)\n* `(first p)` returns the `p` property of the first fact\n* `(count)` returns the tally of facts\n* `(max p)` returns the maximum value of the `p` property in all facts\n* `(min p)` returns the minimum\n* `(sum p)` returns the arithmetic sum of all values of the `p` property\n\nIt's also possible to use a sub-query in a shorter form than with the `agg`:\n\n* `(empty q)` is true if the subquery `q` is empty\n\nIt's possible to post-process a list of facts, for `agg` and `join`:\n\n* `(sorted p expr)` sorts them by the value of `p` property\n* `(inverted expr)` reverses them\n* `(head n expr)` takes only `n` facts from the head of the list\n\nThere are some system-level terms:\n\n* `(env v1 v2)` returns the value of environment variable `v1` or the string\n`v2` if it's not set\n\n## How to contribute\n\nRead\n[these guidelines](https://www.yegor256.com/2014/04/15/github-guidelines.html).\nMake sure your build is green before you contribute\nyour pull request. You will need to have\n[Ruby](https://www.ruby-lang.org/en/) 3.4+ and\n[Bundler](https://bundler.io/) installed. Then:\n\n```bash\nbundle update\nbundle exec rake\n```\n\nIf it's clean and you don't see any error messages, submit your pull request.\n\n## Benchmark\n\nThis is the result of the benchmark:\n\n\u003c!-- benchmark_begin --\u003e\n```text\n                                                                       \nquery all facts from an empty factbase                             0.00\ninsert 20000 facts                                                 0.62\nexport 20000 facts                                                 0.02\nimport 411033 bytes (20000 facts)                                  0.01\ninsert 10 facts                                                    0.04\nquery 10 times w/txn                                               2.41\nquery 10 times w/o txn                                             0.14\nmodify 10 attrs w/txn                                              1.82\ndelete 10 facts w/txn                                              2.85\nbuild index on 5000 facts                                          0.04\nexport 5000 facts with index                                       0.04\nimport 5000 facts with persisted index                             0.06\nquery 5000 facts using persisted index                             0.08\nexport 5000 facts without index                                    0.00\nimport 5000 facts without index                                    0.01\nquery 5000 facts building index on-the-fly                         0.08\nquery 15k facts  sel: 20%  card: 10  absent plain                  0.29\nquery 15k facts  sel: 20%  card: 10  absent indexed(cold)          0.06\nquery 15k facts  sel: 20%  card: 10  absent indexed(warm)          0.28\nquery 15k facts  sel: 20%  card: 10  exists plain                  0.29\nquery 15k facts  sel: 20%  card: 10  exists indexed(cold)          0.01\nquery 15k facts  sel: 20%  card: 10  exists indexed(warm)          0.09\nquery 15k facts  sel: 20%  card: 10  eq plain                      0.45\nquery 15k facts  sel: 20%  card: 10  eq indexed(cold)              0.02\nquery 15k facts  sel: 20%  card: 10  eq indexed(warm)              0.11\nquery 15k facts  sel: 20%  card: 10  not plain                     0.59\nquery 15k facts  sel: 20%  card: 10  not indexed(cold)             0.13\nquery 15k facts  sel: 20%  card: 10  not indexed(warm)             0.58\nquery 15k facts  sel: 20%  card: 10  gt plain                      0.45\nquery 15k facts  sel: 20%  card: 10  gt indexed(cold)              0.02\nquery 15k facts  sel: 20%  card: 10  gt indexed(warm)              0.12\nquery 15k facts  sel: 20%  card: 10  lt plain                      0.43\nquery 15k facts  sel: 20%  card: 10  lt indexed(cold)              0.03\nquery 15k facts  sel: 20%  card: 10  lt indexed(warm)              0.12\nquery 15k facts  sel: 20%  card: 10  and plain                     0.71\nquery 15k facts  sel: 20%  card: 10  and indexed(cold)             0.05\nquery 15k facts  sel: 20%  card: 10  and indexed(warm)             0.20\nquery 15k facts  sel: 20%  card: 10  one plain                     0.38\nquery 15k facts  sel: 20%  card: 10  one indexed(cold)             0.02\nquery 15k facts  sel: 20%  card: 10  one indexed(warm)             0.08\nquery 15k facts  sel: 20%  card: 10  or plain                      1.01\nquery 15k facts  sel: 20%  card: 10  or indexed(cold)              0.04\nquery 15k facts  sel: 20%  card: 10  or indexed(warm)              0.19\nquery 15k facts  sel: 20%  card: 10  unique plain                  0.96\nquery 15k facts  sel: 20%  card: 10  unique indexed(cold)          0.06\nquery 15k facts  sel: 20%  card: 10  unique indexed(warm)          0.22\n(and (eq what 'issue-was-closed') (exists... -\u003e 200                1.08\n(and (eq what 'issue-was-closed') (exists... -\u003e 200/txn           12.82\n(and (eq what 'issue-was-closed') (exists... -\u003e zero               1.08\n(and (eq what 'issue-was-closed') (exists... -\u003e zero/txn          16.87\ntransaction rollback on factbase with 100000 facts                 0.25\n(gt time '2024-03-23T03:21:43Z')                                   0.31\n(gt cost 50)                                                       0.20\n(eq title 'Object Thinking 5000')                                  0.03\n(and (eq foo 42.998) (or (gt bar 200) (absent z...                 0.03\n(and (exists foo) (not (exists blue)))                             1.37\n(eq id (agg (always) (max id)))                                    2.73\n(join \"c\u003c=cost,b\u003c=bar\" (eq id (agg (always) (ma...                 4.57\n(and (eq what \"foo\") (join \"w\u003c=what\" (and (eq i...                 7.41\ndelete!                                                            0.51\n(and (eq issue *) (eq repository *) (eq what '*') (eq where '*'))  0.37\nTaped.append() x50000                                              0.02\nTaped.each() x125                                                  1.08\nTaped.delete_if() x375                                             0.86\n50000 facts: read-only txn (no copy needed)                        5.02\n50000 facts: rollback txn (no copy needed)                         4.94\n50000 facts: insert in txn (copy triggered)                        3.48\n50000 facts: modify in txn (copy triggered)                       35.96\n100000 facts: read-only txn (no copy needed)                      11.83\n100000 facts: rollback txn (no copy needed)                       11.70\n100000 facts: insert in txn (copy triggered)                       7.14\n100000 facts: modify in txn (copy triggered)                      72.06\n```\n\nThe results were calculated in [this GHA job][benchmark-gha]\non 2026-02-15 at 05:49,\non Linux with 4 CPUs.\n\u003c!-- benchmark_end --\u003e\n\n[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/22030549344\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyegor256%2Ffactbase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyegor256%2Ffactbase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyegor256%2Ffactbase/lists"}