class NoSE::Statement
Allow statements to materialize views
A CQL statement and its associated data
Attributes
Public Class Methods
# 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 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
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
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
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
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
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
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
Specifies if the statement modifies any data @return [Boolean]
# File lib/nose/statements.rb, line 412 def read_only? false end
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
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
:nocov:
# File lib/nose/statements.rb, line 429 def to_color "#{@text} [magenta]#{@longest_entity_path.map(&:name).join ', '}[/]" end
Protected Instance Methods
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
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
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
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
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
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