factbase 0.19.10 → 0.19.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -4
  3. data/Gemfile.lock +18 -14
  4. data/README.md +127 -20
  5. data/Rakefile +2 -7
  6. data/factbase.gemspec +11 -11
  7. data/lib/factbase/accum.rb +1 -1
  8. data/lib/factbase/cached/cached_fact.rb +1 -2
  9. data/lib/factbase/cached/cached_factbase.rb +3 -3
  10. data/lib/factbase/cached/cached_query.rb +4 -6
  11. data/lib/factbase/cached/cached_term.rb +1 -2
  12. data/lib/factbase/churn.rb +4 -8
  13. data/lib/factbase/fact.rb +12 -9
  14. data/lib/factbase/flatten.rb +2 -2
  15. data/lib/factbase/impatient.rb +14 -13
  16. data/lib/factbase/indexed/indexed_and.rb +14 -20
  17. data/lib/factbase/indexed/indexed_eq.rb +5 -1
  18. data/lib/factbase/indexed/indexed_fact.rb +1 -4
  19. data/lib/factbase/indexed/indexed_factbase.rb +4 -4
  20. data/lib/factbase/indexed/indexed_gt.rb +3 -1
  21. data/lib/factbase/indexed/indexed_gte.rb +51 -0
  22. data/lib/factbase/indexed/indexed_lt.rb +3 -1
  23. data/lib/factbase/indexed/indexed_lte.rb +51 -0
  24. data/lib/factbase/indexed/indexed_not.rb +1 -1
  25. data/lib/factbase/indexed/indexed_or.rb +2 -2
  26. data/lib/factbase/indexed/indexed_query.rb +6 -7
  27. data/lib/factbase/indexed/indexed_term.rb +10 -6
  28. data/lib/factbase/indexed/indexed_unique.rb +4 -2
  29. data/lib/factbase/inv.rb +3 -3
  30. data/lib/factbase/lazy_taped.rb +10 -13
  31. data/lib/factbase/lazy_taped_hash.rb +2 -1
  32. data/lib/factbase/light.rb +1 -1
  33. data/lib/factbase/logged.rb +37 -34
  34. data/lib/factbase/pre.rb +3 -3
  35. data/lib/factbase/query.rb +4 -5
  36. data/lib/factbase/rules.rb +8 -8
  37. data/lib/factbase/sync/sync_factbase.rb +2 -2
  38. data/lib/factbase/syntax.rb +19 -20
  39. data/lib/factbase/tallied.rb +7 -8
  40. data/lib/factbase/taped.rb +5 -11
  41. data/lib/factbase/tee.rb +2 -2
  42. data/lib/factbase/term.rb +58 -59
  43. data/lib/factbase/terms/agg.rb +3 -4
  44. data/lib/factbase/terms/arithmetic.rb +7 -7
  45. data/lib/factbase/terms/as.rb +2 -2
  46. data/lib/factbase/terms/assert.rb +5 -13
  47. data/lib/factbase/terms/base.rb +7 -10
  48. data/lib/factbase/terms/best.rb +1 -1
  49. data/lib/factbase/terms/boolean.rb +1 -1
  50. data/lib/factbase/terms/compare.rb +17 -1
  51. data/lib/factbase/terms/contains.rb +28 -0
  52. data/lib/factbase/terms/defn.rb +8 -6
  53. data/lib/factbase/terms/empty.rb +1 -1
  54. data/lib/factbase/terms/ends_with.rb +27 -0
  55. data/lib/factbase/terms/first.rb +2 -2
  56. data/lib/factbase/terms/head.rb +3 -3
  57. data/lib/factbase/terms/inverted.rb +2 -2
  58. data/lib/factbase/terms/join.rb +8 -7
  59. data/lib/factbase/terms/matches.rb +14 -4
  60. data/lib/factbase/terms/max.rb +1 -1
  61. data/lib/factbase/terms/min.rb +1 -1
  62. data/lib/factbase/terms/nth.rb +3 -3
  63. data/lib/factbase/terms/plus.rb +1 -1
  64. data/lib/factbase/terms/prev.rb +3 -6
  65. data/lib/factbase/terms/sorted.rb +2 -2
  66. data/lib/factbase/terms/sprintf.rb +11 -2
  67. data/lib/factbase/terms/starts_with.rb +27 -0
  68. data/lib/factbase/terms/sum.rb +2 -2
  69. data/lib/factbase/terms/to_float.rb +2 -2
  70. data/lib/factbase/terms/to_integer.rb +2 -2
  71. data/lib/factbase/terms/to_string.rb +1 -1
  72. data/lib/factbase/terms/to_time.rb +10 -2
  73. data/lib/factbase/terms/traced.rb +2 -2
  74. data/lib/factbase/terms/undef.rb +2 -2
  75. data/lib/factbase/terms/unique.rb +3 -7
  76. data/lib/factbase/terms/when.rb +2 -3
  77. data/lib/factbase/to_json.rb +2 -2
  78. data/lib/factbase/to_xml.rb +6 -10
  79. data/lib/factbase/to_yaml.rb +1 -1
  80. data/lib/factbase/version.rb +1 -2
  81. data/lib/factbase.rb +27 -13
  82. data/lib/fuzz.rb +3 -3
  83. metadata +6 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b573b12e8a04a0b69a2e4c33c6ccd17c21c1c14a52a644c87564738388de546
