Class: Trifle::Stats::Driver::Sqlite

Inherits:
Object
  • Object
show all
Includes:
Mixins::Packer
Defined in:
lib/trifle/stats/driver/sqlite.rb

Overview

rubocop:disable Metrics/ClassLength

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mixins::Packer

included

Constructor Details

#initialize(client = SQLite3::Database.new('stats.db'), table_name: 'trifle_stats', joined_identifier: :full, ping_table_name: nil, system_tracking: true) ⇒ Sqlite

rubocop:disable Layout/LineLength



14
15
16
17
18
19
20
21
# File 'lib/trifle/stats/driver/sqlite.rb', line 14

def initialize(client = SQLite3::Database.new('stats.db'), table_name: 'trifle_stats', joined_identifier: :full, ping_table_name: nil, system_tracking: true) # rubocop:disable Layout/LineLength
  @client = client
  @table_name = table_name
  @ping_table_name = ping_table_name || "#{table_name}_ping"
  @joined_identifier = self.class.normalize_joined_identifier(joined_identifier)
  @system_tracking = system_tracking
  @separator = '::'
end

Instance Attribute Details

#clientObject

Returns the value of attribute client.



12
13
14
# File 'lib/trifle/stats/driver/sqlite.rb', line 12

def client
  @client
end

#ping_table_nameObject

Returns the value of attribute ping_table_name.



12
13
14
# File 'lib/trifle/stats/driver/sqlite.rb', line 12

def ping_table_name
  @ping_table_name
end

#separatorObject (readonly)

Returns the value of attribute separator.



50
51
52
# File 'lib/trifle/stats/driver/sqlite.rb', line 50

def separator
  @separator
end

#table_nameObject

Returns the value of attribute table_name.



12
13
14
# File 'lib/trifle/stats/driver/sqlite.rb', line 12

def table_name
  @table_name
end

Class Method Details

.normalize_joined_identifier(value) ⇒ Object



198
199
200
201
202
203
204
205
# File 'lib/trifle/stats/driver/sqlite.rb', line 198

def self.normalize_joined_identifier(value)
  case value
  when nil, :full, 'full', :partial, 'partial'
    value.nil? ? nil : value.to_sym
  else
    raise ArgumentError, 'joined_identifier must be nil, :full, "full", :partial, or "partial"'
  end
end

.setup!(client = SQLite3::Database.new('stats.db'), table_name: 'trifle_stats', joined_identifier: :full, ping_table_name: nil) ⇒ Object

rubocop:disable Layout/LineLength, Metrics/MethodLength



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/trifle/stats/driver/sqlite.rb', line 23

def self.setup!(client = SQLite3::Database.new('stats.db'), table_name: 'trifle_stats', joined_identifier: :full, ping_table_name: nil) # rubocop:disable Layout/LineLength, Metrics/MethodLength
  ping_table_name ||= "#{table_name}_ping"
  identifier_mode = normalize_joined_identifier(joined_identifier)

  case identifier_mode
  when :full
    client.execute("CREATE TABLE #{table_name} (key varchar(255), data json);")
    client.execute("CREATE UNIQUE INDEX idx_#{table_name}_key ON #{table_name} (key);")
  when :partial
    client.execute("CREATE TABLE #{table_name} (key varchar(255) NOT NULL, at datetime NOT NULL, data json, PRIMARY KEY (key, at));") # rubocop:disable Layout/LineLength
  else
    client.execute("CREATE TABLE #{table_name} (key varchar(255) NOT NULL, granularity varchar(255) NOT NULL, at datetime NOT NULL, data json, PRIMARY KEY (key, granularity, at));") # rubocop:disable Layout/LineLength

    # Create ping table for separated mode only
    client.execute("CREATE TABLE #{ping_table_name} (key varchar(255) PRIMARY KEY, at datetime NOT NULL, data json);") # rubocop:disable Layout/LineLength
  end
end

Instance Method Details

#descriptionObject



41
42
43
44
45
46
47
48
# File 'lib/trifle/stats/driver/sqlite.rb', line 41

def description
  mode = if @joined_identifier == :full
           'J'
         else
           @joined_identifier == :partial ? 'P' : 'S'
         end
  "#{self.class.name}(#{mode})"
end

#get(keys:) ⇒ Object



126
127
128
129
130
# File 'lib/trifle/stats/driver/sqlite.rb', line 126

def get(keys:)
  identifiers = keys.map { |key| identifier_for(key) }
  data = get_all(identifiers: identifiers)
  identifiers.map { |identifier| self.class.unpack(hash: data.fetch(identifier, {})) }
end

#get_all(identifiers:) ⇒ Object

rubocop:disable Metrics/AbcSize



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/trifle/stats/driver/sqlite.rb', line 132

def get_all(identifiers:) # rubocop:disable Metrics/AbcSize
  query, params = get_query(identifiers: identifiers)
  results = client.execute(query, params).to_a
  sample = identifiers.first

  results.each_with_object(Hash.new({})) do |r, o|
    identifier = sample.each_with_index.to_h { |(k, _), i| [k, k == :at ? Time.iso8601(r[i]) : r[i]] }

    o[identifier] = JSON.parse(r.last)
  rescue JSON::ParserError
    nil
  end
end

#get_query(identifiers:) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
# File 'lib/trifle/stats/driver/sqlite.rb', line 146

def get_query(identifiers:)
  params = []
  conditions = identifiers.map do |identifier|
    identifier.map { |k, v| build_field_condition(k, v, params) }.join(' AND ')
  end.join(' OR ')

  query = <<-SQL
    SELECT * FROM #{table_name} WHERE #{conditions};
  SQL
  [query, params]
end

