chrono_model 1.2.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +19 -20
  3. data/README.md +62 -40
  4. data/lib/active_record/connection_adapters/chronomodel_adapter.rb +17 -11
  5. data/lib/active_record/tasks/chronomodel_database_tasks.rb +64 -23
  6. data/lib/chrono_model/adapter/ddl.rb +168 -153
  7. data/lib/chrono_model/adapter/indexes.rb +99 -94
  8. data/lib/chrono_model/adapter/migrations.rb +81 -104
  9. data/lib/chrono_model/adapter/migrations_modules/legacy.rb +41 -0
  10. data/lib/chrono_model/adapter/migrations_modules/stable.rb +41 -0
  11. data/lib/chrono_model/adapter/tsrange.rb +20 -5
  12. data/lib/chrono_model/adapter/upgrade.rb +89 -91
  13. data/lib/chrono_model/adapter.rb +64 -31
  14. data/lib/chrono_model/chrono.rb +17 -0
  15. data/lib/chrono_model/conversions.rb +15 -9
  16. data/lib/chrono_model/db_console.rb +9 -0
  17. data/lib/chrono_model/json.rb +9 -6
  18. data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
  19. data/lib/chrono_model/patches/as_of_time_relation.rb +2 -2
  20. data/lib/chrono_model/patches/association.rb +15 -12
  21. data/lib/chrono_model/patches/batches.rb +17 -0
  22. data/lib/chrono_model/patches/db_console.rb +20 -4
  23. data/lib/chrono_model/patches/join_node.rb +4 -4
  24. data/lib/chrono_model/patches/preloader.rb +41 -11
  25. data/lib/chrono_model/patches/relation.rb +53 -8
  26. data/lib/chrono_model/patches.rb +3 -1
  27. data/lib/chrono_model/railtie.rb +29 -24
  28. data/lib/chrono_model/time_gate.rb +3 -3
  29. data/lib/chrono_model/time_machine/history_model.rb +65 -31
  30. data/lib/chrono_model/time_machine/time_query.rb +65 -49
  31. data/lib/chrono_model/time_machine/timeline.rb +52 -28
  32. data/lib/chrono_model/time_machine.rb +66 -25
  33. data/lib/chrono_model/utilities.rb +3 -3
  34. data/lib/chrono_model/version.rb +3 -1
  35. data/lib/chrono_model.rb +31 -36
  36. metadata +39 -136
  37. data/.gitignore +0 -21
  38. data/.rspec +0 -2
  39. data/.travis.yml +0 -41
  40. data/Gemfile +0 -4
  41. data/README.sql +0 -161
  42. data/Rakefile +0 -25
  43. data/chrono_model.gemspec +0 -33
  44. data/gemfiles/rails_5.0.gemfile +0 -6
  45. data/gemfiles/rails_5.1.gemfile +0 -6
  46. data/gemfiles/rails_5.2.gemfile +0 -6
  47. data/spec/aruba/dbconsole_spec.rb +0 -25
  48. data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
  49. data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
  50. data/spec/aruba/fixtures/empty_structure.sql +0 -27
  51. data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
  52. data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
  53. data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
  54. data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
  55. data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
  56. data/spec/aruba/migrations_spec.rb +0 -48
  57. data/spec/aruba/rake_task_spec.rb +0 -71
  58. data/spec/chrono_model/adapter/base_spec.rb +0 -157
  59. data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
  60. data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
  61. data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
  62. data/spec/chrono_model/conversions_spec.rb +0 -43
  63. data/spec/chrono_model/history_models_spec.rb +0 -32
  64. data/spec/chrono_model/json_ops_spec.rb +0 -59
  65. data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
  66. data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
  67. data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
  68. data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
  69. data/spec/chrono_model/time_machine/history_spec.rb +0 -104
  70. data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
  71. data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
  72. data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
  73. data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
  74. data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
  75. data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
  76. data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
  77. data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
  78. data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
  79. data/spec/config.travis.yml +0 -5
  80. data/spec/config.yml.example +0 -9
  81. data/spec/spec_helper.rb +0 -33
  82. data/spec/support/adapter/helpers.rb +0 -53
  83. data/spec/support/adapter/structure.rb +0 -44
  84. data/spec/support/aruba.rb +0 -44
  85. data/spec/support/connection.rb +0 -70
  86. data/spec/support/matchers/base.rb +0 -56
  87. data/spec/support/matchers/column.rb +0 -99
  88. data/spec/support/matchers/function.rb +0 -79
  89. data/spec/support/matchers/index.rb +0 -69
  90. data/spec/support/matchers/schema.rb +0 -39
  91. data/spec/support/matchers/table.rb +0 -275
  92. data/spec/support/time_machine/helpers.rb +0 -47
  93. data/spec/support/time_machine/structure.rb +0 -111
  94. data/sql/json_ops.sql +0 -56
  95. data/sql/uninstall-json_ops.sql +0 -24
