temporal_tables 0.6.10 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +3 -1
  4. data/README.md +1 -1
  5. data/gemfiles/Gemfile.5.0.mysql.lock +3 -3
  6. data/gemfiles/Gemfile.5.0.pg.lock +3 -3
  7. data/gemfiles/Gemfile.5.1.mysql.lock +3 -3
  8. data/gemfiles/Gemfile.5.1.pg.lock +3 -3
  9. data/gemfiles/Gemfile.5.2.mysql.lock +3 -3
  10. data/gemfiles/Gemfile.5.2.pg.lock +3 -3
  11. data/gemfiles/Gemfile.6.0.mysql +16 -0
  12. data/gemfiles/Gemfile.6.0.mysql.lock +171 -0
  13. data/gemfiles/Gemfile.6.0.pg +16 -0
  14. data/gemfiles/Gemfile.6.0.pg.lock +171 -0
  15. data/lib/temporal_tables/arel_table.rb +19 -0
  16. data/lib/temporal_tables/association_extensions.rb +16 -16
  17. data/lib/temporal_tables/connection_adapters/mysql_adapter.rb +54 -54
  18. data/lib/temporal_tables/connection_adapters/postgresql_adapter.rb +64 -64
  19. data/lib/temporal_tables/history_hook.rb +43 -43
  20. data/lib/temporal_tables/join_extensions.rb +14 -14
  21. data/lib/temporal_tables/preloader_extensions.rb +14 -14
  22. data/lib/temporal_tables/reflection_extensions.rb +14 -14
  23. data/lib/temporal_tables/relation_extensions.rb +79 -79
  24. data/lib/temporal_tables/temporal_adapter.rb +181 -174
  25. data/lib/temporal_tables/temporal_class.rb +101 -101
  26. data/lib/temporal_tables/version.rb +1 -1
  27. data/lib/temporal_tables/whodunnit.rb +16 -16
  28. data/lib/temporal_tables.rb +46 -45
  29. data/spec/basic_history_spec.rb +138 -138
  30. data/spec/internal/app/models/flying_machine.rb +1 -1
  31. data/spec/internal/app/models/person.rb +3 -3
  32. data/spec/internal/db/schema.rb +17 -17
  33. data/spec/spec_helper.rb +4 -4
  34. data/spec/support/database.rb +7 -7
  35. data/temporal_tables.gemspec +15 -15
  36. metadata +16 -6
@@ -1,56 +1,56 @@
1
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
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
56
  end
@@ -1,81 +1,81 @@
1
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
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
9
 
10
- def create_temporal_triggers(table_name)
11
- column_names = columns(table_name).map(&:name)
10
+ def create_temporal_triggers(table_name)
11
+ column_names = columns(table_name).map(&:name)
12
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;
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
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);
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
22
 
23
- return null;
24
- end
25
- $#{table_name}_ai$ language plpgsql;
23
+ return null;
24
+ end
25
+ $#{table_name}_ai$ language plpgsql;
26
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
- }
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
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;
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
38
 
39
- update #{temporal_name(table_name)} set eff_to = cur_time
40
- where id = new.id
41
- and eff_to = '9999-12-31';
39
+ update #{temporal_name(table_name)} set eff_to = cur_time
40
+ where id = new.id
41
+ and eff_to = '9999-12-31';
42
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);
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
45
 
46
- return null;
47
- end
48
- $#{table_name}_au$ language plpgsql;
46
+ return null;
47
+ end
48
+ $#{table_name}_au$ language plpgsql;
49
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
- }
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
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;
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
61
 
62
- update #{temporal_name(table_name)} set eff_to = cur_time
63
- where id = old.id
64
- and eff_to = '9999-12-31';
62
+ update #{temporal_name(table_name)} set eff_to = cur_time
63
+ where id = old.id
64
+ and eff_to = '9999-12-31';
65
65
 
66
- return null;
67
- end
68
- $#{table_name}_ad$ language plpgsql;
66
+ return null;
67
+ end
68
+ $#{table_name}_ad$ language plpgsql;
69
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
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
75
 
76
- def column_list(column_names)
77
- column_names.map { |c| "\"#{c}\"" }.join(', ')
78
- end
79
- end
80
- end
76
+ def column_list(column_names)
77
+ column_names.map { |c| "\"#{c}\"" }.join(', ')
78
+ end
79
+ end
80
+ end
81
81
  end