#inc(keys:, values:, count: 1, tracking_key: nil) ⇒ Object



62
63
64
65
66
67
68
69
70
71
# File 'lib/trifle/stats/driver/sqlite.rb', line 62

def inc(keys:, values:, count: 1, tracking_key: nil)
  data = self.class.pack(hash: values)
  client.transaction do |c|
    keys.each do |key|
      identifier = identifier_for(key)
      batch_data_operations(identifier: identifier, data: data, connection: c, operation: :inc)
      track_system_data(c, key, count, tracking_key)
    end
  end
end

#inc_query(identifier:, data:) ⇒ Object

rubocop:disable Metrics/MethodLength



73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/trifle/stats/driver/sqlite.rb', line 73

def inc_query(identifier:, data:) # rubocop:disable Metrics/MethodLength
  columns = identifier.keys.join(', ')
  placeholders = (['?'] * identifier.size).join(', ')
  expression = data.inject('data') do |o, (k, v)|
    path = json_path_for(k)
    "json_set(#{o}, '#{path}', IFNULL(json_extract(data, '#{path}'), 0) + #{numeric_value(k, v)})"
  end

  query = <<-SQL
    INSERT INTO #{table_name} (#{columns}, data) VALUES (#{placeholders}, json(?))
    ON CONFLICT (#{columns}) DO UPDATE SET data = #{expression};
  SQL
  [query, query_params(identifier) + [data.to_json]]
end

#ping(key:, values:) ⇒ Object



158
159
160
161
162
163
164
165
166
# File 'lib/trifle/stats/driver/sqlite.rb', line 158

def ping(key:, values:)
  return [] if @joined_identifier

  data = self.class.pack(hash: { data: values, at: key.at })
  query, params = ping_query(key: key.key, at: key.at, data: data)
  client.transaction do |c|
    c.execute(query, params) # TODO: should use batch_data_operations
  end
end

#ping_query(key:, at:, data:) ⇒ Object



168
169
170
171
172
173
174
175
176
# File 'lib/trifle/stats/driver/sqlite.rb', line 168

def ping_query(key:, at:, data:)
  at_formatted = format_time_value(at)

  query = <<-SQL
    INSERT INTO #{ping_table_name} (key, at, data) VALUES (?, ?, json(?))
    ON CONFLICT (key) DO UPDATE SET at = ?, data = json(?);
  SQL
  [query, [key.to_s, at_formatted, data.to_json, at_formatted, data.to_json]]
end

#scan(key:) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/trifle/stats/driver/sqlite.rb', line 178

def scan(key:)
  return [] if @joined_identifier

  query, params = scan_query(key: key.key)
  result = client.execute(query, params).first
  return [] if result.nil?

  # SQLite returns columns in order: key, at, data
  [Time.iso8601(result[1]), self.class.unpack(hash: JSON.parse(result[2]))]
rescue JSON::ParserError
  []
end

#scan_query(key:) ⇒ Object



191
192
193
194
195
196
# File 'lib/trifle/stats/driver/sqlite.rb', line 191

def scan_query(key:)
  query = <<-SQL
    SELECT key, at, data FROM #{ping_table_name} WHERE key = ? ORDER BY at DESC LIMIT 1;
  SQL
  [query, [key.to_s]]
end

#set(keys:, values:, count: 1, tracking_key: nil) ⇒ Object



88
89
90
91
92
93
94
95
96
97
# File 'lib/trifle/stats/driver/sqlite.rb', line 88

def set(keys:, values:, count: 1, tracking_key: nil)
  data = self.class.pack(hash: values)
  client.transaction do |c|
    keys.each do |key|
      identifier = identifier_for(key)
      batch_data_operations(identifier: identifier, data: data, connection: c, operation: :set)
      track_system_data(c, key, count, tracking_key)
    end
  end
end

#set_query(identifier:, data:) ⇒ Object

rubocop:disable Metrics/MethodLength



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/trifle/stats/driver/sqlite.rb', line 110

def set_query(identifier:, data:) # rubocop:disable Metrics/MethodLength
  columns = identifier.keys.join(', ')
  placeholders = (['?'] * identifier.size).join(', ')
  params = query_params(identifier) + [data.to_json]
  expression = data.inject('data') do |o, (k, v)|
    params << v.to_json
    "json_set(#{o}, '#{json_path_for(k)}', json(?))"
  end

  query = <<-SQL
    INSERT INTO #{table_name} (#{columns}, data) VALUES (#{placeholders}, json(?))
    ON CONFLICT (#{columns}) DO UPDATE SET data = #{expression};
  SQL
  [query, params]
end

#system_data_for(key:, count: 1, tracking_key: nil) ⇒ Object



57
58
59
60
# File 'lib/trifle/stats/driver/sqlite.rb', line 57

def system_data_for(key:, count: 1, tracking_key: nil)
  tracking_key ||= key.key
  self.class.pack(hash: { count: count, keys: { tracking_key => count } })
end

#system_identifier_for(key:) ⇒ Object



52
53
54
55
# File 'lib/trifle/stats/driver/sqlite.rb', line 52

def system_identifier_for(key:)
  key = Nocturnal::Key.new(key: '__system__key__', granularity: key.granularity, at: key.at)
  identifier_for(key)
end

#track_system_data(connection, key, count, tracking_key) ⇒ Object



99
100
101
102
103
104
105
106
107
108
# File 'lib/trifle/stats/driver/sqlite.rb', line 99

def track_system_data(connection, key, count, tracking_key)
  return unless @system_tracking

  batch_data_operations(
    identifier: system_identifier_for(key: key),
    data: system_data_for(key: key, count: count, tracking_key: tracking_key),
    connection: connection,
    operation: :inc
  )
end