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.
- checksums.yaml +4 -4
- data/LICENSE +19 -20
- data/README.md +62 -40
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +17 -11
- data/lib/active_record/tasks/chronomodel_database_tasks.rb +64 -23
- data/lib/chrono_model/adapter/ddl.rb +168 -153
- data/lib/chrono_model/adapter/indexes.rb +99 -94
- data/lib/chrono_model/adapter/migrations.rb +81 -104
- data/lib/chrono_model/adapter/migrations_modules/legacy.rb +41 -0
- data/lib/chrono_model/adapter/migrations_modules/stable.rb +41 -0
- data/lib/chrono_model/adapter/tsrange.rb +20 -5
- data/lib/chrono_model/adapter/upgrade.rb +89 -91
- data/lib/chrono_model/adapter.rb +64 -31
- data/lib/chrono_model/chrono.rb +17 -0
- data/lib/chrono_model/conversions.rb +15 -9
- data/lib/chrono_model/db_console.rb +9 -0
- data/lib/chrono_model/json.rb +9 -6
- data/lib/chrono_model/patches/as_of_time_holder.rb +2 -2
- data/lib/chrono_model/patches/as_of_time_relation.rb +2 -2
- data/lib/chrono_model/patches/association.rb +15 -12
- data/lib/chrono_model/patches/batches.rb +17 -0
- data/lib/chrono_model/patches/db_console.rb +20 -4
- data/lib/chrono_model/patches/join_node.rb +4 -4
- data/lib/chrono_model/patches/preloader.rb +41 -11
- data/lib/chrono_model/patches/relation.rb +53 -8
- data/lib/chrono_model/patches.rb +3 -1
- data/lib/chrono_model/railtie.rb +29 -24
- data/lib/chrono_model/time_gate.rb +3 -3
- data/lib/chrono_model/time_machine/history_model.rb +65 -31
- data/lib/chrono_model/time_machine/time_query.rb +65 -49
- data/lib/chrono_model/time_machine/timeline.rb +52 -28
- data/lib/chrono_model/time_machine.rb +66 -25
- data/lib/chrono_model/utilities.rb +3 -3
- data/lib/chrono_model/version.rb +3 -1
- data/lib/chrono_model.rb +31 -36
- metadata +39 -136
- data/.gitignore +0 -21
- data/.rspec +0 -2
- data/.travis.yml +0 -41
- data/Gemfile +0 -4
- data/README.sql +0 -161
- data/Rakefile +0 -25
- data/chrono_model.gemspec +0 -33
- data/gemfiles/rails_5.0.gemfile +0 -6
- data/gemfiles/rails_5.1.gemfile +0 -6
- data/gemfiles/rails_5.2.gemfile +0 -6
- data/spec/aruba/dbconsole_spec.rb +0 -25
- data/spec/aruba/fixtures/database_with_default_username_and_password.yml +0 -14
- data/spec/aruba/fixtures/database_without_username_and_password.yml +0 -11
- data/spec/aruba/fixtures/empty_structure.sql +0 -27
- data/spec/aruba/fixtures/migrations/56/20160812190335_create_impressions.rb +0 -10
- data/spec/aruba/fixtures/migrations/56/20171115195229_add_temporal_extension_to_impressions.rb +0 -10
- data/spec/aruba/fixtures/railsapp/config/application.rb +0 -17
- data/spec/aruba/fixtures/railsapp/config/boot.rb +0 -5
- data/spec/aruba/fixtures/railsapp/config/environments/development.rb +0 -38
- data/spec/aruba/migrations_spec.rb +0 -48
- data/spec/aruba/rake_task_spec.rb +0 -71
- data/spec/chrono_model/adapter/base_spec.rb +0 -157
- data/spec/chrono_model/adapter/ddl_spec.rb +0 -243
- data/spec/chrono_model/adapter/indexes_spec.rb +0 -72
- data/spec/chrono_model/adapter/migrations_spec.rb +0 -312
- data/spec/chrono_model/conversions_spec.rb +0 -43
- data/spec/chrono_model/history_models_spec.rb +0 -32
- data/spec/chrono_model/json_ops_spec.rb +0 -59
- data/spec/chrono_model/time_machine/as_of_spec.rb +0 -188
- data/spec/chrono_model/time_machine/changes_spec.rb +0 -50
- data/spec/chrono_model/time_machine/counter_cache_race_spec.rb +0 -46
- data/spec/chrono_model/time_machine/default_scope_spec.rb +0 -37
- data/spec/chrono_model/time_machine/history_spec.rb +0 -104
- data/spec/chrono_model/time_machine/keep_cool_spec.rb +0 -27
- data/spec/chrono_model/time_machine/manipulations_spec.rb +0 -84
- data/spec/chrono_model/time_machine/model_identification_spec.rb +0 -46
- data/spec/chrono_model/time_machine/sequence_spec.rb +0 -74
- data/spec/chrono_model/time_machine/sti_spec.rb +0 -100
- data/spec/chrono_model/time_machine/time_query_spec.rb +0 -261
- data/spec/chrono_model/time_machine/timeline_spec.rb +0 -63
- data/spec/chrono_model/time_machine/timestamps_spec.rb +0 -43
- data/spec/chrono_model/time_machine/transactions_spec.rb +0 -69
- data/spec/config.travis.yml +0 -5
- data/spec/config.yml.example +0 -9
- data/spec/spec_helper.rb +0 -33
- data/spec/support/adapter/helpers.rb +0 -53
- data/spec/support/adapter/structure.rb +0 -44
- data/spec/support/aruba.rb +0 -44
- data/spec/support/connection.rb +0 -70
- data/spec/support/matchers/base.rb +0 -56
- data/spec/support/matchers/column.rb +0 -99
- data/spec/support/matchers/function.rb +0 -79
- data/spec/support/matchers/index.rb +0 -69
- data/spec/support/matchers/schema.rb +0 -39
- data/spec/support/matchers/table.rb +0 -275
- data/spec/support/time_machine/helpers.rb +0 -47
- data/spec/support/time_machine/structure.rb +0 -111
- data/sql/json_ops.sql +0 -56
- 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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
25
|
+
chrono_metadata_set(table, options.merge(chronomodel: VERSION))
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
37
|
+
next if column.name == pk || default.nil?
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
execute "ALTER VIEW #{table} ALTER COLUMN #{quote_column_name(column.name)} SET DEFAULT #{default}"
|
|
40
|
+
end
|
|
37
41
|
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
columns = self.columns(table).map { |c| quote_column_name(c.name) }
|
|
43
|
+
columns.delete(quote_column_name(pk))
|
|
40
44
|
|
|
41
|
-
|
|
45
|
+
fields = columns.join(', ')
|
|
46
|
+
values = columns.map { |c| "NEW.#{c}" }.join(', ')
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
58
|
+
execute <<-SQL.squish
|
|
54
59
|
CREATE TABLE #{table} (
|
|
55
|
-
hid
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
add_history_validity_constraint(table, p_pkey)
|
|
64
|
+
SQL
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
end
|
|
66
|
+
add_history_validity_constraint(table, p_pkey)
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
end
|
|
68
|
+
chrono_create_history_indexes_for(table, p_pkey)
|
|
69
|
+
end
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
END IF;
|
|
87
|
+
BEGIN
|
|
88
|
+
#{insert_sequence_sql(pk, current)} INTO #{current} ( #{pk}, #{fields} )
|
|
89
|
+
VALUES ( NEW.#{pk}, #{values} );
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
+
INSERT INTO #{history} ( #{pk}, #{fields}, validity )
|
|
92
|
+
VALUES ( NEW.#{pk}, #{values}, tsrange(timezone('UTC', now()), NULL) );
|
|
91
93
|
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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 - [
|
|
133
|
+
columns - [quote_column_name('updated_at')]
|
|
133
134
|
end
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
journal &= columns
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs
|
|
138
139
|
CREATE OR REPLACE FUNCTION chronomodel_#{table}_update() RETURNS TRIGGER AS $$
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
197
|
-
|
|
201
|
+
UPDATE #{history} SET validity = tsrange(lower(validity), _now)
|
|
202
|
+
WHERE #{pk} = old.#{pk} AND upper_inf(validity);
|
|
198
203
|
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
DELETE FROM ONLY #{current}
|
|
205
|
+
WHERE #{pk} = old.#{pk};
|
|
201
206
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
+
FOR EACH ROW EXECUTE PROCEDURE chronomodel_#{table}_delete();
|
|
216
|
+
SQL
|
|
217
|
+
end
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
#
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
98
|
+
old_names = temporal_index_names(name, :validity) +
|
|
99
|
+
standard_index_names.map { |i| "#{name}_#{i}" }
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
new_names = temporal_index_names(new_name, :validity) +
|
|
102
|
+
standard_index_names.map { |i| "#{new_name}_#{i}" }
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
138
|
+
temporal_indexes.each do |index|
|
|
139
|
+
next if history_indexes.include?(index.name)
|
|
137
140
|
|
|
138
|
-
|
|
139
|
-
|
|
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} ( #{
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
#
|
|
166
|
-
# propagated both on the temporal table and the history one.
|
|
161
|
+
# When creating computed indexes
|
|
167
162
|
#
|
|
168
|
-
#
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|