class NoSE::Random::StatementGenerator

Generates random queries over entities in a given model

Public Class Methods

new(model) click to toggle source
# File lib/nose/random.rb, line 126
def initialize(model)
  @model = model
end

Public Instance Methods

random_connection(entity) click to toggle source

Generate a random connection for an Insert

# File lib/nose/random.rb, line 146
def random_connection(entity)
  connections = entity.foreign_keys.values.sample(2)
  'AND CONNECT TO ' + connections.map do |connection|
    "#{connection.name}(?)"
  end.join(', ')
end
random_delete() click to toggle source

Generate a new random deletion of entities in the model @return [Delete]

# File lib/nose/random.rb, line 181
def random_delete
  path = random_path(1)

  from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
  delete = "DELETE #{from.first} FROM #{from.join '.'} " +
           random_where_clause(path, 1)

  Statement.parse delete, @model
end
random_graph(max_nodes) click to toggle source

Produce a random query graph over the entity graph

# File lib/nose/random.rb, line 274
def random_graph(max_nodes)
  graph = QueryGraph::Graph.new
  last_node = graph.add_node @model.entities.values.sample
  while graph.size < max_nodes
    # Get the possible foreign keys to use
    keys = last_node.entity.foreign_keys.values
    keys.reject! { |key| graph.nodes.map(&:entity).include? key.entity }
    break if keys.empty?

    # Pick a random foreign key to traverse
    next_key = keys.sample
    graph.add_edge last_node, next_key.entity, next_key

    # Select a new node to start from, making sure we pick one
    # that still has valid outgoing edges
    last_node = graph.nodes.reject do |node|
      (node.entity.foreign_keys.each_value.map(&:entity) -
       graph.nodes.map(&:entity)).empty?
    end.sample
    break if last_node.nil?
  end

  graph
end
random_insert(connect = true) click to toggle source

Generate a new random insertion to entities in the model @return [Insert]

# File lib/nose/random.rb, line 132
def random_insert(connect = true)
  entity = @model.entities.values.sample
  settings = entity.fields.each_value.map do |field|
    "#{field.name}=?"
  end.join ', '
  insert = "INSERT INTO #{entity.name} SET #{settings} "

  # Optionally add connections to other entities
  insert += random_connection(entity) if connect

  Statement.parse insert, @model
end
random_path(max_length) click to toggle source

Return a random path through the entity graph @return [KeyPath]

# File lib/nose/random.rb, line 251
def random_path(max_length)
  # Pick the start of path weighted based on
  # the number of deges from each entity
  pick = Pickup.new(Hash[@model.entities.each_value.map do |entity|
    [entity, entity.foreign_keys.length]
  end])
  path = [pick.pick.id_field]

  while path.length < max_length
    # Find a list of keys to entities we have not seen before
    last_entity = path.last.entity
    keys = last_entity.foreign_keys.values
    keys.reject! { |key| path.map(&:entity).include? key.entity }
    break if keys.empty?

    # Add a random new key to the path
    path << keys.sample
  end

  KeyPath.new path
end
random_query(path_length = 3, selected_fields = 2, condition_count = 2, order = false) click to toggle source

Generate a new random query from entities in the model @return [Query]

# File lib/nose/random.rb, line 193
def random_query(path_length = 3, selected_fields = 2,
                 condition_count = 2, order = false)
  path = random_path path_length
  graph = QueryGraph::Graph.from_path path

  conditions = [
    Condition.new(path.entities.first.fields.values.sample, :'=', nil)
  ]
  condition_count -= 1

  new_fields = random_where_conditions(path, condition_count,
                                       conditions.map(&:field).to_set)
  conditions += new_fields.map do |field|
    Condition.new(field, :'>', nil)
  end

  conditions = Hash[conditions.map do |condition|
    [condition.field.id, condition]
  end]

  params = {
    select: random_select(path, selected_fields),
    model: @model,
    graph: graph,
    key_path: graph.longest_path,
    entity: graph.longest_path.first.parent,
    conditions: conditions,
    order: order ? [graph.entities.to_a.sample.fields.values.sample] : []
  }

  query = Query.new params, nil
  query.hash

  query
end
random_select(path, selected_fields) click to toggle source

Get random fields to select for a Query @return [Set<Fields::Field>]

# File lib/nose/random.rb, line 231
def random_select(path, selected_fields)
  fields = Set.new
  while fields.length < selected_fields
    fields.add path.entities.sample.fields.values.sample
  end

  fields
end
random_settings(path, updated_fields) click to toggle source

Get random settings for an update @return [String]

# File lib/nose/random.rb, line 169
def random_settings(path, updated_fields)
  # Don't update key fields
  update_fields = path.entities.first.fields.values
  update_fields.reject! { |field| field.is_a? Fields::IDField }

  update_fields.sample(updated_fields).map do |field|
    "#{field.name}=?"
  end.join ', '
end
random_statement(weights = { query: 80, insert: 10, update: 5, delete: 5 }) click to toggle source

Produce a random statement according to a given set of weights @return [Statement]

# File lib/nose/random.rb, line 242
def random_statement(weights = { query: 80, insert: 10, update: 5,
                                 delete: 5 })
  pick = Pickup.new(weights)
  type = pick.pick
  send(('random_' + type.to_s).to_sym)
end
random_update(path_length = 1, updated_fields = 2, condition_count = 1) click to toggle source

Generate a new random update of entities in the model @return [Update]

# File lib/nose/random.rb, line 155
def random_update(path_length = 1, updated_fields = 2,
                  condition_count = 1)
  path = random_path(path_length)
  settings = random_settings path, updated_fields
  from = [path.first.parent.name] + path.entries[1..-1].map(&:name)
  update = "UPDATE #{from.first} FROM #{from.join '.'} " \
           "SET #{settings} " +
           random_where_clause(path, condition_count)

  Statement.parse update, @model
end

Private Instance Methods

condition_field_name(field, path) click to toggle source

Get the name to be used in the query for a condition field @return [String]

# File lib/nose/random.rb, line 327
def condition_field_name(field, path)
  field_path = path.first.name
  path_end = path.index(field.parent)
  last_entity = path.first
  path[1..path_end].each do |entity|
    fk = last_entity.foreign_keys.each_value.find do |key|
      key.entity == entity
    end
    field_path += '.' << fk.name
    last_entity = entity
  end

  field_path
end
random_where_clause(path, count = 2) click to toggle source

Produce a random where clause using fields along a given path @return [String]

# File lib/nose/random.rb, line 303
def random_where_clause(path, count = 2)
  # Ensure we have at least one condition at the beginning of the path
  conditions = [path.entities.first.fields.values.sample]
  conditions += random_where_conditions path, count - 1

  return '' if conditions.empty?
  "WHERE #{conditions.map do |field|
    "#{path.find_field_parent(field).name}.#{field.name} = ?"
  end.join ' AND '}"
end
random_where_conditions(path, count, exclude = Set.new) click to toggle source

Produce a random set of fields for a where clause @return [Array<Fields::Field>]

# File lib/nose/random.rb, line 316
def random_where_conditions(path, count, exclude = Set.new)
  1.upto(count).map do
    field = path.entities.sample.fields.values.sample
    next nil if field.name == '**' || exclude.include?(field)

    field
  end.compact
end