typed_dag 1.0.1 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +20 -5
- data/lib/typed_dag/configuration.rb +4 -0
- data/lib/typed_dag/edge/closure_maintenance.rb +16 -2
- data/lib/typed_dag/sql.rb +1 -0
- data/lib/typed_dag/sql/add_closure.rb +36 -5
- data/lib/typed_dag/sql/delete_zero_count.rb +22 -0
- data/lib/typed_dag/sql/get_circular.rb +4 -2
- data/lib/typed_dag/sql/helper.rb +15 -1
- data/lib/typed_dag/sql/insert_closure_of_depth.rb +44 -5
- data/lib/typed_dag/sql/insert_reflexive.rb +3 -2
- data/lib/typed_dag/sql/relation_access.rb +1 -0
- data/lib/typed_dag/sql/select_closure.rb +20 -9
- data/lib/typed_dag/sql/truncate_closure.rb +23 -128
- data/lib/typed_dag/version.rb +1 -1
- data/spec/support/helpers.rb +10 -1
- data/spec/test_app/config/database.yml +1 -3
- data/spec/test_app/db/migrate/20170831093433_create_nodes_and_endges.rb +3 -4
- data/spec/test_app/db/schema.rb +7 -6
- data/spec/test_app/log/test.log +0 -0
- data/spec/typed_dag/edge_spec.rb +33 -33
- data/spec/typed_dag/node_spec.rb +43 -50
- data/spec/typed_dag/sql/add_closure_spec.rb +103 -37
- data/spec/typed_dag/sql/truncate_closure_spec.rb +93 -121
- metadata +3 -2
data/lib/typed_dag/version.rb
CHANGED
data/spec/support/helpers.rb
CHANGED
@@ -30,9 +30,18 @@ module TypedDag
|
|
30
30
|
[r.from.text,
|
31
31
|
r.to.text,
|
32
32
|
r.hierarchy,
|
33
|
-
r.invalidate
|
33
|
+
r.invalidate,
|
34
|
+
r.count]
|
34
35
|
end
|
35
36
|
end
|
37
|
+
|
38
|
+
def mysql_db?
|
39
|
+
ActiveRecord::Base.connection.adapter_name == 'Mysql2'
|
40
|
+
end
|
41
|
+
|
42
|
+
def harmonize_string(string)
|
43
|
+
string.squish.gsub('( ', '(').gsub(' )', ')')
|
44
|
+
end
|
36
45
|
end
|
37
46
|
end
|
38
47
|
end
|
@@ -5,12 +5,10 @@
|
|
5
5
|
# gem 'sqlite3'
|
6
6
|
#
|
7
7
|
default: &default
|
8
|
-
|
9
|
-
adapter: mysql2
|
8
|
+
adapter: postgresql
|
10
9
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
11
10
|
database: typed_dag_test
|
12
11
|
timeout: 5000
|
13
|
-
username: root
|
14
12
|
|
15
13
|
development:
|
16
14
|
<<: *default
|
@@ -10,11 +10,10 @@ class CreateNodesAndEndges < ActiveRecord::Migration[5.1]
|
|
10
10
|
|
11
11
|
t.column :hierarchy, :integer, null: false, default: 0
|
12
12
|
t.column :invalidate, :integer, null: false, default: 0
|
13
|
-
t.column :mention, :integer, null: false, default: 0
|
14
13
|
|
15
|
-
t.
|
16
|
-
|
17
|
-
t.index :
|
14
|
+
t.column :count, :integer, null: false, default: 1
|
15
|
+
|
16
|
+
t.index %i(ancestor_id descendant_id hierarchy invalidate), unique: true, name: 'unique_constraint'
|
18
17
|
end
|
19
18
|
|
20
19
|
add_foreign_key :relations, :messages, column: :ancestor_id
|
data/spec/test_app/db/schema.rb
CHANGED
@@ -12,21 +12,22 @@
|
|
12
12
|
|
13
13
|
ActiveRecord::Schema.define(version: 20170831093433) do
|
14
14
|
|
15
|
-
|
15
|
+
# These are extensions that must be enabled in order to support this database
|
16
|
+
enable_extension "plpgsql"
|
17
|
+
|
18
|
+
create_table "messages", force: :cascade do |t|
|
16
19
|
t.string "text"
|
17
20
|
end
|
18
21
|
|
19
|
-
create_table "relations", force: :cascade
|
22
|
+
create_table "relations", force: :cascade do |t|
|
20
23
|
t.bigint "ancestor_id", null: false
|
21
24
|
t.bigint "descendant_id", null: false
|
22
25
|
t.integer "hierarchy", default: 0, null: false
|
23
26
|
t.integer "invalidate", default: 0, null: false
|
24
|
-
t.integer "
|
27
|
+
t.integer "count", default: 0, null: false
|
28
|
+
t.index ["ancestor_id", "descendant_id", "hierarchy", "invalidate"], name: "unique_constraint", unique: true
|
25
29
|
t.index ["ancestor_id"], name: "index_relations_on_ancestor_id"
|
26
30
|
t.index ["descendant_id"], name: "index_relations_on_descendant_id"
|
27
|
-
t.index ["hierarchy"], name: "index_relations_on_hierarchy"
|
28
|
-
t.index ["invalidate"], name: "index_relations_on_invalidate"
|
29
|
-
t.index ["mention"], name: "index_relations_on_mention"
|
30
31
|
end
|
31
32
|
|
32
33
|
add_foreign_key "relations", "messages", column: "ancestor_id"
|
data/spec/test_app/log/test.log
CHANGED
Binary file
|
data/spec/typed_dag/edge_spec.rb
CHANGED
@@ -347,15 +347,15 @@ RSpec.describe 'Edge' do
|
|
347
347
|
it 'expects the transitive relations to be updated' do
|
348
348
|
attribute_array = to_attribute_array Relation.non_reflexive
|
349
349
|
expect(attribute_array)
|
350
|
-
.to match_array([['A', 'B', 0, 1],
|
351
|
-
['A', 'C', 1, 1],
|
352
|
-
['A', 'D', 1, 2],
|
353
|
-
['A', 'E', 2, 1],
|
354
|
-
['B', 'C', 1, 0],
|
355
|
-
['B', 'D', 1, 1],
|
356
|
-
['B', 'E', 2, 0],
|
357
|
-
['C', 'D', 0, 1],
|
358
|
-
['C', 'E', 1, 0]])
|
350
|
+
.to match_array([['A', 'B', 0, 1, 1],
|
351
|
+
['A', 'C', 1, 1, 1],
|
352
|
+
['A', 'D', 1, 2, 1],
|
353
|
+
['A', 'E', 2, 1, 1],
|
354
|
+
['B', 'C', 1, 0, 1],
|
355
|
+
['B', 'D', 1, 1, 1],
|
356
|
+
['B', 'E', 2, 0, 1],
|
357
|
+
['C', 'D', 0, 1, 1],
|
358
|
+
['C', 'E', 1, 0, 1]])
|
359
359
|
end
|
360
360
|
end
|
361
361
|
end
|
@@ -415,15 +415,15 @@ RSpec.describe 'Edge' do
|
|
415
415
|
it 'expects the transitive relations to be updated' do
|
416
416
|
attribute_array = to_attribute_array Relation.non_reflexive
|
417
417
|
expect(attribute_array)
|
418
|
-
.to match_array([['F', 'B', 0, 1],
|
419
|
-
['F', 'C', 0, 2],
|
420
|
-
['F', 'D', 0, 3],
|
421
|
-
['F', 'E', 1, 2],
|
422
|
-
['B', 'C', 0, 1],
|
423
|
-
['B', 'D', 0, 2],
|
424
|
-
['B', 'E', 1, 1],
|
425
|
-
['C', 'D', 0, 1],
|
426
|
-
['C', 'E', 1, 0]])
|
418
|
+
.to match_array([['F', 'B', 0, 1, 1],
|
419
|
+
['F', 'C', 0, 2, 1],
|
420
|
+
['F', 'D', 0, 3, 1],
|
421
|
+
['F', 'E', 1, 2, 1],
|
422
|
+
['B', 'C', 0, 1, 1],
|
423
|
+
['B', 'D', 0, 2, 1],
|
424
|
+
['B', 'E', 1, 1, 1],
|
425
|
+
['C', 'D', 0, 1, 1],
|
426
|
+
['C', 'E', 1, 0, 1]])
|
427
427
|
end
|
428
428
|
end
|
429
429
|
end
|
@@ -483,15 +483,15 @@ RSpec.describe 'Edge' do
|
|
483
483
|
it 'expects the transitive relations to be updated' do
|
484
484
|
attribute_array = to_attribute_array Relation.non_reflexive
|
485
485
|
expect(attribute_array)
|
486
|
-
.to match_array([['A', 'B', 0, 1],
|
487
|
-
['A', 'C', 0, 2],
|
488
|
-
['A', 'F', 0, 3],
|
489
|
-
['A', 'E', 1, 2],
|
490
|
-
['B', 'C', 0, 1],
|
491
|
-
['B', 'F', 0, 2],
|
492
|
-
['B', 'E', 1, 1],
|
493
|
-
['C', 'F', 0, 1],
|
494
|
-
['C', 'E', 1, 0]])
|
486
|
+
.to match_array([['A', 'B', 0, 1, 1],
|
487
|
+
['A', 'C', 0, 2, 1],
|
488
|
+
['A', 'F', 0, 3, 1],
|
489
|
+
['A', 'E', 1, 2, 1],
|
490
|
+
['B', 'C', 0, 1, 1],
|
491
|
+
['B', 'F', 0, 2, 1],
|
492
|
+
['B', 'E', 1, 1, 1],
|
493
|
+
['C', 'F', 0, 1, 1],
|
494
|
+
['C', 'E', 1, 0, 1]])
|
495
495
|
end
|
496
496
|
end
|
497
497
|
end
|
@@ -551,12 +551,12 @@ RSpec.describe 'Edge' do
|
|
551
551
|
it 'expects the transitive relations to be updated' do
|
552
552
|
attribute_array = to_attribute_array Relation.non_reflexive
|
553
553
|
expect(attribute_array)
|
554
|
-
.to match_array([['A', 'B', 0, 1],
|
555
|
-
['F', 'C', 0, 1],
|
556
|
-
['C', 'D', 0, 1],
|
557
|
-
['C', 'E', 1, 0],
|
558
|
-
['F', 'D', 0, 2],
|
559
|
-
['F', 'E', 1, 1]])
|
554
|
+
.to match_array([['A', 'B', 0, 1, 1],
|
555
|
+
['F', 'C', 0, 1, 1],
|
556
|
+
['C', 'D', 0, 1, 1],
|
557
|
+
['C', 'E', 1, 0, 1],
|
558
|
+
['F', 'D', 0, 2, 1],
|
559
|
+
['F', 'E', 1, 1, 1]])
|
560
560
|
end
|
561
561
|
end
|
562
562
|
end
|
data/spec/typed_dag/node_spec.rb
CHANGED
@@ -1953,6 +1953,7 @@ RSpec.describe TypedDag::Node, 'included in Message' do
|
|
1953
1953
|
end
|
1954
1954
|
|
1955
1955
|
describe '#rebuild_dag!' do
|
1956
|
+
|
1956
1957
|
description = <<-'WITH'
|
1957
1958
|
|
1958
1959
|
DAG (messed up transitive closures):
|
@@ -1998,44 +1999,32 @@ RSpec.describe TypedDag::Node, 'included in Message' do
|
|
1998
1999
|
Message.rebuild_dag!
|
1999
2000
|
end
|
2000
2001
|
|
2001
|
-
it '
|
2002
|
-
|
2003
|
-
|
2004
|
-
|
2005
|
-
|
2006
|
-
|
2007
|
-
|
2008
|
-
|
2009
|
-
|
2010
|
-
|
2011
|
-
|
2012
|
-
|
2013
|
-
|
2014
|
-
|
2015
|
-
|
2016
|
-
|
2017
|
-
|
2018
|
-
|
2019
|
-
|
2020
|
-
|
2021
|
-
|
2022
|
-
|
2023
|
-
|
2024
|
-
|
2025
|
-
|
2026
|
-
|
2027
|
-
expect(a.all_invalidates_of_depth(3))
|
2028
|
-
.to match_array([g, f])
|
2029
|
-
end
|
2030
|
-
|
2031
|
-
it '#descendants_of_depth(4) for A is empty' do
|
2032
|
-
expect(a.descendants_of_depth(4))
|
2033
|
-
.to be_empty
|
2034
|
-
end
|
2035
|
-
|
2036
|
-
it '#all_invalidates_of_depth(4) for A is G' do
|
2037
|
-
expect(a.all_invalidates_of_depth(4))
|
2038
|
-
.to match_array([g])
|
2002
|
+
it 'rebuilds the closure for A correctly' do
|
2003
|
+
attribute_array = to_attribute_array a.relations_to
|
2004
|
+
expect(attribute_array)
|
2005
|
+
.to match_array([['A', 'A', 0, 0, 1],
|
2006
|
+
['A', 'B', 0, 1, 1],
|
2007
|
+
['A', 'C', 0, 1, 1],
|
2008
|
+
['A', 'C', 0, 2, 2],
|
2009
|
+
['A', 'D', 0, 1, 1],
|
2010
|
+
['A', 'E', 0, 2, 1],
|
2011
|
+
['A', 'F', 0, 2, 1],
|
2012
|
+
['A', 'F', 0, 3, 3],
|
2013
|
+
['A', 'G', 0, 3, 1],
|
2014
|
+
['A', 'G', 0, 4, 3],
|
2015
|
+
['A', 'G', 3, 0, 1],
|
2016
|
+
['A', 'H', 1, 0, 1],
|
2017
|
+
['A', 'I', 2, 0, 1]])
|
2018
|
+
end
|
2019
|
+
|
2020
|
+
it 'rebuilds the closure for B correctly' do
|
2021
|
+
attribute_array = to_attribute_array b.relations_to
|
2022
|
+
expect(attribute_array)
|
2023
|
+
.to match_array([['B', 'B', 0, 0, 1],
|
2024
|
+
['B', 'C', 0, 1, 1],
|
2025
|
+
['B', 'E', 0, 1, 1],
|
2026
|
+
['B', 'F', 0, 2, 2],
|
2027
|
+
['B', 'G', 0, 3, 2]])
|
2039
2028
|
end
|
2040
2029
|
|
2041
2030
|
it 'rebuilds all reflexive relations' do
|
@@ -2096,32 +2085,36 @@ RSpec.describe TypedDag::Node, 'included in Message' do
|
|
2096
2085
|
description = <<-'WITH'
|
2097
2086
|
|
2098
2087
|
DAG (invalid) before:
|
2099
|
-
|
2100
|
-
|
2101
|
-
|
2102
|
-
|
2103
|
-
|
2088
|
+
C+-h--B
|
2089
|
+
|+ +
|
2090
|
+
| \ |
|
2091
|
+
h i h
|
2092
|
+
| \ |
|
2093
|
+
+ \|
|
2094
|
+
D--h-+A
|
2104
2095
|
WITH
|
2105
2096
|
|
2106
2097
|
context description do
|
2107
2098
|
let!(:a) { Message.create text: 'A' }
|
2108
2099
|
let!(:b) { Message.create text: 'B', parent: a }
|
2109
2100
|
let!(:c) { Message.create text: 'C', parent: b }
|
2110
|
-
let!(:
|
2101
|
+
let!(:d) { Message.create text: 'D', parent: c }
|
2102
|
+
let!(:invalid_relation1) do
|
2111
2103
|
Relation
|
2112
|
-
.new(from:
|
2113
|
-
to:
|
2104
|
+
.new(from: a,
|
2105
|
+
to: c,
|
2114
2106
|
invalidate: 1)
|
2115
2107
|
.save(validate: false)
|
2116
|
-
|
2108
|
+
end
|
2109
|
+
let!(:invalid_relation2) do
|
2117
2110
|
Relation
|
2118
|
-
.new(from:
|
2111
|
+
.new(from: d,
|
2119
2112
|
to: a,
|
2120
|
-
|
2113
|
+
hierarchy: 1)
|
2121
2114
|
.save(validate: false)
|
2122
2115
|
end
|
2123
2116
|
|
2124
|
-
it 'throws an error if more
|
2117
|
+
it 'throws an error if more attempts than specified are made' do
|
2125
2118
|
expect { Message.rebuild_dag!(1) }
|
2126
2119
|
.to raise_error(TypedDag::RebuildDag::AttemptsExceededError)
|
2127
2120
|
end
|
@@ -1,47 +1,113 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe TypedDag::Sql::AddClosure do
|
4
|
+
include TypedDag::Specs::Helpers
|
5
|
+
|
4
6
|
describe '.sql' do
|
5
7
|
let(:relation) { Relation.new id: 11, ancestor_id: 4, descendant_id: 6, invalidate: 1 }
|
6
8
|
|
7
|
-
def harmonize_string(string)
|
8
|
-
string.squish.gsub('( ', '(').gsub(' )', ')').gsub(' , ', ', ')
|
9
|
-
end
|
10
|
-
|
11
9
|
it 'produces the correct sql' do
|
12
|
-
expected_sql =
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
10
|
+
expected_sql = if mysql_db?
|
11
|
+
<<-SQL
|
12
|
+
INSERT INTO
|
13
|
+
|
14
|
+
relations
|
15
|
+
|
16
|
+
(ancestor_id,
|
17
|
+
descendant_id,
|
18
|
+
hierarchy,
|
19
|
+
invalidate,
|
20
|
+
count)
|
21
|
+
SELECT
|
22
|
+
ancestor_id,
|
23
|
+
descendant_id,
|
24
|
+
hierarchy,
|
25
|
+
invalidate,
|
26
|
+
SUM(count) AS count
|
27
|
+
FROM
|
28
|
+
(SELECT
|
29
|
+
r1.ancestor_id,
|
30
|
+
r2.descendant_id,
|
31
|
+
CASE
|
32
|
+
WHEN r1.descendant_id = r2.ancestor_id AND (r1.hierarchy > 0 OR r2.hierarchy > 0)
|
33
|
+
THEN r1.hierarchy + r2.hierarchy
|
34
|
+
WHEN r1.descendant_id != r2.ancestor_id
|
35
|
+
THEN r1.hierarchy + r2.hierarchy + 0
|
36
|
+
ELSE 0
|
37
|
+
END AS hierarchy,
|
38
|
+
CASE
|
39
|
+
WHEN r1.descendant_id = r2.ancestor_id AND (r1.invalidate > 0 OR r2.invalidate > 0)
|
40
|
+
THEN r1.invalidate + r2.invalidate
|
41
|
+
WHEN r1.descendant_id != r2.ancestor_id
|
42
|
+
THEN r1.invalidate + r2.invalidate + 1
|
43
|
+
ELSE 0
|
44
|
+
END AS invalidate,
|
45
|
+
r1.count * r2.count AS count
|
46
|
+
FROM
|
47
|
+
relations r1
|
48
|
+
JOIN
|
49
|
+
relations r2
|
50
|
+
ON
|
51
|
+
(r1.descendant_id = 4 AND r2.ancestor_id = 6 AND NOT (r1.ancestor_id = 4 AND r2.descendant_id = 6))) unique_rows
|
52
|
+
GROUP BY
|
53
|
+
ancestor_id,
|
54
|
+
descendant_id,
|
55
|
+
hierarchy,
|
56
|
+
invalidate
|
57
|
+
ON DUPLICATE KEY
|
58
|
+
UPDATE count = relations.count + VALUES(count)
|
59
|
+
SQL
|
60
|
+
else
|
61
|
+
<<-SQL
|
62
|
+
INSERT INTO
|
63
|
+
|
64
|
+
relations
|
65
|
+
|
66
|
+
(ancestor_id,
|
67
|
+
descendant_id,
|
68
|
+
hierarchy,
|
69
|
+
invalidate,
|
70
|
+
count)
|
71
|
+
SELECT
|
72
|
+
ancestor_id,
|
73
|
+
descendant_id,
|
74
|
+
hierarchy,
|
75
|
+
invalidate,
|
76
|
+
SUM(count) AS count
|
77
|
+
FROM
|
78
|
+
(SELECT
|
79
|
+
r1.ancestor_id,
|
80
|
+
r2.descendant_id,
|
81
|
+
CASE
|
82
|
+
WHEN r1.descendant_id = r2.ancestor_id AND (r1.hierarchy > 0 OR r2.hierarchy > 0)
|
83
|
+
THEN r1.hierarchy + r2.hierarchy
|
84
|
+
WHEN r1.descendant_id != r2.ancestor_id
|
85
|
+
THEN r1.hierarchy + r2.hierarchy + 0
|
86
|
+
ELSE 0
|
87
|
+
END AS hierarchy,
|
88
|
+
CASE
|
89
|
+
WHEN r1.descendant_id = r2.ancestor_id AND (r1.invalidate > 0 OR r2.invalidate > 0)
|
90
|
+
THEN r1.invalidate + r2.invalidate
|
91
|
+
WHEN r1.descendant_id != r2.ancestor_id
|
92
|
+
THEN r1.invalidate + r2.invalidate + 1
|
93
|
+
ELSE 0
|
94
|
+
END AS invalidate,
|
95
|
+
r1.count * r2.count AS count
|
96
|
+
FROM
|
97
|
+
relations r1
|
98
|
+
JOIN
|
99
|
+
relations r2
|
100
|
+
ON
|
101
|
+
(r1.descendant_id = 4 AND r2.ancestor_id = 6 AND NOT (r1.ancestor_id = 4 AND r2.descendant_id = 6))) unique_rows
|
102
|
+
GROUP BY
|
103
|
+
ancestor_id,
|
104
|
+
descendant_id,
|
105
|
+
hierarchy,
|
106
|
+
invalidate
|
107
|
+
ON CONFLICT (ancestor_id, descendant_id, hierarchy, invalidate)
|
108
|
+
DO UPDATE SET count = relations.count + EXCLUDED.count
|
109
|
+
SQL
|
110
|
+
end
|
45
111
|
|
46
112
|
expect(harmonize_string(described_class.sql(relation)))
|
47
113
|
.to eql harmonize_string(expected_sql)
|