4
- data.tar.gz: b4c888226e04aee8054ff26d056cc04f34fd3a1fb29455ff6c77ce4dd2103d45
3
+ metadata.gz: f97152f34dd9eb3ba72cc5380a87d321bbec59c316078108b38c2c078c23a0fd
4
+ data.tar.gz: 0f678a254dc256d1983965742771c37d0956a89f93ec91e098f63b4d2957117a
5
5
  SHA512:
6
- metadata.gz: a96ff08cc40deb0d17e25cc7c30f8e67d5c09b0179e623d7e941ade194ac7c52c2135730fb4a813faf7370097a1d151db74e1559de81a619bf572becb1eec408
7
- data.tar.gz: 1f57d718a82c2c8d107c033c3d8b59440170822f5810fadfc3b99837cf4072493e96ec8a39bff41c73a913cc3dba75837a9f4ecc0804976da37ebe02c09b1777
6
+ metadata.gz: a8ebb9cd303ab0a6b34842ec06fa2d5f4ed1ec1d2c8fff89e1068531782386c1c6da2bba4f2d116635ca37532d314c25626fe0fdcac03a10ead6b57871483485
7
+ data.tar.gz: 1f2be490a544b5417059853ed135011c85215bbea7fb9b96551bcd67badc32e20a9384acf97d6a3b4908e63332937bb4502df168c79aa19ed95afc1a364ee4bd
data/Gemfile CHANGED
@@ -10,16 +10,17 @@ gem 'benchmark', '~>0.5', require: false
10
10
  gem 'minitest', '~>6.0', require: false
11
11
  gem 'minitest-reporters', '~>1.7', require: false
12
12
  gem 'os', '~>1.1', require: false
13
- gem 'psych', '5.3.1', require: false # GPL
13
+ gem 'psych', '5.3.1', require: false
14
14
  gem 'qbash', '~>0.4', require: false
15
15
  gem 'rake', '~>13.2', require: false
16
- gem 'rdoc', '7.1.0', require: false # GPL
16
+ gem 'rdoc', '7.1.0', require: false
17
17
  gem 'rubocop', '~>1.74', require: false
18
+ gem 'rubocop-elegant', '~>0.5', require: false
18
19
  gem 'rubocop-minitest', '~>0.38', require: false
19
20
  gem 'rubocop-performance', '~>1.25', require: false
20
21
  gem 'rubocop-rake', '~>0.7', require: false
21
22
  gem 'simplecov', '~>0.22', require: false
22
23
  gem 'simplecov-cobertura', '~>3.0', require: false
23
- gem 'stackprof', '0.2.27', require: false, platforms: [:ruby] # GPL
24
+ gem 'stackprof', '0.2.27', require: false, platforms: [:ruby]
24
25
  gem 'threads', '~>0.4', require: false
25
- gem 'yard', '0.9.38', require: false # GPL
26
+ gem 'yard', '0.9.38', require: false
data/Gemfile.lock CHANGED
@@ -30,15 +30,15 @@ GEM
30
30
  loog (~> 0.6)
31
31
  tago (~> 0.1)
32
32
  ellipsized (0.3.0)
33
- erb (6.0.1)
34
- json (2.18.1)
33
+ erb (6.0.4)
34
+ json (2.19.5)
35
35
  language_server-protocol (3.17.0.5)
36
36
  lint_roller (1.1.0)
37
37
  logger (1.7.0)
38
38
  loog (0.8.0)
39
39
  ellipsized
40
40
  logger (~> 1.0)
41
- minitest (6.0.5)
41
+ minitest (6.0.6)
42
42
  drb (~> 2.0)
43
43
  prism (~> 1.5)
44
44
  minitest-reporters (1.8.0)
@@ -46,25 +46,25 @@ GEM
46
46
  builder
47
47
  minitest (>= 5.0, < 7)
48
48
  ruby-progressbar
49
- nokogiri (1.19.0-arm64-darwin)
49
+ nokogiri (1.19.3-arm64-darwin)
50
50
  racc (~> 1.4)
