activerecord-temporal 0.1.0 → 0.2.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 +8 -0
- data/README.md +775 -7
- data/lib/activerecord/temporal/application_versioning/application_versioned.rb +122 -0
- data/lib/activerecord/temporal/application_versioning/command_recorder.rb +14 -0
- data/lib/activerecord/temporal/application_versioning/migration.rb +25 -0
- data/lib/activerecord/temporal/application_versioning/schema_statements.rb +33 -0
- data/lib/activerecord/temporal/application_versioning.rb +3 -69
- data/lib/activerecord/temporal/patches/association_reflection.rb +10 -3
- data/lib/activerecord/temporal/patches/command_recorder.rb +23 -0
- data/lib/activerecord/temporal/patches/join_dependency.rb +3 -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/command_recorder.rb +27 -1
- 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/migration.rb +35 -0
- data/lib/activerecord/temporal/system_versioning/schema_creation.rb +2 -2
- data/lib/activerecord/temporal/system_versioning/schema_statements.rb +80 -8
- 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 +75 -30
- metadata +27 -14
- 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,70 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module Querying
|
|
3
|
+
class Scoping
|
|
4
|
+
class << self
|
|
5
|
+
def at(time_or_time_coords, &block)
|
|
6
|
+
if time_or_time_coords.is_a?(Hash)
|
|
7
|
+
with_global_constraint(time_or_time_coords, &block)
|
|
8
|
+
else
|
|
9
|
+
without_global_constraints do
|
|
10
|
+
with_universal_global_constraint_time(time_or_time_coords, &block)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def as_of(time_coords, &block)
|
|
16
|
+
with_association_constraints(time_coords) do
|
|
17
|
+
with_association_tags(time_coords, &block)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def with_association_constraints(time_coords, &block)
|
|
24
|
+
original = ScopeRegistry.association_constraints
|
|
25
|
+
ScopeRegistry.set_association_constraints(time_coords)
|
|
26
|
+
|
|
27
|
+
block.call
|
|
28
|
+
ensure
|
|
29
|
+
ScopeRegistry.association_constraints = original
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_association_tags(time_coords, &block)
|
|
33
|
+
original = ScopeRegistry.association_tags
|
|
34
|
+
ScopeRegistry.set_association_tags(time_coords)
|
|
35
|
+
|
|
36
|
+
block.call
|
|
37
|
+
ensure
|
|
38
|
+
ScopeRegistry.association_tags = original
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def with_global_constraint(value, &block)
|
|
42
|
+
original = ScopeRegistry.global_constraints
|
|
43
|
+
ScopeRegistry.set_global_constraints(value)
|
|
44
|
+
|
|
45
|
+
block.call
|
|
46
|
+
ensure
|
|
47
|
+
ScopeRegistry.global_constraints = original
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def without_global_constraints(&block)
|
|
51
|
+
original = ScopeRegistry.global_constraints
|
|
52
|
+
ScopeRegistry.global_constraints = {}
|
|
53
|
+
|
|
54
|
+
block.call
|
|
55
|
+
ensure
|
|
56
|
+
ScopeRegistry.global_constraints = original
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def with_universal_global_constraint_time(time, &block)
|
|
60
|
+
original = ScopeRegistry.universal_global_constraint_time
|
|
61
|
+
ScopeRegistry.universal_global_constraint_time = time
|
|
62
|
+
|
|
63
|
+
block.call
|
|
64
|
+
ensure
|
|
65
|
+
ScopeRegistry.universal_global_constraint_time = original
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
|
-
module
|
|
2
|
+
module Querying
|
|
3
3
|
module TimeDimensions
|
|
4
4
|
extend ActiveSupport::Concern
|
|
5
5
|
|
|
@@ -8,7 +8,9 @@ module ActiveRecord::Temporal
|
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
class_methods do
|
|
11
|
-
def
|
|
11
|
+
def time_dimensions=(*dimensions)
|
|
12
|
+
dimensions = dimensions.flatten
|
|
13
|
+
|
|
12
14
|
define_singleton_method(:time_dimensions) { dimensions }
|
|
13
15
|
define_singleton_method(:default_time_dimension) { dimensions.first }
|
|
14
16
|
end
|
|
@@ -17,7 +19,15 @@ module ActiveRecord::Temporal
|
|
|
17
19
|
def default_time_dimension = nil
|
|
18
20
|
|
|
19
21
|
def time_dimension_column?(time_dimension)
|
|
20
|
-
|
|
22
|
+
@time_dimension_column_cache ||= {}
|
|
23
|
+
|
|
24
|
+
@time_dimension_column_cache[time_dimension] ||= connection.column_exists?(table_name, time_dimension)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def time_dimension_columns
|
|
28
|
+
time_dimensions.select do |dimension|
|
|
29
|
+
time_dimension_column?(dimension)
|
|
30
|
+
end
|
|
21
31
|
end
|
|
22
32
|
end
|
|
23
33
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module Querying
|
|
3
|
+
module WhereClauseRefinement
|
|
4
|
+
refine ActiveRecord::Relation::WhereClause do
|
|
5
|
+
def except_contains(columns)
|
|
6
|
+
columns = columns.map(&:to_s)
|
|
7
|
+
|
|
8
|
+
remaining_predications = predicates.reject do |node|
|
|
9
|
+
node.is_a?(Arel::Nodes::Contains) && columns.include?(node.left.name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
self.class.new(remaining_predications)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -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
|
|
@@ -1,10 +1,26 @@
|
|
|
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
|
+
|
|
4
18
|
[
|
|
5
19
|
:create_versioning_hook,
|
|
6
20
|
:drop_versioning_hook,
|
|
7
|
-
:change_versioning_hook
|
|
21
|
+
:change_versioning_hook,
|
|
22
|
+
:create_table_with_system_versioning,
|
|
23
|
+
:drop_table_with_system_versioning
|
|
8
24
|
].each do |method|
|
|
9
25
|
class_eval <<-EOV, __FILE__, __LINE__ + 1
|
|
10
26
|
def #{method}(*args)
|
|
@@ -42,6 +58,16 @@ module ActiveRecord::Temporal
|
|
|
42
58
|
]
|
|
43
59
|
]
|
|
44
60
|
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
|
|
45
71
|
end
|
|
46
72
|
end
|
|
47
73
|
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
|
|
@@ -0,0 +1,35 @@
|
|
|
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
|
|
@@ -52,7 +52,7 @@ module ActiveRecord::Temporal
|
|
|
52
52
|
|
|
53
53
|
def visit_UpdateHookDefinition(o)
|
|
54
54
|
column_names = o.columns.map { |c| quote_column_name(c) }
|
|
55
|
-
primary_key_quoted = o.primary_key.map { |c| quote_column_name(c) }
|
|
55
|
+
primary_key_quoted = Array(o.primary_key).map { |c| quote_column_name(c) }
|
|
56
56
|
fields = column_names.join(", ")
|
|
57
57
|
values = column_names.map { |c| "NEW.#{c}" }.join(", ")
|
|
58
58
|
update_pk_predicates = primary_key_quoted.map { |c| "#{c} = OLD.#{c}" }.join(" AND ")
|
|
@@ -94,7 +94,7 @@ module ActiveRecord::Temporal
|
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
def visit_DeleteHookDefinition(o)
|
|
97
|
-
primary_key_quoted = o.primary_key.map { |c| quote_column_name(c) }
|
|
97
|
+
primary_key_quoted = Array(o.primary_key).map { |c| quote_column_name(c) }
|
|
98
98
|
function_name = versioning_function_name(o.source_table, :delete)
|
|
99
99
|
pk_predicates = primary_key_quoted.map { |c| "#{c} = OLD.#{c}" }.join(" AND ")
|
|
100
100
|
metadata = {
|
|
@@ -1,32 +1,96 @@
|
|
|
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
|
+
|
|
4
49
|
def create_versioning_hook(source_table, history_table, **options)
|
|
5
|
-
|
|
6
|
-
|
|
50
|
+
options.assert_valid_keys(:columns, :primary_key)
|
|
51
|
+
|
|
52
|
+
column_names = if (columns = options.fetch(:columns)) == :all
|
|
53
|
+
columns(source_table).map(&:name)
|
|
54
|
+
else
|
|
55
|
+
Array(columns).map(&:to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
primary_key = options.fetch(:primary_key, :id)
|
|
59
|
+
|
|
60
|
+
primary_key = if primary_key.is_a?(Array) && primary_key.length == 1
|
|
61
|
+
primary_key.first
|
|
62
|
+
else
|
|
63
|
+
primary_key
|
|
64
|
+
end
|
|
7
65
|
|
|
8
66
|
ensure_table_exists!(source_table)
|
|
9
67
|
ensure_table_exists!(history_table)
|
|
10
|
-
ensure_columns_match!(source_table, history_table,
|
|
11
|
-
ensure_columns_exists!(source_table, primary_key)
|
|
68
|
+
ensure_columns_match!(source_table, history_table, column_names)
|
|
69
|
+
ensure_columns_exists!(source_table, Array(primary_key))
|
|
12
70
|
|
|
13
71
|
schema_creation = SchemaCreation.new(self)
|
|
14
72
|
|
|
15
73
|
hook_definition = VersioningHookDefinition.new(
|
|
16
74
|
source_table,
|
|
17
75
|
history_table,
|
|
18
|
-
columns:
|
|
76
|
+
columns: column_names,
|
|
19
77
|
primary_key: primary_key
|
|
20
78
|
)
|
|
21
79
|
|
|
22
80
|
execute schema_creation.accept(hook_definition)
|
|
23
81
|
end
|
|
24
82
|
|
|
25
|
-
def drop_versioning_hook(source_table, history_table,
|
|
83
|
+
def drop_versioning_hook(source_table, history_table, **options)
|
|
84
|
+
options.assert_valid_keys(:columns, :primary_key, :if_exists)
|
|
85
|
+
|
|
26
86
|
%i[insert update delete].each do |verb|
|
|
27
87
|
function_name = versioning_function_name(source_table, verb)
|
|
28
88
|
|
|
29
|
-
|
|
89
|
+
sql = "DROP FUNCTION"
|
|
90
|
+
sql << " IF EXISTS" if options[:if_exists]
|
|
91
|
+
sql << " #{function_name}() CASCADE"
|
|
92
|
+
|
|
93
|
+
execute sql
|
|
30
94
|
end
|
|
31
95
|
end
|
|
32
96
|
|
|
@@ -56,6 +120,8 @@ module ActiveRecord::Temporal
|
|
|
56
120
|
end
|
|
57
121
|
|
|
58
122
|
def change_versioning_hook(source_table, history_table, options)
|
|
123
|
+
options.assert_valid_keys(:add_columns, :remove_columns)
|
|
124
|
+
|
|
59
125
|
add_columns = (options[:add_columns] || []).map(&:to_s)
|
|
60
126
|
remove_columns = (options[:remove_columns] || []).map(&:to_s)
|
|
61
127
|
|
|
@@ -71,7 +137,10 @@ module ActiveRecord::Temporal
|
|
|
71
137
|
|
|
72
138
|
new_columns = hook_definition.columns + add_columns - remove_columns
|
|
73
139
|
|
|
74
|
-
create_versioning_hook
|
|
140
|
+
create_versioning_hook source_table,
|
|
141
|
+
history_table,
|
|
142
|
+
columns: new_columns,
|
|
143
|
+
primary_key: hook_definition.primary_key
|
|
75
144
|
end
|
|
76
145
|
|
|
77
146
|
def history_table(source_table)
|
|
@@ -89,6 +158,9 @@ module ActiveRecord::Temporal
|
|
|
89
158
|
|
|
90
159
|
private
|
|
91
160
|
|
|
161
|
+
def validate_create_versioning_hook_options!(options)
|
|
162
|
+
end
|
|
163
|
+
|
|
92
164
|
def ensure_table_exists!(table_name)
|
|
93
165
|
return if table_exists?(table_name)
|
|
94
166
|
|
|
@@ -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
|