@@ -1,225 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/string/strip'
1
4
  require 'multi_json'
2
5
 
3
6
  module ChronoModel
4
7
  class Adapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
5
-
6
8
  module DDL
7
9
  private
8
- # Create the public view and its INSTEAD OF triggers
9
- #
10
- def chrono_public_view_ddl(table, options = nil)
11
- pk = primary_key(table)
12
- current = [TEMPORAL_SCHEMA, table].join('.')
13
- history = [HISTORY_SCHEMA, table].join('.')
14
10
 
15
- options ||= chrono_metadata_for(table)
11
+ # Create the public view and its INSTEAD OF triggers
12
+ #
13
+ def chrono_public_view_ddl(table, options = nil)
14
+ pk = primary_key(table)
15
+ current = "#{TEMPORAL_SCHEMA}.#{table}"
16
+ history = "#{HISTORY_SCHEMA}.#{table}"
16
17
 
17
- # SELECT - return only current data
18
- #
19
- execute "DROP VIEW #{table}" if data_source_exists? table
20
- execute "CREATE VIEW #{table} AS SELECT * FROM ONLY #{current}"
18
+ options ||= chrono_metadata_for(table)
19
+
20
+ # SELECT - return only current data
21
+ #
22
+ execute "DROP VIEW #{table}" if data_source_exists? table
23
+ execute "CREATE VIEW #{table} AS SELECT * FROM ONLY #{current}"
21
24
 
22
- chrono_metadata_set(table, options.merge(chronomodel: VERSION))
25
+ chrono_metadata_set(table, options.merge(chronomodel: VERSION))
23
26
 
24
- # Set default values on the view (closes #12)
25
- #
26
- columns(table).each do |column|
27
- default = if column.default.nil?
27
+ # Set default values on the view (closes #12)
28
+ #
29
+ columns(table).each do |column|
30
+ default =
31
+ if column.default.nil?
28
32
  column.default_function
29
33
  else
30
34
  quote(column.default)
31
35
  end
32
36
 
33
- next if column.name == pk || default.nil?
37
+ next if column.name == pk || default.nil?
34
38
 
35
- execute "ALTER VIEW #{table} ALTER COLUMN #{quote_column_name(column.name)} SET DEFAULT #{default}"
36
- end
39
+ execute "ALTER VIEW #{table} ALTER COLUMN #{quote_column_name(column.name)} SET DEFAULT #{default}"
40
+ end
37
41
 
38
- columns = self.columns(table).map {|c| quote_column_name(c.name)}
39
- columns.delete(quote_column_name(pk))
42
+ columns = self.columns(table).map { |c| quote_column_name(c.name) }
43
+ columns.delete(quote_column_name(pk))
40
44
 
41
- fields, values = columns.join(', '), columns.map {|c| "NEW.#{c}"}.join(', ')
45
+ fields = columns.join(', ')
46
+ values = columns.map { |c| "NEW.#{c}" }.join(', ')
42
47
 
43
- chrono_create_INSERT_trigger(table, pk, current, history, fields, values)
44
- chrono_create_UPDATE_trigger(table, pk, current, history, fields, values, options, columns)
45
- chrono_create_DELETE_trigger(table, pk, current, history)
46
- end
48
+ chrono_create_INSERT_trigger(table, pk, current, history, fields, values)
49
+ chrono_create_UPDATE_trigger(table, pk, current, history, fields, values, options, columns)
50
+ chrono_create_DELETE_trigger(table, pk, current, history)
51
+ end
47
52
 
