factbase 0.19.6 → 0.19.7
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +3 -3
- data/README.md +74 -37
- data/lib/factbase/indexed/indexed_absent.rb +14 -18
- data/lib/factbase/indexed/indexed_and.rb +3 -7
- data/lib/factbase/indexed/indexed_eq.rb +27 -30
- data/lib/factbase/indexed/indexed_exists.rb +3 -4
- data/lib/factbase/indexed/indexed_gt.rb +28 -30
- data/lib/factbase/indexed/indexed_lt.rb +28 -35
- data/lib/factbase/indexed/indexed_not.rb +21 -24
- data/lib/factbase/indexed/indexed_one.rb +14 -18
- data/lib/factbase/indexed/indexed_unique.rb +2 -6
- data/lib/factbase/lazy_taped_array.rb +0 -4
- data/lib/factbase/lazy_taped_hash.rb +0 -4
- data/lib/factbase/version.rb +1 -1
- data/lib/fuzz.rb +75 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c38336019e70a00d0845dfdbcc4a0fb033a4be8c3fd0ff8595c3de8bdc25cc1
|
|
4
|
+
data.tar.gz: 55b820f5d37a074a1a5f7ca2f9dafeb8d5180cdfd52ee354a855e2c402398d7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a018f96e8929e0a3a7946ad4b25cfdaf042c968cad1e0689d484235ab9aaa3b29b3a17557673e9c8b7ba32226a2746a7b0f1fa5ea1e27700aa96cfa18fa7580
|
|
7
|
+
data.tar.gz: a1dc92d273a4a6a6a870dd050a20fd23e2d06175861a2c094a45c68a80deaee479adb98ab027df1bee7791eb48e03bf520523be999ab49b88b2d4a57e0f29ddd
|
data/Gemfile.lock
CHANGED
|
@@ -34,7 +34,7 @@ GEM
|
|
|
34
34
|
language_server-protocol (3.17.0.5)
|
|
35
35
|
lint_roller (1.1.0)
|
|
36
36
|
logger (1.7.0)
|
|
37
|
-
loog (0.
|
|
37
|
+
loog (0.8.0)
|
|
38
38
|
ellipsized
|
|
39
39
|
logger (~> 1.0)
|
|
40
40
|
minitest (6.0.1)
|
|
@@ -62,7 +62,7 @@ GEM
|
|
|
62
62
|
psych (5.3.1)
|
|
63
63
|
date
|
|
64
64
|
stringio
|
|
65
|
-
qbash (0.
|
|
65
|
+
qbash (0.8.0)
|
|
66
66
|
backtrace (> 0)
|
|
67
67
|
elapsed (> 0)
|
|
68
68
|
loog (> 0)
|
|
@@ -76,7 +76,7 @@ GEM
|
|
|
76
76
|
tsort
|
|
77
77
|
regexp_parser (2.11.3)
|
|
78
78
|
rexml (3.4.4)
|
|
79
|
-
rubocop (1.84.
|
|
79
|
+
rubocop (1.84.2)
|
|
80
80
|
json (~> 2.3)
|
|
81
81
|
language_server-protocol (~> 3.17.0.2)
|
|
82
82
|
lint_roller (~> 1.1.0)
|
data/README.md
CHANGED
|
@@ -227,52 +227,89 @@ This is the result of the benchmark:
|
|
|
227
227
|
```text
|
|
228
228
|
|
|
229
229
|
query all facts from an empty factbase 0.00
|
|
230
|
-
insert 20000 facts 0.
|
|
230
|
+
insert 20000 facts 0.63
|
|
231
231
|
export 20000 facts 0.02
|
|
232
|
-
import
|
|
233
|
-
insert 10 facts 0.
|
|
234
|
-
query 10 times w/txn 2.
|
|
235
|
-
query 10 times w/o txn 0.
|
|
236
|
-
modify 10 attrs w/txn 1.
|
|
237
|
-
delete 10 facts w/txn
|
|
238
|
-
build index on 5000 facts 0.
|
|
239
|
-
export 5000 facts with index 0.
|
|
232
|
+
import 411003 bytes (20000 facts) 0.01
|
|
233
|
+
insert 10 facts 0.03
|
|
234
|
+
query 10 times w/txn 2.03
|
|
235
|
+
query 10 times w/o txn 0.13
|
|
236
|
+
modify 10 attrs w/txn 1.51
|
|
237
|
+
delete 10 facts w/txn 2.97
|
|
238
|
+
build index on 5000 facts 0.03
|
|
239
|
+
export 5000 facts with index 0.04
|
|
240
240
|
import 5000 facts with persisted index 0.03
|
|
241
|
-
query 5000 facts using persisted index 0.
|
|
242
|
-
export 5000 facts without index 0.
|
|
241
|
+
query 5000 facts using persisted index 0.07
|
|
242
|
+
export 5000 facts without index 0.01
|
|
243
243
|
import 5000 facts without index 0.01
|
|
244
|
-
query 5000 facts building index on-the-fly 0.
|
|
245
|
-
|
|
244
|
+
query 5000 facts building index on-the-fly 0.07
|
|
245
|
+
query 15k facts sel: 20% card: 10 absent plain 0.61
|
|
246
|
+
query 15k facts sel: 20% card: 10 absent indexed(cold) 0.16
|
|
247
|
+
query 15k facts sel: 20% card: 10 absent indexed(warm) 0.13
|
|
248
|
+
query 15k facts sel: 20% card: 10 exists plain 0.56
|
|
249
|
+
query 15k facts sel: 20% card: 10 exists indexed(cold) 0.14
|
|
250
|
+
query 15k facts sel: 20% card: 10 exists indexed(warm) 0.13
|
|
251
|
+
query 15k facts sel: 20% card: 10 eq plain 0.83
|
|
252
|
+
query 15k facts sel: 20% card: 10 eq indexed(cold) 0.22
|
|
253
|
+
query 15k facts sel: 20% card: 10 eq indexed(warm) 0.24
|
|
254
|
+
query 15k facts sel: 20% card: 10 not plain 1.11
|
|
255
|
+
query 15k facts sel: 20% card: 10 not indexed(cold) 0.50
|
|
256
|
+
query 15k facts sel: 20% card: 10 not indexed(warm) 0.43
|
|
257
|
+
query 15k facts sel: 20% card: 10 gt plain 0.84
|
|
258
|
+
query 15k facts sel: 20% card: 10 gt indexed(cold) 0.26
|
|
259
|
+
query 15k facts sel: 20% card: 10 gt indexed(warm) 0.19
|
|
260
|
+
query 15k facts sel: 20% card: 10 lt plain 0.84
|
|
261
|
+
query 15k facts sel: 20% card: 10 lt indexed(cold) 0.27
|
|
262
|
+
query 15k facts sel: 20% card: 10 lt indexed(warm) 0.20
|
|
263
|
+
query 15k facts sel: 20% card: 10 and eq plain 1.39
|
|
264
|
+
query 15k facts sel: 20% card: 10 and eq indexed(cold) 0.85
|
|
265
|
+
query 15k facts sel: 20% card: 10 and eq indexed(warm) 0.46
|
|
266
|
+
query 15k facts sel: 20% card: 10 and complex plain 1.32
|
|
267
|
+
query 15k facts sel: 20% card: 10 and complex indexed(cold) 0.48
|
|
268
|
+
query 15k facts sel: 20% card: 10 and complex indexed(warm) 0.43
|
|
269
|
+
query 15k facts sel: 20% card: 10 one plain 0.72
|
|
270
|
+
query 15k facts sel: 20% card: 10 one indexed(cold) 0.19
|
|
271
|
+
query 15k facts sel: 20% card: 10 one indexed(warm) 0.15
|
|
272
|
+
query 15k facts sel: 20% card: 10 or plain 2.02
|
|
273
|
+
query 15k facts sel: 20% card: 10 or indexed(cold) 0.42
|
|
274
|
+
query 15k facts sel: 20% card: 10 or indexed(warm) 0.36
|
|
275
|
+
query 15k facts sel: 20% card: 10 unique plain 1.86
|
|
276
|
+
query 15k facts sel: 20% card: 10 unique indexed(cold) 0.61
|
|
277
|
+
query 15k facts sel: 20% card: 10 unique indexed(warm) 0.37
|
|
278
|
+
(and (eq what 'issue-was-closed') (exists... -> 200 1.04
|
|
246
279
|
(and (eq what 'issue-was-closed') (exists... -> 200/txn 1.23
|
|
247
|
-
(and (eq what 'issue-was-closed') (exists... -> zero 1.
|
|
248
|
-
(and (eq what 'issue-was-closed') (exists... -> zero/txn 1.
|
|
249
|
-
transaction rollback on factbase with 100000 facts 0.
|
|
250
|
-
(gt time '2024-03-23T03:21:43Z') 0.
|
|
251
|
-
(gt cost 50) 0.
|
|
280
|
+
(and (eq what 'issue-was-closed') (exists... -> zero 1.06
|
|
281
|
+
(and (eq what 'issue-was-closed') (exists... -> zero/txn 1.23
|
|
282
|
+
transaction rollback on factbase with 100000 facts 0.26
|
|
283
|
+
(gt time '2024-03-23T03:21:43Z') 0.22
|
|
284
|
+
(gt cost 50) 0.10
|
|
252
285
|
(eq title 'Object Thinking 5000') 0.03
|
|
253
|
-
(and (eq foo 42.998) (or (gt bar 200) (absent z... 0.
|
|
254
|
-
(and (exists foo) (not (exists blue))) 1.
|
|
255
|
-
(eq id (agg (always) (max id))) 2.
|
|
256
|
-
(join "c<=cost,b<=bar" (eq id (agg (always) (ma... 4.
|
|
257
|
-
(and (eq what "foo") (join "w<=what" (and (eq i... 7.
|
|
258
|
-
delete! 0.
|
|
259
|
-
(and (eq issue *) (eq repository *) (eq what '*') (eq where '*')) 0.
|
|
286
|
+
(and (eq foo 42.998) (or (gt bar 200) (absent z... 0.02
|
|
287
|
+
(and (exists foo) (not (exists blue))) 1.12
|
|
288
|
+
(eq id (agg (always) (max id))) 2.76
|
|
289
|
+
(join "c<=cost,b<=bar" (eq id (agg (always) (ma... 4.58
|
|
290
|
+
(and (eq what "foo") (join "w<=what" (and (eq i... 7.26
|
|
291
|
+
delete! 0.42
|
|
292
|
+
(and (eq issue *) (eq repository *) (eq what '*') (eq where '*')) 0.38
|
|
260
293
|
Taped.append() x50000 0.02
|
|
261
|
-
Taped.each() x125 1.
|
|
262
|
-
Taped.delete_if() x375 0.
|
|
263
|
-
50000 facts: read
|
|
264
|
-
50000 facts:
|
|
265
|
-
50000 facts: insert
|
|
266
|
-
50000 facts:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
100000 facts:
|
|
270
|
-
100000 facts:
|
|
294
|
+
Taped.each() x125 1.08
|
|
295
|
+
Taped.delete_if() x375 0.86
|
|
296
|
+
50000 facts: plain read (no txn) 4.06
|
|
297
|
+
50000 facts: read-only txn (no copy) 4.85
|
|
298
|
+
50000 facts: plain insert (no txn) 0.00
|
|
299
|
+
50000 facts: insert in txn (copy triggered) 3.31
|
|
300
|
+
50000 facts: plain modify (no txn) 28.21
|
|
301
|
+
50000 facts: modify in txn (copy triggered) 35.14
|
|
302
|
+
100000 facts: plain read (no txn) 8.06
|
|
303
|
+
100000 facts: read-only txn (no copy) 9.94
|
|
304
|
+
100000 facts: plain insert (no txn) 0.00
|
|
305
|
+
100000 facts: insert in txn (copy triggered) 6.68
|
|
306
|
+
100000 facts: plain modify (no txn) 56.21
|
|
307
|
+
100000 facts: modify in txn (copy triggered) 70.09
|
|
271
308
|
```
|
|
272
309
|
|
|
273
310
|
The results were calculated in [this GHA job][benchmark-gha]
|
|
274
|
-
on 2026-02-
|
|
311
|
+
on 2026-02-17 at 18:48,
|
|
275
312
|
on Linux with 4 CPUs.
|
|
276
313
|
<!-- benchmark_end -->
|
|
277
314
|
|
|
278
|
-
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/
|
|
315
|
+
[benchmark-gha]: https://github.com/yegor256/factbase/actions/runs/22111136309
|
|
@@ -11,25 +11,21 @@ class Factbase::IndexedAbsent
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, _fb, _params)
|
|
14
|
-
|
|
15
|
-
key = [maps.object_id,
|
|
14
|
+
prop = @term.operands[0].to_s
|
|
15
|
+
key = [maps.object_id, prop, @term.op]
|
|
16
|
+
@idx[key] ||= { facts: [], count: 0 }
|
|
16
17
|
entry = @idx[key]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
entry[:indexed_count] = maps_array.size
|
|
28
|
-
end
|
|
29
|
-
if maps.respond_to?(:ensure_copied!)
|
|
30
|
-
maps & entry[:facts]
|
|
31
|
-
else
|
|
32
|
-
(maps & []) | entry[:facts]
|
|
18
|
+
_feed(maps.to_a, entry, prop)
|
|
19
|
+
maps.respond_to?(:repack) ? maps.repack(entry[:facts]) : entry[:facts]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def _feed(facts, entry, prop)
|
|
25
|
+
return unless entry[:count] < facts.size
|
|
26
|
+
facts[entry[:count]..].each do |f|
|
|
27
|
+
entry[:facts] << f if f[prop].nil?
|
|
33
28
|
end
|
|
29
|
+
entry[:count] = facts.size
|
|
34
30
|
end
|
|
35
31
|
end
|
|
@@ -43,12 +43,7 @@ class Factbase::IndexedAnd
|
|
|
43
43
|
end
|
|
44
44
|
)
|
|
45
45
|
j = tuples.flat_map { |t| entry[:index][t] || [] }.uniq(&:object_id)
|
|
46
|
-
r =
|
|
47
|
-
if maps.respond_to?(:inserted)
|
|
48
|
-
maps & j
|
|
49
|
-
else
|
|
50
|
-
j
|
|
51
|
-
end
|
|
46
|
+
r = maps.respond_to?(:repack) ? maps.repack(j) : j
|
|
52
47
|
else
|
|
53
48
|
@term.operands.each do |o|
|
|
54
49
|
n = o.predict(maps, fb, params)
|
|
@@ -56,7 +51,8 @@ class Factbase::IndexedAnd
|
|
|
56
51
|
if r.nil?
|
|
57
52
|
r = n
|
|
58
53
|
elsif n.size < r.size * 8 # to skip some obvious matchings
|
|
59
|
-
|
|
54
|
+
ids = n.to_set(&:object_id)
|
|
55
|
+
r = r.select { |f| ids.include?(f.object_id) }
|
|
60
56
|
end
|
|
61
57
|
break if r.size < maps.size / 32 # it's already small enough
|
|
62
58
|
break if r.size < 128 # it's obviously already small enough
|
|
@@ -11,37 +11,18 @@ class Factbase::IndexedEq
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, _fb, params)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return unless
|
|
14
|
+
first_operand = @term.operands[0]
|
|
15
|
+
second_operand = @term.operands[1]
|
|
16
|
+
return unless first_operand.is_a?(Symbol) && _scalar?(second_operand)
|
|
17
|
+
first_operand = first_operand.to_s
|
|
18
|
+
key = [maps.object_id, first_operand, @term.op]
|
|
19
|
+
@idx[key] ||= { facts: {}, count: 0 }
|
|
17
20
|
entry = @idx[key]
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if entry[:indexed_count] < maps_array.size
|
|
24
|
-
prop = @term.operands.first.to_s
|
|
25
|
-
maps_array[entry[:indexed_count]..].each do |m|
|
|
26
|
-
m[prop]&.each do |v|
|
|
27
|
-
entry[:index][v] ||= []
|
|
28
|
-
entry[:index][v] << m
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
entry[:indexed_count] = maps_array.size
|
|
32
|
-
end
|
|
33
|
-
vv =
|
|
34
|
-
if @term.operands[1].is_a?(Symbol)
|
|
35
|
-
params[@term.operands[1].to_s] || []
|
|
36
|
-
else
|
|
37
|
-
[@term.operands[1]]
|
|
38
|
-
end
|
|
39
|
-
j = vv.flat_map { |v| entry[:index][v] || [] }.uniq(&:object_id)
|
|
40
|
-
if maps.respond_to?(:inserted)
|
|
41
|
-
maps & j
|
|
42
|
-
else
|
|
43
|
-
j
|
|
44
|
-
end
|
|
21
|
+
_feed(maps.to_a, entry, first_operand)
|
|
22
|
+
keys = _resolve(second_operand, params)
|
|
23
|
+
matches = keys.flat_map { |k| entry[:facts][k] || [] }
|
|
24
|
+
matches = matches.uniq(&:object_id) if keys.size > 1
|
|
25
|
+
maps.respond_to?(:repack) ? maps.repack(matches) : matches
|
|
45
26
|
end
|
|
46
27
|
|
|
47
28
|
private
|
|
@@ -49,4 +30,20 @@ class Factbase::IndexedEq
|
|
|
49
30
|
def _scalar?(item)
|
|
50
31
|
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
51
32
|
end
|
|
33
|
+
|
|
34
|
+
def _feed(facts, entry, operand)
|
|
35
|
+
return unless entry[:count] < facts.size
|
|
36
|
+
facts[entry[:count]..].each do |m|
|
|
37
|
+
m[operand]&.each do |v|
|
|
38
|
+
entry[:facts][v] ||= []
|
|
39
|
+
entry[:facts][v] << m
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
entry[:count] = facts.size
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def _resolve(operand, params)
|
|
46
|
+
return Array(operand) unless operand.is_a?(Symbol)
|
|
47
|
+
params[operand.to_s] || []
|
|
48
|
+
end
|
|
52
49
|
end
|
|
@@ -20,14 +20,13 @@ class Factbase::IndexedExists
|
|
|
20
20
|
key = [maps.object_id, operand, @term.op]
|
|
21
21
|
@idx[key] = { facts: [], count: 0 } if @idx[key].nil?
|
|
22
22
|
entry = @idx[key]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
entry[:facts]
|
|
23
|
+
_feed(maps.to_a, entry, operand)
|
|
24
|
+
maps.respond_to?(:repack) ? maps.repack(entry[:facts]) : entry[:facts]
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
private
|
|
29
28
|
|
|
30
|
-
def
|
|
29
|
+
def _feed(facts, entry, operand)
|
|
31
30
|
facts[entry[:count]..].each do |m|
|
|
32
31
|
entry[:facts] << m unless m[operand].nil?
|
|
33
32
|
end
|
|
@@ -11,36 +11,17 @@ class Factbase::IndexedGt
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, _fb, params)
|
|
14
|
-
|
|
15
|
-
return unless
|
|
16
|
-
prop =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
maps_array[entry[:indexed_count]..].each do |m|
|
|
26
|
-
values = m[prop]
|
|
27
|
-
next if values.nil?
|
|
28
|
-
values.each do |v|
|
|
29
|
-
entry[:sorted] << [v, m]
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
entry[:sorted].sort_by! { |pair| pair[0] }
|
|
33
|
-
entry[:indexed_count] = maps_array.size
|
|
34
|
-
end
|
|
35
|
-
threshold = @term.operands[1].is_a?(Symbol) ? params[@term.operands[1].to_s]&.first : @term.operands[1]
|
|
36
|
-
return nil if threshold.nil?
|
|
37
|
-
i = entry[:sorted].bsearch_index { |pair| pair[0] > threshold } || entry[:sorted].size
|
|
38
|
-
result = entry[:sorted][i..].map { |pair| pair[1] }.uniq
|
|
39
|
-
if maps.respond_to?(:ensure_copied!)
|
|
40
|
-
maps & result
|
|
41
|
-
else
|
|
42
|
-
(maps & []) | result
|
|
43
|
-
end
|
|
14
|
+
op1, op2 = @term.operands
|
|
15
|
+
return unless op1.is_a?(Symbol) && _scalar?(op2)
|
|
16
|
+
prop = op1.to_s
|
|
17
|
+
target = op2.is_a?(Symbol) ? params[op2.to_s]&.first : op2
|
|
18
|
+
return maps || [] if target.nil?
|
|
19
|
+
key = [maps.object_id, prop, :facts]
|
|
20
|
+
@idx[key] ||= { facts: [], count: 0 }
|
|
21
|
+
entry = @idx[key]
|
|
22
|
+
_feed(maps.to_a, entry, prop)
|
|
23
|
+
matched = _search(entry, target)
|
|
24
|
+
maps.respond_to?(:repack) ? maps.repack(matched) : matched
|
|
44
25
|
end
|
|
45
26
|
|
|
46
27
|
private
|
|
@@ -48,4 +29,21 @@ class Factbase::IndexedGt
|
|
|
48
29
|
def _scalar?(item)
|
|
49
30
|
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
50
31
|
end
|
|
32
|
+
|
|
33
|
+
def _feed(facts, entry, prop)
|
|
34
|
+
return unless entry[:count] < facts.size
|
|
35
|
+
facts[entry[:count]..].each do |fact|
|
|
36
|
+
fact[prop]&.each do |v|
|
|
37
|
+
entry[:facts] << [v, fact]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
entry[:facts].sort_by! { |pair| pair[0] }
|
|
41
|
+
entry[:count] = facts.size
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _search(entry, target)
|
|
45
|
+
idx = entry[:facts].bsearch_index { |v, _| v > target }
|
|
46
|
+
return [] if idx.nil?
|
|
47
|
+
entry[:facts][idx..].map { |_, f| f }.uniq(&:object_id)
|
|
48
|
+
end
|
|
51
49
|
end
|
|
@@ -11,41 +11,17 @@ class Factbase::IndexedLt
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, _fb, params)
|
|
14
|
-
|
|
15
|
-
return unless
|
|
16
|
-
prop =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
new_pairs = []
|
|
26
|
-
maps_array[entry[:indexed_count]..].each do |m|
|
|
27
|
-
values = m[prop]
|
|
28
|
-
next if values.nil?
|
|
29
|
-
values.each do |v|
|
|
30
|
-
new_pairs << [v, m]
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
unless new_pairs.empty?
|
|
34
|
-
entry[:sorted].concat(new_pairs)
|
|
35
|
-
entry[:sorted].sort_by! { |pair| pair[0] }
|
|
36
|
-
end
|
|
37
|
-
entry[:indexed_count] = maps_array.size
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
threshold = @term.operands[1].is_a?(Symbol) ? params[@term.operands[1].to_s]&.first : @term.operands[1]
|
|
41
|
-
return nil if threshold.nil?
|
|
42
|
-
i = entry[:sorted].bsearch_index { |pair| pair[0] >= threshold } || entry[:sorted].size
|
|
43
|
-
result = entry[:sorted][0...i].map { |pair| pair[1] }.uniq
|
|
44
|
-
if maps.respond_to?(:ensure_copied!)
|
|
45
|
-
maps & result
|
|
46
|
-
else
|
|
47
|
-
(maps & []) | result
|
|
48
|
-
end
|
|
14
|
+
op1, op2 = @term.operands
|
|
15
|
+
return unless op1.is_a?(Symbol) && _scalar?(op2)
|
|
16
|
+
prop = op1.to_s
|
|
17
|
+
target = op2.is_a?(Symbol) ? params[op2.to_s]&.first : op2
|
|
18
|
+
return maps || [] if target.nil?
|
|
19
|
+
key = [maps.object_id, prop, :facts]
|
|
20
|
+
@idx[key] ||= { facts: [], count: 0 }
|
|
21
|
+
entry = @idx[key]
|
|
22
|
+
_feed(maps.to_a, entry, prop)
|
|
23
|
+
matched = _search(entry, target)
|
|
24
|
+
maps.respond_to?(:repack) ? maps.repack(matched) : matched
|
|
49
25
|
end
|
|
50
26
|
|
|
51
27
|
private
|
|
@@ -53,4 +29,21 @@ class Factbase::IndexedLt
|
|
|
53
29
|
def _scalar?(item)
|
|
54
30
|
item.is_a?(String) || item.is_a?(Time) || item.is_a?(Integer) || item.is_a?(Float) || item.is_a?(Symbol)
|
|
55
31
|
end
|
|
32
|
+
|
|
33
|
+
def _feed(facts, entry, prop)
|
|
34
|
+
return unless entry[:count] < facts.size
|
|
35
|
+
facts[entry[:count]..].each do |fact|
|
|
36
|
+
fact[prop]&.each do |v|
|
|
37
|
+
entry[:facts] << [v, fact]
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
entry[:facts].sort_by! { |pair| pair[0] }
|
|
41
|
+
entry[:count] = facts.size
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _search(entry, target)
|
|
45
|
+
idx = entry[:facts].bsearch_index { |v, _| v >= target }
|
|
46
|
+
res = idx.nil? ? entry[:facts] : entry[:facts][0...idx]
|
|
47
|
+
res.map { |_, f| f }.uniq(&:object_id)
|
|
48
|
+
end
|
|
56
49
|
end
|
|
@@ -11,33 +11,30 @@ class Factbase::IndexedNot
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, fb, params)
|
|
14
|
-
|
|
15
|
-
key = [maps.object_id,
|
|
14
|
+
sub = @term.operands.first
|
|
15
|
+
key = [maps.object_id, sub, @term.op]
|
|
16
|
+
@idx[key] ||= { facts: nil, count: 0, yes_set: nil }
|
|
16
17
|
entry = @idx[key]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
entry = { facts: nil, indexed_count: 0, yes_set: nil }
|
|
20
|
-
@idx[key] = entry
|
|
18
|
+
_feed(maps.to_a, entry) do
|
|
19
|
+
sub.predict(maps, fb, params)
|
|
21
20
|
end
|
|
22
|
-
if entry[:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
entry[:
|
|
33
|
-
|
|
34
|
-
r = entry[:facts]
|
|
35
|
-
if r.nil?
|
|
36
|
-
nil
|
|
37
|
-
elsif maps.respond_to?(:ensure_copied!)
|
|
38
|
-
maps & r
|
|
21
|
+
return nil if entry[:facts].nil?
|
|
22
|
+
maps.respond_to?(:repack) ? maps.repack(entry[:facts]) : entry[:facts]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def _feed(facts, entry)
|
|
28
|
+
return unless entry[:count] < facts.size
|
|
29
|
+
yes = yield
|
|
30
|
+
if yes.nil?
|
|
31
|
+
entry[:facts] = nil
|
|
32
|
+
entry[:yes_set] = nil
|
|
39
33
|
else
|
|
40
|
-
|
|
34
|
+
yes_set = yes.to_a.to_set
|
|
35
|
+
entry[:yes_set] = yes_set
|
|
36
|
+
entry[:facts] = facts.reject { |m| yes_set.include?(m) }
|
|
41
37
|
end
|
|
38
|
+
entry[:count] = facts.size
|
|
42
39
|
end
|
|
43
40
|
end
|
|
@@ -11,25 +11,21 @@ class Factbase::IndexedOne
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def predict(maps, _fb, _params)
|
|
14
|
-
|
|
15
|
-
key = [maps.object_id,
|
|
14
|
+
prop = @term.operands.first.to_s
|
|
15
|
+
key = [maps.object_id, prop, @term.op]
|
|
16
|
+
@idx[key] ||= { facts: [], count: 0 }
|
|
16
17
|
entry = @idx[key]
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
entry[:indexed_count] = maps_array.size
|
|
28
|
-
end
|
|
29
|
-
if maps.respond_to?(:ensure_copied!)
|
|
30
|
-
maps & entry[:facts]
|
|
31
|
-
else
|
|
32
|
-
(maps & []) | entry[:facts]
|
|
18
|
+
_feed(maps.to_a, entry, prop)
|
|
19
|
+
maps.respond_to?(:repack) ? maps.repack(entry[:facts]) : entry[:facts]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def _feed(facts, entry, prop)
|
|
25
|
+
return unless entry[:count] < facts.size
|
|
26
|
+
facts[entry[:count]..].each do |f|
|
|
27
|
+
entry[:facts] << f if !f[prop].nil? && f[prop].size == 1
|
|
33
28
|
end
|
|
29
|
+
entry[:count] = facts.size
|
|
34
30
|
end
|
|
35
31
|
end
|
|
@@ -33,12 +33,8 @@ class Factbase::IndexedUnique
|
|
|
33
33
|
idx_key = [maps.object_id, @term.op.to_s, bucket_key]
|
|
34
34
|
entry = (@idx[idx_key] ||= { buckets: {}, count: 0 })
|
|
35
35
|
feed(maps.to_a, entry, operands, bucket_key)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
maps & (bucket[:facts] || [])
|
|
39
|
-
else
|
|
40
|
-
(maps & []) | (bucket[:facts] || [])
|
|
41
|
-
end
|
|
36
|
+
matches = entry[:buckets][bucket_key][:facts]
|
|
37
|
+
maps.respond_to?(:repack) ? maps.repack(matches) : matches
|
|
42
38
|
end
|
|
43
39
|
|
|
44
40
|
private
|
|
@@ -7,10 +7,6 @@ require_relative '../factbase'
|
|
|
7
7
|
|
|
8
8
|
class Factbase::LazyTaped
|
|
9
9
|
# Decorator of Array that triggers copy-on-write.
|
|
10
|
-
# @todo #424:30min Add dedicated unit tests for LazyTapedArray class.
|
|
11
|
-
# Currently this class is tested indirectly through LazyTaped tests.
|
|
12
|
-
# We should add explicit tests for all public methods including each, [],
|
|
13
|
-
# to_a, any?, <<, and uniq! to ensure proper copy-on-write behavior.
|
|
14
10
|
class LazyTapedArray
|
|
15
11
|
# Creates a new lazy array wrapper.
|
|
16
12
|
# @param origin [Array] The original array to wrap
|
|
@@ -8,10 +8,6 @@ require_relative 'lazy_taped_array'
|
|
|
8
8
|
|
|
9
9
|
class Factbase::LazyTaped
|
|
10
10
|
# Decorator of Hash that triggers copy-on-write.
|
|
11
|
-
# @todo #424:30min Add dedicated unit tests for LazyTapedHash class.
|
|
12
|
-
# Currently this class is tested indirectly through LazyTaped tests.
|
|
13
|
-
# We should add explicit tests for all public methods including keys, map,
|
|
14
|
-
# bracket access, bracket assignment, and the copy-on-write behavior.
|
|
15
11
|
class LazyTapedHash
|
|
16
12
|
# Creates a new LazyTapedHash decorator.
|
|
17
13
|
# @param origin [Hash] The original hash being wrapped (not yet copied)
|
data/lib/factbase/version.rb
CHANGED
data/lib/fuzz.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
4
|
+
# SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
6
|
+
# Fuzzing generator for Factbase.
|
|
7
|
+
#
|
|
8
|
+
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
|
9
|
+
# Author:: Philip Belousov (belousovfilip@gmail.com)
|
|
10
|
+
# Copyright:: Copyright (c) 2024-2026 Yegor Bugayenko
|
|
11
|
+
# License:: MIT
|
|
12
|
+
class Factbase::Fuzz
|
|
13
|
+
LABELS = ['bug', 'enhancement', 'documentation', 'duplicate', 'question', 'good first issue', 'help wanted'].freeze
|
|
14
|
+
AUTHORS = ['Noah Williams', 'Mason Jones', 'Rocket Man 🚀', 'Иван Иванович', '黒さん', 'Σωκράτης', 'المنطق سي'].freeze
|
|
15
|
+
TITLES = ['Clean Code', 'Adding more elegance', 'Удаление статики', 'Re de Müller-Lyer', '纯代码', '✨✨✨'].freeze
|
|
16
|
+
MESSAGES = [
|
|
17
|
+
'Good point, thanks',
|
|
18
|
+
'This is not an object, it is a data holder!',
|
|
19
|
+
'Why is this method static? Please refactor.',
|
|
20
|
+
'I dont like this name. It is not a noun.',
|
|
21
|
+
'Please add a unit test for this change.',
|
|
22
|
+
'NULL is evil, never use it here.',
|
|
23
|
+
'Pure elegance! Very object-oriented.',
|
|
24
|
+
'Finally, a clean decorator! Good job.',
|
|
25
|
+
'Exquisite! No getters, no setters, just behavior.',
|
|
26
|
+
'This PR makes me happy. It is very elegant.',
|
|
27
|
+
'Исправь кодировку!',
|
|
28
|
+
'デザインが悪い (Poor design)',
|
|
29
|
+
'C’est magnifique! ',
|
|
30
|
+
'Λογική χωρίς ',
|
|
31
|
+
'المنطق سيء للغاية',
|
|
32
|
+
'Este código no es elegante',
|
|
33
|
+
'❤️❤️❤️'
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
def initialize
|
|
37
|
+
@next_num = 0
|
|
38
|
+
@max_comments = 10
|
|
39
|
+
raise 'Not enough messages for fuzzing' if MESSAGES.size < @max_comments
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.make(count = 1000)
|
|
43
|
+
raise "Count must be positive: #{count}" if count.negative?
|
|
44
|
+
fb = Factbase.new
|
|
45
|
+
Factbase::Fuzz.new.feed(fb, count)
|
|
46
|
+
fb
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def feed(fb, count = 1)
|
|
50
|
+
raise "Count must be positive: #{count}" if count.negative?
|
|
51
|
+
count.times do
|
|
52
|
+
pull_request(fb, @next_num += 1)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def pull_request(fb, idx)
|
|
59
|
+
f = fb.insert
|
|
60
|
+
f.number = idx
|
|
61
|
+
f.ready = rand(2)
|
|
62
|
+
f.cost = rand(1..32)
|
|
63
|
+
f.kind = 'pull_request'
|
|
64
|
+
f.author = AUTHORS.sample
|
|
65
|
+
f.diff_size = rand(10..5000)
|
|
66
|
+
f.state = %w[open merged closed].sample
|
|
67
|
+
f.title = "#{TITLES.sample} in #{LABELS.sample}"
|
|
68
|
+
f.test_coverage = rand(0.0..100.0).round(2)
|
|
69
|
+
f.created_at = Time.now - rand(1..(60 * 60 * 24 * 180))
|
|
70
|
+
MESSAGES.sample(idx % (@max_comments + 1)).each do |comment|
|
|
71
|
+
f.comments = comment
|
|
72
|
+
end
|
|
73
|
+
f
|
|
74
|
+
end
|
|
75
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: factbase
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.19.
|
|
4
|
+
version: 0.19.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yegor Bugayenko
|
|
@@ -271,6 +271,7 @@ files:
|
|
|
271
271
|
- lib/factbase/to_xml.rb
|
|
272
272
|
- lib/factbase/to_yaml.rb
|
|
273
273
|
- lib/factbase/version.rb
|
|
274
|
+
- lib/fuzz.rb
|
|
274
275
|
homepage: https://github.com/yegor256/factbase.rb
|
|
275
276
|
licenses:
|
|
276
277
|
- MIT
|