Class: Mochigome::QueryLine

Inherits:
Object
  • Object
show all
Defined in:
lib/query.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(layer_path, access_filter) ⇒ QueryLine

Returns a new instance of QueryLine.



191
192
193
194
195
196
197
198
199
200
# File 'lib/query.rb', line 191

def initialize(layer_path, access_filter)
  # TODO: Validate layer types: not empty, AR, act_as_mochigome_focus
  @layer_path = layer_path
  @access_filter = access_filter

  @ids_rel = Relation.new(@layer_path)
  @ids_rel.apply_access_filter_func(@access_filter)

  @aggregate_rels = ActiveSupport::OrderedHash.new
end

Instance Attribute Details

#ids_relObject

Returns the value of attribute ids_rel.



189
190
191
# File 'lib/query.rb', line 189

def ids_rel
  @ids_rel
end

#layer_pathObject

Returns the value of attribute layer_path.



188
189
190
# File 'lib/query.rb', line 188

def layer_path
  @layer_path
end

Instance Method Details

#add_aggregate_source(depth, a) ⇒ Object



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/query.rb', line 202

def add_aggregate_source(depth, a)
  focus_model, data_model, agg_setting_name = nil, nil, nil
  if a.is_a?(Array) then
    focus_model = a.select{|e| e.is_a?(Class)}.first
    data_model = a.select{|e| e.is_a?(Class)}.last
    agg_setting_name = a.select{|e| e.is_a?(Symbol)}.first || :default
  else
    focus_model = data_model = a
    agg_setting_name = :default
  end
  # TODO Raise exception if a isn't correctly formatted

  agg_rel = Relation.new(@layer_path.take(depth))
  key_cols = agg_rel.spine_layers.map{|m| m.arel_primary_key}
  data_cols = key_cols + [data_model.arel_primary_key]

  agg_fields = data_model.
    mochigome_aggregation_settings(agg_setting_name).
    options[:fields].reject{|a| a[:in_ruby]}

  agg_rel.join_on_path_thru([focus_model, data_model])
  agg_rel.apply_access_filter_func(@access_filter)

  agg_fields.each_with_index do |a, i|
    d_expr = a[:value_proc].call(data_model.arel_table)
    d_expr = d_expr.expr if d_expr.respond_to?(:expr)
    agg_rel.select_expr(d_expr.as("d%03u" % i))
  end

  agg_rel_key = {
    :focus_model => focus_model,
    :data_model => data_model,
    :agg_setting_name => agg_setting_name,
    :depth => depth
  }

  @aggregate_rels[agg_rel_key] = lambda {|cond|
    inner_rel = agg_rel.dup
    inner_rel.apply_condition(cond)
    inner_query = inner_rel.to_arel
    data_cols.each_with_index do |col, i|
      inner_query.project(col.as("g%03u" % i)).group(col)
    end

    # FIXME: This subselect won't be necessary for all aggregation funcs.
    # When we can avoid it, we should, because subselects are slow.
    rel = Arel::SelectManager.new(
      Arel::Table.engine,
      Arel.sql("(#{inner_query.to_sql}) as mochigome_data")
    )
    d_tbl = Arel::Table.new("mochigome_data")
    agg_fields.each_with_index do |a, i|
      name = "d%03u" % i
      rel.project(a[:agg_proc].call(d_tbl[name]).as(name))
    end
    key_cols.each_with_index do |col, i|
      name = "g%03u" % i
      rel.project(d_tbl[name].as(name)).group(name)
    end
    rel
  }
end

#build_id_table(cond) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/query.rb', line 274

def build_id_table(cond)
  if @layer_path.empty?
    return []
  else
    r = @ids_rel.clone
    r.apply_condition(cond)
    ids_sql = r.to_sql
    return connection.select_all(ids_sql).map do |row|
      row.each do |k,v|
        row[k] = denilify(v)
      end
    end
  end
end

#connectionObject



265
266
267
# File 'lib/query.rb', line 265

def connection
  ActiveRecord::Base.connection
end

#denilify(v) ⇒ Object

TODO: Write a test for situations that use this



270
271
272
# File 'lib/query.rb', line 270

def denilify(v)
  (v.nil? || v.to_s.strip.empty?) ? "(None)" : v
end

#insert_aggregate_data_fields(node, table, agg_settings, path, tgt_depth) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/query.rb', line 306

def insert_aggregate_data_fields(node, table, agg_settings, path, tgt_depth)
  # Ignore nodes inserted by other QueryLines
  return unless path.size == 0 || node[:internal_type] == @layer_path[path.size-1].name

  if path.size == tgt_depth
    fields = agg_settings.options[:fields]
    # Pre-fill the node with default values in the right order
    fields.each{|fld| node[fld[:name]] = fld[:default] unless fld[:hidden] }
    agg_row = {} # Hold regular results here to be used in ruby-based fields
    node_data = table[path]
    return unless node_data
    fields.reject{|fld| fld[:in_ruby]}.zip(node_data).each do |fld, v|
      v ||= fld[:default]
      agg_row[fld[:name]] = v
      node[fld[:name]] = v unless fld[:hidden]
    end
    fields.select{|fld| fld[:in_ruby]}.each do |fld|
      node[fld[:name]] = fld[:ruby_proc].call(agg_row)
    end
  else
    node.children.each do |c|
      insert_aggregate_data_fields(c, table, agg_settings, path + [c[:id]], tgt_depth)
    end
  end
end

#load_aggregate_data(node, cond) ⇒ Object



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/query.rb', line 289

def load_aggregate_data(node, cond)
  @aggregate_rels.each do |key, rel_func|
    data_model = key[:data_model]
    agg_name = key[:agg_setting_name]
    agg_settings = data_model.mochigome_aggregation_settings(agg_name)

    q = rel_func.call(cond)
    data_table = {}
    connection.select_all(q.to_sql).each do |row|
      group_values = row.keys.select{|k| k.start_with?("g")}.sort.map{|k| denilify(row[k])}
      data_values = row.keys.select{|k| k.start_with?("d")}.sort.map{|k| row[k]}
      data_table[group_values] = data_values
    end
    insert_aggregate_data_fields(node, data_table, agg_settings, [], key[:depth])
  end
end