class Flounder::Query::Select

A query obtained by calling any of the chain methods on an entity.

Attributes

default_projection[R]

All projected fields if no custom projection is made. Fields are encoded so that they can be traced back to the entity that contributed them.

has_projection[R]

Set to true once project is used. From that point on, no fields can be added through default projections.

join_entity[R]
last_join[R]
manager[R]

Arel SqlManager that accumulates this query.

projection_prefixes[R]

Each projection has a unique prefix mapping to the entity that uses this prefix during this query.

relation_entity_map[R]

maps relations that were included in this select to their entities.

Public Class Methods

new(domain, from_entity) click to toggle source
Calls superclass method Flounder::Query::Base::new
# File lib/flounder/query/select.rb, line 30
def initialize domain, from_entity
  super domain, Arel::SelectManager, from_entity
  
  @has_projection = false

  @projection_prefixes = Hash.new
  @default_projection = []

  # by default, joins are made to the entity that you start the query with
  @join_entity = entity

  @relation_entity_map = {}

  add_fields_to_default from_entity

  manager.from from_entity.table
end

Public Instance Methods

_join(join_node, entity) click to toggle source
# File lib/flounder/query/select.rb, line 48
def _join join_node, entity
  entity = convert_to_entity(entity)

  @last_join = entity

  table = entity.table
  manager.join(table, join_node)
  add_fields_to_default(entity)

  self
end
add_fields_to_default(entity) click to toggle source
# File lib/flounder/query/select.rb, line 60
def add_fields_to_default entity
  # Don't add fields through the default method if #project was already
  # used.
  return if has_projection

  prefix = entity.name.to_s
  table = entity.table
  
  warn "Table alias #{prefix} already used in select; field aliasing will occur!" \
    if projection_prefixes.has_key? prefix

  projection_prefixes[prefix] = entity

  entity.column_names.each do |name|
    default_projection << table[name].as("_#{prefix}_#{name}")
  end
end
anchor() click to toggle source
# File lib/flounder/query/select.rb, line 94
def anchor
  @join_entity = last_join
  self
end
connect(*connect_spec) click to toggle source

Follows relationships on the currently anchored entity. This is like an explicit join, but allows to eliminate repetition.

Example:

users.
  connect(:posts => {:comments => :author}).
  where(:author, id: 2)

# roughly aequivalent to
authors = users.as(:authors, :author)
users.
  join(:posts).on(:id => :user_id).anchor.
  join(:comments).on(:id => :post_id).anchor.
  join(authors).on(:author_id => :id).
  where(authors, id: 2)
# File lib/flounder/query/select.rb, line 185
def connect *connect_spec
  follow_relation_spec(join_entity, connect_spec)

  self
end
each(&block) click to toggle source
# File lib/flounder/query/select.rb, line 147
def each &block
  all.each(&block)
end
first() click to toggle source
# File lib/flounder/query/select.rb, line 152
def first
  manager.take(1)

  all.first
end
group_by(*field_list) click to toggle source
# File lib/flounder/query/select.rb, line 115
def group_by *field_list
  manager.group *map_to_arel(field_list)
  self
end
having(*conditions) click to toggle source
# File lib/flounder/query/select.rb, line 119
def having *conditions
  parse_conditions(*conditions) { |bit| manager.having(bit) }
end
hoist() click to toggle source
# File lib/flounder/query/select.rb, line 98
def hoist
  @join_entity = entity
  self
end
inner_join(*args) click to toggle source
# File lib/flounder/query/select.rb, line 78
def inner_join *args
  _join(Arel::Nodes::InnerJoin, *args)
end
Also aliased as: join
join(*args)
Alias for: inner_join
limit(n) click to toggle source
# File lib/flounder/query/select.rb, line 136
def limit n
  manager.take n
  self
end
offset(n) click to toggle source
# File lib/flounder/query/select.rb, line 140
def offset n
  manager.skip n
  self
end
on(join_conditions) click to toggle source
# File lib/flounder/query/select.rb, line 87
def on join_conditions
  manager.on(
    *join_conditions.map { |(k, v)| transform_tuple(
      join_entity, k, join_field(v)) })
  self
end
order_by(*field_list) click to toggle source

Orders by a list of field references.

Note: Replaces previous order_by.

# File lib/flounder/query/select.rb, line 127
def order_by *field_list
  manager.orders.clear
  field_list.each do |field|
    field = transform field
    manager.order field
  end
  self
end
outer_join(*args) click to toggle source
# File lib/flounder/query/select.rb, line 83
def outer_join *args
  _join(Arel::Nodes::OuterJoin, *args)
end
project(*field_list) click to toggle source

Adds a field to the projection clause of the SQL statement (the part between SELECT and FROM). Projection of ‘*’ is the default, so you can omit this call entirely if you want that.

# File lib/flounder/query/select.rb, line 107
def project *field_list
  @has_projection = true
  @default_projection = []

  manager.project *map_to_arel(field_list)
  self
end
size() click to toggle source