51
- nokogiri (1.19.0-x64-mingw-ucrt)
51
+ nokogiri (1.19.3-x64-mingw-ucrt)
52
52
  racc (~> 1.4)
53
- nokogiri (1.19.0-x86_64-darwin)
53
+ nokogiri (1.19.3-x86_64-darwin)
54
54
  racc (~> 1.4)
55
- nokogiri (1.19.0-x86_64-linux-gnu)
55
+ nokogiri (1.19.3-x86_64-linux-gnu)
56
56
  racc (~> 1.4)
57
57
  os (1.1.4)
58
58
  others (0.1.1)
59
- parallel (1.27.0)
60
- parser (3.3.10.2)
59
+ parallel (2.1.0)
60
+ parser (3.3.11.1)
61
61
  ast (~> 2.4.1)
62
62
  racc
63
63
  prism (1.9.0)
64
64
  psych (5.3.1)
65
65
  date
66
66
  stringio
67
- qbash (0.8.3)
67
+ qbash (0.8.4)
68
68
  backtrace (> 0)
69
69
  elapsed (> 0)
70
70
  loog (> 0)
@@ -76,22 +76,25 @@ GEM
76
76
  erb
77
77
  psych (>= 4.0.0)
78
78
  tsort
79
- regexp_parser (2.11.3)
79
+ regexp_parser (2.12.0)
80
80
  rexml (3.4.4)
81
- rubocop (1.84.2)
81
+ rubocop (1.86.2)
82
82
  json (~> 2.3)
83
83
  language_server-protocol (~> 3.17.0.2)
84
84
  lint_roller (~> 1.1.0)
85
- parallel (~> 1.10)
85
+ parallel (>= 1.10)
86
86
  parser (>= 3.3.0.2)
87
87
  rainbow (>= 2.2.2, < 4.0)
88
88
  regexp_parser (>= 2.9.3, < 3.0)
89
89
  rubocop-ast (>= 1.49.0, < 2.0)
90
90
  ruby-progressbar (~> 1.7)
91
91
  unicode-display_width (>= 2.4.0, < 4.0)
92
- rubocop-ast (1.49.0)
92
+ rubocop-ast (1.49.1)
93
93
  parser (>= 3.3.7.2)
94
94
  prism (~> 1.7)
95
+ rubocop-elegant (0.5.1)
96
+ lint_roller (~> 1.1)
97
+ rubocop (~> 1.75)
95
98
  rubocop-minitest (0.39.1)
96
99
  lint_roller (~> 1.1)
97
100
  rubocop (>= 1.75.0, < 2.0)
@@ -148,6 +151,7 @@ DEPENDENCIES
148
151
  rake (~> 13.2)
149
152
  rdoc (= 7.1.0)
150
153
  rubocop (~> 1.74)
154
+ rubocop-elegant (~> 0.5)
151
155
  rubocop-minitest (~> 0.38)
152
156
  rubocop-performance (~> 1.25)
153
157
  rubocop-rake (~> 0.7)
