class Ferret::Expression_Parser

Attributes

expr[R]

Public Class Methods

new(raw_expr, schema) click to toggle source
Calls superclass method
# File lib/sql-ferret.rb, line 1061
def initialize raw_expr, schema
  super()
  @raw_expr = raw_expr
  @schema = schema

  @expr = Ferret::Expression.new
  @scanner = Ferret::Scanner.new @raw_expr

  @first_star_offset = nil

  first_table_name = @scanner.get_id 'table-name'
  @expr.stages[0].table = @schema[first_table_name] or
      ugh 'unknown-table',
           table: first_table_name,
           offset: @scanner.last_token_offset,
           expr: @raw_expr

  @scanner.pass :':'

  parenthesised = @scanner.pass? :'('
  loop do
    exemplar_escaped, exemplar_column_name =
        @scanner.get_optional_escaped_id 'column-expected'
    if exemplar_column_name then
      exemplar_column =
          @expr.stages[0].table[exemplar_column_name] or
              ugh 'unknown-field',
                  field: exemplar_column_name,
                  table: @expr.stages[0].table.name,
                  role: 'key-field',
                  offset: @scanner.last_token_offset,
                  expr: @raw_expr
      # the key column must be a column, not a ghost field
      unless exemplar_column.column? then
        ugh 'not-a-column', field: exemplar_column.name,
            table: @expr.stages[0].table.name,
            offset: @scanner.last_token_offset,
            expr: @raw_expr
      end
      exemplar_interpretation = exemplar_escaped ?
          nil : exemplar_column.interpretation
      key_output_name =
          parse_optional_output_name_override ||
              exemplar_column_name
      @expr.exemplars.push Ferret::Exemplar.new(
          exemplar_column, exemplar_interpretation)
      @expr.selectees.push Ferret::Selectee.new(
          @expr.stages[0], exemplar_column,
          key_output_name, exemplar_interpretation)
    end
    break unless parenthesised and @scanner.pass? :','
  end
  @scanner.pass :')' if parenthesised

  if @scanner.pass? :':' then
    # Colon without dereference: we should expect a fetch
    # verb.
    @expr.type = parse_fetch_verb
  else
    @scanner.pass :'->'

    if @scanner.pass? :':' then
      # Colon past dereference: we should expect an update
      # verb.
      @expr.type = parse_update_verb
    else
      # Note that [[parse_stage]] can change [[@expr.type]]
      # if it meets the [[-> :]].
      parse_stage @expr.stages.last,
          parens: false
    end
  end

  if @expr.modification? then
    if @first_star_offset then
      ugh 'star-in-modification',
          offset: @first_star_offset,
          expr: @raw_expr
    end
  end

  @scanner.expected_eof!

  if @expr.modification? then
    ugh 'multiple-columns-selected-in-modification' \
        if @expr.multicolumn
  end

  unless @expr.multicolumn then
    # In single-column expressions, only the very last
    # selectee is actually selected.
    @expr.selectees[0 ... -1] = []
  end

  @expr.assign_stage_qualifiers @schema.alias_generator
  return
end

Public Instance Methods

parse_fetch_verb() click to toggle source
# File lib/sql-ferret.rb, line 1314
def parse_fetch_verb
  verb = @scanner.get_id 'fetch-verb'
  case verb
    when 'select' then
      return @scanner.pass?('distinct') ?
          :select_distinct : :select
    when 'distinct' then
      return :select_distinct
    else
      ugh 'unknown-fetch-verb',
          got: verb,
          input: @expr,
          offset: @scanner.last_token_offset
  end
end
parse_optional_join_arrow() click to toggle source
# File lib/sql-ferret.rb, line 1308
def parse_optional_join_arrow
  return :left if @scanner.pass? :'->'
  return :inner if @scanner.pass? :'<->'
  return nil
end
parse_optional_output_name_override() click to toggle source
# File lib/sql-ferret.rb, line 1298
def parse_optional_output_name_override
  if @scanner.pass? :"'" then
    override = @scanner.get_id 'output-name-override'
    @scanner.pass :"'"
    return override
  else
    return nil
  end
