temporal_tables 0.6.10 → 0.7.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.
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