chrono_model 0.4.0 → 0.5.0.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.md +64 -35
- data/README.sql +73 -27
- data/lib/chrono_model/adapter.rb +235 -44
- data/lib/chrono_model/patches.rb +28 -75
- data/lib/chrono_model/railtie.rb +0 -25
- data/lib/chrono_model/time_gate.rb +36 -0
- data/lib/chrono_model/time_machine.rb +345 -122
- data/lib/chrono_model/utils.rb +89 -0
- data/lib/chrono_model/version.rb +1 -1
- data/lib/chrono_model.rb +2 -7
- data/spec/adapter_spec.rb +74 -7
- data/spec/support/connection.rb +1 -1
- data/spec/support/helpers.rb +20 -1
- data/spec/time_machine_spec.rb +216 -12
- metadata +11 -9
data/lib/chrono_model/patches.rb
CHANGED
@@ -13,92 +13,45 @@ module ChronoModel
|
|
13
13
|
#
|
14
14
|
class Association < ActiveRecord::Associations::Association
|
15
15
|
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
16
|
+
# If the association class or the through association are ChronoModels,
|
17
|
+
# then fetches the records from a virtual table using a subquery scoped
|
18
|
+
# to a specific timestamp.
|
19
19
|
def scoped
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
20
|
+
scoped = super
|
21
|
+
return scoped unless _chrono_record?
|
22
|
+
|
23
|
+
klass = reflection.options[:polymorphic] ?
|
24
|
+
owner.public_send(reflection.foreign_type).constantize :
|
25
|
+
reflection.klass
|
26
|
+
|
27
|
+
if klass.chrono?
|
28
|
+
# For standard associations, replace the table name with the virtual
|
29
|
+
# as-of table name at the owner's as-of-time
|
30
|
+
#
|
31
|
+
scoped = scoped.readonly.from(klass.history.virtual_table_at(owner.as_of_time))
|
32
|
+
elsif respond_to?(:through_reflection) && through_reflection.klass.chrono?
|
33
|
+
|
34
|
+
# For through associations, replace the joined table name instead.
|
35
|
+
#
|
36
|
+
scoped.join_sources.each do |join|
|
37
|
+
if join.left.name == through_reflection.klass.table_name
|
38
|
+
v_table = through_reflection.klass.history.virtual_table_at(
|
39
|
+
owner.as_of_time, join.left.table_alias || join.left.table_name)
|
40
|
+
|
41
|
+
join.left = Arel::Nodes::SqlLiteral.new(v_table)
|
42
|
+
end
|
43
|
+
end
|
27
44
|
|
28
|
-
if respond_to?(:through_reflection) && through_reflection.klass.chrono?
|
29
|
-
ctes.update _chrono_ctes_for(through_reflection.klass)
|
30
45
|
end
|
31
46
|
|
32
|
-
scoped
|
33
|
-
ctes.each {|table, cte| scoped = scoped.with(table, cte) }
|
34
|
-
return scoped.readonly
|
47
|
+
return scoped
|
35
48
|
end
|
36
49
|
|
37
50
|
private
|
38
|
-
def _chrono_ctes_for(klass)
|
39
|
-
klass.as_of(owner.as_of_time).with_values
|
40
|
-
end
|
41
|
-
|
42
51
|
def _chrono_record?
|
43
52
|
owner.respond_to?(:as_of_time) && owner.as_of_time.present?
|
44
53
|
end
|
45
54
|
end
|
46
55
|
|
47
|
-
# Adds the WITH queries (Common Table Expressions) support to
|
48
|
-
# ActiveRecord::Relation.
|
49
|
-
#
|
50
|
-
# \name is the CTE you want
|
51
|
-
# \value can be a plain SQL query or another AR::Relation
|
52
|
-
#
|
53
|
-
# Example:
|
54
|
-
#
|
55
|
-
# Post.with('posts',
|
56
|
-
# Post.from('history.posts').
|
57
|
-
# where('? BETWEEN valid_from AND valid_to', 1.month.ago)
|
58
|
-
# ).where(:author_id => 1)
|
59
|
-
#
|
60
|
-
# yields:
|
61
|
-
#
|
62
|
-
# WITH posts AS (
|
63
|
-
# SELECT * FROM history.posts WHERE ... BETWEEN valid_from AND valid_to
|
64
|
-
# ) SELECT * FROM posts
|
65
|
-
#
|
66
|
-
# PG Documentation:
|
67
|
-
# http://www.postgresql.org/docs/9.0/static/queries-with.html
|
68
|
-
#
|
69
|
-
module QueryMethods
|
70
|
-
attr_accessor :with_values
|
71
|
-
|
72
|
-
def with(name, value)
|
73
|
-
clone.tap do |relation|
|
74
|
-
relation.with_values ||= {}
|
75
|
-
value = value.to_sql if value.respond_to? :to_sql
|
76
|
-
relation.with_values[name] = value
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def build_arel
|
81
|
-
super.tap {|arel| arel.with with_values if with_values.present? }
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
module Querying
|
86
|
-
delegate :with, :to => :scoped
|
87
|
-
end
|
88
|
-
|
89
|
-
# Fixes ARel's WITH visitor method with the correct SQL syntax
|
90
|
-
#
|
91
|
-
# FIXME: the .children.first is messy. This should be properly
|
92
|
-
# fixed in ARel.
|
93
|
-
#
|
94
|
-
class Visitor < Arel::Visitors::PostgreSQL
|
95
|
-
def visit_Arel_Nodes_With o
|
96
|
-
values = o.children.first.map do |name, value|
|
97
|
-
[name, ' AS (', value.is_a?(String) ? value : visit(value), ')'].join
|
98
|
-
end
|
99
|
-
"WITH #{values.join ', '}"
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
56
|
end
|
104
57
|
end
|
data/lib/chrono_model/railtie.rb
CHANGED
@@ -17,30 +17,5 @@ module ChronoModel
|
|
17
17
|
|
18
18
|
task 'db:schema:load' => 'db:chrono:create_schemas'
|
19
19
|
end
|
20
|
-
|
21
|
-
class SchemaDumper < ::ActiveRecord::SchemaDumper
|
22
|
-
def tables(*)
|
23
|
-
super
|
24
|
-
@connection.send(:_on_temporal_schema) { super }
|
25
|
-
end
|
26
|
-
|
27
|
-
def indexes(table, stream)
|
28
|
-
super
|
29
|
-
if @connection.is_chrono?(table)
|
30
|
-
stream.rewind
|
31
|
-
t = stream.read.sub(':force => true', '\&, :temporal => true') # HACK
|
32
|
-
stream.seek(0)
|
33
|
-
stream.truncate(0)
|
34
|
-
stream.write(t)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
# I'm getting (too) used to this (dirty) override scheme.
|
41
|
-
#
|
42
|
-
silence_warnings do
|
43
|
-
::ActiveRecord::SchemaDumper = SchemaDumper
|
44
|
-
end
|
45
20
|
end
|
46
21
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ChronoModel
|
2
|
+
|
3
|
+
# Provides the TimeMachine API to non-temporal models that associate
|
4
|
+
# temporal ones.
|
5
|
+
#
|
6
|
+
module TimeGate
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def as_of(time)
|
11
|
+
time = Conversions.time_to_utc_string(time.utc) if time.kind_of? Time
|
12
|
+
|
13
|
+
virtual_table = select(%[
|
14
|
+
#{quoted_table_name}.*, #{connection.quote(time)} AS "as_of_time"]
|
15
|
+
).to_sql
|
16
|
+
|
17
|
+
as_of = scoped.from("(#{virtual_table}) #{quoted_table_name}")
|
18
|
+
|
19
|
+
as_of.instance_variable_set(:@temporal, time)
|
20
|
+
|
21
|
+
return as_of
|
22
|
+
end
|
23
|
+
|
24
|
+
include TimeMachine::HistoryMethods::Timeline
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_of(time)
|
28
|
+
self.class.as_of(time).where(:id => self.id).first!
|
29
|
+
end
|
30
|
+
|
31
|
+
def timeline
|
32
|
+
self.class.timeline(self)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|