end
parse_stage(stage, parens: false) click to toggle source
# File lib/sql-ferret.rb, line 1178
def parse_stage stage, parens: false
  starred = false
  stage_empty = true
  loop do
    field_escaped, field_name =
        @scanner.get_optional_escaped_id 'field-expected'
    if field_name then
      field_offset = @scanner.last_token_offset

      raise 'assertion failed' unless stage.table

      field = stage.table[field_name] or
          ugh 'unknown-field', field: field_name,
              expr: @raw_expr,
              offset: field_offset

      field_output_name =
          parse_optional_output_name_override ||
              field_name

      # Has this column, or its name, been used already?
      (0 ... @expr.selectees.length).reverse_each do |i|
        selectee = @expr.selectees[i]
        if (selectee.stage == stage and
                selectee.field == field) or
            selectee.output_name == field_output_name then
          # Possible conflict detected.
          if selectee.star? then
            # The previous selectee was implicit, added due
            # to star expansion.  We'll just discard it, for
            # explicit fields take precedence.
            @expr.selectees.delete_at i
          else
            ugh 'duplicate-field-in-stage',
                field: field.name,
                output_name: field_output_name,
                expr: @raw_expr,
                offset: field_offset
          end
        end
      end
      @expr.selectees.push Ferret::Selectee.new(
          stage, field,
          field_output_name, field_escaped ?
              nil : field.interpretation)
      stage_empty = false
      if @scanner.pass? :'(' then
        join_type = parse_optional_join_arrow or
            expected!('join-arrow',
                candidates: '-> <->')
        # If something goes wrong trying to start a new
        # stage, it must be the last field's fault.
        # ([[start_subsequent_stage]] won't attach the
        # offset to the ugh on its own just because it
        # doesn't _have_ the offset.)
        ugh? offset: field_offset do
          start_subsequent_stage stage, field, join_type
        end
        parse_stage @expr.stages.last,
            parens: true
        @scanner.pass :')'
      else
        if !parens and @scanner.pass? :':' then
          @expr.type = parse_fetch_verb
          break
        end
        if join_type = parse_optional_join_arrow then
          ugh? offset: field_offset do
            start_subsequent_stage stage, field, join_type
          end
          if !parens and @scanner.pass? :':' then
            @expr.type = parse_update_verb
          else
            parse_stage @expr.stages.last,
                parens: parens
          end
          break
        end
      end
    elsif @scanner.pass? :'*' then
      @first_star_offset ||= @scanner.last_token_offset
      # only one star per stage
      @scanner.expected! 'field-name' if starred
      starred = true
      @expr.multicolumn = true

      stage.table.columns.each do |column|
        # We'll skip columns that have been selected (at
        # this stage) already, or columns whose names have
        # already been used.
        next if @expr.selectees.any? do |selectee|
          (selectee.stage == stage and
                  selectee.column == column) or
              selectee.output_name == column.name
        end
        @expr.selectees.push Ferret::Selectee.new(
            stage, column,
            column.name, column.interpretation,
            true)
      end

      # Note that [[->]] can not appear immediately
      # following a [[*]].
      break
    else
      if stage_empty then
        @scanner.expected! 'field-name'
      end
      break
    end

    if @scanner.pass? :',' then
      @expr.multicolumn = true
    else
      break
    end
  end
  return
end
parse_update_verb() click to toggle source
# File lib/sql-ferret.rb, line 1330
def parse_update_verb
  verb = @scanner.get_id 'update-verb'
  case verb
    when 'update', 'set' then
      return :update
    when 'delete' then
      return :delete
    else
      ugh 'unknown-update-verb',
          got: verb,
          input: @expr,
          offset: @scanner.last_token_offset
  end
end
start_subsequent_stage(parent, stalk, join_type) click to toggle source
# File lib/sql-ferret.rb, line 1159
def start_subsequent_stage parent, stalk, join_type
  raise 'type mismatch' \
      unless parent.is_a? Ferret::Stage
  raise 'type mismatch' \
      unless stalk.is_a? Ferret::Field
  raise 'assertion failed' \
      unless [:left, :inner].include? join_type

  # Note that we don't have the field's offset.  But the
  # caller might.
  unless stalk.ref then
    ugh 'unable-to-dereference', field: field.name
  end

  @expr.stages.push Ferret::Stage.new(
      parent, stalk, join_type)
  return
end