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,122 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module ApplicationVersioning
|
|
3
|
+
module ApplicationVersioned
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
delegate :scope_time, to: :class
|
|
8
|
+
|
|
9
|
+
include Querying
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_methods do
|
|
13
|
+
def originate
|
|
14
|
+
originate_at(scope_time || Time.current)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def originate_at(time)
|
|
18
|
+
Original.new(self, time, save: true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def original
|
|
22
|
+
original_at(scope_time || Time.current)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def original_at(time)
|
|
26
|
+
Original.new(self, time)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def scope_time
|
|
30
|
+
Querying::ScopeRegistry.global_constraint_for(time_dimensions.first)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Revision
|
|
35
|
+
attr_reader :record, :time, :options
|
|
36
|
+
|
|
37
|
+
def initialize(record, time, **options)
|
|
38
|
+
@record = record
|
|
39
|
+
@time = time
|
|
40
|
+
@options = options
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def with(attributes)
|
|
44
|
+
new_revision = record.dup
|
|
45
|
+
new_revision.assign_attributes(attributes)
|
|
46
|
+
new_revision.id = record.id
|
|
47
|
+
new_revision.set_time_dimension_start(time)
|
|
48
|
+
new_revision.time_tags = record.time_tags
|
|
49
|
+
record.set_time_dimension_end(time)
|
|
50
|
+
|
|
51
|
+
new_revision.after_initialize_revision(record)
|
|
52
|
+
|
|
53
|
+
if options[:save]
|
|
54
|
+
record.class.transaction do
|
|
55
|
+
new_revision.save if record.save
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
new_revision
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class Original
|
|
64
|
+
attr_reader :klass, :time, :options
|
|
65
|
+
|
|
66
|
+
def initialize(klass, time, **options)
|
|
67
|
+
@klass = klass
|
|
68
|
+
@time = time
|
|
69
|
+
@options = options
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def with(attributes)
|
|
73
|
+
new_record = klass.new(attributes)
|
|
74
|
+
new_record.set_time_dimension_start(time)
|
|
75
|
+
|
|
76
|
+
new_record.save if options[:save]
|
|
77
|
+
|
|
78
|
+
new_record
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def after_initialize_revision(old_revision)
|
|
83
|
+
self.version = old_revision.version + 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def head_revision?
|
|
87
|
+
time_dimension && !time_dimension_end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def revise
|
|
91
|
+
revise_at(scope_time || Time.current)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def revise_at(time)
|
|
95
|
+
raise "not head revision" unless head_revision?
|
|
96
|
+
|
|
97
|
+
Revision.new(self, time, save: true)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def revision
|
|
101
|
+
revision_at(scope_time || Time.current)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def revision_at(time)
|
|
105
|
+
raise "not head revision" unless head_revision?
|
|
106
|
+
|
|
107
|
+
Revision.new(self, time, save: false)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def inactivate
|
|
111
|
+
inactivate_at(scope_time || Time.current)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def inactivate_at(time)
|
|
115
|
+
raise "not head revision" unless head_revision?
|
|
116
|
+
|
|
117
|
+
set_time_dimension_end(time)
|
|
118
|
+
save
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
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
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
|
@@ -2,76 +2,10 @@ module ActiveRecord::Temporal
|
|
|
2
2
|
module ApplicationVersioning
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Revision
|
|
10
|
-
attr_reader :record, :time, :options
|
|
11
|
-
|
|
12
|
-
def initialize(record, time, **options)
|
|
13
|
-
@record = record
|
|
14
|
-
@time = time
|
|
15
|
-
@options = options
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def with(attributes)
|
|
19
|
-
new_revision = record.dup
|
|
20
|
-
new_revision.assign_attributes(attributes)
|
|
21
|
-
new_revision.set_time_dimension_start(time)
|
|
22
|
-
new_revision.time_scopes = record.time_scopes
|
|
23
|
-
record.set_time_dimension_end(time)
|
|
24
|
-
|
|
25
|
-
new_revision.after_initialize_revision(record)
|
|
26
|
-
|
|
27
|
-
if options[:save]
|
|
28
|
-
record.class.transaction do
|
|
29
|
-
new_revision.save if record.save
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
[new_revision, record]
|
|
5
|
+
class_methods do
|
|
6
|
+
def application_versioned
|
|
7
|
+
include ApplicationVersioned
|
|
34
8
|
end
|
|
35
9
|
end
|
|
36
|
-
|
|
37
|
-
def after_initialize_revision(old_revision)
|
|
38
|
-
self.version = old_revision.version + 1
|
|
39
|
-
self.id_value = old_revision.id_value
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def head_revision?
|
|
43
|
-
time_dimension && !time_dimension_end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def revise
|
|
47
|
-
revise_at(Time.current)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def revise_at(time)
|
|
51
|
-
raise "not head revision" unless head_revision?
|
|
52
|
-
|
|
53
|
-
Revision.new(self, time, save: true)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def revision
|
|
57
|
-
revision_at(Time.current)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def revision_at(time)
|
|
61
|
-
raise "not head revision" unless head_revision?
|
|
62
|
-
|
|
63
|
-
Revision.new(self, time, save: false)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def inactivate
|
|
67
|
-
inactivate_at(Time.current)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def inactivate_at(time)
|
|
71
|
-
raise "not head revision" unless head_revision?
|
|
72
|
-
|
|
73
|
-
set_time_dimension_end(time)
|
|
74
|
-
save
|
|
75
|
-
end
|
|
76
10
|
end
|
|
77
11
|
end
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module Patches
|
|
3
|
+
# This permits association scopes generated by this gem to be eager-load if
|
|
4
|
+
# they are "optionally instance-dependent." That is to say, they accept but
|
|
5
|
+
# don't require arguments.
|
|
6
|
+
#
|
|
7
|
+
# I think permitting eager-loading of such scopes would make sense as a
|
|
8
|
+
# standalone feature for Active Record. See this PR for my justification:
|
|
9
|
+
# https://github.com/rails/rails/pull/56004
|
|
3
10
|
module AssociationReflection
|
|
4
11
|
def check_eager_loadable!
|
|
5
|
-
super unless
|
|
12
|
+
super unless temporal_scope? && scope_requires_no_params?
|
|
6
13
|
end
|
|
7
14
|
|
|
8
15
|
private
|
|
9
16
|
|
|
10
|
-
def
|
|
11
|
-
scope.respond_to?(:
|
|
17
|
+
def temporal_scope?
|
|
18
|
+
scope.respond_to?(:temporal_scope?) && scope.temporal_scope?
|
|
12
19
|
end
|
|
13
20
|
|
|
14
21
|
def scope_requires_no_params?
|
|
@@ -0,0 +1,23 @@
|
|
|
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,5 +1,8 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module Patches
|
|
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 versions of
|
|
5
|
+
# Active Record, but until those patches are released it's included here.
|
|
3
6
|
module JoinDependency
|
|
4
7
|
def instantiate(result_set, strict_loading_value, &block)
|
|
5
8
|
primary_key = Array(join_root.primary_key).map { |column| aliases.column_alias(join_root, column) }
|
|
@@ -4,22 +4,26 @@ module ActiveRecord::Temporal
|
|
|
4
4
|
private
|
|
5
5
|
|
|
6
6
|
if ActiveRecord.version > Gem::Version.new("8.0.4")
|
|
7
|
-
def build_arel(
|
|
8
|
-
|
|
7
|
+
def build_arel(aliases)
|
|
8
|
+
Querying::Scoping.as_of(time_tag_values) do
|
|
9
|
+
super
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
12
|
else
|
|
11
|
-
def build_arel(
|
|
12
|
-
|
|
13
|
+
def build_arel(aliases, connection = nil)
|
|
14
|
+
Querying::Scoping.as_of(time_tag_values) do
|
|
15
|
+
super
|
|
16
|
+
end
|
|
13
17
|
end
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
def instantiate_records(rows, &block)
|
|
17
|
-
return super if
|
|
21
|
+
return super if time_tag_values.empty?
|
|
18
22
|
|
|
19
23
|
records = super
|
|
20
24
|
|
|
21
25
|
records.each do |record|
|
|
22
|
-
record.
|
|
26
|
+
record.initialize_time_tags_from_relation(self) if record.is_a?(Querying)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
records
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
module ActiveRecord::Temporal
|
|
2
2
|
module Patches
|
|
3
|
+
# Patches the preloader's `through_scope` method to pass along the relation's
|
|
4
|
+
# time tag values when it handles has-many-through associations. The handler
|
|
5
|
+
# for has-many associations uses `Relation#merge`, but this one doesn't.
|
|
3
6
|
module ThroughAssociation
|
|
4
7
|
def through_scope
|
|
5
8
|
super.tap do |scope|
|
|
6
|
-
scope.
|
|
9
|
+
scope.time_tag_values = reflection_scope.time_tag_values
|
|
7
10
|
end
|
|
8
11
|
end
|
|
9
12
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module Querying
|
|
3
|
+
class AssociationScope
|
|
4
|
+
class << self
|
|
5
|
+
def build(block)
|
|
6
|
+
scope = merge_scopes(block)
|
|
7
|
+
|
|
8
|
+
def scope.temporal_scope? = true
|
|
9
|
+
|
|
10
|
+
scope
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def merge_scopes(block)
|
|
16
|
+
temporal_scope = build_temporal_scope
|
|
17
|
+
|
|
18
|
+
if !block
|
|
19
|
+
return ->(owner = nil) do
|
|
20
|
+
instance_exec(owner, all, &temporal_scope)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
if block.arity != 0
|
|
25
|
+
return ->(owner) do
|
|
26
|
+
base = instance_exec(owner, &block)
|
|
27
|
+
instance_exec(owner, base, &temporal_scope)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
->(owner = nil) do
|
|
32
|
+
base = instance_exec(&block)
|
|
33
|
+
instance_exec(owner, base, &temporal_scope)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def build_temporal_scope
|
|
38
|
+
->(owner, base) do
|
|
39
|
+
registry_constraints = ScopeRegistry
|
|
40
|
+
.association_constraints_for(time_dimensions)
|
|
41
|
+
|
|
42
|
+
registry_time_tags = ScopeRegistry
|
|
43
|
+
.association_tags_for(time_dimensions)
|
|
44
|
+
|
|
45
|
+
owner_time_tags = owner&.time_tags_for(time_dimensions) || {}
|
|
46
|
+
|
|
47
|
+
base
|
|
48
|
+
.at_time(registry_constraints.merge(owner_time_tags))
|
|
49
|
+
.time_tags(owner_time_tags.merge(registry_time_tags))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module ActiveRecord::Temporal::Querying
|
|
2
|
+
class PredicateBuilder
|
|
3
|
+
class ContainsHandler
|
|
4
|
+
def initialize(predicate_builder)
|
|
5
|
+
@predicate_builder = predicate_builder
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(attribute, value)
|
|
9
|
+
time_as_tstz = Arel::Nodes::As.new(
|
|
10
|
+
Arel::Nodes::Quoted.new(value.time),
|
|
11
|
+
Arel::Nodes::SqlLiteral.new("timestamptz")
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
cast_value = Arel::Nodes::NamedFunction.new("CAST", [time_as_tstz])
|
|
15
|
+
|
|
16
|
+
attribute.contains(cast_value)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :predicate_builder
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module ActiveRecord::Temporal::Querying
|
|
2
|
+
class PredicateBuilder
|
|
3
|
+
require_relative "contains_handler"
|
|
4
|
+
|
|
5
|
+
module Handlers
|
|
6
|
+
Contains = Struct.new(:time)
|
|
7
|
+
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
def contains(time)
|
|
12
|
+
Contains.new(time)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def predicate_builder
|
|
16
|
+
super.tap do |base_predicate_builder|
|
|
17
|
+
register_predicate_builder_handlers(base_predicate_builder)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def register_predicate_builder_handlers(base_predicate_builder)
|
|
24
|
+
@register_predicate_builder_handlers ||= base_predicate_builder
|
|
25
|
+
.register_handler Contains,
|
|
26
|
+
PredicateBuilder::ContainsHandler.new(base_predicate_builder)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module Querying
|
|
3
|
+
module QueryMethods
|
|
4
|
+
require "activerecord/temporal/querying/where_clause_refinement"
|
|
5
|
+
|
|
6
|
+
using Querying::WhereClauseRefinement
|
|
7
|
+
|
|
8
|
+
def time_tags(scope)
|
|
9
|
+
spawn.time_tags!(scope)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def time_tags!(scope)
|
|
13
|
+
self.time_tag_values = time_tag_values.merge(scope)
|
|
14
|
+
self
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def time_tag_values
|
|
18
|
+
@values.fetch(:time_tags, ActiveRecord::QueryMethods::FROZEN_EMPTY_HASH)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def time_tag_values=(scope)
|
|
22
|
+
assert_modifiable! # TODO: write test
|
|
23
|
+
|
|
24
|
+
@values[:time_tags] = scope
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def rewhere_contains(conditions)
|
|
28
|
+
scope = spawn
|
|
29
|
+
|
|
30
|
+
scope.where_clause = where_clause.except_contains(conditions.keys)
|
|
31
|
+
scope.where_clause += build_where_clause(conditions)
|
|
32
|
+
|
|
33
|
+
scope
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module ActiveRecord::Temporal
|
|
2
|
+
module Querying
|
|
3
|
+
class ScopeRegistry
|
|
4
|
+
class << self
|
|
5
|
+
delegate :global_constraints,
|
|
6
|
+
:association_constraints,
|
|
7
|
+
:association_tags,
|
|
8
|
+
:universal_global_constraint_time,
|
|
9
|
+
:global_constraints=,
|
|
10
|
+
:association_constraints=,
|
|
11
|
+
:association_tags=,
|
|
12
|
+
:universal_global_constraint_time=,
|
|
13
|
+
:global_constraint_for,
|
|
14
|
+
:association_constraint_for,
|
|
15
|
+
:association_tag_for,
|
|
16
|
+
:global_constraints_for,
|
|
17
|
+
:association_constraints_for,
|
|
18
|
+
:association_tags_for,
|
|
19
|
+
:set_global_constraints,
|
|
20
|
+
:set_association_constraints,
|
|
21
|
+
:set_association_tags,
|
|
22
|
+
to: :instance
|
|
23
|
+
|
|
24
|
+
def instance
|
|
25
|
+
ActiveSupport::IsolatedExecutionState[:temporal_querying_registry] ||= new
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
attr_accessor :global_constraints,
|
|
30
|
+
:association_constraints,
|
|
31
|
+
:association_tags,
|
|
32
|
+
:universal_global_constraint_time
|
|
33
|
+
|
|
34
|
+
def initialize(
|
|
35
|
+
global_constraints: nil,
|
|
36
|
+
association_constraints: nil,
|
|
37
|
+
association_tags: nil,
|
|
38
|
+
default_association_constraint_time: nil,
|
|
39
|
+
universal_global_constraint_time: nil
|
|
40
|
+
)
|
|
41
|
+
@global_constraints = global_constraints || {}
|
|
42
|
+
@association_constraints = association_constraints || {}
|
|
43
|
+
@association_tags = association_tags || {}
|
|
44
|
+
@default_association_constraint_time = default_association_constraint_time ||
|
|
45
|
+
-> { Time.current }
|
|
46
|
+
@universal_global_constraint_time = universal_global_constraint_time
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def global_constraint_for(dimension)
|
|
50
|
+
global_constraints[dimension] || universal_global_constraint_time
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def association_constraint_for(dimension)
|
|
54
|
+
association_constraints[dimension] ||
|
|
55
|
+
global_constraint_for(dimension) ||
|
|
56
|
+
@default_association_constraint_time.call
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def association_tag_for(dimension)
|
|
60
|
+
association_tags[dimension]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def global_constraints_for(*dimensions)
|
|
64
|
+
dimensions.flatten.index_with do |dimension|
|
|
65
|
+
global_constraint_for(dimension)
|
|
66
|
+
end.compact
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def association_constraints_for(*dimensions)
|
|
70
|
+
dimensions.flatten.index_with do |dimension|
|
|
71
|
+
association_constraint_for(dimension)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def association_tags_for(*dimensions)
|
|
76
|
+
association_tags.slice(*dimensions.flatten)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def set_global_constraints(time_coords)
|
|
80
|
+
self.global_constraints = global_constraints
|
|
81
|
+
.merge(time_coords)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def set_association_constraints(time_coords)
|
|
85
|
+
self.association_constraints = association_constraints
|
|
86
|
+
.merge(time_coords)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def set_association_tags(time_coords)
|
|
90
|
+
self.association_tags = association_tags
|
|
91
|
+
.merge(time_coords)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|