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.
@@ -13,92 +13,45 @@ module ChronoModel
13
13
  #
14
14
  class Association < ActiveRecord::Associations::Association
15
15
 
16
- # Add temporal Common Table Expressions (WITH queries) to the resulting
17
- # scope, checking whether either the association class or the through
18
- # association one are ChronoModels.
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
- return super unless _chrono_record?
21
-
22
- ctes = {}
23
-
24
- if reflection.klass.chrono?
25
- ctes.update _chrono_ctes_for(reflection.klass)
26
- end
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 = super
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
@@ -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