@@ -1,50 +1,50 @@
1
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$/
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
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
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
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
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
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
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
48
  end
49
49
 
50
50
  ActiveRecord::Base.send :include, TemporalTables::HistoryHook
@@ -1,20 +1,20 @@
1
1
  module TemporalTables
2
- # This is required for eager_load to work in Rails 5.0.x
3
- module JoinDependencyExtensions
4
- def build_constraint(klass, table, key, foreign_table, foreign_key)
5
- constraint = super
6
- if at_value = Thread.current[:at_time]
7
- constraint = constraint.and(klass.build_temporal_constraint(at_value))
8
- end
9
- constraint
10
- end
11
- end
2
+ # This is required for eager_load to work in Rails 5.0.x
3
+ module JoinDependencyExtensions
4
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
5
+ constraint = super
6
+ if at_value = Thread.current[:at_time]
7
+ constraint = constraint.and(klass.build_temporal_constraint(at_value))
8
+ end
9
+ constraint
10
+ end
11
+ end
12
12
  end
13
13
 
14
14
  case Rails::VERSION::MAJOR
15
15
  when 5
16
- case Rails::VERSION::MINOR
17
- when 0, 1
18
- ActiveRecord::Associations::JoinDependency::JoinAssociation.prepend TemporalTables::JoinDependencyExtensions
19
- end
16
+ case Rails::VERSION::MINOR
17
+ when 0, 1
18
+ ActiveRecord::Associations::JoinDependency::JoinAssociation.prepend TemporalTables::JoinDependencyExtensions
19
+ end
20
20
  end
@@ -1,19 +1,19 @@
1
1
  module TemporalTables
2
- # Uses the at time when fetching preloaded records
3
- module PreloaderExtensions
4
- def build_scope
5
- # It seems the at time can be in either of these places, but not both,
6
- # depending on when the preloading happens to be done
7
- at_time = @owners.first.at_value if @owners.first.respond_to?(:at_value)
8
- at_time ||= Thread.current[:at_time]
2
+ # Uses the at time when fetching preloaded records
3
+ module PreloaderExtensions
4
+ def build_scope
5
+ # It seems the at time can be in either of these places, but not both,
6
+ # depending on when the preloading happens to be done
7
+ at_time = @owners.first.at_value if @owners.first.respond_to?(:at_value)
8
+ at_time ||= Thread.current[:at_time]
9
9
 
10
- if at_time
11
- super.at(at_time)
12
- else
13
- super
14
- end
15
- end
16
- end
10
+ if at_time
11
+ super.at(at_time)
12
+ else
13
+ super
14
+ end
15
+ end
16
+ end
17
17
  end
18
18
 
19
19
  ActiveRecord::Associations::Preloader::Association.prepend TemporalTables::PreloaderExtensions
@@ -1,21 +1,21 @@
1
1
  module TemporalTables
2
- # This is required for eager_load to work in Rails 5.2.x
3
- module AbstractReflectionExtensions
4
- def build_join_constraint(table, foreign_table)
5
- constraint = super
6
- if at_value = Thread.current[:at_time]
7
- constraint = constraint.and(klass.build_temporal_constraint(at_value))
8
- end
9
- constraint
10
- end
11
- end
2
+ # This is required for eager_load to work in Rails 5.2.x
3
+ module AbstractReflectionExtensions
4
+ def build_join_constraint(table, foreign_table)
5
+ constraint = super
6
+ if at_value = Thread.current[:at_time]
7
+ constraint = constraint.and(klass.build_temporal_constraint(at_value))
8
+ end
9
+ constraint
10
+ end
11
+ end
12
12
  end
13
13
 
14
14
  case Rails::VERSION::MAJOR
15
15
  when 5
16
- case Rails::VERSION::MINOR
17
- when 2
18
- ActiveRecord::Reflection::AbstractReflection.prepend TemporalTables::AbstractReflectionExtensions
19
- end
16
+ case Rails::VERSION::MINOR
17
+ when 2
18
+ ActiveRecord::Reflection::AbstractReflection.prepend TemporalTables::AbstractReflectionExtensions
19
+ end
20
20
  end
