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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e47a0669cc0a0487993711a18e1a37df86b809a5
4
- data.tar.gz: 11f39e3b2f2a189bb28d7f93d6ff4f3b309d6584
3
+ metadata.gz: a7e31c0c6a123f535ad41b59c8aabaac96129a70
4
+ data.tar.gz: 78ee741a3bf16ea7014f2cf69a8a3f5903c9fab1
5
5
  SHA512:
6
- metadata.gz: c11feb64fa4401c79b9285c2629b03d1e7558c6e8ef6f1202b258726beb9b464c9d406ee0bf23a14ed713260f2cbf49e56c630ade313e38fd4f39f2a06e4b57c
7
- data.tar.gz: 5662fb1a45f66349acc32697f56b09b3e292b9096c17b43ecd94c068203d164e62e569298120cad2e320a312de1f6a1b13da45f5fac1f4389f602d11446998d2
6
+ metadata.gz: 7941c61304bd469a8bff392405f09719f7246e6817e991cf835f7435d0570d8b832b37edaf87186ab359b57a20a0f5a5e949ffb65f87149b6745b4ab21be3b3c
7
+ data.tar.gz: 206c14332f5e58353063cf45ac0e8d279f632f017a14bbd1c96a0a2f4ab8e9cf3424f33cc1b2378c13eaf9363259caf2a8b26e920f37ec4a7eea412e24f2e383
data/README.md CHANGED
@@ -35,6 +35,10 @@ Again using the forum application as an example, one would have messages in a hi
35
35
 
36
36
  Please note that the name of the scopes (e.g. `children`, `hierarchy_roots` and `referenced`) and constants (`Message`) have to be configured.
37
37
 
38
+ ## Requirements
39
+
40
+ * Rails >= 5.0
41
+ * MySQL or PostgreSQL (>= 9.5) as `UPSERT` statements are used
38
42
 
39
43
  ## Installation
40
44
 
@@ -62,10 +66,12 @@ To avoid having to configure TypedDag twice, in the node and in the edge AR mode
62
66
 
