Module: ChronoModel::TimeMachine::Timeline

Included in:
ChronoModel::TimeGate::ClassMethods, HistoryModel::ClassMethods
Defined in:
lib/chrono_model/time_machine/timeline.rb

Instance Method Summary collapse

Instance Method Details

#has_timeline(options) ⇒ Object



88
89
90
91
92
93
94
# File 'lib/chrono_model/time_machine/timeline.rb', line 88

def has_timeline(options)
  options.assert_valid_keys(:with)

  timeline_associations_from(options[:with]).tap do |assocs|
    timeline_associations.concat assocs
  end
end

#quoted_history_fieldsObject



107
108
109
110
111
112
113
# File 'lib/chrono_model/time_machine/timeline.rb', line 107

def quoted_history_fields
  @quoted_history_fields ||= begin
    validity = "#{quoted_table_name}.#{connection.quote_column_name('validity')}"

    %w[lower upper].map! { |func| "#{func}(#{validity})" }
  end
end

#timeline(record = nil, options = {}) ⇒ Object

Returns an Array of unique UTC timestamps for which at least an history record exists. Takes temporal associations into account.



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/chrono_model/time_machine/timeline.rb', line 9

def timeline(record = nil, options = {})
  rid =
    if record
      if record.respond_to?(:rid)
        record.rid
      else
        record.id
      end
    end

  assocs =
    if options.key?(:with)
      timeline_associations_from(options[:with])
    else
      timeline_associations
    end

  models = []
  models.push self if chrono?
  models.concat(assocs.map { |a| a.klass.history })

  return [] if models.empty?

  fields = models.inject([]) { |a, m| a.concat m.quoted_history_fields }

  relation = except(:order)
             .select("DISTINCT UNNEST(ARRAY[#{fields.join(',')}]) AS ts")

  if assocs.present?
    assocs.each do |ass|
      association_quoted_table_name = connection.quote_table_name(ass.table_name)
      # `join` first, then use `where`s
      relation =
        if ass.belongs_to?
          relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.association_primary_key} = #{quoted_table_name}.#{ass.association_foreign_key}")
        else
          relation.joins("LEFT JOIN #{association_quoted_table_name} ON #{association_quoted_table_name}.#{ass.foreign_key} = #{quoted_table_name}.#{primary_key}")
        end
    end
  end

  relation_order =
    if options[:reverse]
      'DESC'
    else
      'ASC'
    end

  relation = relation.order("ts #{relation_order}")

  relation = relation.from("public.#{quoted_table_name}") unless chrono?
  relation = relation.where(id: rid) if rid

  sql = +"SELECT ts FROM ( #{relation.to_sql} ) AS foo WHERE ts IS NOT NULL"

  if options.key?(:before)
    sql << " AND ts < '#{Conversions.time_to_utc_string(options[:before])}'"
  end

  if options.key?(:after)
    sql << " AND ts > '#{Conversions.time_to_utc_string(options[:after])}'"
  end

  if rid && !options[:with]
    sql <<
      if chrono?
        %{ AND ts <@ ( SELECT tsrange(min(lower(validity)), max(upper(validity)), '[]') FROM #{quoted_table_name} WHERE id = #{rid} ) }
      else
        %[ AND ts < NOW() ]
      end
  end

  sql << " LIMIT #{options[:limit].to_i}" if options.key?(:limit)

  connection.on_schema(Adapter::HISTORY_SCHEMA) do
    connection.select_values(sql, "#{name} periods")
  end
end

#timeline_associationsObject



96
97
98
# File 'lib/chrono_model/time_machine/timeline.rb', line 96

def timeline_associations
  @timeline_associations ||= []
end

#timeline_associations_from(names) ⇒ Object



100
101
102
103
104
105
# File 'lib/chrono_model/time_machine/timeline.rb', line 100

def timeline_associations_from(names)
  Array.wrap(names).map do |name|
    reflect_on_association(name) or raise ArgumentError,
                                          "No association found for name `#{name}'"
  end
end