activerecord-temporal 0.1.0 → 0.3.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 +14 -0
- data/README.md +778 -7
- data/lib/activerecord/temporal/application_versioning/application_versioned.rb +122 -0
- data/lib/activerecord/temporal/application_versioning.rb +4 -68
- data/lib/activerecord/temporal/patches/association_reflection.rb +10 -3
- data/lib/activerecord/temporal/patches/join_dependency.rb +4 -0
- data/lib/activerecord/temporal/patches/merger.rb +1 -1
- data/lib/activerecord/temporal/patches/relation.rb +10 -6
- data/lib/activerecord/temporal/patches/through_association.rb +4 -1
- data/lib/activerecord/temporal/{as_of_query → querying}/association_macros.rb +1 -1
- data/lib/activerecord/temporal/querying/association_scope.rb +55 -0
- data/lib/activerecord/temporal/{as_of_query → querying}/association_walker.rb +1 -1
- data/lib/activerecord/temporal/querying/predicate_builder/contains_handler.rb +24 -0
- data/lib/activerecord/temporal/querying/predicate_builder/handlers.rb +31 -0
- data/lib/activerecord/temporal/querying/query_methods.rb +37 -0
- data/lib/activerecord/temporal/querying/scope_registry.rb +95 -0
- data/lib/activerecord/temporal/querying/scoping.rb +70 -0
- data/lib/activerecord/temporal/{as_of_query → querying}/time_dimensions.rb +13 -3
- data/lib/activerecord/temporal/querying/where_clause_refinement.rb +17 -0
- data/lib/activerecord/temporal/querying.rb +95 -0
- data/lib/activerecord/temporal/scoping.rb +7 -0
- data/lib/activerecord/temporal/system_versioning/history_model.rb +47 -0
- data/lib/activerecord/temporal/system_versioning/history_model_namespace.rb +45 -0
- data/lib/activerecord/temporal/system_versioning/history_models.rb +29 -0
- data/lib/activerecord/temporal/system_versioning/schema_creation.rb +8 -5
- data/lib/activerecord/temporal/system_versioning/schema_definitions.rb +10 -8
- data/lib/activerecord/temporal/system_versioning/schema_statements.rb +62 -24
- data/lib/activerecord/temporal/system_versioning/system_versioned.rb +13 -0
- data/lib/activerecord/temporal/system_versioning.rb +6 -18
- data/lib/activerecord/temporal/version.rb +1 -1
- data/lib/activerecord/temporal.rb +64 -33
- metadata +23 -15
- data/lib/activerecord/temporal/as_of_query/association_scope.rb +0 -54
- data/lib/activerecord/temporal/as_of_query/query_methods.rb +0 -24
- data/lib/activerecord/temporal/as_of_query/scope_registry.rb +0 -38
- data/lib/activerecord/temporal/as_of_query.rb +0 -109
- data/lib/activerecord/temporal/system_versioning/model.rb +0 -37
- data/lib/activerecord/temporal/system_versioning/namespace.rb +0 -34
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module Querying
|
|
3
|
+
class RangeError < StandardError; end
|
|
4
|
+
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
class_methods do
|
|
8
|
+
def resolve_time_coords(time_or_time_coords)
|
|
9
|
+
return time_or_time_coords if time_or_time_coords.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
{default_time_dimension.to_sym => time_or_time_coords}
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
included do
|
|
16
|
+
include AssociationMacros
|
|
17
|
+
include TimeDimensions
|
|
18
|
+
include PredicateBuilder::Handlers
|
|
19
|
+
|
|
20
|
+
delegate :resolve_time_coords, to: :class
|
|
21
|
+
|
|
22
|
+
default_scope do
|
|
23
|
+
at_time(Querying::ScopeRegistry.global_constraints_for(time_dimensions))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
scope :as_of, ->(time) do
|
|
27
|
+
time_coords = resolve_time_coords(time)
|
|
28
|
+
|
|
29
|
+
at_time(time_coords).time_tags(time_coords)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
scope :at_time, ->(time) do
|
|
33
|
+
time_coords = resolve_time_coords(time)
|
|
34
|
+
|
|
35
|
+
constraints = time_coords.slice(*time_dimension_columns)
|
|
36
|
+
|
|
37
|
+
return if constraints.empty?
|
|
38
|
+
|
|
39
|
+
rewhere_contains(constraints.transform_values { |v| contains(v) })
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def time_tags
|
|
44
|
+
@time_tags || {}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def time_tags=(value)
|
|
48
|
+
@time_tags = value&.slice(*time_dimensions)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def time_tag
|
|
52
|
+
time_tags[default_time_dimension]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def time_tags_for(time_dimensions)
|
|
56
|
+
time_tags.slice(*time_dimensions)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def as_of!(time)
|
|
60
|
+
time_coords = resolve_time_coords(time)
|
|
61
|
+
|
|
62
|
+
ensure_time_tags_in_bounds!(time_coords)
|
|
63
|
+
|
|
64
|
+
reload
|
|
65
|
+
|
|
66
|
+
self.time_tags = time_coords
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def as_of(time)
|
|
70
|
+
time_coords = resolve_time_coords(time)
|
|
71
|
+
|
|
72
|
+
self.class.as_of(time_coords).find_by(self.class.primary_key => [id])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def initialize_time_tags_from_relation(relation)
|
|
76
|
+
associations = relation.includes_values | relation.eager_load_values
|
|
77
|
+
|
|
78
|
+
self.time_tags = relation.time_tag_values
|
|
79
|
+
|
|
80
|
+
AssociationWalker.each_target(self, associations) do |target|
|
|
81
|
+
target.time_tags = relation.time_tag_values
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def ensure_time_tags_in_bounds!(time_tags)
|
|
88
|
+
time_tags.each do |dimension, time|
|
|
89
|
+
if time_dimension_column?(dimension) && !time_dimension(dimension).cover?(time)
|
|
90
|
+
raise RangeError, "#{time} is outside of '#{dimension}' range"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module SystemVersioning
|
|
3
|
+
module HistoryModel
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
def polymorphic_class_for(name)
|
|
8
|
+
super.version_model
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def sti_name
|
|
12
|
+
superclass.sti_name
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find_sti_class(type_name)
|
|
16
|
+
superclass.send(:find_sti_class, type_name).history_model
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def finder_needs_type_condition?
|
|
20
|
+
superclass.finder_needs_type_condition?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
included do
|
|
25
|
+
include Querying
|
|
26
|
+
|
|
27
|
+
if include?(SystemVersioned)
|
|
28
|
+
self.table_name = history_table_name
|
|
29
|
+
self.primary_key = Array(primary_key) + [:system_period]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
self.time_dimensions = time_dimensions + [:system_period]
|
|
33
|
+
|
|
34
|
+
reflect_on_all_associations.each do |reflection|
|
|
35
|
+
next if reflection.scope&.temporal_scope?
|
|
36
|
+
|
|
37
|
+
send(
|
|
38
|
+
reflection.macro,
|
|
39
|
+
reflection.name,
|
|
40
|
+
reflection.scope,
|
|
41
|
+
**reflection.options.merge(temporal: true)
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module SystemVersioning
|
|
3
|
+
module HistoryModelNamespace
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
def const_missing(model_name)
|
|
8
|
+
model = join(@root, model_name).constantize
|
|
9
|
+
rescue NameError
|
|
10
|
+
super
|
|
11
|
+
else
|
|
12
|
+
unless model.is_a?(Class) && model < ActiveRecord::Base
|
|
13
|
+
raise NameError, "#{model} is not a descendent of ActiveRecord::Base"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
history_model = Class.new(model) do
|
|
17
|
+
include HistoryModel
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
const_set(model_name, history_model)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def namespace(name, &block)
|
|
24
|
+
new_namespace = Module.new do
|
|
25
|
+
include HistoryModelNamespace
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
const_set(name, new_namespace)
|
|
29
|
+
|
|
30
|
+
new_namespace.root(join(@root, name))
|
|
31
|
+
|
|
32
|
+
new_namespace.instance_eval(&block) if block
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def root(name)
|
|
36
|
+
@root = name
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def join(base, name)
|
|
40
|
+
[base, name].compact.join("::")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module SystemVersioning
|
|
3
|
+
module HistoryModels
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
delegate :at_time, :as_of, to: :history
|
|
10
|
+
|
|
11
|
+
def history_model
|
|
12
|
+
raise Error, "abstract classes cannot have a history model" if abstract_class?
|
|
13
|
+
|
|
14
|
+
[history_model_namespace, name].join("::").constantize
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def history
|
|
18
|
+
ActiveRecord::Relation.create(history_model)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def history_model_namespace
|
|
24
|
+
History
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
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
|
|
@@ -52,7 +53,7 @@ module ActiveRecord::Temporal
|
|
|
52
53
|
|
|
53
54
|
def visit_UpdateHookDefinition(o)
|
|
54
55
|
column_names = o.columns.map { |c| quote_column_name(c) }
|
|
55
|
-
primary_key_quoted = o.primary_key.map { |c| quote_column_name(c) }
|
|
56
|
+
primary_key_quoted = Array(o.primary_key).map { |c| quote_column_name(c) }
|
|
56
57
|
fields = column_names.join(", ")
|
|
57
58
|
values = column_names.map { |c| "NEW.#{c}" }.join(", ")
|
|
58
59
|
update_pk_predicates = primary_key_quoted.map { |c| "#{c} = OLD.#{c}" }.join(" AND ")
|
|
@@ -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
|
|
@@ -94,14 +96,15 @@ module ActiveRecord::Temporal
|
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def visit_DeleteHookDefinition(o)
|
|
97
|
-
primary_key_quoted = o.primary_key.map { |c| quote_column_name(c) }
|
|
99
|
+
primary_key_quoted = Array(o.primary_key).map { |c| quote_column_name(c) }
|
|
98
100
|
function_name = versioning_function_name(o.source_table, :delete)
|
|
99
101
|
pk_predicates = primary_key_quoted.map { |c| "#{c} = OLD.#{c}" }.join(" AND ")
|
|
100
102
|
metadata = {
|
|
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
|
|
@@ -2,45 +2,67 @@ module ActiveRecord::Temporal
|
|
|
2
2
|
module SystemVersioning
|
|
3
3
|
module SchemaStatements
|
|
4
4
|
def create_versioning_hook(source_table, history_table, **options)
|
|
5
|
-
|
|
6
|
-
primary_key = Array(options.fetch(:primary_key, :id))
|
|
5
|
+
options.assert_valid_keys(:columns, :primary_key)
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
columns = options.fetch(:columns, :all)
|
|
8
|
+
primary_key = options.fetch(:primary_key, :id)
|
|
9
|
+
|
|
10
|
+
column_names = if columns == :all
|
|
11
|
+
columns(source_table).map(&:name)
|
|
12
|
+
else
|
|
13
|
+
Array(columns).map(&:to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
primary_key = if primary_key.is_a?(Array) && primary_key.length == 1
|
|
17
|
+
primary_key.first
|
|
18
|
+
else
|
|
19
|
+
primary_key
|
|
20
|
+
end
|
|
21
|
+
|
|
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))
|
|
12
27
|
|
|
13
28
|
schema_creation = SchemaCreation.new(self)
|
|
14
29
|
|
|
15
30
|
hook_definition = VersioningHookDefinition.new(
|
|
16
31
|
source_table,
|
|
17
32
|
history_table,
|
|
18
|
-
columns:
|
|
19
|
-
primary_key: primary_key
|
|
33
|
+
columns: column_names,
|
|
34
|
+
primary_key: primary_key,
|
|
35
|
+
gem_version: VERSION
|
|
20
36
|
)
|
|
21
37
|
|
|
22
38
|
execute schema_creation.accept(hook_definition)
|
|
23
39
|
end
|
|
24
40
|
|
|
25
|
-
def drop_versioning_hook(source_table, history_table,
|
|
41
|
+
def drop_versioning_hook(source_table, history_table, **options)
|
|
42
|
+
options.assert_valid_keys(:columns, :primary_key, :if_exists)
|
|
43
|
+
|
|
26
44
|
%i[insert update delete].each do |verb|
|
|
27
45
|
function_name = versioning_function_name(source_table, verb)
|
|
28
46
|
|
|
29
|
-
|
|
47
|
+
sql = "DROP FUNCTION"
|
|
48
|
+
sql << " IF EXISTS" if options[:if_exists]
|
|
49
|
+
sql << " #{function_name}() CASCADE"
|
|
50
|
+
|
|
51
|
+
execute sql
|
|
30
52
|
end
|
|
31
53
|
end
|
|
32
54
|
|
|
33
55
|
def versioning_hook(source_table)
|
|
34
56
|
update_function_name = versioning_function_name(source_table, :update)
|
|
35
57
|
|
|
36
|
-
row =
|
|
58
|
+
row = exec_query(<<~SQL.squish, "SQL", [update_function_name]).first
|
|
37
59
|
SELECT
|
|
38
60
|
pg_proc.proname as function_name,
|
|
39
61
|
obj_description(pg_proc.oid, 'pg_proc') as comment
|
|
40
62
|
FROM pg_proc
|
|
41
63
|
JOIN pg_namespace ON pg_proc.pronamespace = pg_namespace.oid
|
|
42
64
|
WHERE pg_namespace.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
43
|
-
AND pg_proc.proname =
|
|
65
|
+
AND pg_proc.proname = $1
|
|
44
66
|
SQL
|
|
45
67
|
|
|
46
68
|
return unless row
|
|
@@ -51,27 +73,33 @@ module ActiveRecord::Temporal
|
|
|
51
73
|
metadata["source_table"],
|
|
52
74
|
metadata["history_table"],
|
|
53
75
|
columns: metadata["columns"],
|
|
54
|
-
primary_key: metadata["primary_key"]
|
|
76
|
+
primary_key: metadata["primary_key"],
|
|
77
|
+
gem_version: metadata["gem_version"]
|
|
55
78
|
)
|
|
56
79
|
end
|
|
57
80
|
|
|
58
81
|
def change_versioning_hook(source_table, history_table, options)
|
|
82
|
+
options.assert_valid_keys(:add_columns, :remove_columns)
|
|
83
|
+
|
|
59
84
|
add_columns = (options[:add_columns] || []).map(&:to_s)
|
|
60
85
|
remove_columns = (options[:remove_columns] || []).map(&:to_s)
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
assert_table_exists!(source_table)
|
|
88
|
+
assert_table_exists!(history_table)
|
|
89
|
+
assert_columns_match!(source_table, history_table, add_columns)
|
|
65
90
|
|
|
66
91
|
hook_definition = versioning_hook(source_table)
|
|
67
92
|
|
|
68
|
-
|
|
93
|
+
assert_hook_has_columns!(hook_definition, remove_columns)
|
|
69
94
|
|
|
70
95
|
drop_versioning_hook(source_table, history_table)
|
|
71
96
|
|
|
72
97
|
new_columns = hook_definition.columns + add_columns - remove_columns
|
|
73
98
|
|
|
74
|
-
create_versioning_hook
|
|
99
|
+
create_versioning_hook source_table,
|
|
100
|
+
history_table,
|
|
101
|
+
columns: new_columns,
|
|
102
|
+
primary_key: hook_definition.primary_key
|
|
75
103
|
end
|
|
76
104
|
|
|
77
105
|
def history_table(source_table)
|
|
@@ -89,15 +117,18 @@ module ActiveRecord::Temporal
|
|
|
89
117
|
|
|
90
118
|
private
|
|
91
119
|
|
|
92
|
-
def
|
|
120
|
+
def validate_create_versioning_hook_options!(options)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def assert_table_exists!(table_name)
|
|
93
124
|
return if table_exists?(table_name)
|
|
94
125
|
|
|
95
126
|
raise ArgumentError, "table '#{table_name}' does not exist"
|
|
96
127
|
end
|
|
97
128
|
|
|
98
|
-
def
|
|
99
|
-
|
|
100
|
-
|
|
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)
|
|
101
132
|
|
|
102
133
|
column_names.each do |column|
|
|
103
134
|
source_column = columns(source_table).find { _1.name == column }
|
|
@@ -109,7 +140,7 @@ module ActiveRecord::Temporal
|
|
|
109
140
|
end
|
|
110
141
|
end
|
|
111
142
|
|
|
112
|
-
def
|
|
143
|
+
def assert_columns_exists!(table_name, column_names)
|
|
113
144
|
column_names.each do |column|
|
|
114
145
|
next if column_exists?(table_name, column)
|
|
115
146
|
|
|
@@ -117,7 +148,14 @@ module ActiveRecord::Temporal
|
|
|
117
148
|
end
|
|
118
149
|
end
|
|
119
150
|
|
|
120
|
-
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)
|
|
121
159
|
column_names.each do |column_name|
|
|
122
160
|
next if hook.columns.include?(column_name)
|
|
123
161
|
|
|
@@ -2,25 +2,13 @@ module ActiveRecord::Temporal
|
|
|
2
2
|
module SystemVersioning
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def primary_key_from_db
|
|
11
|
-
Array(connection.primary_key(table_name)).map(&:to_sym)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def version_model
|
|
15
|
-
"Version::#{name}".constantize
|
|
16
|
-
end
|
|
5
|
+
included do
|
|
6
|
+
include HistoryModels
|
|
7
|
+
end
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
mod.include(Namespace)
|
|
22
|
-
Object.const_set(namespace, mod)
|
|
23
|
-
end
|
|
9
|
+
class_methods do
|
|
10
|
+
def system_versioned
|
|
11
|
+
include SystemVersioned
|
|
24
12
|
end
|
|
25
13
|
end
|
|
26
14
|
end
|
|
@@ -1,63 +1,94 @@
|
|
|
1
1
|
require "active_support"
|
|
2
2
|
|
|
3
|
-
require_relative "temporal/application_versioning"
|
|
4
|
-
require_relative "temporal/
|
|
5
|
-
require_relative "temporal/
|
|
6
|
-
require_relative "temporal/
|
|
7
|
-
require_relative "temporal/
|
|
8
|
-
require_relative "temporal/
|
|
9
|
-
require_relative "temporal/
|
|
10
|
-
require_relative "temporal/
|
|
3
|
+
require_relative "temporal/application_versioning/application_versioned"
|
|
4
|
+
require_relative "temporal/querying/association_macros"
|
|
5
|
+
require_relative "temporal/querying/association_scope"
|
|
6
|
+
require_relative "temporal/querying/association_walker"
|
|
7
|
+
require_relative "temporal/querying/predicate_builder/handlers"
|
|
8
|
+
require_relative "temporal/querying/query_methods"
|
|
9
|
+
require_relative "temporal/querying/scope_registry"
|
|
10
|
+
require_relative "temporal/querying/scoping"
|
|
11
|
+
require_relative "temporal/querying/time_dimensions"
|
|
11
12
|
require_relative "temporal/patches/association_reflection"
|
|
12
13
|
require_relative "temporal/patches/join_dependency"
|
|
13
14
|
require_relative "temporal/patches/merger"
|
|
14
15
|
require_relative "temporal/patches/relation"
|
|
15
16
|
require_relative "temporal/patches/through_association"
|
|
16
|
-
require_relative "temporal/system_versioning"
|
|
17
17
|
require_relative "temporal/system_versioning/command_recorder"
|
|
18
|
-
require_relative "temporal/system_versioning/
|
|
19
|
-
require_relative "temporal/system_versioning/
|
|
18
|
+
require_relative "temporal/system_versioning/history_model_namespace"
|
|
19
|
+
require_relative "temporal/system_versioning/history_model"
|
|
20
|
+
require_relative "temporal/system_versioning/history_models"
|
|
20
21
|
require_relative "temporal/system_versioning/schema_creation"
|
|
21
22
|
require_relative "temporal/system_versioning/schema_definitions"
|
|
22
23
|
require_relative "temporal/system_versioning/schema_statements"
|
|
24
|
+
require_relative "temporal/system_versioning/system_versioned"
|
|
25
|
+
require_relative "temporal/application_versioning"
|
|
26
|
+
require_relative "temporal/querying"
|
|
27
|
+
require_relative "temporal/scoping"
|
|
28
|
+
require_relative "temporal/system_versioning"
|
|
29
|
+
require_relative "temporal/version"
|
|
30
|
+
|
|
31
|
+
module ActiveRecord::Temporal
|
|
32
|
+
def system_versioning
|
|
33
|
+
include SystemVersioning
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def application_versioning(**options)
|
|
37
|
+
include Querying
|
|
38
|
+
include ApplicationVersioning
|
|
39
|
+
|
|
40
|
+
self.time_dimensions = options[:dimensions] if options[:dimensions]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
23
43
|
|
|
24
44
|
ActiveSupport.on_load(:active_record) do
|
|
25
|
-
require "active_record/connection_adapters/postgresql_adapter"
|
|
45
|
+
require "active_record/connection_adapters/postgresql_adapter"
|
|
46
|
+
|
|
47
|
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
|
48
|
+
.include ActiveRecord::Temporal::SystemVersioning::SchemaStatements
|
|
49
|
+
|
|
50
|
+
ActiveRecord::Migration::CommandRecorder
|
|
51
|
+
.include ActiveRecord::Temporal::SystemVersioning::CommandRecorder
|
|
26
52
|
|
|
27
|
-
ActiveRecord::
|
|
28
|
-
|
|
29
|
-
ActiveRecord::Relation.include(ActiveRecord::Temporal::AsOfQuery::QueryMethods)
|
|
53
|
+
ActiveRecord::Relation
|
|
54
|
+
.include ActiveRecord::Temporal::Querying::QueryMethods
|
|
30
55
|
|
|
31
56
|
# Patches
|
|
32
57
|
|
|
33
|
-
# Patches
|
|
58
|
+
# Patches `#build_arel` to wrap itself in the as-of query scope registry.
|
|
34
59
|
# This is what allows temporal association scopes to be aware of the time-scope
|
|
35
60
|
# value of the relation that included them.
|
|
36
61
|
#
|
|
37
|
-
# Patches
|
|
62
|
+
# Patches `#instantiate_records` method to call `initialize_time_tags_from_relation`
|
|
38
63
|
# on each loaded record.
|
|
39
|
-
ActiveRecord::Relation
|
|
64
|
+
ActiveRecord::Relation
|
|
65
|
+
.prepend ActiveRecord::Temporal::Patches::Relation
|
|
40
66
|
|
|
41
|
-
# Patches
|
|
42
|
-
#
|
|
43
|
-
ActiveRecord::Relation::Merger
|
|
67
|
+
# Patches `#merge` (called by `Relation#merge`) to handle the new query method
|
|
68
|
+
# `time_tags` that this gem adds.
|
|
69
|
+
ActiveRecord::Relation::Merger
|
|
70
|
+
.prepend ActiveRecord::Temporal::Patches::Merger
|
|
44
71
|
|
|
45
|
-
# Patches
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
ActiveRecord::Associations::Preloader::ThroughAssociation
|
|
72
|
+
# Patches `#through_scope` to pass along the relation's time tag values when it
|
|
73
|
+
# handles has-many-through associations. The handler for has-many association
|
|
74
|
+
# uses `Relation#merge`, but this one doesn't.
|
|
75
|
+
ActiveRecord::Associations::Preloader::ThroughAssociation
|
|
76
|
+
.prepend ActiveRecord::Temporal::Patches::ThroughAssociation
|
|
49
77
|
|
|
50
78
|
# This permits association scopes generated by this gem to be eager-load if they
|
|
51
|
-
# are "optionally instance-dependent." That is to say, they accept
|
|
52
|
-
#
|
|
79
|
+
# are "optionally instance-dependent." That is to say, they accept but don't
|
|
80
|
+
# require arguments.
|
|
53
81
|
#
|
|
54
|
-
# I think permitting eager-loading
|
|
55
|
-
#
|
|
82
|
+
# I think permitting eager-loading of such scopes would make sense as a
|
|
83
|
+
# standalone feature for Active Record. See this PR for my justification:
|
|
56
84
|
# https://github.com/rails/rails/pull/56004
|
|
57
|
-
ActiveRecord::Reflection::AssociationReflection
|
|
85
|
+
ActiveRecord::Reflection::AssociationReflection
|
|
86
|
+
.prepend ActiveRecord::Temporal::Patches::AssociationReflection
|
|
58
87
|
|
|
59
88
|
# This is a copy of a fix from https://github.com/rails/rails/pull/56088 that
|
|
60
|
-
# impacts this gem. I has been backported to supported stable
|
|
61
|
-
# Active Record, but until those patches are released it's included
|
|
62
|
-
|
|
89
|
+
# impacts this gem. I has been merged and backported to supported stable
|
|
90
|
+
# versions of Active Record, but until those patches are released it's included
|
|
91
|
+
# here.
|
|
92
|
+
ActiveRecord::Associations::JoinDependency
|
|
93
|
+
.prepend ActiveRecord::Temporal::Patches::JoinDependency
|
|
63
94
|
end
|