temporal_tables 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +4 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +139 -0
- data/Rakefile +9 -0
- data/config.ru +7 -0
- data/gemfiles/Gemfile.5.2.mysql +16 -0
- data/gemfiles/Gemfile.5.2.mysql.lock +155 -0
- data/gemfiles/Gemfile.5.2.pg +16 -0
- data/gemfiles/Gemfile.5.2.pg.lock +155 -0
- data/lib/temporal_tables.rb +63 -0
- data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +56 -0
- data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +81 -0
- data/lib/temporal_tables/history_hook.rb +50 -0
- data/lib/temporal_tables/relation_extensions.rb +125 -0
- data/lib/temporal_tables/scope_extensions.rb +13 -0
- data/lib/temporal_tables/temporal_adapter.rb +159 -0
- data/lib/temporal_tables/temporal_class.rb +91 -0
- data/lib/temporal_tables/version.rb +3 -0
- data/lib/temporal_tables/whodunnit.rb +19 -0
- data/spec/basic_history_spec.rb +91 -0
- data/spec/extensions/combustion.rb +9 -0
- data/spec/internal/app/models/coven.rb +3 -0
- data/spec/internal/app/models/person.rb +4 -0
- data/spec/internal/app/models/wart.rb +7 -0
- data/spec/internal/config/database.sample.yml +12 -0
- data/spec/internal/config/routes.rb +3 -0
- data/spec/internal/db/schema.rb +16 -0
- data/spec/internal/log/.gitignore +1 -0
- data/spec/internal/public/favicon.ico +0 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/database.rb +9 -0
- data/temporal_tables.gemspec +24 -0
- metadata +149 -0
@@ -0,0 +1,16 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Runtime dependencies
|
4
|
+
gem 'rails', '~> 5.2.0'
|
5
|
+
gem 'pg', '>= 0.18', '< 2.0'
|
6
|
+
|
7
|
+
# Development dependencies
|
8
|
+
gem 'rspec', '~> 3.4'
|
9
|
+
gem 'rake'
|
10
|
+
gem 'byebug'
|
11
|
+
gem 'database_cleaner'
|
12
|
+
gem 'combustion'
|
13
|
+
gem 'gemika'
|
14
|
+
|
15
|
+
# Gem under test
|
16
|
+
gem 'temporal_tables', :path => '..'
|
@@ -0,0 +1,155 @@
|
|
1
|
+
PATH
|
2
|
+
remote: ..
|
3
|
+
specs:
|
4
|
+
temporal_tables (0.6.0)
|
5
|
+
rails (~> 5.2)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actioncable (5.2.0)
|
11
|
+
actionpack (= 5.2.0)
|
12
|
+
nio4r (~> 2.0)
|
13
|
+
websocket-driver (>= 0.6.1)
|
14
|
+
actionmailer (5.2.0)
|
15
|
+
actionpack (= 5.2.0)
|
16
|
+
actionview (= 5.2.0)
|
17
|
+
activejob (= 5.2.0)
|
18
|
+
mail (~> 2.5, >= 2.5.4)
|
19
|
+
rails-dom-testing (~> 2.0)
|
20
|
+
actionpack (5.2.0)
|
21
|
+
actionview (= 5.2.0)
|
22
|
+
activesupport (= 5.2.0)
|
23
|
+
rack (~> 2.0)
|
24
|
+
rack-test (>= 0.6.3)
|
25
|
+
rails-dom-testing (~> 2.0)
|
26
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
27
|
+
actionview (5.2.0)
|
28
|
+
activesupport (= 5.2.0)
|
29
|
+
builder (~> 3.1)
|
30
|
+
erubi (~> 1.4)
|
31
|
+
rails-dom-testing (~> 2.0)
|
32
|
+
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
33
|
+
activejob (5.2.0)
|
34
|
+
activesupport (= 5.2.0)
|
35
|
+
globalid (>= 0.3.6)
|
36
|
+
activemodel (5.2.0)
|
37
|
+
activesupport (= 5.2.0)
|
38
|
+
activerecord (5.2.0)
|
39
|
+
activemodel (= 5.2.0)
|
40
|
+
activesupport (= 5.2.0)
|
41
|
+
arel (>= 9.0)
|
42
|
+
activestorage (5.2.0)
|
43
|
+
actionpack (= 5.2.0)
|
44
|
+
activerecord (= 5.2.0)
|
45
|
+
marcel (~> 0.3.1)
|
46
|
+
activesupport (5.2.0)
|
47
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
48
|
+
i18n (>= 0.7, < 2)
|
49
|
+
minitest (~> 5.1)
|
50
|
+
tzinfo (~> 1.1)
|
51
|
+
arel (9.0.0)
|
52
|
+
builder (3.2.3)
|
53
|
+
byebug (10.0.2)
|
54
|
+
combustion (0.9.1)
|
55
|
+
activesupport (>= 3.0.0)
|
56
|
+
railties (>= 3.0.0)
|
57
|
+
thor (>= 0.14.6)
|
58
|
+
concurrent-ruby (1.0.5)
|
59
|
+
crass (1.0.4)
|
60
|
+
database_cleaner (1.7.0)
|
61
|
+
diff-lcs (1.3)
|
62
|
+
erubi (1.7.1)
|
63
|
+
gemika (0.3.2)
|
64
|
+
globalid (0.4.1)
|
65
|
+
activesupport (>= 4.2.0)
|
66
|
+
i18n (1.0.1)
|
67
|
+
concurrent-ruby (~> 1.0)
|
68
|
+
loofah (2.2.2)
|
69
|
+
crass (~> 1.0.2)
|
70
|
+
nokogiri (>= 1.5.9)
|
71
|
+
mail (2.7.0)
|
72
|
+
mini_mime (>= 0.1.1)
|
73
|
+
marcel (0.3.2)
|
74
|
+
mimemagic (~> 0.3.2)
|
75
|
+
method_source (0.9.0)
|
76
|
+
mimemagic (0.3.2)
|
77
|
+
mini_mime (1.0.0)
|
78
|
+
mini_portile2 (2.3.0)
|
79
|
+
minitest (5.11.3)
|
80
|
+
nio4r (2.3.1)
|
81
|
+
nokogiri (1.8.4)
|
82
|
+
mini_portile2 (~> 2.3.0)
|
83
|
+
pg (1.0.0)
|
84
|
+
rack (2.0.5)
|
85
|
+
rack-test (1.0.0)
|
86
|
+
rack (>= 1.0, < 3)
|
87
|
+
rails (5.2.0)
|
88
|
+
actioncable (= 5.2.0)
|
89
|
+
actionmailer (= 5.2.0)
|
90
|
+
actionpack (= 5.2.0)
|
91
|
+
actionview (= 5.2.0)
|
92
|
+
activejob (= 5.2.0)
|
93
|
+
activemodel (= 5.2.0)
|
94
|
+
activerecord (= 5.2.0)
|
95
|
+
activestorage (= 5.2.0)
|
96
|
+
activesupport (= 5.2.0)
|
97
|
+
bundler (>= 1.3.0)
|
98
|
+
railties (= 5.2.0)
|
99
|
+
sprockets-rails (>= 2.0.0)
|
100
|
+
rails-dom-testing (2.0.3)
|
101
|
+
activesupport (>= 4.2.0)
|
102
|
+
nokogiri (>= 1.6)
|
103
|
+
rails-html-sanitizer (1.0.4)
|
104
|
+
loofah (~> 2.2, >= 2.2.2)
|
105
|
+
railties (5.2.0)
|
106
|
+
actionpack (= 5.2.0)
|
107
|
+
activesupport (= 5.2.0)
|
108
|
+
method_source
|
109
|
+
rake (>= 0.8.7)
|
110
|
+
thor (>= 0.18.1, < 2.0)
|
111
|
+
rake (12.3.1)
|
112
|
+
rspec (3.7.0)
|
113
|
+
rspec-core (~> 3.7.0)
|
114
|
+
rspec-expectations (~> 3.7.0)
|
115
|
+
rspec-mocks (~> 3.7.0)
|
116
|
+
rspec-core (3.7.1)
|
117
|
+
rspec-support (~> 3.7.0)
|
118
|
+
rspec-expectations (3.7.0)
|
119
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
120
|
+
rspec-support (~> 3.7.0)
|
121
|
+
rspec-mocks (3.7.0)
|
122
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
123
|
+
rspec-support (~> 3.7.0)
|
124
|
+
rspec-support (3.7.1)
|
125
|
+
sprockets (3.7.2)
|
126
|
+
concurrent-ruby (~> 1.0)
|
127
|
+
rack (> 1, < 3)
|
128
|
+
sprockets-rails (3.2.1)
|
129
|
+
actionpack (>= 4.0)
|
130
|
+
activesupport (>= 4.0)
|
131
|
+
sprockets (>= 3.0.0)
|
132
|
+
thor (0.20.0)
|
133
|
+
thread_safe (0.3.6)
|
134
|
+
tzinfo (1.2.5)
|
135
|
+
thread_safe (~> 0.1)
|
136
|
+
websocket-driver (0.7.0)
|
137
|
+
websocket-extensions (>= 0.1.0)
|
138
|
+
websocket-extensions (0.1.3)
|
139
|
+
|
140
|
+
PLATFORMS
|
141
|
+
ruby
|
142
|
+
|
143
|
+
DEPENDENCIES
|
144
|
+
byebug
|
145
|
+
combustion
|
146
|
+
database_cleaner
|
147
|
+
gemika
|
148
|
+
pg (>= 0.18, < 2.0)
|
149
|
+
rails (~> 5.2.0)
|
150
|
+
rake
|
151
|
+
rspec (~> 3.4)
|
152
|
+
temporal_tables!
|
153
|
+
|
154
|
+
BUNDLED WITH
|
155
|
+
1.16.1
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "temporal_tables/temporal_adapter"
|
2
|
+
require "temporal_tables/connection_adapters/mysql_adapter"
|
3
|
+
require "temporal_tables/connection_adapters/postgresql_adapter"
|
4
|
+
require "temporal_tables/whodunnit"
|
5
|
+
require "temporal_tables/temporal_class"
|
6
|
+
require "temporal_tables/history_hook"
|
7
|
+
require "temporal_tables/relation_extensions"
|
8
|
+
require "temporal_tables/scope_extensions"
|
9
|
+
require "temporal_tables/version"
|
10
|
+
|
11
|
+
module TemporalTables
|
12
|
+
class Railtie < ::Rails::Railtie
|
13
|
+
initializer "temporal_tables.load" do
|
14
|
+
# Iterating the subclasses will find any adapter implementations
|
15
|
+
# which are in use by the rails app, and mixin the temporal functionality.
|
16
|
+
# It's necessary to do this on the implementations in order for the
|
17
|
+
# alias method chain hooks to work.
|
18
|
+
ActiveRecord::ConnectionAdapters::AbstractAdapter.subclasses.each do |subclass|
|
19
|
+
subclass.send :prepend, TemporalTables::TemporalAdapter
|
20
|
+
|
21
|
+
module_name = subclass.name.split("::").last
|
22
|
+
subclass.send :prepend, TemporalTables::ConnectionAdapters.const_get(module_name) if TemporalTables::ConnectionAdapters.const_defined?(module_name)
|
23
|
+
end
|
24
|
+
|
25
|
+
ActiveRecord::Base.send :prepend, TemporalTables::Whodunnit
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
@@create_by_default = false
|
30
|
+
def self.create_by_default
|
31
|
+
@@create_by_default
|
32
|
+
end
|
33
|
+
def self.create_by_default=(default)
|
34
|
+
@@create_by_default = default
|
35
|
+
end
|
36
|
+
|
37
|
+
@@skipped_temporal_tables = [:schema_migrations, :sessions]
|
38
|
+
def self.skip_temporal_table_for(*tables)
|
39
|
+
@@skipped_temporal_tables += tables
|
40
|
+
end
|
41
|
+
def self.skipped_temporal_tables
|
42
|
+
@@skipped_temporal_tables.dup
|
43
|
+
end
|
44
|
+
|
45
|
+
@@add_updated_by_field = false
|
46
|
+
@@updated_by_type = :string
|
47
|
+
@@updated_by_proc = nil
|
48
|
+
def self.updated_by_type
|
49
|
+
@@updated_by_type
|
50
|
+
end
|
51
|
+
def self.updated_by_proc
|
52
|
+
@@updated_by_proc
|
53
|
+
end
|
54
|
+
def self.add_updated_by_field(type = :string, &block)
|
55
|
+
if block_given?
|
56
|
+
@@add_updated_by_field = true
|
57
|
+
@@updated_by_type = type
|
58
|
+
@@updated_by_proc = block
|
59
|
+
end
|
60
|
+
|
61
|
+
@@add_updated_by_field
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module TemporalTables
|
2
|
+
module ConnectionAdapters
|
3
|
+
module AbstractMysqlAdapter
|
4
|
+
def drop_temporal_triggers(table_name)
|
5
|
+
execute "drop trigger #{table_name}_ai"
|
6
|
+
execute "drop trigger #{table_name}_au"
|
7
|
+
execute "drop trigger #{table_name}_ad"
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_temporal_triggers(table_name)
|
11
|
+
column_names = columns(table_name).map(&:name)
|
12
|
+
|
13
|
+
execute %{
|
14
|
+
create trigger #{table_name}_ai after insert on #{table_name}
|
15
|
+
for each row
|
16
|
+
begin
|
17
|
+
set @current_time = utc_timestamp(6);
|
18
|
+
|
19
|
+
insert into #{temporal_name(table_name)} (#{column_names.join(', ')}, eff_from)
|
20
|
+
values (#{column_names.collect {|c| "new.#{c}"}.join(', ')}, @current_time);
|
21
|
+
|
22
|
+
end
|
23
|
+
}
|
24
|
+
|
25
|
+
execute %{
|
26
|
+
create trigger #{table_name}_au after update on #{table_name}
|
27
|
+
for each row
|
28
|
+
begin
|
29
|
+
set @current_time = utc_timestamp(6);
|
30
|
+
|
31
|
+
update #{temporal_name(table_name)} set eff_to = @current_time
|
32
|
+
where id = new.id
|
33
|
+
and eff_to = '9999-12-31';
|
34
|
+
|
35
|
+
insert into #{temporal_name(table_name)} (#{column_names.join(', ')}, eff_from)
|
36
|
+
values (#{column_names.collect {|c| "new.#{c}"}.join(', ')}, @current_time);
|
37
|
+
|
38
|
+
end
|
39
|
+
}
|
40
|
+
|
41
|
+
execute %{
|
42
|
+
create trigger #{table_name}_ad after delete on #{table_name}
|
43
|
+
for each row
|
44
|
+
begin
|
45
|
+
set @current_time = utc_timestamp(6);
|
46
|
+
|
47
|
+
update #{temporal_name(table_name)} set eff_to = @current_time
|
48
|
+
where id = old.id
|
49
|
+
and eff_to = '9999-12-31';
|
50
|
+
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module TemporalTables
|
2
|
+
module ConnectionAdapters
|
3
|
+
module PostgreSQLAdapter
|
4
|
+
def drop_temporal_triggers(table_name)
|
5
|
+
execute "drop trigger #{table_name}_ai on #{table_name}"
|
6
|
+
execute "drop trigger #{table_name}_au on #{table_name}"
|
7
|
+
execute "drop trigger #{table_name}_ad on #{table_name}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def create_temporal_triggers(table_name)
|
11
|
+
column_names = columns(table_name).map(&:name)
|
12
|
+
|
13
|
+
execute %{
|
14
|
+
create or replace function #{table_name}_ai() returns trigger as $#{table_name}_ai$
|
15
|
+
declare
|
16
|
+
cur_time timestamp without time zone;
|
17
|
+
begin
|
18
|
+
cur_time := localtimestamp;
|
19
|
+
|
20
|
+
insert into #{temporal_name(table_name)} (#{column_list(column_names)}, eff_from)
|
21
|
+
values (#{column_names.collect {|c| "new.#{c}"}.join(', ')}, cur_time);
|
22
|
+
|
23
|
+
return null;
|
24
|
+
end
|
25
|
+
$#{table_name}_ai$ language plpgsql;
|
26
|
+
|
27
|
+
drop trigger if exists #{table_name}_ai on #{table_name};
|
28
|
+
create trigger #{table_name}_ai after insert on #{table_name}
|
29
|
+
for each row execute procedure #{table_name}_ai();
|
30
|
+
}
|
31
|
+
|
32
|
+
execute %{
|
33
|
+
create or replace function #{table_name}_au() returns trigger as $#{table_name}_au$
|
34
|
+
declare
|
35
|
+
cur_time timestamp without time zone;
|
36
|
+
begin
|
37
|
+
cur_time := localtimestamp;
|
38
|
+
|
39
|
+
update #{temporal_name(table_name)} set eff_to = cur_time
|
40
|
+
where id = new.id
|
41
|
+
and eff_to = '9999-12-31';
|
42
|
+
|
43
|
+
insert into #{temporal_name(table_name)} (#{column_list(column_names)}, eff_from)
|
44
|
+
values (#{column_names.collect {|c| "new.#{c}"}.join(', ')}, cur_time);
|
45
|
+
|
46
|
+
return null;
|
47
|
+
end
|
48
|
+
$#{table_name}_au$ language plpgsql;
|
49
|
+
|
50
|
+
drop trigger if exists #{table_name}_au on #{table_name};
|
51
|
+
create trigger #{table_name}_au after update on #{table_name}
|
52
|
+
for each row execute procedure #{table_name}_au();
|
53
|
+
}
|
54
|
+
|
55
|
+
execute %{
|
56
|
+
create or replace function #{table_name}_ad() returns trigger as $#{table_name}_ad$
|
57
|
+
declare
|
58
|
+
cur_time timestamp without time zone;
|
59
|
+
begin
|
60
|
+
cur_time := localtimestamp;
|
61
|
+
|
62
|
+
update #{temporal_name(table_name)} set eff_to = cur_time
|
63
|
+
where id = old.id
|
64
|
+
and eff_to = '9999-12-31';
|
65
|
+
|
66
|
+
return null;
|
67
|
+
end
|
68
|
+
$#{table_name}_ad$ language plpgsql;
|
69
|
+
|
70
|
+
drop trigger if exists #{table_name}_ad on #{table_name};
|
71
|
+
create trigger #{table_name}_ad after delete on #{table_name}
|
72
|
+
for each row execute procedure #{table_name}_ad();
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
def column_list(column_names)
|
77
|
+
column_names.map { |c| "\"#{c}\"" }.join(', ')
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module TemporalTables
|
2
|
+
# This hooks in a "history" method to ActiveRecord::Base which will
|
3
|
+
# return the class's History class. The history class extends the original
|
4
|
+
# class, but runs against the history table to provide temporal results.
|
5
|
+
#
|
6
|
+
# class Person < ActiveRecord::Base
|
7
|
+
# attr_accessible :name
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# Person #=> Person(id: integer, name: string)
|
11
|
+
# Person.history #=> PersonHistory(history_id: integer, id: integer, name: string, eff_from: datetime, eff_to: datetime)
|
12
|
+
module HistoryHook
|
13
|
+
def self.included(base)
|
14
|
+
base.class_eval do
|
15
|
+
# Return this class's history class.
|
16
|
+
# If it doesn't exist yet, create and initialize it, as well
|
17
|
+
# as all dependent classes (through associations).
|
18
|
+
def self.history
|
19
|
+
raise "Can't view history of history" if name =~ /History$/
|
20
|
+
|
21
|
+
history_class = "#{name}History"
|
22
|
+
history_class.constantize
|
23
|
+
rescue NameError
|
24
|
+
# If the history class doesn't exist yet, create it
|
25
|
+
new_class = Class.new(self) do
|
26
|
+
include TemporalTables::TemporalClass
|
27
|
+
end
|
28
|
+
segments = history_class.split("::")
|
29
|
+
object_class = segments[0...-1].inject(Object) { |o, s| o.const_get(s) }
|
30
|
+
object_class.const_set segments.last, new_class
|
31
|
+
|
32
|
+
# Traverse associations and make sure they have
|
33
|
+
# history classes too.
|
34
|
+
history_class.constantize.temporalize_associations!
|
35
|
+
history_class.constantize
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns a scope for the list of all history records for this
|
41
|
+
# particular object.
|
42
|
+
def history
|
43
|
+
clazz = is_a?(TemporalTables::TemporalClass) ? self.class : self.class.history
|
44
|
+
oid = is_a?(TemporalTables::TemporalClass) ? orig_class.primary_key : self.class.primary_key
|
45
|
+
clazz.unscoped.where(id: attributes[oid]).order(:eff_from)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
ActiveRecord::Base.send :include, TemporalTables::HistoryHook
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module TemporalTables
|
2
|
+
# Stores the time from the "at" field into each of the resulting objects
|
3
|
+
# so that it can be carried forward in subsequent queries.
|
4
|
+
module RelationExtensions
|
5
|
+
def self.included(base)
|
6
|
+
base.class_eval do
|
7
|
+
ActiveRecord::Relation::SINGLE_VALUE_METHODS << :at
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def at_value
|
12
|
+
get_value(:at) || Thread.current[:at_time]
|
13
|
+
end
|
14
|
+
|
15
|
+
def at_value=(value)
|
16
|
+
set_value(:at, value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def at(*args)
|
20
|
+
spawn.at!(*args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def at!(value)
|
24
|
+
self.at_value = value
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def where_clause
|
29
|
+
s = super
|
30
|
+
|
31
|
+
at_clauses = []
|
32
|
+
if historical?
|
33
|
+
at_clauses << where_clause_factory.build(
|
34
|
+
arel_table[:eff_to].gteq(at_value).and(
|
35
|
+
arel_table[:eff_from].lteq(at_value)
|
36
|
+
),
|
37
|
+
[]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
[s, *at_clauses.compact].sum
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_sql(*args)
|
45
|
+
threadify_at do
|
46
|
+
super *args
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def threadify_at
|
51
|
+
if at_value && !Thread.current[:at_time]
|
52
|
+
Thread.current[:at_time] = at_value
|
53
|
+
result = yield
|
54
|
+
Thread.current[:at_time] = nil
|
55
|
+
else
|
56
|
+
result = yield
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
|
61
|
+
def limited_ids_for(*args)
|
62
|
+
threadify_at do
|
63
|
+
super *args
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def exec_queries
|
68
|
+
# Note that record preloading, like when you specify
|
69
|
+
# MyClass.includes(:associations)
|
70
|
+
# happens within this exec_queries call. That's why we needed to
|
71
|
+
# store the at_time in the thread above.
|
72
|
+
threadify_at do
|
73
|
+
super
|
74
|
+
end
|
75
|
+
|
76
|
+
if historical?
|
77
|
+
# Store the at value on each record returned
|
78
|
+
# TODO: traverse preloaded associations too
|
79
|
+
@records.each do |r|
|
80
|
+
r.at_value = at_value
|
81
|
+
end
|
82
|
+
end
|
83
|
+
@records
|
84
|
+
end
|
85
|
+
|
86
|
+
def historical?
|
87
|
+
table_name =~ /_h$/i && at_value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Uses the time from the "at" field stored in the record to filter queries
|
92
|
+
# made to associations.
|
93
|
+
module AssociationExtensions
|
94
|
+
def target_scope
|
95
|
+
if @owner.respond_to?(:at_value)
|
96
|
+
# If this is a history record but no at time was given,
|
97
|
+
# assume the record's effective to date
|
98
|
+
super.at(@owner.at_value || @owner.eff_to)
|
99
|
+
else
|
100
|
+
super
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Uses the at time when fetching preloaded records
|
106
|
+
module PreloaderExtensions
|
107
|
+
def build_scope
|
108
|
+
# It seems the at time can be in either of these places, but not both,
|
109
|
+
# depending on when the preloading happens to be done
|
110
|
+
at_time = @owners.first.at_value if @owners.first.respond_to?(:at_value)
|
111
|
+
at_time ||= Thread.current[:at_time]
|
112
|
+
|
113
|
+
if at_time
|
114
|
+
super.at(at_time)
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
ActiveRecord::Relation.send :prepend, TemporalTables::RelationExtensions
|
124
|
+
ActiveRecord::Associations::Association.send :prepend, TemporalTables::AssociationExtensions
|
125
|
+
ActiveRecord::Associations::Preloader::Association.send :prepend, TemporalTables::PreloaderExtensions
|