activerecord-temporal 0.2.0 → 0.4.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/CHANGELOG.md +11 -0
- data/README.md +25 -13
- data/lib/activerecord/temporal/application_versioning/application_versioned.rb +3 -3
- data/lib/activerecord/temporal/application_versioning.rb +2 -0
- data/lib/activerecord/temporal/patches/join_dependency.rb +3 -2
- data/lib/activerecord/temporal/system_versioning/command_recorder.rb +1 -27
- data/lib/activerecord/temporal/system_versioning/schema_creation.rb +6 -3
- data/lib/activerecord/temporal/system_versioning/schema_definitions.rb +10 -8
- data/lib/activerecord/temporal/system_versioning/schema_statements.rb +32 -66
- data/lib/activerecord/temporal/version.rb +1 -1
- data/lib/activerecord/temporal.rb +19 -25
- metadata +2 -27
- data/lib/activerecord/temporal/application_versioning/command_recorder.rb +0 -14
- data/lib/activerecord/temporal/application_versioning/migration.rb +0 -25
- data/lib/activerecord/temporal/application_versioning/schema_statements.rb +0 -33
- data/lib/activerecord/temporal/patches/command_recorder.rb +0 -23
- data/lib/activerecord/temporal/system_versioning/migration.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 230f188aed78df98d3121bf21436905bb21cb314b6626c7f741eaf40f250e43c
|
|
4
|
+
data.tar.gz: 7c8822ad52d0d529222c0a619f2fdfae3436485ce6c479b61095e3d0bdc5b9e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4db33aa953f22cba846b1742b263f4051fea0b58cdbe92332a6d3b54f3fab4b64ffdb3edb2c3bd8de4dd4a84724a312d7cff589619495f54594c123ed152948f
|
|
7
|
+
data.tar.gz: 0636ff55084af4222059a8d5235df0eb538bee8132fb5a80c9f7bc58579c6b05f790d5150b5f8a1e3d8f39fecb80404acf679587b4ed1056169fa6cb9fa7f852
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2025-12-02
|
|
4
|
+
|
|
5
|
+
- Nest `HistoryModelNamespace` directly in `ActiveRecord::Temporal` as per the README
|
|
6
|
+
- Fixed issue where including `ActiveRecord::Temporal` would not extend the base module properly
|
|
7
|
+
|
|
8
|
+
## [0.3.0] - 2025-11-20
|
|
9
|
+
|
|
10
|
+
- Application versioning: Raise `ClosedRevisionError` when trying to revise or inactive closed versions.
|
|
11
|
+
- System versioning: Store gem version in versioning hooks
|
|
12
|
+
- System versioning: Add assertion for matching primary key on versioning hook creation
|
|
13
|
+
|
|
3
14
|
## [0.2.0] - 2025-11-19
|
|
4
15
|
|
|
5
16
|
- Added ambient/global time scopes
|
data/README.md
CHANGED
|
@@ -8,18 +8,19 @@ It provides both system versioning and application versioning. They can be used
|
|
|
8
8
|
|
|
9
9
|
As applications mature, changing business requirements become increasingly complicated by the need to handle historical data. You might need to:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
- Allow users to see
|
|
13
|
-
-
|
|
14
|
-
-
|
|
11
|
+
- Know the price of a product at the time it was added to a cart
|
|
12
|
+
- Allow users to see content as it was before their subscription ended
|
|
13
|
+
- Track the lifetime of long-lived, slowly changing entities like projects or customers
|
|
14
|
+
- Generate reports based on the data as it was known at some time in the past
|
|
15
15
|
|
|
16
16
|
Many Rails applications use a patchwork of approaches:
|
|
17
17
|
|
|
18
18
|
- **Soft deletes** with a `deleted_at` column, but updates that still permanently overwrite data.
|
|
19
|
-
- **Audit gems or JSON
|
|
19
|
+
- **Audit gems or JSON fields** that serialize rows. Their data doesn't evolve with schema changes and can't be easily integrated into Active Record queries, scopes, and associations.
|
|
20
20
|
- **Event systems** that are used to fill gaps in the data model and gradually take on responsibilities that are implementation details with no business relevance.
|
|
21
|
+
- **Ad-hoc snapshot columns** that result in important business entities having their historical data duplicated across many different and incohesive tables.
|
|
21
22
|
|
|
22
|
-
Temporal
|
|
23
|
+
Temporal tables address these problems by providing a simple and coherent data model to reach for whenever historical data is needed.
|
|
23
24
|
|
|
24
25
|
This can be a versioning strategy that operates automatically at the database level or one where versioning is used up front as the default method for all CRUD operations on a table.
|
|
25
26
|
|
|
@@ -28,6 +29,10 @@ This can be a versioning strategy that operates automatically at the database le
|
|
|
28
29
|
- Active Record >= 8
|
|
29
30
|
- PostgreSQL >= 13
|
|
30
31
|
|
|
32
|
+
## Stability
|
|
33
|
+
|
|
34
|
+
⚠️ Currently a beta release. Breaking changes will bump the minor version until the 1.x.x release.
|
|
35
|
+
|
|
31
36
|
## Quick Start
|
|
32
37
|
|
|
33
38
|
```ruby
|
|
@@ -35,8 +40,15 @@ This can be a versioning strategy that operates automatically at the database le
|
|
|
35
40
|
|
|
36
41
|
gem "activerecord-temporal"
|
|
37
42
|
```
|
|
43
|
+
### Versioning Strategies
|
|
44
|
+
|
|
45
|
+
This gem supports two versioning strategies:
|
|
46
|
+
1. **System versioning**, where database triggers automatically maintain a separate history table that records transaction time.
|
|
47
|
+
2. **Application versioning**, where versioning is managed by the application using a single table whose time dimension can represent validity or any other temporal business concept.
|
|
48
|
+
|
|
49
|
+
### Creating a System Versioned Table
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
Make sure you're using the `:sql` schema dumper.
|
|
40
52
|
|
|
41
53
|
Create your regular `employees` table. For the `employees_history` table, add the `system_period` column and include it in the table's primary key. `#create_versioning_hook` is what enables system versioning.
|
|
42
54
|
|
|
@@ -118,7 +130,7 @@ Employee.history.as_of(Time.parse("2000-01-10"))
|
|
|
118
130
|
- [System Versioning](#system-versioning)
|
|
119
131
|
- [History Model Namespace](#history-model-namespace)
|
|
120
132
|
|
|
121
|
-
###
|
|
133
|
+
### Creating an Application Versioned Table
|
|
122
134
|
|
|
123
135
|
Create an `employees` table with a `version` column in the primary key and a `tstzrange` column to be the time dimension.
|
|
124
136
|
|
|
@@ -189,7 +201,7 @@ Employee.as_of(Time.parse("2000-02-15"))
|
|
|
189
201
|
- [Application Versioning](#application-versioning)
|
|
190
202
|
- [Foreign Key Constraints](#foreign-key-constraints)
|
|
191
203
|
|
|
192
|
-
###
|
|
204
|
+
### Making Time-travel Queries
|
|
193
205
|
|
|
194
206
|
This interface works the same with system versioning and application. But this example assumes at least the `Product` and `Order` models are system versioned:
|
|
195
207
|
|
|
@@ -412,7 +424,7 @@ class LineItem < ApplicationRecord
|
|
|
412
424
|
end
|
|
413
425
|
|
|
414
426
|
module History
|
|
415
|
-
include Temporal::
|
|
427
|
+
include ActiveRecord::Temporal::HistoryModelNamespace
|
|
416
428
|
end
|
|
417
429
|
|
|
418
430
|
History::Product # => History::Product(id: integer, system_period: tstzrange, name: string)
|
|
@@ -442,7 +454,7 @@ By default, calling `system_versioning` will look for a namespace called `Histor
|
|
|
442
454
|
|
|
443
455
|
```ruby
|
|
444
456
|
module Versions
|
|
445
|
-
include Temporal::
|
|
457
|
+
include ActiveRecord::Temporal::HistoryModelNamespace
|
|
446
458
|
end
|
|
447
459
|
|
|
448
460
|
class ApplicationRecord < ActiveRecord::Base
|
|
@@ -462,7 +474,7 @@ By default, the namespace will only provide history models for models in the roo
|
|
|
462
474
|
|
|
463
475
|
```ruby
|
|
464
476
|
module History
|
|
465
|
-
include Temporal::
|
|
477
|
+
include ActiveRecord::Temporal::HistoryModelNamespace
|
|
466
478
|
|
|
467
479
|
namespace "Tenant"
|
|
468
480
|
|
|
@@ -507,7 +519,7 @@ class Product < ApplicationRecord
|
|
|
507
519
|
end
|
|
508
520
|
```
|
|
509
521
|
|
|
510
|
-
The only strict requirements for
|
|
522
|
+
The only strict requirements for an application versioned table are:
|
|
511
523
|
1. It must have a `tstzrange` column (name doesn't matter)
|
|
512
524
|
2. It must have a numeric `version` column with a default value
|
|
513
525
|
|
|
@@ -92,7 +92,7 @@ module ActiveRecord::Temporal
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def revise_at(time)
|
|
95
|
-
raise "
|
|
95
|
+
raise ClosedRevisionError, "Cannot revise closed version" unless head_revision?
|
|
96
96
|
|
|
97
97
|
Revision.new(self, time, save: true)
|
|
98
98
|
end
|
|
@@ -102,7 +102,7 @@ module ActiveRecord::Temporal
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def revision_at(time)
|
|
105
|
-
raise "
|
|
105
|
+
raise ClosedRevisionError, "Cannot revise closed version" unless head_revision?
|
|
106
106
|
|
|
107
107
|
Revision.new(self, time, save: false)
|
|
108
108
|
end
|
|
@@ -112,7 +112,7 @@ module ActiveRecord::Temporal
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def inactivate_at(time)
|
|
115
|
-
raise "
|
|
115
|
+
raise ClosedRevisionError, "Cannot inactivate closed version" unless head_revision?
|
|
116
116
|
|
|
117
117
|
set_time_dimension_end(time)
|
|
118
118
|
save
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module Patches
|
|
3
3
|
# This is a copy of a fix from https://github.com/rails/rails/pull/56088 that
|
|
4
|
-
# impacts this gem. I has been backported to supported stable
|
|
5
|
-
# Active Record, but until those patches are released it's included
|
|
4
|
+
# impacts this gem. I has been merged and backported to supported stable
|
|
5
|
+
# versions of Active Record, but until those patches are released it's included
|
|
6
|
+
# here.
|
|
6
7
|
module JoinDependency
|
|
7
8
|
def instantiate(result_set, strict_loading_value, &block)
|
|
8
9
|
primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) }
|
|
@@ -1,26 +1,10 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module SystemVersioning
|
|
3
3
|
module CommandRecorder
|
|
4
|
-
module ArrayExtractOptions
|
|
5
|
-
refine Array do
|
|
6
|
-
def extract_options
|
|
7
|
-
if last.is_a?(Hash) && last.extractable_options?
|
|
8
|
-
last
|
|
9
|
-
else
|
|
10
|
-
{}
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
using ArrayExtractOptions
|
|
17
|
-
|
|
18
4
|
[
|
|
19
5
|
:create_versioning_hook,
|
|
20
6
|
:drop_versioning_hook,
|
|
21
|
-
:change_versioning_hook
|
|
22
|
-
:create_table_with_system_versioning,
|
|
23
|
-
:drop_table_with_system_versioning
|
|
7
|
+
:change_versioning_hook
|
|
24
8
|
].each do |method|
|
|
25
9
|
class_eval <<-EOV, __FILE__, __LINE__ + 1
|
|
26
10
|
def #{method}(*args)
|
|
@@ -58,16 +42,6 @@ module ActiveRecord::Temporal
|
|
|
58
42
|
]
|
|
59
43
|
]
|
|
60
44
|
end
|
|
61
|
-
|
|
62
|
-
def invert_create_table_with_system_versioning(args)
|
|
63
|
-
[:drop_table_with_system_versioning, args]
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def invert_drop_table_with_system_versioning(args)
|
|
67
|
-
# TODO make this reversible
|
|
68
|
-
|
|
69
|
-
raise ActiveRecord::IrreversibleMigration, "drop_table_with_system_versioning is not reversible"
|
|
70
|
-
end
|
|
71
45
|
end
|
|
72
46
|
end
|
|
73
47
|
end
|
|
@@ -30,7 +30,8 @@ module ActiveRecord::Temporal
|
|
|
30
30
|
verb: :insert,
|
|
31
31
|
source_table: o.source_table,
|
|
32
32
|
history_table: o.history_table,
|
|
33
|
-
columns: o.columns
|
|
33
|
+
columns: o.columns,
|
|
34
|
+
gem_version: o.gem_version
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
<<~SQL
|
|
@@ -64,7 +65,8 @@ module ActiveRecord::Temporal
|
|
|
64
65
|
source_table: o.source_table,
|
|
65
66
|
history_table: o.history_table,
|
|
66
67
|
columns: o.columns,
|
|
67
|
-
primary_key: o.primary_key
|
|
68
|
+
primary_key: o.primary_key,
|
|
69
|
+
gem_version: o.gem_version
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
<<~SQL
|
|
@@ -101,7 +103,8 @@ module ActiveRecord::Temporal
|
|
|
101
103
|
verb: :delete,
|
|
102
104
|
source_table: o.source_table,
|
|
103
105
|
history_table: o.history_table,
|
|
104
|
-
primary_key: o.primary_key
|
|
106
|
+
primary_key: o.primary_key,
|
|
107
|
+
gem_version: o.gem_version
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
<<~SQL
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module SystemVersioning
|
|
3
3
|
class VersioningHookDefinition
|
|
4
|
-
attr_accessor :source_table, :history_table, :columns, :primary_key
|
|
4
|
+
attr_accessor :source_table, :history_table, :columns, :primary_key, :gem_version
|
|
5
5
|
|
|
6
6
|
def initialize(
|
|
7
7
|
source_table,
|
|
8
8
|
history_table,
|
|
9
9
|
columns:,
|
|
10
|
-
primary_key
|
|
10
|
+
primary_key:,
|
|
11
|
+
gem_version:
|
|
11
12
|
)
|
|
12
13
|
@source_table = source_table
|
|
13
14
|
@history_table = history_table
|
|
14
15
|
@columns = columns
|
|
15
16
|
@primary_key = primary_key
|
|
17
|
+
@gem_version = gem_version
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def insert_hook
|
|
19
|
-
InsertHookDefinition.new(@source_table, @history_table, @columns)
|
|
21
|
+
InsertHookDefinition.new(@source_table, @history_table, @columns, @gem_version)
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def update_hook
|
|
23
|
-
UpdateHookDefinition.new(@source_table, @history_table, @columns, @primary_key)
|
|
25
|
+
UpdateHookDefinition.new(@source_table, @history_table, @columns, @primary_key, @gem_version)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def delete_hook
|
|
27
|
-
DeleteHookDefinition.new(@source_table, @history_table, @primary_key)
|
|
29
|
+
DeleteHookDefinition.new(@source_table, @history_table, @primary_key, @gem_version)
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
InsertHookDefinition = Struct.new(:source_table, :history_table, :columns)
|
|
33
|
+
InsertHookDefinition = Struct.new(:source_table, :history_table, :columns, :gem_version)
|
|
32
34
|
|
|
33
|
-
UpdateHookDefinition = Struct.new(:source_table, :history_table, :columns, :primary_key)
|
|
35
|
+
UpdateHookDefinition = Struct.new(:source_table, :history_table, :columns, :primary_key, :gem_version)
|
|
34
36
|
|
|
35
|
-
DeleteHookDefinition = Struct.new(:source_table, :history_table, :primary_key)
|
|
37
|
+
DeleteHookDefinition = Struct.new(:source_table, :history_table, :primary_key, :gem_version)
|
|
36
38
|
end
|
|
37
39
|
end
|
|
@@ -1,72 +1,29 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module SystemVersioning
|
|
3
3
|
module SchemaStatements
|
|
4
|
-
def create_table_with_system_versioning(table_name, **options, &block)
|
|
5
|
-
create_table(table_name, **options, &block)
|
|
6
|
-
|
|
7
|
-
source_pk = Array(primary_key(table_name))
|
|
8
|
-
history_options = options.merge(primary_key: source_pk + ["system_period"])
|
|
9
|
-
|
|
10
|
-
exclusion_constraint_expression = source_pk.map do |col|
|
|
11
|
-
"#{col} WITH ="
|
|
12
|
-
end.join(", ") + ", system_period WITH &&"
|
|
13
|
-
|
|
14
|
-
create_table("#{table_name}_history", **history_options) do |t|
|
|
15
|
-
columns(table_name).each do |column|
|
|
16
|
-
t.send(
|
|
17
|
-
column.type,
|
|
18
|
-
column.name,
|
|
19
|
-
comment: column.comment,
|
|
20
|
-
collation: column.collation,
|
|
21
|
-
default: nil,
|
|
22
|
-
limit: column.limit,
|
|
23
|
-
null: column.null,
|
|
24
|
-
precision: column.precision,
|
|
25
|
-
scale: column.scale
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
t.tstzrange :system_period, null: false
|
|
30
|
-
t.exclusion_constraint exclusion_constraint_expression, using: :gist
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
create_versioning_hook table_name,
|
|
34
|
-
"#{table_name}_history",
|
|
35
|
-
columns: :all,
|
|
36
|
-
primary_key: source_pk
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def drop_table_with_system_versioning(*table_names, **options)
|
|
40
|
-
table_names.each do |table_name|
|
|
41
|
-
history_table_name = "#{table_name}_history"
|
|
42
|
-
|
|
43
|
-
drop_table(table_name, **options)
|
|
44
|
-
drop_table(history_table_name, **options)
|
|
45
|
-
drop_versioning_hook(table_name, history_table_name, **options.slice(:columns, :primary_key, :if_exists))
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
4
|
def create_versioning_hook(source_table, history_table, **options)
|
|
50
5
|
options.assert_valid_keys(:columns, :primary_key)
|
|
51
6
|
|
|
52
|
-
|
|
7
|
+
columns = options.fetch(:columns, :all)
|
|
8
|
+
primary_key = options.fetch(:primary_key, :id)
|
|
9
|
+
|
|
10
|
+
column_names = if columns == :all
|
|
53
11
|
columns(source_table).map(&:name)
|
|
54
12
|
else
|
|
55
13
|
Array(columns).map(&:to_s)
|
|
56
14
|
end
|
|
57
15
|
|
|
58
|
-
primary_key = options.fetch(:primary_key, :id)
|
|
59
|
-
|
|
60
16
|
primary_key = if primary_key.is_a?(Array) && primary_key.length == 1
|
|
61
17
|
primary_key.first
|
|
62
18
|
else
|
|
63
19
|
primary_key
|
|
64
20
|
end
|
|
65
21
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
22
|
+
assert_table_exists!(source_table)
|
|
23
|
+
assert_table_exists!(history_table)
|
|
24
|
+
assert_columns_match!(source_table, history_table, column_names)
|
|
25
|
+
assert_columns_exists!(source_table, Array(primary_key))
|
|
26
|
+
assert_primary_key_matches!(source_table, Array(primary_key))
|
|
70
27
|
|
|
71
28
|
schema_creation = SchemaCreation.new(self)
|
|
72
29
|
|
|
@@ -74,7 +31,8 @@ module ActiveRecord::Temporal
|
|
|
74
31
|
source_table,
|
|
75
32
|
history_table,
|
|
76
33
|
columns: column_names,
|
|
77
|
-
primary_key: primary_key
|
|
34
|
+
primary_key: primary_key,
|
|
35
|
+
gem_version: VERSION
|
|
78
36
|
)
|
|
79
37
|
|
|
80
38
|
execute schema_creation.accept(hook_definition)
|
|
@@ -97,14 +55,14 @@ module ActiveRecord::Temporal
|
|
|
97
55
|
def versioning_hook(source_table)
|
|
98
56
|
update_function_name = versioning_function_name(source_table, :update)
|
|
99
57
|
|
|
100
|
-
row =
|
|
58
|
+
row = exec_query(<<~SQL.squish, "SQL", [update_function_name]).first
|
|
101
59
|
SELECT
|
|
102
60
|
pg_proc.proname as function_name,
|
|
103
61
|
obj_description(pg_proc.oid, 'pg_proc') as comment
|
|
104
62
|
FROM pg_proc
|
|
105
63
|
JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid
|
|
106
64
|
WHERE pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
107
|
-
AND pg_proc.proname =
|
|
65
|
+
AND pg_proc.proname = $1
|
|
108
66
|
SQL
|
|
109
67
|
|
|
110
68
|
return unless row
|
|
@@ -115,7 +73,8 @@ module ActiveRecord::Temporal
|
|
|
115
73
|
metadata["source_table"],
|
|
116
74
|
metadata["history_table"],
|
|
117
75
|
columns: metadata["columns"],
|
|
118
|
-
primary_key: metadata["primary_key"]
|
|
76
|
+
primary_key: metadata["primary_key"],
|
|
77
|
+
gem_version: metadata["gem_version"]
|
|
119
78
|
)
|
|
120
79
|
end
|
|
121
80
|
|
|
@@ -125,13 +84,13 @@ module ActiveRecord::Temporal
|
|
|
125
84
|
add_columns = (options[:add_columns] || []).map(&:to_s)
|
|
126
85
|
remove_columns = (options[:remove_columns] || []).map(&:to_s)
|
|
127
86
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
87
|
+
assert_table_exists!(source_table)
|
|
88
|
+
assert_table_exists!(history_table)
|
|
89
|
+
assert_columns_match!(source_table, history_table, add_columns)
|
|
131
90
|
|
|
132
91
|
hook_definition = versioning_hook(source_table)
|
|
133
92
|
|
|
134
|
-
|
|
93
|
+
assert_hook_has_columns!(hook_definition, remove_columns)
|
|
135
94
|
|
|
136
95
|
drop_versioning_hook(source_table, history_table)
|
|
137
96
|
|
|
@@ -161,15 +120,15 @@ module ActiveRecord::Temporal
|
|
|
161
120
|
def validate_create_versioning_hook_options!(options)
|
|
162
121
|
end
|
|
163
122
|
|
|
164
|
-
def
|
|
123
|
+
def assert_table_exists!(table_name)
|
|
165
124
|
return if table_exists?(table_name)
|
|
166
125
|
|
|
167
126
|
raise ArgumentError, "table '#{table_name}' does not exist"
|
|
168
127
|
end
|
|
169
128
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
|
|
129
|
+
def assert_columns_match!(source_table, history_table, column_names)
|
|
130
|
+
assert_columns_exists!(source_table, column_names)
|
|
131
|
+
assert_columns_exists!(history_table, column_names)
|
|
173
132
|
|
|
174
133
|
column_names.each do |column|
|
|
175
134
|
source_column = columns(source_table).find { _1.name == column }
|
|
@@ -181,7 +140,7 @@ module ActiveRecord::Temporal
|
|
|
181
140
|
end
|
|
182
141
|
end
|
|
183
142
|
|
|
184
|
-
def
|
|
143
|
+
def assert_columns_exists!(table_name, column_names)
|
|
185
144
|
column_names.each do |column|
|
|
186
145
|
next if column_exists?(table_name, column)
|
|
187
146
|
|
|
@@ -189,7 +148,14 @@ module ActiveRecord::Temporal
|
|
|
189
148
|
end
|
|
190
149
|
end
|
|
191
150
|
|
|
192
|
-
def
|
|
151
|
+
def assert_primary_key_matches!(source_table, primary_key)
|
|
152
|
+
primary_key = primary_key&.map(&:to_s)
|
|
153
|
+
unless Array(primary_key(source_table)) == primary_key
|
|
154
|
+
raise ArgumentError, "table '#{source_table}' does not have primary key #{primary_key}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def assert_hook_has_columns!(hook, column_names)
|
|
193
159
|
column_names.each do |column_name|
|
|
194
160
|
next if hook.columns.include?(column_name)
|
|
195
161
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
require "active_support"
|
|
2
2
|
|
|
3
3
|
require_relative "temporal/application_versioning/application_versioned"
|
|
4
|
-
require_relative "temporal/application_versioning/command_recorder"
|
|
5
|
-
require_relative "temporal/application_versioning/migration"
|
|
6
|
-
require_relative "temporal/application_versioning/schema_statements"
|
|
7
4
|
require_relative "temporal/querying/association_macros"
|
|
8
5
|
require_relative "temporal/querying/association_scope"
|
|
9
6
|
require_relative "temporal/querying/association_walker"
|
|
@@ -13,7 +10,6 @@ require_relative "temporal/querying/scope_registry"
|
|
|
13
10
|
require_relative "temporal/querying/scoping"
|
|
14
11
|
require_relative "temporal/querying/time_dimensions"
|
|
15
12
|
require_relative "temporal/patches/association_reflection"
|
|
16
|
-
require_relative "temporal/patches/command_recorder"
|
|
17
13
|
require_relative "temporal/patches/join_dependency"
|
|
18
14
|
require_relative "temporal/patches/merger"
|
|
19
15
|
require_relative "temporal/patches/relation"
|
|
@@ -22,7 +18,6 @@ require_relative "temporal/system_versioning/command_recorder"
|
|
|
22
18
|
require_relative "temporal/system_versioning/history_model_namespace"
|
|
23
19
|
require_relative "temporal/system_versioning/history_model"
|
|
24
20
|
require_relative "temporal/system_versioning/history_models"
|
|
25
|
-
require_relative "temporal/system_versioning/migration"
|
|
26
21
|
require_relative "temporal/system_versioning/schema_creation"
|
|
27
22
|
require_relative "temporal/system_versioning/schema_definitions"
|
|
28
23
|
require_relative "temporal/system_versioning/schema_statements"
|
|
@@ -31,28 +26,31 @@ require_relative "temporal/application_versioning"
|
|
|
31
26
|
require_relative "temporal/querying"
|
|
32
27
|
require_relative "temporal/scoping"
|
|
33
28
|
require_relative "temporal/system_versioning"
|
|
29
|
+
require_relative "temporal/version"
|
|
34
30
|
|
|
35
31
|
module ActiveRecord::Temporal
|
|
36
|
-
def
|
|
37
|
-
|
|
32
|
+
def self.included(base)
|
|
33
|
+
base.extend ClassMethods
|
|
38
34
|
end
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
module ClassMethods
|
|
37
|
+
def system_versioning
|
|
38
|
+
include SystemVersioning
|
|
39
|
+
end
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
def application_versioning(**options)
|
|
42
|
+
include Querying
|
|
43
|
+
include ApplicationVersioning
|
|
44
|
+
|
|
45
|
+
self.time_dimensions = options[:dimensions] if options[:dimensions]
|
|
46
|
+
end
|
|
45
47
|
end
|
|
48
|
+
|
|
49
|
+
HistoryModelNamespace = SystemVersioning::HistoryModelNamespace
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
ActiveSupport.on_load(:active_record) do
|
|
49
|
-
require "active_record/connection_adapters/postgresql_adapter"
|
|
50
|
-
|
|
51
|
-
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
|
52
|
-
.include ActiveRecord::Temporal::ApplicationVersioning::SchemaStatements
|
|
53
|
-
|
|
54
|
-
ActiveRecord::Migration::CommandRecorder
|
|
55
|
-
.include ActiveRecord::Temporal::ApplicationVersioning::CommandRecorder
|
|
53
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
|
56
54
|
|
|
57
55
|
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
|
58
56
|
.include ActiveRecord::Temporal::SystemVersioning::SchemaStatements
|
|
@@ -65,11 +63,6 @@ ActiveSupport.on_load(:active_record) do
|
|
|
65
63
|
|
|
66
64
|
# Patches
|
|
67
65
|
|
|
68
|
-
# Patches `#invert_drop` to remove the `system_versioning` option. The original
|
|
69
|
-
# method determines reversibility by looking for the presence of arguments
|
|
70
|
-
ActiveRecord::Migration::CommandRecorder
|
|
71
|
-
.prepend ActiveRecord::Temporal::Patches::CommandRecorder
|
|
72
|
-
|
|
73
66
|
# Patches `#build_arel` to wrap itself in the as-of query scope registry.
|
|
74
67
|
# This is what allows temporal association scopes to be aware of the time-scope
|
|
75
68
|
# value of the relation that included them.
|
|
@@ -101,8 +94,9 @@ ActiveSupport.on_load(:active_record) do
|
|
|
101
94
|
.prepend ActiveRecord::Temporal::Patches::AssociationReflection
|
|
102
95
|
|
|
103
96
|
# This is a copy of a fix from https://github.com/rails/rails/pull/56088 that
|
|
104
|
-
# impacts this gem. I has been backported to supported stable
|
|
105
|
-
# Active Record, but until those patches are released it's included
|
|
97
|
+
# impacts this gem. I has been merged and backported to supported stable
|
|
98
|
+
# versions of Active Record, but until those patches are released it's included
|
|
99
|
+
# here.
|
|
106
100
|
ActiveRecord::Associations::JoinDependency
|
|
107
101
|
.prepend ActiveRecord::Temporal::Patches::JoinDependency
|
|
108
102
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activerecord-temporal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Martin-Alexander
|
|
@@ -29,26 +29,6 @@ dependencies:
|
|
|
29
29
|
- - "<"
|
|
30
30
|
- !ruby/object:Gem::Version
|
|
31
31
|
version: '9.0'
|
|
32
|
-
- !ruby/object:Gem::Dependency
|
|
33
|
-
name: activesupport
|
|
34
|
-
requirement: !ruby/object:Gem::Requirement
|
|
35
|
-
requirements:
|
|
36
|
-
- - ">="
|
|
37
|
-
- !ruby/object:Gem::Version
|
|
38
|
-
version: '8'
|
|
39
|
-
- - "<"
|
|
40
|
-
- !ruby/object:Gem::Version
|
|
41
|
-
version: '9.0'
|
|
42
|
-
type: :runtime
|
|
43
|
-
prerelease: false
|
|
44
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
-
requirements:
|
|
46
|
-
- - ">="
|
|
47
|
-
- !ruby/object:Gem::Version
|
|
48
|
-
version: '8'
|
|
49
|
-
- - "<"
|
|
50
|
-
- !ruby/object:Gem::Version
|
|
51
|
-
version: '9.0'
|
|
52
32
|
- !ruby/object:Gem::Dependency
|
|
53
33
|
name: pg
|
|
54
34
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -78,11 +58,7 @@ files:
|
|
|
78
58
|
- lib/activerecord/temporal.rb
|
|
79
59
|
- lib/activerecord/temporal/application_versioning.rb
|
|
80
60
|
- lib/activerecord/temporal/application_versioning/application_versioned.rb
|
|
81
|
-
- lib/activerecord/temporal/application_versioning/command_recorder.rb
|
|
82
|
-
- lib/activerecord/temporal/application_versioning/migration.rb
|
|
83
|
-
- lib/activerecord/temporal/application_versioning/schema_statements.rb
|
|
84
61
|
- lib/activerecord/temporal/patches/association_reflection.rb
|
|
85
|
-
- lib/activerecord/temporal/patches/command_recorder.rb
|
|
86
62
|
- lib/activerecord/temporal/patches/join_dependency.rb
|
|
87
63
|
- lib/activerecord/temporal/patches/merger.rb
|
|
88
64
|
- lib/activerecord/temporal/patches/relation.rb
|
|
@@ -104,7 +80,6 @@ files:
|
|
|
104
80
|
- lib/activerecord/temporal/system_versioning/history_model.rb
|
|
105
81
|
- lib/activerecord/temporal/system_versioning/history_model_namespace.rb
|
|
106
82
|
- lib/activerecord/temporal/system_versioning/history_models.rb
|
|
107
|
-
- lib/activerecord/temporal/system_versioning/migration.rb
|
|
108
83
|
- lib/activerecord/temporal/system_versioning/schema_creation.rb
|
|
109
84
|
- lib/activerecord/temporal/system_versioning/schema_definitions.rb
|
|
110
85
|
- lib/activerecord/temporal/system_versioning/schema_statements.rb
|
|
@@ -115,7 +90,7 @@ licenses:
|
|
|
115
90
|
- MIT
|
|
116
91
|
metadata:
|
|
117
92
|
bug_tracker_uri: https://github.com/Martin-Alexander/activerecord-temporal/issues
|
|
118
|
-
changelog_uri: https://github.com/Martin-Alexander/activerecord-temporal/CHANGELOG.md
|
|
93
|
+
changelog_uri: https://github.com/Martin-Alexander/activerecord-temporal/blob/master/CHANGELOG.md
|
|
119
94
|
homepage_uri: https://github.com/Martin-Alexander/activerecord-temporal
|
|
120
95
|
source_code_uri: https://github.com/Martin-Alexander/activerecord-temporal
|
|
121
96
|
rdoc_options: []
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module ActiveRecord::Temporal
|
|
2
|
-
module ApplicationVersioning
|
|
3
|
-
module CommandRecorder
|
|
4
|
-
def create_application_versioned_table(*args)
|
|
5
|
-
record(:create_application_versioned_table, args)
|
|
6
|
-
end
|
|
7
|
-
ruby2_keywords(:create_application_versioned_table)
|
|
8
|
-
|
|
9
|
-
def invert_create_application_versioned_table(args)
|
|
10
|
-
[:drop_table, args]
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
module ActiveRecord::Temporal
|
|
2
|
-
module ApplicationVersioning
|
|
3
|
-
module Migration
|
|
4
|
-
extend ActiveSupport::Concern
|
|
5
|
-
|
|
6
|
-
included do
|
|
7
|
-
prepend Patches
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
module Patches
|
|
11
|
-
def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options, &block)
|
|
12
|
-
application_versioning = options.delete(:application_versioning)
|
|
13
|
-
|
|
14
|
-
if application_versioning
|
|
15
|
-
create_application_versioned_table(
|
|
16
|
-
table_name, id:, primary_key:, force:, **options, &block
|
|
17
|
-
)
|
|
18
|
-
else
|
|
19
|
-
super
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module ActiveRecord::Temporal
|
|
2
|
-
module ApplicationVersioning
|
|
3
|
-
module SchemaStatements
|
|
4
|
-
def create_application_versioned_table(table_name, **options, &block)
|
|
5
|
-
pk_option = options[:primary_key]
|
|
6
|
-
|
|
7
|
-
primary_key = if options[:primary_key]
|
|
8
|
-
Array(options[:primary_key]) | [:version]
|
|
9
|
-
else
|
|
10
|
-
[:id, :version]
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
exclusion_constraint_expression = (primary_key - [:version]).map do |col|
|
|
14
|
-
"#{col} WITH ="
|
|
15
|
-
end.join(", ") + ", validity WITH &&"
|
|
16
|
-
|
|
17
|
-
options = options.merge(primary_key: primary_key)
|
|
18
|
-
|
|
19
|
-
create_table(table_name, **options) do |t|
|
|
20
|
-
unless pk_option.is_a?(Array)
|
|
21
|
-
t.bigserial pk_option || :id, null: false
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
t.bigint :version, null: false, default: 1
|
|
25
|
-
t.tstzrange :validity, null: false
|
|
26
|
-
t.exclusion_constraint exclusion_constraint_expression, using: :gist
|
|
27
|
-
|
|
28
|
-
instance_exec(t, &block)
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
module ActiveRecord::Temporal
|
|
2
|
-
module Patches
|
|
3
|
-
module CommandRecorder
|
|
4
|
-
def invert_drop_table(args, &block)
|
|
5
|
-
if extract_options(args).delete(:system_versioning)
|
|
6
|
-
raise ActiveRecord::IrreversibleMigration, "drop_table with system versioning is not supported"
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
super
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
private
|
|
13
|
-
|
|
14
|
-
def extract_options(array)
|
|
15
|
-
if array.last.is_a?(Hash) && array.last.extractable_options?
|
|
16
|
-
array.last
|
|
17
|
-
else
|
|
18
|
-
{}
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
module ActiveRecord::Temporal
|
|
2
|
-
module SystemVersioning
|
|
3
|
-
module Migration
|
|
4
|
-
extend ActiveSupport::Concern
|
|
5
|
-
|
|
6
|
-
included do
|
|
7
|
-
prepend Patches
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
module Patches
|
|
11
|
-
def create_table(table_name, id: :primary_key, primary_key: nil, force: nil, **options, &block)
|
|
12
|
-
system_versioning = options.delete(:system_versioning)
|
|
13
|
-
|
|
14
|
-
if system_versioning
|
|
15
|
-
create_table_with_system_versioning(
|
|
16
|
-
table_name, id:, primary_key:, force:, **options, &block
|
|
17
|
-
)
|
|
18
|
-
else
|
|
19
|
-
super
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def drop_table(*table_names, **options)
|
|
24
|
-
system_versioning = options.delete(:system_versioning)
|
|
25
|
-
|
|
26
|
-
if system_versioning
|
|
27
|
-
drop_table_with_system_versioning(*table_names, **options)
|
|
28
|
-
else
|
|
29
|
-
super
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|