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,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 ClosedRevisionError, "Cannot revise closed version" 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 ClosedRevisionError, "Cannot revise closed version" 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 ClosedRevisionError, "Cannot inactivate closed version" unless head_revision?
|
|
116
|
+
|
|
117
|
+
set_time_dimension_end(time)
|
|
118
|
+
save
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -2,76 +2,12 @@ module ActiveRecord::Temporal
|
|
|
2
2
|
module ApplicationVersioning
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
include AsOfQuery
|
|
7
|
-
end
|
|
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
|
|
5
|
+
class ClosedRevisionError < StandardError; end
|
|
32
6
|
|
|
33
|
-
|
|
7
|
+
class_methods do
|
|
8
|
+
def application_versioned
|
|
9
|
+
include ApplicationVersioned
|
|
34
10
|
end
|
|
35
11
|
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
12
|
end
|
|
77
13
|
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?
|
|
@@ -1,5 +1,9 @@
|
|
|
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 merged and backported to supported stable
|
|
5
|
+
# versions of Active Record, but until those patches are released it's included
|
|
6
|
+
# here.
|
|
3
7
|
module JoinDependency
|
|
4
8
|
def instantiate(result_set, strict_loading_value, &block)
|
|
5
9
|
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
|
|
@@ -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
|