48
- # Create the history table in the history schema
49
- def chrono_history_table_ddl(table)
50
- parent = "#{TEMPORAL_SCHEMA}.#{table}"
51
- p_pkey = primary_key(parent)
53
+ # Create the history table in the history schema
54
+ def chrono_history_table_ddl(table)
55
+ parent = "#{TEMPORAL_SCHEMA}.#{table}"
56
+ p_pkey = primary_key(parent)
52
57
 
53
- execute <<-SQL
58
+ execute <<-SQL.squish
54
59
  CREATE TABLE #{table} (
55
- hid SERIAL PRIMARY KEY,
60
+ hid BIGSERIAL PRIMARY KEY,
56
61
  validity tsrange NOT NULL,
57
62
  recorded_at timestamp NOT NULL DEFAULT timezone('UTC', now())
58
63
  ) INHERITS ( #{parent} )
59
- SQL
60
-
61
- add_history_validity_constraint(table, p_pkey)
64
+ SQL
62
65
 
63
- chrono_create_history_indexes_for(table, p_pkey)
64
- end
66
+ add_history_validity_constraint(table, p_pkey)
65
67
 
66
- def add_history_validity_constraint(table, pkey)
67
- add_timeline_consistency_constraint(table, :validity, id: pkey, on_current_schema: true)
68
- end
68
+ chrono_create_history_indexes_for(table, p_pkey)
69
+ end
69
70
 
70
- def remove_history_validity_constraint(table, options = {})
71
- remove_timeline_consistency_constraint(table, options.merge(on_current_schema: true))
72
- end
71
+ def add_history_validity_constraint(table, pkey)
72
+ add_timeline_consistency_constraint(table, :validity, id: pkey, on_current_schema: true)
73
+ end
73
74
 
74
- # INSERT - insert data both in the temporal table and in the history one.
75
- #
76
- # The serial sequence is invoked manually only if the PK is NULL, to
77
- # allow setting the PK to a specific value (think migration scenario).
78
- #
79
- def chrono_create_INSERT_trigger(table, pk, current, history, fields, values)
80
- seq = serial_sequence(current, pk)
75
+ def remove_history_validity_constraint(table, options = {})
76
+ remove_timeline_consistency_constraint(table, options.merge(on_current_schema: true))
77
+ end
81
78
 
82
- execute <<-SQL
79
+ # INSERT - insert data both in the temporal table and in the history one.
80
+ #
81
+ # The serial sequence is invoked manually only if the PK is NULL, to
82
+ # allow setting the PK to a specific value (think migration scenario).
83
+ #
84
+ def chrono_create_INSERT_trigger(table, pk, current, history, fields, values)
85
+ execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
83
86
  CREATE OR REPLACE FUNCTION chronomodel_#{table}_insert() RETURNS TRIGGER AS $$
84
- BEGIN
85
- IF NEW.#{pk} IS NULL THEN
86
- NEW.#{pk} := nextval('#{seq}');
87
- END IF;
87
+ BEGIN
88
+ #{insert_sequence_sql(pk, current)} INTO #{current} ( #{pk}, #{fields} )
89
+ VALUES ( NEW.#{pk}, #{values} );
88
90
 
89
- INSERT INTO #{current} ( #{pk}, #{fields} )
90
- VALUES ( NEW.#{pk}, #{values} );
91
+ INSERT INTO #{history} ( #{pk}, #{fields}, validity )
92
+ VALUES ( NEW.#{pk}, #{values}, tsrange(timezone('UTC', now()), NULL) );
91
93
 
92
- INSERT INTO #{history} ( #{pk}, #{fields}, validity )
93
- VALUES ( NEW.#{pk}, #{values}, tsrange(timezone('UTC', now()), NULL) );
94
+ RETURN NEW;
95
+ END;
94
96
 
95
- RETURN NEW;
96
- END;
97
97
  $$ LANGUAGE plpgsql;
98
98
 
99
99
  DROP TRIGGER IF EXISTS chronomodel_insert ON #{table};
100
100
 
101
101
  CREATE TRIGGER chronomodel_insert INSTEAD OF INSERT ON #{table}
102
- FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_insert();
103
- SQL
104
- end
105
-
106
- # UPDATE - set the last history entry validity to now, save the current data
107
- # in a new history entry and update the temporal table with the new data.
108
- #
109
- # If there are no changes, this trigger suppresses redundant updates.
110
- #
111
- # If a row in the history with the current ID and current timestamp already
112
- # exists, update it with new data. This logic makes possible to "squash"
113
- # together changes made in a transaction in a single history row.
114
- #
115
- # If you want to disable this behaviour, set the CHRONOMODEL_NO_SQUASH
116
- # environment variable. This is useful when running scenarios inside
117
- # cucumber, in which everything runs in the same transaction.
102
+ FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_insert();
103
+ SQL
104
+ end
105
+
106
+ # UPDATE - set the last history entry validity to now, save the current data
107
+ # in a new history entry and update the temporal table with the new data.
108
+ #
109
+ # If there are no changes, this trigger suppresses redundant updates.
110
+ #
111
+ # If a row in the history with the current ID and current timestamp already
112
+ # exists, update it with new data. This logic makes possible to "squash"
113
+ # together changes made in a transaction in a single history row.
114
+ #
115
+ # If you want to disable this behaviour, set the CHRONOMODEL_NO_SQUASH
116
+ # environment variable. This is useful when running scenarios inside
117
+ # cucumber, in which everything runs in the same transaction.
118
+ #
119
+ def chrono_create_UPDATE_trigger(table, pk, current, history, fields, values, options, columns)
120
+ # Columns to be journaled. By default everything except updated_at (GH #7)
118
121
  #
119
- def chrono_create_UPDATE_trigger(table, pk, current, history, fields, values, options, columns)
120
- # Columns to be journaled. By default everything except updated_at (GH #7)
121
- #
122
- journal = if options[:journal]
123
- options[:journal].map {|col| quote_column_name(col)}
122
+ journal =
123
+ if options[:journal]
124
+ options[:journal].map { |col| quote_column_name(col) }
124
125
 
125
126
  elsif options[:no_journal]
126
- columns - options[:no_journal].map {|col| quote_column_name(col)}
127
+ columns - options[:no_journal].map { |col| quote_column_name(col) }
127
128
 
128
129
  elsif options[:full_journal]
129
130
  columns
130
131
 
131
132
  else
132
- columns - [ quote_column_name('updated_at') ]
133
+ columns - [quote_column_name('updated_at')]
133
134
  end
134
135
 
135
- journal &= columns
136
+ journal &= columns
136
137
 
137
- execute <<-SQL
138
+ execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
138
139
  CREATE OR REPLACE FUNCTION chronomodel_#{table}_update() RETURNS TRIGGER AS $$
139
- DECLARE _now timestamp;
140
- DECLARE _hid integer;
141
- DECLARE _old record;
142
- DECLARE _new record;
143
- BEGIN
144
- IF OLD IS NOT DISTINCT FROM NEW THEN
145
- RETURN NULL;
146
- END IF;
147
-
148
- _old := row(#{journal.map {|c| "OLD.#{c}" }.join(', ')});
149
- _new := row(#{journal.map {|c| "NEW.#{c}" }.join(', ')});
150
-
151
- IF _old IS NOT DISTINCT FROM _new THEN
152
- UPDATE ONLY #{current} SET ( #{fields} ) = ( #{values} ) WHERE #{pk} = OLD.#{pk};
153
- RETURN NEW;
154
- END IF;
155
-
156
- _now := timezone('UTC', now());
157
- _hid := NULL;
158
-
159
- #{"SELECT hid INTO _hid FROM #{history} WHERE #{pk} = OLD.#{pk} AND lower(validity) = _now;" unless ENV['CHRONOMODEL_NO_SQUASH']}
160
-
161
- IF _hid IS NOT NULL THEN
162
- UPDATE #{history} SET ( #{fields} ) = ( #{values} ) WHERE hid = _hid;
163
- ELSE
164
- UPDATE #{history} SET validity = tsrange(lower(validity), _now)
165
- WHERE #{pk} = OLD.#{pk} AND upper_inf(validity);
166
-
167
- INSERT INTO #{history} ( #{pk}, #{fields}, validity )
168
- VALUES ( OLD.#{pk}, #{values}, tsrange(_now, NULL) );
169
- END IF;
170
-
171
- UPDATE ONLY #{current} SET ( #{fields} ) = ( #{values} ) WHERE #{pk} = OLD.#{pk};
172
-
173
- RETURN NEW;
174
- END;
140
+ DECLARE _now timestamp;
141
+ DECLARE _hid integer;
142
+ DECLARE _old record;
143
+ DECLARE _new record;
144
+ BEGIN
145
+ IF OLD IS NOT DISTINCT FROM NEW THEN
146
+ RETURN NULL;
147
+ END IF;
148
+
149
+ _old := row(#{journal.map { |c| "OLD.#{c}" }.join(', ')});
150
+ _new := row(#{journal.map { |c| "NEW.#{c}" }.join(', ')});
151
+
152
+ IF _old IS NOT DISTINCT FROM _new THEN
153
+ UPDATE ONLY #{current} SET ( #{fields} ) = ( #{values} ) WHERE #{pk} = OLD.#{pk};
154
+ RETURN NEW;
155
+ END IF;
156
+
157
+ _now := timezone('UTC', now());
158
+ _hid := NULL;
159
+
160
+ #{"SELECT hid INTO _hid FROM #{history} WHERE #{pk} = OLD.#{pk} AND lower(validity) = _now;" unless ENV['CHRONOMODEL_NO_SQUASH']}
161
+
162
+ IF _hid IS NOT NULL THEN
163
+ UPDATE #{history} SET ( #{fields} ) = ( #{values} ) WHERE hid = _hid;
164
+ ELSE
165
+ UPDATE #{history} SET validity = tsrange(lower(validity), _now)
166
+ WHERE #{pk} = OLD.#{pk} AND upper_inf(validity);
167
+
168
+ INSERT INTO #{history} ( #{pk}, #{fields}, validity )
169
+ VALUES ( OLD.#{pk}, #{values}, tsrange(_now, NULL) );
170
+ END IF;
171
+
172
+ UPDATE ONLY #{current} SET ( #{fields} ) = ( #{values} ) WHERE #{pk} = OLD.#{pk};
173
+
174
+ RETURN NEW;
175
+ END;
176
+
175
177
  $$ LANGUAGE plpgsql;
176
178
 
177
179
  DROP TRIGGER IF EXISTS chronomodel_update ON #{table};
178
180
 
179
181
  CREATE TRIGGER chronomodel_update INSTEAD OF UPDATE ON #{table}
180
- FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_update();
181
- SQL
182
- end
183
-
184
- # DELETE - save the current data in the history and eventually delete the
185
- # data from the temporal table.
186
- # The first DELETE is required to remove history for records INSERTed and
187
- # DELETEd in the same transaction.
188
- #
189
- def chrono_create_DELETE_trigger(table, pk, current, history)
190
- execute <<-SQL
182
+ FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_update();
183
+ SQL
184
+ end
185
+
186
+ # DELETE - save the current data in the history and eventually delete the
187
+ # data from the temporal table.
188
+ # The first DELETE is required to remove history for records INSERTed and
189
+ # DELETEd in the same transaction.
190
+ #
191
+ def chrono_create_DELETE_trigger(table, pk, current, history)
192
+ execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
191
193
  CREATE OR REPLACE FUNCTION chronomodel_#{table}_delete() RETURNS TRIGGER AS $$
192
- DECLARE _now timestamp;
193
- BEGIN
194
- _now := timezone('UTC', now());
194
+ DECLARE _now timestamp;
195
+ BEGIN
196
+ _now := timezone('UTC', now());
197
+
198
+ DELETE FROM #{history}
199
+ WHERE #{pk} = old.#{pk} AND validity = tsrange(_now, NULL);
195
200
 
196
- DELETE FROM #{history}
197
- WHERE #{pk} = old.#{pk} AND validity = tsrange(_now, NULL);
201
+ UPDATE #{history} SET validity = tsrange(lower(validity), _now)
202
+ WHERE #{pk} = old.#{pk} AND upper_inf(validity);
198
203
 
199
- UPDATE #{history} SET validity = tsrange(lower(validity), _now)
200
- WHERE #{pk} = old.#{pk} AND upper_inf(validity);
204
+ DELETE FROM ONLY #{current}
205
+ WHERE #{pk} = old.#{pk};
201
206
 
202
- DELETE FROM ONLY #{current}
203
- WHERE #{pk} = old.#{pk};
207
+ RETURN OLD;
208
+ END;
204
209
 
205
- RETURN OLD;
206
- END;
207
210
  $$ LANGUAGE plpgsql;
208
211
 
209
212
  DROP TRIGGER IF EXISTS chronomodel_delete ON #{table};
210
213
 
211
214
  CREATE TRIGGER chronomodel_delete INSTEAD OF DELETE ON #{table}
212
- FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_delete();
213
- SQL
214
- end
215
+ FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_delete();
216
+ SQL
217
+ end
215
218
 
216
- def chrono_drop_trigger_functions_for(table_name)
217
- %w( insert update delete ).each do |func|
218
- execute "DROP FUNCTION IF EXISTS chronomodel_#{table_name}_#{func}()"
219
- end
219
+ def chrono_drop_trigger_functions_for(table_name)
220
+ %w[insert update delete].each do |func|
221
+ execute "DROP FUNCTION IF EXISTS chronomodel_#{table_name}_#{func}()"
220
222
  end
223
+ end
224
+
225
+ def insert_sequence_sql(pk, current)
226
+ seq = pk_and_sequence_for(current)
227
+ return 'INSERT' if seq.blank?
228
+
229
+ <<-SQL.strip # rubocop:disable Rails/SquishedSQLHeredocs
230
+ IF NEW.#{pk} IS NULL THEN
231
+ NEW.#{pk} := nextval('#{seq.last}');
232
+ END IF;
233
+
234
+ INSERT
235
+ SQL
236
+ end
221
237
  # private
222
238
  end
223
-
224
239
  end
225
240
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ChronoModel
2
4
  class Adapter < ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
3
-
4
5
  module Indexes
5
6
  # Create temporal indexes for timestamp search.
6
7
  #
@@ -23,7 +24,7 @@ module ChronoModel
23
24
  temporal_index_names(table, range, options)
24
25
 
25
26
  chrono_alter_index(table, options) do
26
- execute <<-SQL
27
+ execute <<-SQL.squish
27
28
  CREATE INDEX #{range_idx} ON #{table} USING gist ( #{range} )
28
29
  SQL
29
30
 
@@ -39,7 +40,7 @@ module ChronoModel
39
40
  indexes = temporal_index_names(table, range, options)
40
41
 
41
42
  chrono_alter_index(table, options) do
42
- indexes.each {|idx| execute "DROP INDEX #{idx}" }
43
+ indexes.each { |idx| execute "DROP INDEX #{idx}" }
43
44
  end
44
45
  end
45
46
 
@@ -52,7 +53,7 @@ module ChronoModel
52
53
  id = options[:id] || primary_key(table)
53
54
 
54
55
  chrono_alter_constraint(table, options) do
55
- execute <<-SQL
56
+ execute <<-SQL.squish
56
57
  ALTER TABLE #{table} ADD CONSTRAINT #{name}
57
58
  EXCLUDE USING gist ( #{id} WITH =, #{range} WITH && )
58
59
  SQL
@@ -63,7 +64,7 @@ module ChronoModel
63
64
  name = timeline_consistency_constraint_name(table)
64
65
 
65
66
  chrono_alter_constraint(table, options) do
66
- execute <<-SQL
67
+ execute <<-SQL.squish
67
68
  ALTER TABLE #{table} DROP CONSTRAINT #{name}
68
69
  SQL
69
70
  end
@@ -74,121 +75,125 @@ module ChronoModel
74
75
  end
75
76
 
76
77
  private
77
- # Creates indexes for a newly made history table
78
- #
79
- def chrono_create_history_indexes_for(table, p_pkey)
80
- add_temporal_indexes table, :validity, on_current_schema: true
81
78
 
82
- execute "CREATE INDEX #{table}_inherit_pkey ON #{table} ( #{p_pkey} )"
83
- execute "CREATE INDEX #{table}_recorded_at ON #{table} ( recorded_at )"
84
- execute "CREATE INDEX #{table}_instance_history ON #{table} ( #{p_pkey}, recorded_at )"
85
- end
79
+ # Creates indexes for a newly made history table
80
+ #
81
+ def chrono_create_history_indexes_for(table, p_pkey)
82
+ add_temporal_indexes table, :validity, on_current_schema: true
86
83
 
87
- # Rename indexes on history schema
88
- #
89
- def chrono_rename_history_indexes(name, new_name)
90
- on_history_schema do
91
- standard_index_names = %w(
92
- inherit_pkey instance_history pkey
93
- recorded_at timeline_consistency )
84
+ execute "CREATE INDEX #{table}_inherit_pkey ON #{table} ( #{p_pkey} )"
85
+ execute "CREATE INDEX #{table}_recorded_at ON #{table} ( recorded_at )"
86
+ execute "CREATE INDEX #{table}_instance_history ON #{table} ( #{p_pkey}, recorded_at )"
87
+ end
88
+
89
+ # Rename indexes on history schema
90
+ #
91
+ def chrono_rename_history_indexes(name, new_name)
92
+ on_history_schema do
93
+ standard_index_names = %w[
94
+ inherit_pkey instance_history pkey
95
+ recorded_at timeline_consistency
96
+ ]
94
97
 
95
- old_names = temporal_index_names(name, :validity) +
96
- standard_index_names.map {|i| [name, i].join('_') }
98
+ old_names = temporal_index_names(name, :validity) +
99
+ standard_index_names.map { |i| "#{name}_#{i}" }
97
100
 
98
- new_names = temporal_index_names(new_name, :validity) +
99
- standard_index_names.map {|i| [new_name, i].join('_') }
101
+ new_names = temporal_index_names(new_name, :validity) +
102
+ standard_index_names.map { |i| "#{new_name}_#{i}" }
100
103
 
101
- old_names.zip(new_names).each do |old, new|
102
- execute "ALTER INDEX #{old} RENAME TO #{new}"
103
- end
104
+ old_names.zip(new_names).each do |old, new|
105
+ execute "ALTER INDEX #{old} RENAME TO #{new}"
104
106
  end
105
107
  end
108
+ end
106
109
 
107
- # Rename indexes on temporal schema
108
- #
109
- def chrono_rename_temporal_indexes(name, new_name)
110
- on_temporal_schema do
111
- temporal_indexes = indexes(new_name)
112
- temporal_indexes.map(&:name).each do |old_idx_name|
113
- if old_idx_name =~ /^index_#{name}_on_(?<columns>.+)/
114
- new_idx_name = "index_#{new_name}_on_#{$~['columns']}"
115
- execute "ALTER INDEX #{old_idx_name} RENAME TO #{new_idx_name}"
116
- end
110
+ # Rename indexes on temporal schema
111
+ #
112
+ def chrono_rename_temporal_indexes(name, new_name)
113
+ on_temporal_schema do
114
+ temporal_indexes = indexes(new_name)
115
+ temporal_indexes.map(&:name).each do |old_idx_name|
116
+ if old_idx_name =~ /^index_#{name}_on_(?<columns>.+)/
117
+ new_idx_name = "index_#{new_name}_on_#{$~['columns']}"
118
+ execute "ALTER INDEX #{old_idx_name} RENAME TO #{new_idx_name}"
117
119
  end
118
120
  end
119
121
  end
122
+ end
120
123
 
121
- # Copy the indexes from the temporal table to the history table
122
- # if the indexes are not already created with the same name.
123
- #
124
- # Uniqueness is voluntarily ignored, as it doesn't make sense on
125
- # history tables.
126
- #
127
- # Used in migrations.
128
- #
129
- # Ref: GitHub pull #21.
130
- #
131
- def chrono_copy_indexes_to_history(table_name)
132
- history_indexes = on_history_schema { indexes(table_name) }.map(&:name)
133
- temporal_indexes = on_temporal_schema { indexes(table_name) }
124
+ # Copy the indexes from the temporal table to the history table
125
+ # if the indexes are not already created with the same name.
126
+ #
127
+ # Uniqueness is voluntarily ignored, as it doesn't make sense on
128
+ # history tables.
129
+ #
130
+ # Used in migrations.
131
+ #
132
+ # Ref: GitHub pull #21.
133
+ #
134
+ def chrono_copy_indexes_to_history(table_name)
135
+ history_indexes = on_history_schema { indexes(table_name) }.map(&:name)
136
+ temporal_indexes = on_temporal_schema { indexes(table_name) }
134
137
 
135
- temporal_indexes.each do |index|
136
- next if history_indexes.include?(index.name)
138
+ temporal_indexes.each do |index|
139
+ next if history_indexes.include?(index.name)
137
140
 
138
- on_history_schema do
139
- execute %[
141
+ on_history_schema do
142
+ # index.columns is an Array for plain indexes,
143
+ # while it is a String for computed indexes.
144
+ #
145
+ columns = Array.wrap(index.columns).join(', ')
146
+
147
+ execute %[
140
148
  CREATE INDEX #{index.name} ON #{table_name}
141
- USING #{index.using} ( #{index.columns.join(', ')} )
149
+ USING #{index.using} ( #{columns} )
142
150
  ], 'Copy index from temporal to history'
143
- end
144
151
  end
145
152
  end
153
+ end
146
154
 
147
- # Returns a suitable index name on the given table and for the
148
- # given range definition.
149
- #
150
- def temporal_index_names(table, range, options = {})
151
- prefix = options[:name].presence || "index_#{table}_temporal"
152
-
153
- # When creating computed indexes
154
- #
155
- # e.g. ends_on::timestamp + time '23:59:59'
156
- #
157
- # remove everything following the field name.
158
- range = range.to_s.sub(/\W.*/, '')
159
-
160
- [range, "lower_#{range}", "upper_#{range}"].map do |suffix|
161
- [prefix, 'on', suffix].join('_')
162
- end
163
- end
155
+ # Returns a suitable index name on the given table and for the
156
+ # given range definition.
157
+ #
158
+ def temporal_index_names(table, range, options = {})
159
+ prefix = options[:name].presence || "index_#{table}_temporal"
164
160
 
165
- # Generic alteration of history tables, where changes have to be
166
- # propagated both on the temporal table and the history one.
161
+ # When creating computed indexes
167
162
  #
168
- # Internally, the :on_current_schema bypasses the +is_chrono?+
169
- # check, as some temporal indexes and constraints are created
170
- # only on the history table, and the creation methods already
171
- # run scoped into the correct schema.
163
+ # e.g. ends_on::timestamp + time '23:59:59'
172
164
  #
173
- def chrono_alter_index(table_name, options)
174
- if is_chrono?(table_name) && !options[:on_current_schema]
175
- on_temporal_schema { yield }
176
- on_history_schema { yield }
177
- else
178
- yield
179
- end
180
- end
165
+ # remove everything following the field name.
166
+ range = range.to_s.sub(/\W.*/, '')
181
167
 
182
- def chrono_alter_constraint(table_name, options)
183
- if is_chrono?(table_name) && !options[:on_current_schema]
184
- on_temporal_schema { yield }
185
- else
186
- yield
187
- end
168
+ [range, "lower_#{range}", "upper_#{range}"].map do |suffix|
169
+ "#{prefix}_on_#{suffix}"
188
170
  end
171
+ end
189
172
 
173
+ # Generic alteration of history tables, where changes have to be
174
+ # propagated both on the temporal table and the history one.
175
+ #
176
+ # Internally, the :on_current_schema bypasses the +is_chrono?+
177
+ # check, as some temporal indexes and constraints are created
178
+ # only on the history table, and the creation methods already
179
+ # run scoped into the correct schema.
180
+ #
181
+ def chrono_alter_index(table_name, options, &block)
182
+ if is_chrono?(table_name) && !options[:on_current_schema]
183
+ on_temporal_schema(&block)
184
+ on_history_schema(&block)
185
+ else
186
+ yield
187
+ end
188
+ end
190
189
 
190
+ def chrono_alter_constraint(table_name, options, &block)
191
+ if is_chrono?(table_name) && !options[:on_current_schema]
192
+ on_temporal_schema(&block)
193
+ else
194
+ yield
195
+ end
196
+ end
191
197
  end
192
-
193
198
  end
194
199
  end