data/README.md CHANGED
@@ -11,7 +11,10 @@
11
11
  [![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/github/yegor256/factbase/master/frames)
12
12
  [![Hits-of-Code](https://hitsofcode.com/github/yegor256/factbase)](https://hitsofcode.com/view/github/yegor256/factbase)
13
13
  [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yegor256/factbase/blob/master/LICENSE.txt)
14
- [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase?ref=badge_shield&issueType=license)
14
+ [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fyegor256%2Ffactbase.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub%2Fyegor256%2Ffactbase?ref=badge_shield&issueType=license)
15
+
16
+ > **WARNING:** `Factbase` is **NOT** thread-safe. Use `Factbase::SyncFactbase`
17
+ > for concurrent access from multiple threads.
15
18
 
16
19
  This Ruby gem manages an in-memory database of facts.
17
20
  A fact is simply an associative array of properties and their values.
@@ -19,7 +22,8 @@ The values are either atomic literals or non-empty sets of literals.
19
22
  It is possible to delete a fact, but impossible to delete a property
20
23
  from a fact.
21
24
 
22
- Here is how you use it (it's thread-safe, by the way):
25
+ Here is how you use it (wrap it with `Factbase::SyncFactbase`
26
+ for thread-safe access):
23
27
 
24
28
  ```ruby
25
29
  fb = Factbase.new
@@ -119,6 +123,27 @@ end
119
123
  assert(0 == fb.size)
120
124
  ```
121
125
 
126
+ ## Query Parameters
127
+
128
+ You can use `$name` variables in queries and pass values at runtime:
129
+
130
+ ```ruby
131
+ fb = Factbase.new
132
+ fb.insert.foo = 42
133
+ fb.insert.foo = 99
134
+ fb.query('(eq foo $bar)').each(Factbase.new, bar: [42]).to_a
135
+ # Returns the fact with foo = 42
136
+ ```
137
+
138
+ Variables are passed as keyword arguments to `each` or `one`. The value must
139
+ be an array (even for a single value). This works with any term:
140
+
141
+ ```ruby
142
+ fb.query(
143
+ '(eq salary (agg (eq dept $dept) (avg salary)))'
144
+ ).each(Factbase.new, dept: ['eng']).to_a
145
+ ```
146
+
122
147
  ## Terms
123
148
 
124
149
  There are some boolean terms available in a query
@@ -129,8 +154,7 @@ There are some boolean terms available in a query
129
154
  * `(not b)` is the inverse of `b`
130
155
  * `(or b1 b2 ...)` is `true` if at least one argument is `true`
131
156
  * `(and b1 b2 ...)` — if all arguments are `true`
132
- * `(when b1 b2)` — if `b1` is `true` and `b2` is `true`
133
- or `b1` is `false`
157
+ * `(when b1 b2)` — true if `b1` is `false`, or if both `b1` and `b2` are `true`
134
158
  * `(exists p)` — if `p` property exists
135
159
  * `(absent p)` — if `p` property is absent
136
160
  * `(zero v)` — if any `v` equals to zero
@@ -145,6 +169,9 @@ There are string manipulators:
145
169
  * `(concat v1 v2 v3 ...)` — concatenates all `v`
146
170
  * `(sprintf v v1 v2 ...)` — creates a string by `v` format with params
147
171
  * `(matches v s)` — if any `v` matches the `s` regular expression
172
+ * `(contains v s)` — if any value of `v` contains any value of `s` (substring match)
173
+ * `(starts_with v s)` — if any `v` starts with `s`
174
+ * `(ends_with v s)` — if any `v` ends with `s`
148
175
 
149
176
  There are a few terms that return non-boolean values:
150
177
 
@@ -152,7 +179,7 @@ There are a few terms that return non-boolean values:
152
179
  * `(size v)` is the cardinality of `v` (zero if `v` is `nil`)
153
180
  * `(type v)` is the type of `v`
154
181
  (`"String"`, `"Integer"`, `"Float"`, `"Time"`, or `"Array"`)
155
- * `(either v1 v1)` is `v2` if `v1` is `nil`
182
+ * `(either v1 v2)` is `v2` if `v1` is `nil`
156
183
 
157
184
  It's possible to modify the facts retrieved, on fly:
158
185
 
@@ -227,6 +254,86 @@ There are some system-level terms:
227
254
  * `(env v1 v2)` returns the value of environment variable `v1` or the string
228
255
  `v2` if it's not set
229
256
 
257
+ ## Architecture
258
+
259
+ The entire database is a single flat [Ruby](https://www.ruby-lang.org/en/)
260
+ `Array` of `Hash` objects held in RAM (`Factbase#@maps`). There are no
261
+ tables, schemas, or type enforcement beyond four scalar types: `Integer`,
262
+ `Float`, `String`, and `Time`. This contrasts with
263
+ [SQLite](https://sqlite.org/) (fixed-column tables on disk) and
264
+ [MongoDB](https://www.mongodb.com/) (typed document collections). New
265
+ programmers must understand that all data vanishes on process exit unless
266
+ `export`/`import` is called explicitly.
267
+
268
+ Each property of a fact is a non-empty ordered set of values rather than a
269
+ single value. Assigning `f.foo = 1` then `f.foo = 2` produces
270
+ `f['foo'] == [1, 2]`; each assignment appends. Reading `f.foo` returns
271
+ the first element; `f['foo']` returns the full array. This accumulative
272
+ semantics differs from [SQL](https://www.iso.org/standard/76583.html)
273
+ (one value per column) and most NoSQL stores where assignment overwrites.
274
+ New programmers must expect multi-element arrays on every property read.
275
+
276
+ Queries use a custom Lisp-style
277
+ [S-expression](https://en.wikipedia.org/wiki/S-expression) language:
278
+ `(and (eq kind 'book') (gt age 10))`. `Factbase::Syntax` tokenizes and
279
+ parses a query string into an AST of `Factbase::Term` objects;
280
+ `Factbase::Query#each` evaluates that AST against every fact. This
281
+ differs from [SQL](https://www.iso.org/standard/76583.html),
282
+ [XPath](https://www.w3.org/TR/xpath-31/), and
283
+ [JSONPath](https://datatracker.ietf.org/doc/html/rfc9535). New
284
+ programmers add operators by implementing a term class, not by modifying
285
+ parser grammar.
286
+
287
+ Each query operator (`eq`, `gt`, `agg`, `join`, etc.) is a separate class
288
+ under `lib/factbase/terms/`. `Factbase::Term` holds a dispatch hash
289
+ (`@terms`) mapping operator symbols to instances and delegates `evaluate`
290
+ and `predict` calls there. This is not a class hierarchy — adding a new
291
+ operator requires a new file in `terms/` and a registration line in the
292
+ `Factbase::Term` constructor. New programmers extending the query
293
+ language must follow this two-step pattern.
294
+
295
+ Transactions are ACID and implemented via lazy copy-on-write journaling.
296
+ `Factbase#txn` wraps the array in `Factbase::LazyTaped`, which defers
297
+ physical duplication of hash objects until the first write. Inserts,
298
+ deletes, and property additions are tracked by Ruby `object_id`. On
299
+ commit the journal is replayed into the main array; raising
300
+ `Factbase::Rollback` discards it. Nesting transactions is explicitly
301
+ forbidden by `Factbase::Light`. This differs from SQLite's
302
+ [WAL](https://sqlite.org/wal.html) and PostgreSQL's
303
+ [MVCC](https://www.postgresql.org/docs/current/mvcc.html).
304
+
305
+ Cross-cutting capabilities — thread safety, indexing, constraint
306
+ validation, logging, and change counting — are added via decorators:
307
+ `Factbase::SyncFactbase`, `Factbase::IndexedFactbase`,
308
+ `Factbase::Rules`, `Factbase::Logged`, and `Factbase::Tallied`. The
309
+ [`decoor`](https://github.com/yegor256/decoor) gem provides delegation
310
+ boilerplate. The bare `Factbase` class is not thread-safe; new
311
+ programmers must wrap it with `SyncFactbase` before sharing across
312
+ threads.
313
+
314
+ Persistence uses Ruby's
315
+ [`Marshal`](https://ruby-doc.org/core/Marshal.html), serializing the
316
+ internal array of hashes to a binary blob via `Marshal.dump`. The format
317
+ is Ruby-version-specific and not portable across major Ruby versions or
318
+ platforms, unlike [JSON](https://www.json.org/json-en.html) or
319
+ [Protocol Buffers](https://protobuf.dev/). Output-only decorators
320
+ `Factbase::ToJSON`, `Factbase::ToXML`, and `Factbase::ToYAML` exist but
321
+ do not support round-trip import. Because `import` calls `Marshal.load`
322
+ on the incoming bytes, the input must come from a source the caller
323
+ trusts; a `Marshal` stream crafted by an attacker can execute arbitrary
324
+ code in the calling process, so factbase blobs received over the
325
+ network or read from a user-supplied path should be authenticated
326
+ out-of-band before being imported, as described in the
327
+ [Ruby security notes](https://docs.ruby-lang.org/en/3.3/security_rdoc.html#label-Marshal.load).
328
+
329
+ `Factbase::IndexedFactbase` lazily builds a hash-based inverted index for
330
+ equality queries, keyed by array `object_id`, property name, and
331
+ operator. The index is built incrementally on each query and invalidated
332
+ entirely on any mutation (delete or property addition). Without this
333
+ decorator every `query#each` call performs a full linear scan over all
334
+ facts. New programmers should add `IndexedFactbase` whenever the
335
+ factbase holds more than a few thousand facts.
336
+
230
337
  ## How to contribute
231
338
 
232
339
  Read
@@ -250,24 +357,24 @@ This is the result of the benchmark:
250
357
  <!-- benchmark_begin -->
251
358
  ```text
252
359
  user
253
- void scan 0.001102
254
- 20k facts: export: 2991KB 0.854774
255
- 20k facts: import: 2991KB 1.035671
256
- 50k facts: read 0.000138
257
- 50k facts: read in txn 0.002754
258
- 50k facts: insert 0.000090
259
- 50k facts: insert in txn 0.000243
260
- 50k facts: modify 1.085214
261
- 50k facts: modify in txn 2.480409
262
- 12k facts: large query: match 3k 14.187596
263
- 12k facts: large query: match 3k in txn 19.396334
264
- 12k facts: large query: match zero 15.139695
265
- 12k facts: large query: match zero in txn 21.046074
360
+ void scan 0.001115
361
+ 20k facts: export: 2979KB 0.979531
362
+ 20k facts: import: 2979KB 1.055363
363
+ 50k facts: read 0.000212
364
+ 50k facts: read in txn 0.001724
365
+ 50k facts: insert 0.000081
366
+ 50k facts: insert in txn 0.000226
367
+ 50k facts: modify 1.604103
368
+ 50k facts: modify in txn 3.126437
369
+ 12k facts: large query: match 3k 13.220472
370
+ 12k facts: large query: match 3k in txn 18.484006
371
+ 12k facts: large query: match zero 13.730576
372
+ 12k facts: large query: match zero in txn 19.485743
266
373
  ```
267
374
 
268
375
  The results were calculated in [this GHA job][benchmark-gha]
269
- on 2026-04-16 at 17:08,
376
+ on 2026-06-14 at 16:40,
270
377
  on Linux with 4 CPUs.
271
378
  <!-- benchmark_end -->
272
379
 
273
- [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/24523476155
380
+ [benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/27505290212
data/Rakefile CHANGED
@@ -63,7 +63,7 @@ end
63
63
  desc 'Benchmark them all'
64
64
  task :benchmark, [:name, :cycles] do |_t, args|
65
65
  bname = args[:name] || 'essential'
66
- cycles = (args[:cycles] || 5).to_i
66
+ cycles = Integer(args[:cycles] || 5)
67
67
  require_relative 'lib/factbase'
68
68
  require_relative 'lib/factbase/cached/cached_factbase'
69
69
  require_relative 'lib/factbase/indexed/indexed_factbase'
@@ -84,16 +84,11 @@ task :benchmark, [:name, :cycles] do |_t, args|
84
84
  fb = Factbase.new
85
85
  fb = Factbase::IndexedFactbase.new(fb)
86
86
  fb = Factbase::CachedFactbase.new(fb)
87
- Kernel.send(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb, cycles)
87
+ Kernel.__send__(File.basename(f).gsub(/\.rb$/, '').to_sym, b, fb, cycles)
88
88
  end
89
89
  end
90
90
  end
91
91
 
92
- # Run profiling on a benchmark and generate a flamegraph.
93
- # To run this task, you need to have stackprof installed.
94
- # https://github.com/tmm1/stackprof
95
- # To run profiling for a specific benchmark you can run:
96
- # bundle exec rake flamegraph\[bench_slow_query\]
97
92
  desc 'Profile a benchmark (e.g., flamegraph[bench_slow_query])'
98
93
  task :flamegraph, [:name] do |_t, args|
99
94
  require 'stackprof'
data/factbase.gemspec CHANGED
@@ -7,7 +7,7 @@ require 'English'
7
7
  require_relative 'lib/factbase/version'
8
8
 
9
9
  Gem::Specification.new do |s|
10
- s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
10
+ s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to?(:required_rubygems_version=)
11
11
  s.required_ruby_version = '>=3.0'
12
12
  s.name = 'factbase'
13
13
  s.version = Factbase::VERSION
@@ -25,15 +25,15 @@ Gem::Specification.new do |s|
25
25
  s.files = `git ls-files | grep -v -E '^(test/|\\.|fixtures|benchmark|renovate)'`.split($RS)
26
26
  s.rdoc_options = ['--charset=UTF-8']
27
27
  s.extra_rdoc_files = ['README.md', 'LICENSE.txt']
28
- s.add_dependency 'backtrace', '~>0.4'
29
- s.add_dependency 'decoor', '~>0.1'
30
- s.add_dependency 'ellipsized', '~>0.3'
31
- s.add_dependency 'json', '~>2.7'
32
- s.add_dependency 'logger', '~>1.0'
33
- s.add_dependency 'loog', '~>0.6'
34
- s.add_dependency 'nokogiri', '~>1.10'
35
- s.add_dependency 'others', '~>0.1'
36
- s.add_dependency 'tago', '~>0.1'
37
- s.add_dependency 'yaml', '~>0.3'
28
+ s.add_dependency('backtrace', '~>0.4')
29
+ s.add_dependency('decoor', '~>0.1')
30
+ s.add_dependency('ellipsized', '~>0.3')
31
+ s.add_dependency('json', '~>2.7')
32
+ s.add_dependency('logger', '~>1.0')
33
+ s.add_dependency('loog', '~>0.6')
34
+ s.add_dependency('nokogiri', '~>1.10')
35
+ s.add_dependency('others', '~>0.1')
36
+ s.add_dependency('tago', '~>0.1')
37
+ s.add_dependency('yaml', '~>0.3')
38
38
  s.metadata['rubygems_mfa_required'] = 'true'
39
39
  end
@@ -27,7 +27,7 @@ class Factbase::Accum
27
27
  end
28
28
 
29
29
  def all_properties
30
- @fact.all_properties
30
+ @fact.all_properties | @props.keys
31
31
  end
32
32
 
33
33
  others do |*args|
@@ -24,9 +24,8 @@ class Factbase::CachedFact
24
24
  @origin.to_s
25
25
  end
26
26
 
27
- # When a method is missing, this method is called.
28
27
  others do |*args|
29
28
  @cache.clear if args[0].to_s.end_with?('=')
30
- @origin.send(*args)
29
+ @origin.__send__(*args)
31
30
  end
32
31
  end
@@ -22,9 +22,9 @@ class Factbase::CachedFactbase
22
22
  # @param [Factbase] origin Original factbase to decorate
23
23
  # @param [Hash] cache Cache to use
24
24
  def initialize(origin, cache = {})
25
- raise 'Wrong type of original' unless origin.respond_to?(:query)
25
+ raise(ArgumentError, 'Wrong type of original') unless origin.respond_to?(:query)
26
26
  @origin = origin
27
- raise 'Wrong type of cache' unless cache.is_a?(Hash)
27
+ raise(ArgumentError, 'Wrong type of cache') unless cache.is_a?(Hash)
28
28
  @cache = cache
29
29
  end
30
30
 
@@ -58,7 +58,7 @@ class Factbase::CachedFactbase
58
58
  # @return [Factbase::Churn] How many facts have been changed (zero if rolled back)
59
59
  def txn
60
60
  @origin.txn do |fbt|
61
- yield Factbase::CachedFactbase.new(fbt, @cache)
61
+ yield(Factbase::CachedFactbase.new(fbt, @cache))
62
62
  end
63
63
  end
64
64
  end
@@ -36,13 +36,12 @@ class Factbase::CachedQuery
36
36
  def each(fb = @fb, params = {})
37
37
  return to_enum(__method__, fb, params) unless block_given?
38
38
  invalidate_if_dirty!
39
- key = "each #{@origin}" # params are ignored!
40
- before = @cache[key]
41
- @cache[key] = @origin.each(fb, params).to_a if before.nil?
39
+ key = "each #{@origin}"
40
+ @cache[key] = @origin.each(fb, params).to_a if @cache[key].nil?
42
41
  c = 0
43
42
  @cache[key].each do |f|
44
43
  c += 1
45
- yield Factbase::CachedFact.new(f, @cache)
44
+ yield(Factbase::CachedFact.new(f, @cache))
46
45
  end
47
46
  c
48
47
  end
@@ -54,8 +53,7 @@ class Factbase::CachedQuery
54
53
  def one(fb = @fb, params = {})
55
54
  invalidate_if_dirty!
56
55
  key = "one: #{@origin} #{params}"
57
- before = @cache[key]
58
- @cache[key] = @origin.one(fb, params) if before.nil?
56
+ @cache[key] = @origin.one(fb, params) if @cache[key].nil?
59
57
  @cache[key]
60
58
  end
61
59
 
@@ -19,8 +19,7 @@ module Factbase::CachedTerm
19
19
  return super unless static? && !abstract?
20
20
  return super if %i[head unique].include?(@op)
21
21
  key = [maps.object_id, to_s]
22
- before = @cache[key]
23
- @cache[key] = super if before.nil?
22
+ @cache[key] = super if @cache[key].nil?
24
23
  @cache[key]
25
24
  end
26
25
  end
@@ -22,16 +22,16 @@ class Factbase::Churn
22
22
  if zero?
23
23
  'nothing'
24
24
  else
25
- "#{@inserted}i/#{@deleted}d/#{@added}a"
25
+ "#{inserted}i/#{deleted}d/#{added}a"
26
26
  end
27
27
  end
28
28
 
29
29
  def zero?
30
- @inserted.zero? && @deleted.zero? && @added.zero?
30
+ inserted.zero? && deleted.zero? && added.zero?
31
31
  end
32
32
 
33
33
  def to_i
34
- @inserted + @deleted + @added
34
+ inserted + deleted + added
35
35
  end
36
36
 
37
37
  def append(ins, del, add)
@@ -43,10 +43,6 @@ class Factbase::Churn
43
43
  end
44
44
 
45
45
  def +(other)
46
- Factbase::Churn.new(
47
- @inserted + other.inserted,
48
- @deleted + other.deleted,
49
- @added + other.added
50
- )
46
+ Factbase::Churn.new(inserted + other.inserted, deleted + other.deleted, added + other.added)
51
47
  end
52
48
  end
data/lib/factbase/fact.rb CHANGED
@@ -4,8 +4,8 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  require 'json'
7
- require 'time'
8
7
  require 'others'
8
+ require 'time'
9
9
  require_relative '../factbase'
10
10
 
11
11
  # A single fact in a factbase.
@@ -37,7 +37,7 @@ class Factbase::Fact
37
37
  # Convert it to a string.
38
38
  # @return [String] String representation of it (in JSON)
39
39
  def to_s
40
- "[ #{@map.map { |k, v| "#{k}: #{v}" }.sort.join(', ')} ]"
40
+ "[ #{@map.map { |k, v| "#{k}: #{v}" }.sort!.join(', ')} ]"
41
41
  end
42
42
 
43
43
  # Get a list of all props.
@@ -50,12 +50,15 @@ class Factbase::Fact
50
50
  k = args[0].to_s
51
51
  if k.end_with?('=')
52
52
  kk = k[0..-2]
53
- raise "Invalid prop name '#{kk}'" unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
54
- raise "Prohibited prop name '#{kk}'" if methods.include?(kk.to_sym)
53
+ raise(ArgumentError, "Invalid prop name '#{kk}'") unless kk.match?(/^[a-z_][_a-zA-Z0-9]*$/)
54
+ raise(ArgumentError, "Prohibited prop name '#{kk}'") if methods.include?(kk.to_sym)
55
55
  v = args[1]
56
- raise "The value of '#{kk}' can't be nil" if v.nil?
57
- raise "The value of '#{kk}' can't be empty" if v == ''
58
- raise "The type '#{v.class}' of '#{kk}' is not allowed" unless [String, Integer, Float, Time].include?(v.class)
56
+ raise(ArgumentError, "The value of '#{kk}' can't be nil") if v.nil?
57
+ raise(ArgumentError, "The value of '#{kk}' can't be empty") if v == ''
58
+ raise(ArgumentError, "The type '#{v.class}' of '#{kk}' is not allowed") unless [
59
+ String, Integer, Float,
60
+ Time
61
+ ].include?(v.class)
59
62
  v = v.utc if v.is_a?(Time)
60
63
  @map[kk] = [] if @map[kk].nil?
61
64
  @map[kk] << v
@@ -65,8 +68,8 @@ class Factbase::Fact
65
68
  else
66
69
  v = @map[k]
67
70
  if v.nil?
68
- raise "Can't get '#{k}', the fact is empty" if @map.empty?
69
- raise "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]"
71
+ raise(ArgumentError, "Can't get '#{k}', the fact is empty") if @map.empty?
72
+ raise(ArgumentError, "Can't find '#{k}' attribute out of [#{@map.keys.join(', ')}]")
70
73
  end
71
74
  v[0]
72
75
  end
@@ -21,8 +21,8 @@ class Factbase::Flatten
21
21
  # @return [Array<HashMap>] The hashmaps, but improved
22
22
  def it
23
23
  @maps
24
- .sort_by { |m| m[@sorter] || [] }
24
+ .sort_by { |m| Array(m[@sorter]).map(&:to_s) }
25
25
  .map { |m| m.sort.to_h }
26
- .map { |m| m.transform_values { |v| v.size == 1 ? v[0] : v } }
26
+ .map! { |m| m.transform_values { |v| v.size == 1 ? v[0] : v } }
27
27
  end
28
28
  end
@@ -18,9 +18,9 @@ class Factbase::Impatient
18
18
  # @param [Factbase] fb The factbase to decorate
19
19
  # @param [Integer] timeout Timeout in seconds
20
20
  def initialize(fb, timeout: 15)
21
- raise 'The "fb" is nil' if fb.nil?
21
+ raise(ArgumentError, 'The "fb" is nil') if fb.nil?
22
22
  @origin = fb
23
- @timeout = timeout.to_f
23
+ @timeout = Float(timeout)
24
24
  end
25
25
 
26
26
  decoor(:origin)
@@ -36,7 +36,7 @@ class Factbase::Impatient
36
36
 
37
37
  def txn
38
38
  @origin.txn do |fbt|
39
- yield Factbase::Impatient.new(fbt, timeout: @timeout)
39
+ yield(Factbase::Impatient.new(fbt, timeout: @timeout))
40
40
  end
41
41
  end
42
42
 
@@ -56,17 +56,15 @@ class Factbase::Impatient
56
56
  end
57
57
 
58
58
  def each(fb = @fb, params = {}, &)
59
- a =
60
- impatient('each') do
61
- @fb.query(@term, @maps).each(fb, params).to_a
59
+ return to_enum(__method__, fb, params) unless block_given?
60
+ n = 0
61
+ impatient('each') do
62
+ @fb.query(@term, @maps).each(fb, params) do |f|
63
+ yield(f)
64
+ n += 1
62
65
  end
63
- return a unless block_given?
64
- yielded = 0
65
- a.each do |f|
66
- yield f
67
- yielded += 1
68
66
  end
69
- yielded
67
+ n
70
68
  end
71
69
 
72
70
  def one(fb = @fb, params = {})
@@ -86,7 +84,10 @@ class Factbase::Impatient
86
84
  def impatient(name, &)
87
85
  Timeout.timeout(@timeout, &)
88
86
  rescue Timeout::Error => e
89
- raise "#{name}() timed out after #{@timeout.seconds} (#{e.message}), fb size is #{@fb.size}: #{@term}"
87
+ raise(
88
+ StandardError,
89
+ "#{name}() timed out after #{@timeout.seconds} (#{e.message}), fb size is #{@fb.size}: #{@term}"
90
+ )
90
91
  end
91
92
  end
92
93
  end