21
21
 
@@ -1,94 +1,94 @@
1
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
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
10
 
11
- def at_value
12
- case Rails::VERSION::MINOR
13
- when 0
14
- @values.fetch(:at, nil) || Thread.current[:at_time]
15
- else
16
- get_value(:at) || Thread.current[:at_time]
17
- end
18
- end
11
+ def at_value
12
+ case Rails::VERSION::MINOR
13
+ when 0
14
+ @values.fetch(:at, nil) || Thread.current[:at_time]
15
+ else
16
+ get_value(:at) || Thread.current[:at_time]
17
+ end
18
+ end
19
19
 
20
- def at_value=(value)
21
- case Rails::VERSION::MINOR
22
- when 0
23
- @values[:at] = value
24
- else
25
- set_value(:at, value)
26
- end
27
- end
20
+ def at_value=(value)
21
+ case Rails::VERSION::MINOR
22
+ when 0
23
+ @values[:at] = value
24
+ else
25
+ set_value(:at, value)
26
+ end
27
+ end
28
28
 
29
- def at(*args)
30
- spawn.at!(*args)
31
- end
29
+ def at(*args)
30
+ spawn.at!(*args)
31
+ end
32
32
 
33
- def at!(value)
34
- self.at_value = value
35
- self.where!(klass.build_temporal_constraint(value))
36
- end
33
+ def at!(value)
34
+ self.at_value = value
35
+ self.where!(klass.build_temporal_constraint(value))
36
+ end
37
37
 
38
- def to_sql(*args)
39
- threadify_at do
40
- super *args
41
- end
42
- end
38
+ def to_sql(*args)
39
+ threadify_at do
40
+ super *args
41
+ end
42
+ end
43
43
 
44
- def threadify_at
45
- if at_value && !Thread.current[:at_time]
46
- Thread.current[:at_time] = at_value
47
- result = yield
48
- Thread.current[:at_time] = nil
49
- else
50
- result = yield
51
- end
52
- result
53
- end
44
+ def threadify_at
45
+ if at_value && !Thread.current[:at_time]
46
+ Thread.current[:at_time] = at_value
47
+ result = yield
48
+ Thread.current[:at_time] = nil
49
+ else
50
+ result = yield
51
+ end
52
+ result
53
+ end
54
54
 
55
- def limited_ids_for(*args)
56
- threadify_at do
57
- super *args
58
- end
59
- end
55
+ def limited_ids_for(*args)
56
+ threadify_at do
57
+ super *args
58
+ end
59
+ end
60
60
 
61
- def exec_queries
62
- # Note that record preloading, like when you specify
63
- # MyClass.includes(:associations)
64
- # happens within this exec_queries call. That's why we needed to
65
- # store the at_time in the thread above.
66
- threadify_at do
67
- super
68
- end
61
+ def exec_queries
62
+ # Note that record preloading, like when you specify
63
+ # MyClass.includes(:associations)
64
+ # happens within this exec_queries call. That's why we needed to
65
+ # store the at_time in the thread above.
66
+ threadify_at do
67
+ super
68
+ end
69
69
 
70
- if historical?
71
- # Store the at value on each record returned
72
- @records.each do |r|
73
- r.at_value = at_value
74
- end
75
- end
76
- @records
77
- end
70
+ if historical?
71
+ # Store the at value on each record returned
72
+ @records.each do |r|
73
+ r.at_value = at_value
74
+ end
75
+ end
76
+ @records
77
+ end
78
78
 
79
- def historical?
80
- table_name =~ /_h$/i && at_value
81
- end
79
+ def historical?
80
+ table_name =~ /_h$/i && at_value
81
+ end
82
82
 
83
- # Only needed for Rails 5.1.x
84
- def default_value_for(name)
85
- if name == :at
86
- nil
87
- else
88
- super(name)
89
- end
90
- end
91
- end
83
+ # Only needed for Rails 5.1.x
84
+ def default_value_for(name)
85
+ if name == :at
86
+ nil
87
+ else
88
+ super(name)
89
+ end
90
+ end
91
+ end
92
92
  end
93
93
 
94
94
  ActiveRecord::Relation.prepend TemporalTables::RelationExtensions