class NoSE::Statement

Allow statements to materialize views

A CQL statement and its associated data

Attributes

comment[R]
entity[R]
eq_fields[R]
graph[R]
group[R]
key_path[R]
label[R]
range_field[R]
text[R]

Public Class Methods

new(params, text, group: nil, label: nil) click to toggle source
# File lib/nose/statements.rb, line 399
def initialize(params, text, group: nil, label: nil)
  @entity = params[:entity]
  @key_path = params[:key_path]
  @longest_entity_path = @key_path.entities
  @graph = params[:graph]
  @model = params[:model]
  @text = text
  @group = group
  @label = label
end
parse(text, model, group: nil, label: nil, support: false) click to toggle source

Parse either a query or an update

# File lib/nose/statements.rb, line 280
def self.parse(text, model, group: nil, label: nil, support: false)
  klass = statement_class text, support
  tree = parse_tree text, klass

  # Ensure we have a valid path in the parse tree
  tree[:path] ||= [tree[:entity]]
  fail InvalidStatementException,
       "FROM clause must start with #{tree[:entity]}" \
       if tree[:entity] && tree[:path].first != tree[:entity]

  params = statement_parameters tree, model
  statement = klass.parse tree, params, text, group: group, label: label
  statement.instance_variable_set :@comment, tree[:comment].to_s

  # Support queries need to populate extra values before finalizing
  unless support
    statement.hash
    statement.freeze
  end

  statement
end

Private Class Methods

add_field_with_prefix(path, field, params) click to toggle source

A helper to look up a field based on the path specified in the statement @return [Fields::Field]

# File lib/nose/statements.rb, line 382
def self.add_field_with_prefix(path, field, params)
  field_path = field.map(&:to_s)
  prefix_index = path.index(field_path.first)
  field_path = path[0..prefix_index - 1] + field_path \
    unless prefix_index.zero?
  field_path.map!(&:to_s)

  # Expand the graph to include any keys which were found
  field_path[0..-2].prefixes.drop(1).each do |key_path|
    key = params[:model].find_field key_path
    params[:graph].add_edge key.parent, key.entity, key
  end

  params[:model].find_field field_path
end
find_longest_path(path_entities, from) click to toggle source

Calculate the longest path of entities traversed by the statement @return [KeyPath]

# File lib/nose/statements.rb, line 364
def self.find_longest_path(path_entities, from)
  path = path_entities.map(&:to_s)[1..-1]
  longest_entity_path = [from]
  keys = [from.id_field]

  path.each do |key|
    # Search through foreign keys
    last_entity = longest_entity_path.last
    longest_entity_path << last_entity[key].entity
    keys << last_entity[key]
  end

  KeyPath.new(keys)
end
parse_tree(text, klass) click to toggle source

Run the parser and produce the parse tree @raise [ParseFailed] @return [Hash]

# File lib/nose/statements.rb, line 328
def self.parse_tree(text, klass)
  # Set the type of the statement
  # (but CONNECT and DISCONNECT use the same parse rule)
  type = klass.name.split('::').last.downcase.to_sym
  type = :connect if type == :disconnect

  # If parsing fails, re-raise as our custom exception
  begin
    tree = CQLT.new.apply(CQLP.new.method(type).call.parse(text))
  rescue Parslet::ParseFailed => exc
    new_exc = ParseFailed.new exc.cause.ascii_tree
    new_exc.set_backtrace exc.backtrace
    raise new_exc
  end

  tree
end
statement_class(text, support) click to toggle source

Produce the class of the statement for the given text @return [Class, Symbol]

# File lib/nose/statements.rb, line 305
def self.statement_class(text, support)
  return SupportQuery if support

  case text.split.first
  when 'INSERT'
    Insert
  when 'DELETE'
    Delete
  when 'UPDATE'
    Update
  when 'CONNECT'
    Connect
  when 'DISCONNECT'
    Disconnect
  else # SELECT
    Query
  end
end
statement_parameters(tree, model) click to toggle source

Produce the parameter hash needed to build a new statement @return [Hash]

# File lib/nose/statements.rb, line 349
def self.statement_parameters(tree, model)
  entity = model[tree[:path].first.to_s]
  key_path = find_longest_path(tree[:path], entity)

  {
    model: model,
    entity: entity,
    key_path: key_path,
    graph: QueryGraph::Graph.from_path(key_path)
  }