Executes a ‘select count(*)` query on the database and returns the result. If you want to avoid modifying your query (!) using this, dup it or execute it first and then call size on the resulting array.

# File lib/flounder/query/select.rb, line 162
def size
  manager.projections = []
  project 'count(*)::int as count'

  all.first.count
end

Private Instance Methods

column_name_to_entity(name) click to toggle source
# File lib/flounder/query/select.rb, line 243
def column_name_to_entity name
  unless default_projection.empty?
    extract_source_info_from_name(name)
  end
end
extract_source_info_from_name(name) click to toggle source
# File lib/flounder/query/select.rb, line 343
def extract_source_info_from_name name
  md = name.match(@re_field)
  fail "ASSERTION FAILURE Source info extraction failed." unless md

  entity = projection_prefixes[md[:prefix]]
  fail "ASSERTION FAILURE entity cannot be nil" unless entity
  name = md[:field_name]

  return entity, name
end
follow_relation_spec(entity, spec) click to toggle source

Follows a given relationship spec. Does NOT change anchoring.

# File lib/flounder/query/select.rb, line 213
def follow_relation_spec entity, spec
  old_join_entity = join_entity

  case spec
    when Symbol
      relation = entity.relations[spec]
      raise "No such relation #{spec.inspect} on entity #{entity}." unless relation

      relation.apply(self)
      relation_entity_map[spec] = relation.linked_entity

    when Hash
      spec.each do |k, v|
        follow_relation_spec(join_entity, k)
        self.anchor

        follow_relation_spec(join_entity, v)
      end
      
    when Array
      spec.each do |v|
        follow_relation_spec(join_entity, v)
      end
  else
    raise ArgumentError, "#{spec.inspect} not allowed in #connect."
  end

  @join_entity = old_join_entity
end
has_entity?(sym) click to toggle source

Checks if a symbol can be turned into an entity. Overwritten from base query because we want to allow entity names that are introduced through connect as well.

Calls superclass method Flounder::Query::Base#has_entity?
# File lib/flounder/query/select.rb, line 196
def has_entity? sym
  super || relation_entity_map.has_key?(sym)
end
join_field(name) click to toggle source

Transforms a simple symbol into either a field of the last .join table, or respects field values passed in.

# File lib/flounder/query/select.rb, line 252
def join_field name
  return @last_join[name] if name.kind_of?(Symbol)
  name
end
map_to_arel(field_list) click to toggle source

Maps a field reference (see fields) to an Arel::Field.

# File lib/flounder/query/select.rb, line 290
def map_to_arel field_list
  map_to_fields(field_list).map(&:to_arel_field)
end
map_to_field(field_ref) click to toggle source
# File lib/flounder/query/select.rb, line 272
def map_to_field field_ref
  case field_ref 
    when Symbol
      entity[field_ref]
    when String
      Flounder::Immediate.new(field_ref)
    when Flounder::Field
      field_ref
    when Flounder::Expression::Expr
      field_ref.to_immediate
  else
    fail Flounder::InvalidFieldReference, 
      "Cannot resolve #{field_ref.inspect} to a field."
  end
end
map_to_fields(field_list) click to toggle source

Maps an array of field references to Flounder::Field objects. A field reference can be:

* a symbol, interpreted as a field name of the main enity of the 
  operation
* a string, interpreted as something to be passed into the SQL statement
  as is. Caution: Don't expose this to unsecure channels!
* a Flounder::Field, left alone (obtained through calling #[] on any 
  entity)
# File lib/flounder/query/select.rb, line 267
def map_to_fields field_list
  field_list.map { |x| 
    map_to_field(x)
  }
end
prepare_kick() click to toggle source

Prepares a kick (aka transformation into sql/result). This should include all actions that need to be performed to validate the query.

# File lib/flounder/query/select.rb, line 297
def prepare_kick
  unless @has_projection
    @has_projection = true

    # Prepare the regular expression that we'll use to extract entities
    # from column names.
    @re_field = %r(
      ^_                             # indicates one of our own 
      (?<prefix>#{projection_prefixes.keys.join('|')})
      _
      (?<field_name>.*)
      $
    )x

    manager.project *default_projection
  end
end
resolve_entity(sym) click to toggle source

Resolves an entity through the domain. Overwritten from base query because we want to allow entity names that are introduced through connect as well.

# File lib/flounder/query/select.rb, line 204
def resolve_entity sym
  return domain[sym] if domain.has_entity?(sym)
  return relation_entity_map[sym] if relation_entity_map.has_key?(sym)

  raise Flounder::NoSuchEntity, "No such entity #{sym.inspect} either in this select statement or in the domain."
end
transform(field) click to toggle source

Called on a value for a

* order by

clause, this returns a field that can be passed to Arel

* #where
* #on
# File lib/flounder/query/select.rb, line 321
def transform field
  if defined?(DataMapper) && 
    defined?(DataMapper::Query::Operator) &&
    field.kind_of?(DataMapper::Query::Operator)

    # interop: datamapper hijacks some of our operators, let's reverse this
    field = Flounder::SymbolExtensions::Modifier.new(
      field.target, field.operator)
  end
  
  case field
    when String
      Flounder::Immediate.new(field).to_arel_field
    when Flounder::Field
      field.fully_qualified_name
    when Flounder::SymbolExtensions::Modifier
      field.to_arel_field(entity).send field.kind
  else
    entity[field].arel_field
  end
end