chrono_model 3.0.1 → 5.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/README.md +104 -41
- data/lib/active_record/connection_adapters/chronomodel_adapter.rb +1 -1
- data/lib/active_record/tasks/chronomodel_database_tasks.rb +15 -2
- data/lib/chrono_model/adapter/ddl.rb +15 -16
- data/lib/chrono_model/adapter/indexes.rb +13 -15
- data/lib/chrono_model/adapter/migrations.rb +9 -11
- data/lib/chrono_model/adapter/migrations_modules/stable.rb +1 -1
- data/lib/chrono_model/adapter/upgrade.rb +7 -8
- data/lib/chrono_model/adapter.rb +9 -16
- data/lib/chrono_model/conversions.rb +0 -15
- data/lib/chrono_model/db_console.rb +1 -1
- data/lib/chrono_model/patches/association.rb +4 -4
- data/lib/chrono_model/patches/batches.rb +35 -1
- data/lib/chrono_model/patches/join_node.rb +6 -17
- data/lib/chrono_model/patches/preloader.rb +7 -33
- data/lib/chrono_model/patches/relation.rb +69 -31
- data/lib/chrono_model/patches.rb +7 -7
- data/lib/chrono_model/railtie.rb +1 -1
- data/lib/chrono_model/time_machine/history_model.rb +45 -19
- data/lib/chrono_model/time_machine/time_query.rb +4 -3
- data/lib/chrono_model/time_machine/timeline.rb +4 -6
- data/lib/chrono_model/time_machine.rb +5 -5
- data/lib/chrono_model/utilities.rb +3 -3
- data/lib/chrono_model/version.rb +1 -1
- data/lib/chrono_model.rb +14 -9
- metadata +3 -7
- data/lib/chrono_model/adapter/tsrange.rb +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6465bfc1eca17fb499f1826a7b938969bc7306b388ac93bce746a09c75a2c1a4
|
|
4
|
+
data.tar.gz: 6958a3018c4a29dbd4b81a56c60bc02d4fc8696780b62fe1ec8778725152e12d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4f0cf88eb3e21ec2cee85ba744a426e5ab125a71c18e4c7cbe31b45cbb5198f8f8dcbd1e17b87766cf3b386ebaf789fcb9dfd5190bb7e47bf61c3d9824fda490
|
|
7
|
+
data.tar.gz: e852d433e7d5c28f69e50511d0f094ae16fcb0f28cfc825273485c607b6e5fb2adc6d9cc82f0ec78c680c6e07392f296bfa27afcfe54d05595c3ac04a1a9e2cb
|
data/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Temporal database system on PostgreSQL using [updatable views][pg-updatable-views], [table inheritance][pg-table-inheritance] and [INSTEAD OF triggers][pg-instead-of-triggers].
|
|
2
2
|
|
|
3
3
|
[![Build Status][build-status-badge]][build-status]
|
|
4
|
-
[![Code Climate][code-analysis-badge]][code-analysis]
|
|
5
|
-
[![Test Coverage][test-coverage-badge]][test-coverage]
|
|
6
4
|
[![Gem Version][gem-version-badge]][gem-version]
|
|
7
5
|
[![Inlinedocs][docs-analysis-badge]][docs-analysis]
|
|
8
6
|
|
|
@@ -67,24 +65,19 @@ All timestamps are _forcibly_ stored in as UTC, bypassing the
|
|
|
67
65
|
* PostgreSQL >= 9.4
|
|
68
66
|
* The `btree_gist` PostgreSQL extension
|
|
69
67
|
|
|
70
|
-
With Homebrew:
|
|
71
|
-
|
|
72
|
-
brew install postgres
|
|
73
|
-
|
|
74
|
-
With apt:
|
|
75
|
-
|
|
76
|
-
apt-get install postgresql-11
|
|
77
|
-
|
|
78
68
|
## Installation
|
|
79
69
|
|
|
80
70
|
Add this line to your application's Gemfile:
|
|
81
71
|
|
|
82
|
-
|
|
72
|
+
```ruby
|
|
73
|
+
gem 'chrono_model'
|
|
74
|
+
```
|
|
83
75
|
|
|
84
76
|
And then execute:
|
|
85
77
|
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
```sh
|
|
79
|
+
$ bundle
|
|
80
|
+
```
|
|
88
81
|
|
|
89
82
|
## Configuration
|
|
90
83
|
|
|
@@ -99,10 +92,83 @@ development:
|
|
|
99
92
|
Configure Active Record in your `config/application.rb` to use the `:sql` schema
|
|
100
93
|
format:
|
|
101
94
|
|
|
102
|
-
```
|
|
95
|
+
```ruby
|
|
103
96
|
config.active_record.schema_format = :sql
|
|
104
97
|
```
|
|
105
98
|
|
|
99
|
+
## Database Permissions (PostgreSQL)
|
|
100
|
+
|
|
101
|
+
ChronoModel creates and manages data in the `temporal` and `history` schemas. Your application database user needs appropriate privileges on these schemas and their objects.
|
|
102
|
+
|
|
103
|
+
### Required Privileges
|
|
104
|
+
|
|
105
|
+
Grant the following privileges to your application database user (replace `app_user` with your actual username):
|
|
106
|
+
|
|
107
|
+
```sql
|
|
108
|
+
-- Schema access
|
|
109
|
+
GRANT USAGE ON SCHEMA temporal TO app_user;
|
|
110
|
+
GRANT USAGE ON SCHEMA history TO app_user;
|
|
111
|
+
|
|
112
|
+
-- Table privileges for existing objects
|
|
113
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA temporal TO app_user;
|
|
114
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA history TO app_user;
|
|
115
|
+
|
|
116
|
+
-- Sequence privileges for existing objects
|
|
117
|
+
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA temporal TO app_user;
|
|
118
|
+
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA history TO app_user;
|
|
119
|
+
|
|
120
|
+
-- Default privileges for future objects
|
|
121
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA temporal GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
|
|
122
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA history GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
|
|
123
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA temporal GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user;
|
|
124
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA history GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO app_user;
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Quick Diagnostics
|
|
128
|
+
|
|
129
|
+
You can verify your privileges are correctly set up by running these queries as your application user:
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
-- Check schema access
|
|
133
|
+
SELECT
|
|
134
|
+
schema_name,
|
|
135
|
+
has_schema_privilege(current_user, schema_name, 'USAGE') AS has_usage
|
|
136
|
+
FROM information_schema.schemata
|
|
137
|
+
WHERE schema_name IN ('temporal', 'history');
|
|
138
|
+
|
|
139
|
+
-- Check table privileges (run after creating temporal tables)
|
|
140
|
+
SELECT
|
|
141
|
+
schemaname,
|
|
142
|
+
tablename,
|
|
143
|
+
has_table_privilege(current_user, schemaname||'.'||tablename, 'SELECT') AS has_select,
|
|
144
|
+
has_table_privilege(current_user, schemaname||'.'||tablename, 'INSERT') AS has_insert,
|
|
145
|
+
has_table_privilege(current_user, schemaname||'.'||tablename, 'UPDATE') AS has_update,
|
|
146
|
+
has_table_privilege(current_user, schemaname||'.'||tablename, 'DELETE') AS has_delete
|
|
147
|
+
FROM pg_tables
|
|
148
|
+
WHERE schemaname IN ('temporal', 'history');
|
|
149
|
+
|
|
150
|
+
-- Check sequence privileges (run after creating temporal tables)
|
|
151
|
+
SELECT
|
|
152
|
+
schemaname,
|
|
153
|
+
sequencename,
|
|
154
|
+
has_sequence_privilege(current_user, schemaname||'.'||sequencename, 'USAGE') AS has_usage,
|
|
155
|
+
has_sequence_privilege(current_user, schemaname||'.'||sequencename, 'SELECT') AS has_select,
|
|
156
|
+
has_sequence_privilege(current_user, schemaname||'.'||sequencename, 'UPDATE') AS has_update
|
|
157
|
+
FROM pg_sequences
|
|
158
|
+
WHERE schemaname IN ('temporal', 'history');
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Troubleshooting
|
|
162
|
+
|
|
163
|
+
If you encounter these symptoms, check your database permissions:
|
|
164
|
+
|
|
165
|
+
- **`ActiveRecord::UnknownPrimaryKey`** errors on temporalized models
|
|
166
|
+
- **`Model.chrono?` returns `false`** for models that should be temporal
|
|
167
|
+
- **Unexpected primary key or temporalization issues** during schema operations
|
|
168
|
+
- **Permission denied errors** when ChronoModel tries to access temporal/history objects
|
|
169
|
+
|
|
170
|
+
These issues often indicate insufficient privileges on the `temporal` and `history` schemas. Run the diagnostic queries above to identify missing privileges, then apply the appropriate `GRANT` statements.
|
|
171
|
+
|
|
106
172
|
## Schema creation
|
|
107
173
|
|
|
108
174
|
ChronoModel hooks all `ActiveRecord::Migration` methods to make them temporal
|
|
@@ -121,7 +187,7 @@ view and all the trigger machinery. Every other housekeeping of the temporal
|
|
|
121
187
|
structure is handled behind the scenes by the other schema statements. E.g.:
|
|
122
188
|
|
|
123
189
|
* `rename_table` - renames tables, views, sequences, indexes and triggers
|
|
124
|
-
* `drop_table` - drops the temporal table and all
|
|
190
|
+
* `drop_table` - drops the temporal table and all dependent objects
|
|
125
191
|
* `add_column` - adds the column to the current table and updates triggers
|
|
126
192
|
* `rename_column` - renames the current table column and updates the triggers
|
|
127
193
|
* `remove_column` - removes the current table column and updates the triggers
|
|
@@ -151,8 +217,7 @@ the `:validity` option:
|
|
|
151
217
|
change_table :your_table, temporal: true, copy_data: true, validity: '1977-01-01'
|
|
152
218
|
```
|
|
153
219
|
|
|
154
|
-
Please note that `change_table` requires you to use
|
|
155
|
-
`down` migrations. It cannot work with Rails 3-style `change` migrations.
|
|
220
|
+
Please note that `change_table` requires you to use `up` and `down` migrations.
|
|
156
221
|
|
|
157
222
|
|
|
158
223
|
## Selective Journaling
|
|
@@ -192,10 +257,12 @@ occur (see https://github.com/ifad/chronomodel/issues/71).
|
|
|
192
257
|
In such cases, ensure to add `no_journal: %w( your_counter_cache_column_name )`
|
|
193
258
|
to your `create_table`. Example:
|
|
194
259
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
260
|
+
```ruby
|
|
261
|
+
create_table 'sections', temporal: true, no_journal: %w[articles_count] do |t|
|
|
262
|
+
t.string :name
|
|
263
|
+
t.integer :articles_count, default: 0
|
|
264
|
+
end
|
|
265
|
+
```
|
|
199
266
|
|
|
200
267
|
## Data querying
|
|
201
268
|
|
|
@@ -282,12 +349,12 @@ cannot be deleted.
|
|
|
282
349
|
|
|
283
350
|
ChronoModel currently performs upgrades by dropping and re-creating the views
|
|
284
351
|
that give access to current data. If you have built other database objects on
|
|
285
|
-
these views, the upgrade cannot be performed automatically as the
|
|
352
|
+
these views, the upgrade cannot be performed automatically as the dependent
|
|
286
353
|
objects must be dropped first.
|
|
287
354
|
|
|
288
355
|
When booting, ChronoModel will issue a warning in your logs about the need of
|
|
289
356
|
a structure upgrade. Structure usually changes across versions. In this case,
|
|
290
|
-
you need to set up a rake task that drops your
|
|
357
|
+
you need to set up a rake task that drops your dependent objects, runs
|
|
291
358
|
ChronoModel.upgrade! and then re-creates them.
|
|
292
359
|
|
|
293
360
|
A migration system should be introduced, but it is seen as overkill for now,
|
|
@@ -304,7 +371,9 @@ You need to connect as a database superuser, because specs need to create the
|
|
|
304
371
|
|
|
305
372
|
To run the full test suite, use
|
|
306
373
|
|
|
307
|
-
|
|
374
|
+
```sh
|
|
375
|
+
$ rake
|
|
376
|
+
```
|
|
308
377
|
|
|
309
378
|
SQL queries are logged to `spec/debug.log`. If you want to see them in your
|
|
310
379
|
output, set the `VERBOSE=true` environment variable.
|
|
@@ -313,7 +382,9 @@ Some tests check the nominal execution of rake tasks within a test Rails app,
|
|
|
313
382
|
and those are quite time consuming. You can run the full ChronoModel tests
|
|
314
383
|
only against ActiveRecord by using
|
|
315
384
|
|
|
316
|
-
|
|
385
|
+
```sh
|
|
386
|
+
$ rspec spec/chrono_model
|
|
387
|
+
```
|
|
317
388
|
|
|
318
389
|
Ensure to run the full test suite before pushing.
|
|
319
390
|
|
|
@@ -328,10 +399,6 @@ Ensure to run the full test suite before pushing.
|
|
|
328
399
|
of an object at a specific point in time within the application could
|
|
329
400
|
lead to inaccuracies.
|
|
330
401
|
|
|
331
|
-
* Rails 4+ support requires disabling tsrange parsing support, as it
|
|
332
|
-
[is broken][r4-tsrange-broken] and [incomplete][r4-tsrange-incomplete]
|
|
333
|
-
as of now, mainly due to a [design clash with ruby][pg-tsrange-and-ruby].
|
|
334
|
-
|
|
335
402
|
* The triggers and temporal indexes cannot be saved in schema.rb. The AR
|
|
336
403
|
schema dumper is quite basic, and it isn't (currently) extensible.
|
|
337
404
|
As we're using many database-specific features, Chronomodel forces the
|
|
@@ -350,9 +417,6 @@ Ensure to run the full test suite before pushing.
|
|
|
350
417
|
|
|
351
418
|
* Foreign keys are not supported. [See issue #174][gh-issue-174]
|
|
352
419
|
|
|
353
|
-
* There may be unexpected results when combining eager loading and joins.
|
|
354
|
-
[See issue #186][gh-issue-186]
|
|
355
|
-
|
|
356
420
|
* Global ID ignores historical objects. [See issue #192][gh-issue-192]
|
|
357
421
|
|
|
358
422
|
* Different historical objects are considered the identical. [See issue
|
|
@@ -362,6 +426,12 @@ Ensure to run the full test suite before pushing.
|
|
|
362
426
|
creates a new record for each modification. This will lead to increased
|
|
363
427
|
storage requirements and bloated history
|
|
364
428
|
|
|
429
|
+
* `*_by_sql` query methods are not supported. [See issue #313][gh-issue-313]
|
|
430
|
+
|
|
431
|
+
* `self.table_name` must be set before `include ChronoModel::TimeMachine`.
|
|
432
|
+
[See issue #336][gh-issue-336]
|
|
433
|
+
|
|
434
|
+
|
|
365
435
|
## Contributing
|
|
366
436
|
|
|
367
437
|
1. Fork it
|
|
@@ -386,14 +456,10 @@ This software is Made in Italy :it: :smile:.
|
|
|
386
456
|
|
|
387
457
|
[build-status]: https://github.com/ifad/chronomodel/actions
|
|
388
458
|
[build-status-badge]: https://github.com/ifad/chronomodel/actions/workflows/ruby.yml/badge.svg
|
|
389
|
-
[code-analysis]: https://codeclimate.com/github/ifad/chronomodel/maintainability
|
|
390
|
-
[code-analysis-badge]: https://api.codeclimate.com/v1/badges/cdee7327938dc2eaff99/maintainability
|
|
391
459
|
[docs-analysis]: https://inch-ci.org/github/ifad/chronomodel
|
|
392
460
|
[docs-analysis-badge]: https://inch-ci.org/github/ifad/chronomodel.svg?branch=master
|
|
393
461
|
[gem-version]: https://rubygems.org/gems/chrono_model
|
|
394
462
|
[gem-version-badge]: https://badge.fury.io/rb/chrono_model.svg
|
|
395
|
-
[test-coverage]: https://codeclimate.com/github/ifad/chronomodel
|
|
396
|
-
[test-coverage-badge]: https://codeclimate.com/github/ifad/chronomodel/badges/coverage.svg
|
|
397
463
|
|
|
398
464
|
[delorean-image]: https://i.imgur.com/DD77F4s.jpg
|
|
399
465
|
|
|
@@ -412,7 +478,6 @@ This software is Made in Italy :it: :smile:.
|
|
|
412
478
|
[pg-exclusion-constraints]: https://www.postgresql.org/docs/9.4/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
|
|
413
479
|
[pg-btree-gist]: https://www.postgresql.org/docs/9.4/btree-gist.html
|
|
414
480
|
[pg-comment]: https://www.postgresql.org/docs/9.4/sql-comment.html
|
|
415
|
-
[pg-tsrange-and-ruby]: https://bugs.ruby-lang.org/issues/6864
|
|
416
481
|
[pg-ctes]: https://www.postgresql.org/docs/9.4/queries-with.html
|
|
417
482
|
[pg-cte-optimization-fence]: https://www.postgresql.org/message-id/201209191305.44674.db@kavod.com
|
|
418
483
|
[pg-cte-opt-out-fence]: https://www.postgresql.org/message-id/CAHyXU0zpM5+Dsb_pKxDmm-ZoWUAt=SkHHaiK_DBqcmtxTas6Nw@mail.gmail.com
|
|
@@ -420,15 +485,13 @@ This software is Made in Italy :it: :smile:.
|
|
|
420
485
|
[pg-json-func]: https://www.postgresql.org/docs/9.4/functions-json.html
|
|
421
486
|
[pg-json-opclass]: https://github.com/ifad/chronomodel/blob/master/sql/json_ops.sql
|
|
422
487
|
|
|
423
|
-
[r4-tsrange-broken]: https://github.com/rails/rails/pull/13793#issuecomment-34608093
|
|
424
|
-
[r4-tsrange-incomplete]: https://github.com/rails/rails/issues/14010
|
|
425
|
-
|
|
426
488
|
[cm-readme-sql]: https://github.com/ifad/chronomodel/blob/master/README.sql
|
|
427
489
|
[cm-timemachine]: https://github.com/ifad/chronomodel/blob/master/lib/chrono_model/time_machine.rb
|
|
428
490
|
[cm-cte-impl]: https://github.com/ifad/chronomodel/commit/18f4c4b
|
|
429
491
|
|
|
430
492
|
[gh-pzac]: https://github.com/pzac
|
|
431
493
|
[gh-issue-174]: https://github.com/ifad/chronomodel/issues/174
|
|
432
|
-
[gh-issue-186]: https://github.com/ifad/chronomodel/issues/186
|
|
433
494
|
[gh-issue-192]: https://github.com/ifad/chronomodel/issues/192
|
|
434
495
|
[gh-issue-206]: https://github.com/ifad/chronomodel/issues/206
|
|
496
|
+
[gh-issue-313]: https://github.com/ifad/chronomodel/issues/313
|
|
497
|
+
[gh-issue-336]: https://github.com/ifad/chronomodel/issues/336
|
|
@@ -32,7 +32,7 @@ module ActiveRecord
|
|
|
32
32
|
args = ['-c', '-f', target.to_s]
|
|
33
33
|
args << chronomodel_configuration[:database]
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
run_cmd_with_compatibility('pg_dump', args, 'dumping data')
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def data_load(source)
|
|
@@ -41,7 +41,7 @@ module ActiveRecord
|
|
|
41
41
|
args = ['-f', source]
|
|
42
42
|
args << chronomodel_configuration[:database]
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
run_cmd_with_compatibility('psql', args, 'loading data')
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
private
|
|
@@ -50,6 +50,19 @@ module ActiveRecord
|
|
|
50
50
|
@chronomodel_configuration ||= @configuration_hash
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# TODO: replace `run_cmd_with_compatibility` with `run_cmd` and remove when dropping Rails < 8.1 support
|
|
54
|
+
# Compatibility method to handle Rails version differences in run_cmd signature
|
|
55
|
+
# Rails < 8.1: run_cmd(cmd, args, action)
|
|
56
|
+
# Rails >= 8.1: run_cmd(cmd, *args, **opts)
|
|
57
|
+
def run_cmd_with_compatibility(cmd, args, action_description)
|
|
58
|
+
# Check if run_cmd method accepts keyword arguments (new signature)
|
|
59
|
+
if method(:run_cmd).parameters.any? { |type, _name| type == :rest }
|
|
60
|
+
run_cmd(cmd, *args)
|
|
61
|
+
else
|
|
62
|
+
run_cmd(cmd, args, action_description)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
53
66
|
# If a schema search path is defined in the configuration file, it will
|
|
54
67
|
# be used by the database tasks class to dump only the specified search
|
|
55
68
|
# path. Here we add also ChronoModel's temporal and history schemas to
|
|
@@ -55,12 +55,12 @@ module ChronoModel
|
|
|
55
55
|
parent = "#{TEMPORAL_SCHEMA}.#{table}"
|
|
56
56
|
p_pkey = primary_key(parent)
|
|
57
57
|
|
|
58
|
-
execute
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
execute <<~SQL.squish
|
|
59
|
+
CREATE TABLE #{table} (
|
|
60
|
+
hid BIGSERIAL PRIMARY KEY,
|
|
61
|
+
validity tsrange NOT NULL,
|
|
62
|
+
recorded_at timestamp NOT NULL DEFAULT timezone('UTC', now())
|
|
63
|
+
) INHERITS (#{parent})
|
|
64
64
|
SQL
|
|
65
65
|
|
|
66
66
|
add_history_validity_constraint(table, p_pkey)
|
|
@@ -85,11 +85,11 @@ module ChronoModel
|
|
|
85
85
|
execute <<-SQL.strip_heredoc # rubocop:disable Rails/SquishedSQLHeredocs,Rails/StripHeredoc
|
|
86
86
|
CREATE OR REPLACE FUNCTION chronomodel_#{table}_insert() RETURNS TRIGGER AS $$
|
|
87
87
|
BEGIN
|
|
88
|
-
#{insert_sequence_sql(pk, current)} INTO #{current} (
|
|
89
|
-
VALUES (
|
|
88
|
+
#{insert_sequence_sql(pk, current)} INTO #{current} (#{pk}, #{fields})
|
|
89
|
+
VALUES (NEW.#{pk}, #{values});
|
|
90
90
|
|
|
91
|
-
INSERT INTO #{history} (
|
|
92
|
-
VALUES (
|
|
91
|
+
INSERT INTO #{history} (#{pk}, #{fields}, validity)
|
|
92
|
+
VALUES (NEW.#{pk}, #{values}, tsrange(timezone('UTC', now()), NULL));
|
|
93
93
|
|
|
94
94
|
RETURN NEW;
|
|
95
95
|
END;
|
|
@@ -150,7 +150,7 @@ module ChronoModel
|
|
|
150
150
|
_new := row(#{journal.map { |c| "NEW.#{c}" }.join(', ')});
|
|
151
151
|
|
|
152
152
|
IF _old IS NOT DISTINCT FROM _new THEN
|
|
153
|
-
UPDATE ONLY #{current} SET (
|
|
153
|
+
UPDATE ONLY #{current} SET (#{fields}) = (#{values}) WHERE #{pk} = OLD.#{pk};
|
|
154
154
|
RETURN NEW;
|
|
155
155
|
END IF;
|
|
156
156
|
|
|
@@ -160,16 +160,16 @@ module ChronoModel
|
|
|
160
160
|
#{"SELECT hid INTO _hid FROM #{history} WHERE #{pk} = OLD.#{pk} AND lower(validity) = _now;" unless ENV['CHRONOMODEL_NO_SQUASH']}
|
|
161
161
|
|
|
162
162
|
IF _hid IS NOT NULL THEN
|
|
163
|
-
UPDATE #{history} SET (
|
|
163
|
+
UPDATE #{history} SET (#{fields}) = (#{values}) WHERE hid = _hid;
|
|
164
164
|
ELSE
|
|
165
165
|
UPDATE #{history} SET validity = tsrange(lower(validity), _now)
|
|
166
166
|
WHERE #{pk} = OLD.#{pk} AND upper_inf(validity);
|
|
167
167
|
|
|
168
|
-
INSERT INTO #{history} (
|
|
169
|
-
VALUES (
|
|
168
|
+
INSERT INTO #{history} (#{pk}, #{fields}, validity)
|
|
169
|
+
VALUES (OLD.#{pk}, #{values}, tsrange(_now, NULL));
|
|
170
170
|
END IF;
|
|
171
171
|
|
|
172
|
-
UPDATE ONLY #{current} SET (
|
|
172
|
+
UPDATE ONLY #{current} SET (#{fields}) = (#{values}) WHERE #{pk} = OLD.#{pk};
|
|
173
173
|
|
|
174
174
|
RETURN NEW;
|
|
175
175
|
END;
|
|
@@ -234,7 +234,6 @@ module ChronoModel
|
|
|
234
234
|
INSERT
|
|
235
235
|
SQL
|
|
236
236
|
end
|
|
237
|
-
# private
|
|
238
237
|
end
|
|
239
238
|
end
|
|
240
239
|
end
|
|
@@ -24,15 +24,13 @@ module ChronoModel
|
|
|
24
24
|
temporal_index_names(table, range, options)
|
|
25
25
|
|
|
26
26
|
chrono_alter_index(table, options) do
|
|
27
|
-
execute
|
|
28
|
-
CREATE INDEX #{range_idx} ON #{table} USING gist ( #{range} )
|
|
29
|
-
SQL
|
|
27
|
+
execute "CREATE INDEX #{range_idx} ON #{table} USING gist (#{range})"
|
|
30
28
|
|
|
31
29
|
# Indexes used for precise history filtering, sorting and, in history
|
|
32
30
|
# tables, by UPDATE / DELETE triggers.
|
|
33
31
|
#
|
|
34
|
-
execute "CREATE INDEX #{lower_idx} ON #{table} (
|
|
35
|
-
execute "CREATE INDEX #{upper_idx} ON #{table} (
|
|
32
|
+
execute "CREATE INDEX #{lower_idx} ON #{table} (lower(#{range}))"
|
|
33
|
+
execute "CREATE INDEX #{upper_idx} ON #{table} (upper(#{range}))"
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
|
@@ -53,9 +51,9 @@ module ChronoModel
|
|
|
53
51
|
id = options[:id] || primary_key(table)
|
|
54
52
|
|
|
55
53
|
chrono_alter_constraint(table, options) do
|
|
56
|
-
execute
|
|
54
|
+
execute <<~SQL.squish
|
|
57
55
|
ALTER TABLE #{table} ADD CONSTRAINT #{name}
|
|
58
|
-
EXCLUDE USING gist (
|
|
56
|
+
EXCLUDE USING gist (#{id} WITH =, #{range} WITH &&)
|
|
59
57
|
SQL
|
|
60
58
|
end
|
|
61
59
|
end
|
|
@@ -64,7 +62,7 @@ module ChronoModel
|
|
|
64
62
|
name = timeline_consistency_constraint_name(table)
|
|
65
63
|
|
|
66
64
|
chrono_alter_constraint(table, options) do
|
|
67
|
-
execute
|
|
65
|
+
execute <<~SQL.squish
|
|
68
66
|
ALTER TABLE #{table} DROP CONSTRAINT #{name}
|
|
69
67
|
SQL
|
|
70
68
|
end
|
|
@@ -81,9 +79,9 @@ module ChronoModel
|
|
|
81
79
|
def chrono_create_history_indexes_for(table, p_pkey)
|
|
82
80
|
add_temporal_indexes table, :validity, on_current_schema: true
|
|
83
81
|
|
|
84
|
-
execute "CREATE INDEX #{table}_inherit_pkey ON #{table} (
|
|
85
|
-
execute "CREATE INDEX #{table}_recorded_at ON #{table} (
|
|
86
|
-
execute "CREATE INDEX #{table}_instance_history ON #{table} (
|
|
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)"
|
|
87
85
|
end
|
|
88
86
|
|
|
89
87
|
# Rename indexes on history schema
|
|
@@ -144,10 +142,10 @@ module ChronoModel
|
|
|
144
142
|
#
|
|
145
143
|
columns = Array.wrap(index.columns).join(', ')
|
|
146
144
|
|
|
147
|
-
execute
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
execute <<~SQL.squish, 'Copy index from temporal to history'
|
|
146
|
+
CREATE INDEX #{index.name} ON #{table_name}
|
|
147
|
+
USING #{index.using} (#{columns})
|
|
148
|
+
SQL
|
|
151
149
|
end
|
|
152
150
|
end
|
|
153
151
|
end
|
|
@@ -84,7 +84,7 @@ module ChronoModel
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
else
|
|
87
|
-
if is_chrono?(table_name)
|
|
87
|
+
if is_chrono?(table_name) && options[:temporal] == false
|
|
88
88
|
chrono_undo_temporal_table(table_name)
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -214,14 +214,14 @@ module ChronoModel
|
|
|
214
214
|
seq = on_history_schema { pk_and_sequence_for(table_name).last.to_s }
|
|
215
215
|
from = options[:validity] || '0001-01-01 00:00:00'
|
|
216
216
|
|
|
217
|
-
execute
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
217
|
+
execute <<~SQL.squish
|
|
218
|
+
INSERT INTO #{HISTORY_SCHEMA}.#{table_name}
|
|
219
|
+
SELECT *,
|
|
220
|
+
nextval('#{seq}') AS hid,
|
|
221
|
+
tsrange('#{from}', NULL) AS validity,
|
|
222
|
+
timezone('UTC', now()) AS recorded_at
|
|
223
|
+
FROM #{TEMPORAL_SCHEMA}.#{table_name}
|
|
224
|
+
SQL
|
|
225
225
|
end
|
|
226
226
|
|
|
227
227
|
# Removes temporal features from this table
|
|
@@ -252,8 +252,6 @@ module ChronoModel
|
|
|
252
252
|
execute "ALTER SEQUENCE #{seq} RENAME TO #{new_seq}"
|
|
253
253
|
execute "ALTER TABLE #{name} RENAME TO #{new_name}"
|
|
254
254
|
end
|
|
255
|
-
|
|
256
|
-
# private
|
|
257
255
|
end
|
|
258
256
|
end
|
|
259
257
|
end
|
|
@@ -17,7 +17,7 @@ module ChronoModel
|
|
|
17
17
|
# Uniqueness constraints do not make sense in the history table
|
|
18
18
|
options = options.dup.tap { |o| o.delete(:unique) } if options[:unique].present?
|
|
19
19
|
|
|
20
|
-
on_history_schema { super
|
|
20
|
+
on_history_schema { super }
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -42,7 +42,7 @@ module ChronoModel
|
|
|
42
42
|
return if upgrade.empty?
|
|
43
43
|
|
|
44
44
|
logger.warn 'ChronoModel: There are tables needing a structure upgrade, and ChronoModel structures need to be recreated.'
|
|
45
|
-
logger.warn 'ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have
|
|
45
|
+
logger.warn 'ChronoModel: Please run ChronoModel.upgrade! to attempt the upgrade. If you have dependent database objects'
|
|
46
46
|
logger.warn 'ChronoModel: the upgrade will fail and you have to drop the dependent objects, run .upgrade! and create them'
|
|
47
47
|
logger.warn 'ChronoModel: again. Sorry. Some features or the whole library may not work correctly until upgrade is complete.'
|
|
48
48
|
logger.warn "ChronoModel: Tables pending upgrade: #{upgrade}"
|
|
@@ -81,12 +81,12 @@ module ChronoModel
|
|
|
81
81
|
p_pkey = primary_key(table_name)
|
|
82
82
|
|
|
83
83
|
execute "ALTER TABLE #{history_table} ADD COLUMN validity tsrange;"
|
|
84
|
-
execute
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
execute <<~SQL.squish
|
|
85
|
+
UPDATE #{history_table} SET validity = tsrange(valid_from,
|
|
86
|
+
CASE WHEN extract(year from valid_to) = 9999 THEN NULL
|
|
87
|
+
ELSE valid_to
|
|
88
|
+
END
|
|
89
|
+
);
|
|
90
90
|
SQL
|
|
91
91
|
|
|
92
92
|
execute "DROP INDEX #{history_table}_temporal_on_valid_from;"
|
|
@@ -112,7 +112,6 @@ module ChronoModel
|
|
|
112
112
|
on_history_schema { add_history_validity_constraint(table_name, p_pkey) }
|
|
113
113
|
on_history_schema { chrono_create_history_indexes_for(table_name, p_pkey) }
|
|
114
114
|
end
|
|
115
|
-
# private
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
117
|
end
|
data/lib/chrono_model/adapter.rb
CHANGED
|
@@ -2,13 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_record/connection_adapters/postgresql_adapter'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
require_relative 'adapter/migrations'
|
|
6
|
+
require_relative 'adapter/migrations_modules/stable'
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
require 'chrono_model/adapter/upgrade'
|
|
8
|
+
require_relative 'adapter/ddl'
|
|
9
|
+
require_relative 'adapter/indexes'
|
|
10
|
+
require_relative 'adapter/upgrade'
|
|
12
11
|
|
|
13
12
|
module ChronoModel
|
|
14
13
|
# This class implements all ActiveRecord::ConnectionAdapters::SchemaStatements
|
|
@@ -19,7 +18,6 @@ module ChronoModel
|
|
|
19
18
|
include ChronoModel::Adapter::Migrations
|
|
20
19
|
include ChronoModel::Adapter::DDL
|
|
21
20
|
include ChronoModel::Adapter::Indexes
|
|
22
|
-
include ChronoModel::Adapter::TSRange
|
|
23
21
|
include ChronoModel::Adapter::Upgrade
|
|
24
22
|
|
|
25
23
|
# The schema holding current data
|
|
@@ -89,15 +87,10 @@ module ChronoModel
|
|
|
89
87
|
# The default search path is included however, since the table
|
|
90
88
|
# may reference types defined in other schemas, which result in their
|
|
91
89
|
# names becoming schema qualified, which will cause type resolutions to fail.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
def column_definitions; end
|
|
96
|
-
|
|
97
|
-
define_method(:column_definitions) do |table_name|
|
|
98
|
-
return super(table_name) unless is_chrono?(table_name)
|
|
90
|
+
def column_definitions(table_name)
|
|
91
|
+
return super unless is_chrono?(table_name)
|
|
99
92
|
|
|
100
|
-
on_schema("#{TEMPORAL_SCHEMA},#{schema_search_path}", recurse: :ignore) { super
|
|
93
|
+
on_schema("#{TEMPORAL_SCHEMA},#{schema_search_path}", recurse: :ignore) { super }
|
|
101
94
|
end
|
|
102
95
|
|
|
103
96
|
# Evaluates the given block in the temporal schema.
|
|
@@ -172,7 +165,7 @@ module ChronoModel
|
|
|
172
165
|
def chrono_metadata_set(view_name, metadata)
|
|
173
166
|
comment = MultiJson.dump(metadata)
|
|
174
167
|
|
|
175
|
-
execute
|
|
168
|
+
execute "COMMENT ON VIEW #{view_name} IS #{quote(comment)}"
|
|
176
169
|
end
|
|
177
170
|
|
|
178
171
|
def valid_table_definition_options
|
|
@@ -4,21 +4,6 @@ module ChronoModel
|
|
|
4
4
|
module Conversions
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(?:\.(\d+))?\z/
|
|
8
|
-
|
|
9
|
-
# rubocop:disable Style/PerlBackrefs
|
|
10
|
-
def string_to_utc_time(string)
|
|
11
|
-
return string if string.is_a?(Time)
|
|
12
|
-
|
|
13
|
-
return unless string =~ ISO_DATETIME
|
|
14
|
-
|
|
15
|
-
# .1 is .100000, not .000001
|
|
16
|
-
usec = $7.ljust(6, '0') unless $7.nil?
|
|
17
|
-
|
|
18
|
-
Time.utc $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec.to_i
|
|
19
|
-
end
|
|
20
|
-
# rubocop:enable Style/PerlBackrefs
|
|
21
|
-
|
|
22
7
|
def time_to_utc_string(time)
|
|
23
8
|
time.to_fs(:db) << '.' << format('%06d', time.usec)
|
|
24
9
|
end
|
|
@@ -11,10 +11,6 @@ module ChronoModel
|
|
|
11
11
|
# on the join model's (:through association) one.
|
|
12
12
|
#
|
|
13
13
|
module Association
|
|
14
|
-
def skip_statement_cache?(*)
|
|
15
|
-
super || _chrono_target?
|
|
16
|
-
end
|
|
17
|
-
|
|
18
14
|
# If the association class or the through association are ChronoModels,
|
|
19
15
|
# then fetches the records from a virtual table using a subquery scope
|
|
20
16
|
# to a specific timestamp.
|
|
@@ -36,6 +32,10 @@ module ChronoModel
|
|
|
36
32
|
|
|
37
33
|
private
|
|
38
34
|
|
|
35
|
+
def skip_statement_cache?(*)
|
|
36
|
+
super || _chrono_target?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
39
|
def _chrono_record?
|
|
40
40
|
owner.class.include?(ChronoModel::Patches::AsOfTimeHolder) && owner.as_of_time.present?
|
|
41
41
|
end
|