chrono_model 0.4.0 → 0.5.0.beta

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.
@@ -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