end

Public Instance Methods

materialize_view() click to toggle source

Construct an index which acts as a materialized view for a query @return [Index]

# File lib/nose/indexes.rb, line 201
def materialize_view
  eq = materialized_view_eq join_order.first
  order_fields = materialized_view_order(join_order.first) - eq

  Index.new(eq, order_fields,
            all_fields - (@eq_fields + @order).to_set, @graph)
end
read_only?() click to toggle source

Specifies if the statement modifies any data @return [Boolean]

# File lib/nose/statements.rb, line 412
def read_only?
  false
end
requires_delete?(_index) click to toggle source

Specifies if the statement will require data to be deleted @return [Boolean]

# File lib/nose/statements.rb, line 424
def requires_delete?(_index)
  false
end
requires_insert?(_index) click to toggle source

Specifies if the statement will require data to be inserted @return [Boolean]

# File lib/nose/statements.rb, line 418
def requires_insert?(_index)
  false
end
to_color() click to toggle source

:nocov:

# File lib/nose/statements.rb, line 429
def to_color
  "#{@text} [magenta]#{@longest_entity_path.map(&:name).join ', '}[/]"
end

Protected Instance Methods

from_path(path, prefix_path = nil, field = nil) click to toggle source

Generate a string which can be used in the “FROM” clause of a statement or optionally to specify a field @return [String]

# File lib/nose/statements.rb, line 454
def from_path(path, prefix_path = nil, field = nil)
  if prefix_path.nil?
    from = path.first.parent.name.dup
  else
    # Find where the two paths intersect to get the first path component
    first_key = prefix_path.entries.find do |key|
      path.entities.include?(key.parent) || \
        key.is_a?(Fields::ForeignKeyField) && \
          path.entities.include?(key.entity)
    end
    from = if first_key.primary_key?
             first_key.parent.name.dup
           else
             first_key.name.dup
           end
  end

  from << '.' << path.entries[1..-1].map(&:name).join('.') \
    if path.length > 1

  unless field.nil?
    from << '.' unless from.empty?
    from << field.name
  end

  from
end
maybe_quote(value, field) click to toggle source

Quote the value of an identifier used as a value for a field, quoted if needed @return [String]

# File lib/nose/statements.rb, line 439
def maybe_quote(value, field)
  if value.nil?
    '?'
  elsif [Fields::IDField,
         Fields::ForeignKeyField,
         Fields::StringField].include? field.class
    "\"#{value}\""
  else
    value.to_s
  end
end
settings_clause() click to toggle source

Produce a string which can be used as the settings clause in a statement @return [String]

# File lib/nose/statements.rb, line 485
def settings_clause
  'SET ' + @settings.map do |setting|
    value = maybe_quote setting.value, setting.field
    "#{setting.field.name} = #{value}"
  end.join(', ')
end
where_clause(field_namer = :to_s.to_proc) click to toggle source

Produce a string which can be used as the WHERE clause in a statement @return [String]

# File lib/nose/statements.rb, line 495
def where_clause(field_namer = :to_s.to_proc)
  ' WHERE ' + @conditions.values.map do |condition|
    value = condition.value.nil? ? '?' : condition.value
    "#{field_namer.call condition.field} #{condition.operator} #{value}"
  end.join(' AND ')
end

Private Instance Methods

materialized_view_eq(hash_entity) click to toggle source

Get the fields used as parition keys for a materialized view based over a given entity @return [Array<Fields::Field>]

# File lib/nose/indexes.rb, line 214
def materialized_view_eq(hash_entity)
  eq = @eq_fields.select { |field| field.parent == hash_entity }
  eq = [join_order.last.id_field] if eq.empty?

  eq
end
materialized_view_order(hash_entity) click to toggle source

Get the ordered keys for a materialized view @return [Array<Fields::Field>]

# File lib/nose/indexes.rb, line 223
def materialized_view_order(hash_entity)
  # Start the ordered fields with the equality predicates
  # on other entities, followed by all of the attributes
  # used in ordering, then the range field
  order_fields = @eq_fields.select do |field|
    field.parent != hash_entity
  end + @order
  if @range_field && !@order.include?(@range_field)
    order_fields << @range_field
  end

  # Ensure we include IDs of the final entity
  order_fields += join_order.map(&:id_field)

  order_fields.uniq
end