63
67
  ```
64
68
  # /config/initializers/typed_dag.rb
69
+ # configuration for Relation/Message
65
70
  TypedDag::Configuration.set edge_class_name: 'Relation',
66
71
  node_class_name: 'Message',
67
72
  from_column: 'ancestor_id',
68
73
  to_column: 'descendant_id',
74
+ count_column: 'amount',
69
75
  types: { hierarchy: { from: { name: :parent, limit: 1 },
70
76
  to: :children,
71
77
  all_from: :ancestors,
@@ -75,6 +81,8 @@ To avoid having to configure TypedDag twice, in the node and in the edge AR mode
75
81
  all_from: :all_invalidated_by,
76
82
  all_to: :all_invalidates } }
77
83
 
84
+
85
+ # unrelated configuration for Edge/Node
78
86
  TypedDag::Configuration.set edge_class_name: 'Edge'
79
87
  node_class_name: 'Node',
80
88
  types: { edge: { from: :edges_from,
@@ -93,6 +101,7 @@ The following options exist:
93
101
  * `node_class_name`: The name of the AR model whose instances serve as the nodes of the dag
94
102
  * `from_column` (default `from_id`): The name of the column in the edges AR model that refer to the node the edge starts from
95
103
  * `to_column` (default `to_id`): The name of the column in the edges AR model that refer to the node the edge ends in
104
+ * `count_column` (default `count`): The name of the column in the edges AR model that keeps track of the number of identical edges between from and to
96
105
  * `types`: The hash of type configurations. The key of each configuration will need to be present as a column in the edge's DB table.
97
106
  * `from`: The AR association's name for nodes having a relation which end in the current node, have the type specified by the key and are not transitive (have only one hop). Only for `from` can one specify a limit to the number of relations a node can have. Doing this turns the DAG into a tree which is usefull for hierarchies. If a limit needs to be specified, the configuration has to be provided as `{ name: [association's name], limit: 1 }`. If no limit is given, the association's name can be provided as a symbol.
98
107
  * `to`: The AR association's name for nodes having a relation which start from the current node, have the type specified by the key and are not transitive (have only one hop)
@@ -156,7 +165,8 @@ The edge's table needs to be created containing at least the following columns (
156
165
 
157
166
  * `from_id`: A reference to the node the edge starts from.
158
167
  * `to_id`: A reference to the node the edge ends in.
159
- * `[key]`: A column of type integer for every type the dag is to support
168
+ * `count`: The counter column for the number of similar (transitive) edges between to and from.
169
+ * `[key]`: A column of type integer for every type the dag is to support.
160
170
 
161
171
  A migration to create such a table could look like this:
162
172
 
@@ -166,6 +176,8 @@ A migration to create such a table could look like this:
166
176
  t.references :from, null: false
167
177
  t.references :to, null: false
168
178
 
179
+ t.column :count, :integer, null: false, default: 0
180
+
169
181
  t.column :hierarchy, :integer, null: false, default: 0
170
182
  t.column :reference, :integer, null: false, default: 0
171
183
  end
@@ -173,17 +185,20 @@ A migration to create such a table could look like this:
173
185
  add_foreign_key :edges, :nodes, column: :from_id
174
186
  add_foreign_key :edges, :nodes, column: :to_id
175
187
 
188
+ # give the index a custom name to avoid running into length limitation when having a couple of columns
189
+ # in the index
176
190
  add_index :edges, [:hierarchy, :reference], name: `index_on_type_columns`
191
+ add_index :edges, :count, where: 'count = 0'
177
192
  end
178
193
  ```
179
194
 
180
- the table can also have additional columns. They will not interfere with TypedDag.
195
+ The table can also have additional columns. They will not interfere with TypedDag.
181
196
 
182
- Which indices to use will depend on the data added but having an index over all the type columns is a good start.
197
+ Which indices to use will depend on the data added but having an index over all the type columns is a good start. A partial index on count speeds up deleting edges while also being very lightweight to maintain.
183
198
 
184
- There are no requirements for the node's table.
199
+ There are no requirements on the node's table.
185
200
 
186
- When migrating from an different library, the details of course depend on the library used. If it is one of the many having a `parent_id` column on the node table, one would first have to create the edge table as outlined above and then add a SQL statement like this:
201
+ When migrating from a different library, the details of course depend on the library used. If it is one of the many having a `parent_id` column on the node table, one would first have to create the edge table as outlined above and then add a SQL statement like this:
187
202
 
188
203
  ```
189
204
  ActiveRecord::Base.connection.execute <<-SQL
@@ -56,6 +56,10 @@ class TypedDag::Configuration
56
56
  config[:to_column] || 'to_id'
57
57
  end
58
58
 
59
+ def count_column
60
+ config[:count_column] || 'count'
61
+ end
62
+
59
63
  def types
60
64
  config[:types] || default_types
61
65
  end
@@ -3,12 +3,17 @@ module TypedDag::Edge
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
+ before_create :set_count
6
7
  after_create :add_closures
7
8
  after_update :alter_closure
8
9
  after_destroy :truncate_closures
9
10
 
10
11
  private
11
12
 
13
+ def set_count
14
+ send("#{_dag_options.count_column}=", 1) if send(_dag_options.count_column).zero?
15
+ end
16
+
12
17
  def add_closures
13
18
  return unless direct?
14
19
 
@@ -20,7 +25,7 @@ module TypedDag::Edge
20
25
  # However, #persisted? will be false for destroyed records.
21
26
  return unless direct? && !new_record?
22
27
 
23
- self.class.connection.execute truncate_dag_closure_sql(self)
28
+ update_and_delete_closure(self)
24
29
  end
25
30
 
26
31
  def truncate_closures_with_former_values
@@ -35,7 +40,7 @@ module TypedDag::Edge
35
40
 
36
41
  former_values_relation.attributes = changes
37
42
 
38
- self.class.connection.execute truncate_dag_closure_sql(former_values_relation)
43
+ update_and_delete_closure(former_values_relation)
39
44
  end
40
45
 
41
46
  def alter_closure
@@ -45,6 +50,11 @@ module TypedDag::Edge
45
50
  add_closures
46
51
  end
47
52
 
53
+ def update_and_delete_closure(relation)
54
+ self.class.connection.execute truncate_dag_closure_sql(relation)
55
+ self.class.connection.execute delete_zero_count_sql(relation)
56
+ end
57
+
48
58
  def add_dag_closure_sql
49
59
  TypedDag::Sql::AddClosure.sql(self)
50
60
  end
@@ -53,6 +63,10 @@ module TypedDag::Edge
53
63
  TypedDag::Sql::TruncateClosure.sql(relation)
54
64
  end
55
65
 
66
+ def delete_zero_count_sql(relation)
67
+ TypedDag::Sql::DeleteZeroCount.sql(relation)
68
+ end
69
+
56
70
  def from_id_value
57
71
  send(_dag_options.from_column)
58
72
  end
data/lib/typed_dag/sql.rb CHANGED
@@ -6,5 +6,6 @@ module TypedDag
6
6
  require 'typed_dag/sql/get_circular'
7
7
  require 'typed_dag/sql/remove_invalid_relation'
8
8
  require 'typed_dag/sql/insert_reflexive'
9
+ require 'typed_dag/sql/delete_zero_count'
9
10
  end
10
11
  end
@@ -15,18 +15,49 @@ module TypedDag::Sql::AddClosure
15
15
 
16
16
  def sql
17
17
  <<-SQL
18
- INSERT INTO #{table_name}
19
- (#{from_column},
20
- #{to_column},
21
- #{type_select_list})
22
- #{closure_select}
18
+ #{insert_sql}
19
+ #{on_duplicate}
23
20
  SQL
24
21
  end
25
22
 
26
23
  private
27
24
 
25
+ def on_duplicate
26
+ if helper.mysql_db?
27
+ on_duplicate_mysql
28
+ else
29
+ on_duplicate_postgresql
30
+ end
31
+ end
32
+
33
+ def on_duplicate_mysql
34
+ <<-SQL
35
+ ON DUPLICATE KEY
36
+ UPDATE #{count_column} = #{table_name}.#{count_column} + VALUES(#{count_column})
37
+ SQL
38
+ end
39
+
40
+ def on_duplicate_postgresql
41
+ <<-SQL
42
+ ON CONFLICT (#{column_list})
43
+ DO UPDATE SET #{count_column} = #{table_name}.#{count_column} + EXCLUDED.#{count_column}
44
+ SQL
45
+ end
46
+
47
+ def insert_sql
48
+ <<-SQL
49
+ INSERT INTO #{table_name}
50
+ (#{column_list}, #{count_column})
51
+ #{closure_select}
52
+ SQL
53
+ end
54
+
28
55
  def closure_select
29
56
  TypedDag::Sql::SelectClosure.sql(relation)
30
57
  end
58
+
59
+ def column_list
60
+ "#{from_column}, #{to_column}, #{type_select_list}"
61
+ end
31
62
  end
32
63
  end
@@ -0,0 +1,22 @@
1
+ require 'typed_dag/sql/helper'
2
+
3
+ module TypedDag::Sql::DeleteZeroCount
4
+ def self.sql(relation)
5
+ Sql.new(relation).sql
6
+ end
7
+
8
+ class Sql
9
+ include TypedDag::Sql::RelationAccess
10
+
11
+ def initialize(relation)
12
+ self.relation = relation
13
+ end
14
+
15
+ def sql
16
+ <<-SQL
17
+ DELETE FROM #{table_name}
18
+ WHERE #{count_column} = 0
19
+ SQL
20
+ end
21
+ end
22
+ end
@@ -13,8 +13,10 @@ module TypedDag::Sql::GetCircular
13
13
  def sql(depth)
14
14
  <<-SQL
15
15
  SELECT
16
- r1.#{helper.from_column},
17
- r1.#{helper.to_column}
16
+ r1.#{helper.from_column} AS r1_from_column,
17
+ r1.#{helper.to_column} AS r1_to_column,
18
+ r2.#{helper.from_column} AS r2_from_column,
19
+ r2.#{helper.to_column} AS r2_to_column
18
20
  FROM #{helper.table_name} r1
19
21
  JOIN #{helper.table_name} r2
20
22
  ON #{join_condition(depth)}
@@ -23,13 +23,23 @@ class TypedDag::Sql::Helper
23
23
  configuration.type_columns
24
24
  end
25
25
 
26
+ def count_column
27
+ configuration.count_column
28
+ end
29
+
26
30
  def type_select_list
27
31
  type_columns.join(', ')
28
32
  end
29
33
 
30
34
  def type_select_summed_columns(prefix1, prefix2)
31
35
  type_columns
32
- .map { |column| "#{prefix1}.#{column} + #{prefix2}.#{column}" }
36
+ .map { |column| "#{prefix1}.#{column} + #{prefix2}.#{column} " }
37
+ .join(', ')
38
+ end
39
+
40
+ def type_select_summed_columns_aliased(prefix1, prefix2)
41
+ type_columns
42
+ .map { |column| "(#{prefix1}.#{column} + #{prefix2}.#{column}) #{column}" }
33
43
  .join(', ')
34
44
  end
35
45
 
@@ -45,6 +55,10 @@ class TypedDag::Sql::Helper
45
55
  type_columns.map { |column| "#{prefix}#{column} = 1" }.join(' XOR ')
46
56
  end
47
57
 
58
+ def mysql_db?
59
+ ActiveRecord::Base.connection.adapter_name == 'Mysql2'
60
+ end
61
+
48
62
  private
49
63
 
50
64
  attr_accessor :configuration
@@ -11,19 +11,50 @@ module TypedDag::Sql::InsertClosureOfDepth
11
11
  end
12
12
 
13
13
  def sql(depth)
14
+ if helper.mysql_db?
15
+ sql_mysql(depth)
16
+ else
17
+ sql_postgresql(depth)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def sql_mysql(depth)
24
+ <<-SQL
25
+ #{insert_sql(depth)}
26
+ ON DUPLICATE KEY
27
+ UPDATE #{helper.table_name}.#{helper.count_column} = #{helper.table_name}.#{helper.count_column} + VALUES(#{helper.count_column})
28
+ SQL
29
+ end
30
+
31
+ def sql_postgresql(depth)
32
+ <<-SQL
33
+ #{insert_sql(depth)}
34
+ ON CONFLICT (#{insert_list})
35
+ DO UPDATE SET #{helper.count_column} = #{helper.table_name}.#{helper.count_column} + EXCLUDED.#{helper.count_column}
36
+ SQL
37
+ end
38
+
39
+ def insert_sql(depth)
14
40
  <<-SQL
15
41
  INSERT INTO #{helper.table_name}
16
- (#{insert_list})
17
- SELECT
18
- #{select_list}
42
+ (#{insert_list}, #{helper.count_column})
43
+ SELECT #{insert_list}, #{helper.count_column} FROM
44
+ (#{sum_select(depth)}) to_insert
45
+ SQL
46
+ end
47
+
48
+ def sum_select(depth)
49
+ <<-SQL
50
+ SELECT #{select_list}, SUM(r1.#{helper.count_column} * r2.#{helper.count_column}) AS #{helper.count_column}
19
51
  FROM #{helper.table_name} r1
20
52
  JOIN #{helper.table_name} r2
21
53
  ON #{join_condition(depth)}
54
+ GROUP BY #{group_list}
22
55
  SQL
23
56
  end
24
57
 
25
- private
26
-
27
58
  def insert_list
28
59
  [helper.from_column,
29
60
  helper.to_column,
@@ -31,6 +62,14 @@ module TypedDag::Sql::InsertClosureOfDepth
31
62
  end
32
63
 
33
64
  def select_list
65
+ <<-SQL
66
+ r1.#{helper.from_column},
67
+ r2.#{helper.to_column},
68
+ #{helper.type_select_summed_columns_aliased('r1', 'r2')}
69
+ SQL
70
+ end
71
+
72
+ def group_list
34
73
  <<-SQL
35
74
  r1.#{helper.from_column},
36
75
  r2.#{helper.to_column},
@@ -14,8 +14,9 @@ module TypedDag::Sql::InsertReflexive
14
14
  <<-SQL
15
15
  INSERT INTO #{helper.table_name}
16
16
  (#{helper.from_column},
17
- #{helper.to_column})
18
- SELECT id, id
17
+ #{helper.to_column},
18
+ #{helper.count_column})
19
+ SELECT id, id, 1
19
20
  FROM #{helper.node_table_name}
20
21
  SQL
21
22
  end
@@ -11,6 +11,7 @@ module TypedDag::Sql::RelationAccess
11
11
  delegate :table_name,
12
12
  :from_column,
13
13
  :to_column,
14
+ :count_column,
14
15
  :type_columns,
15
16
  :type_select_list,
16
17
  to: :helper
@@ -15,15 +15,26 @@ module TypedDag::Sql::SelectClosure
15
15
  def sql
16
16
  <<-SQL
17
17
  SELECT
18
- r1.#{from_column},
19
- r2.#{to_column},
20
- #{depth_sum_case}
18
+ #{from_column},
19
+ #{to_column},
20
+ #{type_columns.join(', ')},
21
+ SUM(#{count_column}) AS #{count_column}
21
22
  FROM
22
- #{table_name} r1
23
- JOIN
24
- #{table_name} r2
25
- ON
26
- (#{relations_join_combines_paths_condition})
23
+ (SELECT
24
+ r1.#{from_column},
25
+ r2.#{to_column},
26
+ #{depth_sum_case},
27
+ r1.#{count_column} * r2.#{count_column} AS #{count_column}
28
+ FROM
29
+ #{table_name} r1
30
+ JOIN
31
+ #{table_name} r2
32
+ ON
33
+ (#{relations_join_combines_paths_condition})) unique_rows
34
+ GROUP BY
35
+ #{from_column},
36
+ #{to_column},
37
+ #{type_columns.join(', ')}
27
38
  SQL
28
39
  end
29
40
 
@@ -40,7 +51,7 @@ module TypedDag::Sql::SelectClosure
40
51
  ELSE 0
41
52
  END AS #{column}
42
53
  SQL
43
- end.join(', ')
54
+ end.map(&:strip).join(', ')
44
55
  end
45
56
 
46
57
  def relations_join_combines_paths_condition
@@ -13,7 +13,7 @@ module TypedDag::Sql::TruncateClosure
13
13
  end
14
14
 
15
15
  def sql
16
- if mysql_db?
16
+ if helper.mysql_db?
17
17
  sql_mysql
18
18
  else
19
19
  sql_postgresql
@@ -26,39 +26,37 @@ module TypedDag::Sql::TruncateClosure
26
26
 
27
27
  def sql_mysql
28
28
  <<-SQL
29
- DELETE
30
- deletion_table
31
- FROM
32
- #{table_name} deletion_table
33
- INNER JOIN
34
- #{selection_table} selection_table
35
- ON deletion_table.id = selection_table.id
29
+ UPDATE #{table_name}
30
+ JOIN
31
+ (#{closure_select}) removed_#{table_name}
32
+ ON #{table_name}.#{from_column} = removed_#{table_name}.#{from_column}
33
+ AND #{table_name}.#{to_column} = removed_#{table_name}.#{to_column}
34
+ AND #{types_equality_condition}
35
+ SET
36
+ #{table_name}.#{count_column} = #{table_name}.#{count_column} - removed_#{table_name}.#{count_column}
36
37
  SQL
37
38
  end
38
39
 
39
40
  def sql_postgresql
40
41
  <<-SQL
41
- DELETE FROM
42
- #{table_name} deletion_table
43
- USING
44
- #{selection_table} selection_table
45
- WHERE deletion_table.id = selection_table.id
42
+ UPDATE #{table_name}
43
+ SET
44
+ #{count_column} = #{table_name}.#{count_column} - removed_#{table_name}.#{count_column}
45
+ FROM
46
+ (#{closure_select}) removed_#{table_name}
47
+ WHERE #{table_name}.#{from_column} = removed_#{table_name}.#{from_column}
48
+ AND #{table_name}.#{to_column} = removed_#{table_name}.#{to_column}
49
+ AND #{types_equality_condition}
46
50
  SQL
47
51
  end
48
52
 
49
53
  def selection_table
50
54
  <<-SQL
51
- (SELECT id
52
- FROM (
53
- SELECT COUNT(*) count, #{from_column}, #{to_column}, #{type_select_list}
54
- FROM
55
- (#{closure_select}) aggregation
56
- GROUP BY #{from_column}, #{to_column}, #{type_select_list}) criteria
57
-
58
- JOIN
59
- (#{rank_similar_relations}) ranked
60
- ON
61
- #{ranked_critieria_join_condition})
55
+ (
56
+ SELECT COUNT(*) #{count_column}, #{from_column}, #{to_column}, #{type_select_list}
57
+ FROM
58
+ (#{closure_select}) aggregation
59
+ GROUP BY #{from_column}, #{to_column}, #{type_select_list})
62
60
  SQL
63
61
  end
64
62
 
@@ -66,113 +64,10 @@ module TypedDag::Sql::TruncateClosure
66
64
  TypedDag::Sql::SelectClosure.sql(relation)
67
65
  end
68
66
 
69
- def rank_similar_relations
70
- if mysql_db?
71
- rank_similar_relations_mysql
72
- else
73
- rank_similar_relations_postgresql
74
- end
75
- end
76
-
77
- def ranked_critieria_join_condition
78
- <<-SQL
79
- ranked.#{from_column} = criteria.#{from_column}
80
- AND ranked.#{to_column} = criteria.#{to_column}
81
- AND #{types_equality_condition}
82
- AND count >= row_number
83
- SQL
84
- end
85
-
86
- def type_column_values_pairs
87
- type_columns.map do |column|
88
- [column, relation.send(column)]
89
- end
90
- end
91
-
92
67
  def types_equality_condition
93
68
  type_columns.map do |column|
94
- "ranked.#{column} = criteria.#{column}"
69
+ "#{table_name}.#{column} = removed_#{table_name}.#{column}"
95
70
  end.join(' AND ')
96
71
  end
97
-
98
- def mysql_db?
99
- ActiveRecord::Base.connection.adapter_name == 'Mysql2'
100
- end
101
-
102
- def rank_similar_relations_mysql
103
- <<-SQL
104
- SELECT
105
- id,
106
- #{from_column},
107
- #{to_column},
108
- #{type_select_list},
109
- greatest(@cur_count := IF(#{compare_mysql_variables},
110
- @cur_count + 1, 1),
111
- least(0, #{assign_mysql_variables})) AS row_number
112
- FROM
113
- #{table_name}
114
-
115
- CROSS JOIN (SELECT #{initialize_mysql_variables}) params_initialization
116
-
117
- WHERE
118
- #{only_relations_in_closure_condition}
119
-
120
- ORDER BY #{from_column}, #{to_column}, #{type_select_list}
121
- SQL
122
- end
123
-
124
- def rank_similar_relations_postgresql
125
- <<-SQL
126
- SELECT *, ROW_NUMBER() OVER(
127
- PARTITION BY #{from_column}, #{to_column}, #{type_select_list}
128
- )
129
- FROM
130
- #{table_name}
131
- WHERE
132
- #{only_relations_in_closure_condition}
133
- SQL
134
- end
135
-
136
- def only_relations_in_closure_condition
137
- <<-SQL
138
- #{from_column} IN (SELECT #{from_column} FROM #{table_name} WHERE #{to_column} = #{from_id_value}) OR #{from_column} = #{from_id_value}
139
- AND
140
- #{to_column} IN (SELECT #{to_column} FROM #{table_name} WHERE #{from_column} = #{from_id_value})
141
- SQL
142
- end
143
-
144
- def initialize_mysql_variables
145
- variable_string = "@cur_count := NULL,
146
- @cur_#{from_column} := NULL,
147
- @cur_#{to_column} := NULL"
148
-
149
- type_columns.each do |column|
150
- variable_string += ", @cur_#{column} := NULL"
151
- end
152
-
153
- variable_string
154
- end
155
-
156
- def assign_mysql_variables
157
- variable_string = "@cur_#{from_column} := #{from_column},
158
- @cur_#{to_column} := #{to_column}"
159
-
160
- type_columns.each do |column|
161
- variable_string += ", @cur_#{column} := #{column}"
162
- end
163
-
164
- variable_string
165
- end
166
-
167
- def compare_mysql_variables
168
- variable_string = "@cur_#{from_column} = #{from_column} AND
169
- @cur_#{to_column} = #{to_column}"
170
-
171
- type_columns.each do |column|
172
- variable_string += " AND @cur_#{column} = #{column}"
173
- end
174
-
175
- variable_string
176
- end
177
72
  end
178
73
  end