chrono_model 1.2.2 → 2.0.0

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 (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