class Ferret

Constants

Annotated_SQL_Template
[sql]

is a [[String]] of the SQL template together with

placeholders. [[outputs]] is [[nil]] if this SQL is not a query, or a [[Hash]] containing name->interpretation mappings (in the order the values are [[select]]:ed by this SQL statement) if it is. [[shape]] describes the expected result set that would result, assuming all the inputs are single values.

LEXICAL_RULESET

Attributes

schema[R]

Public Class Methods

deterpret(interpretation, object) click to toggle source
# File lib/sql-ferret.rb, line 396
def self::deterpret interpretation, object
  # Note that we're not handling [[nil]] any specially.  If
  # this field permits [[null]] values, it's the caller's --
  # who lives somewhere in the query execution wrapper of
  # Ferret -- to handle [[nil]], and if it doesn't, passing
  # [[nil]] to [[deterpret]] is either an error or, in case of
  # YAML, requires special escaping.
  case interpretation
    when nil then
      return object
    when :unix_time then
      ugh 'deterpreted-value-type-error',
              input: object.inspect,
              expected: 'Time' \
          unless object.is_a? Time
      return object.to_i
    when :subsecond_unix_time then
      ugh 'deterpreted-value-type-error',
              input: object.inspect,
              expected: 'Time' \
          unless object.is_a? Time
      return object.to_f
    when :iso8601 then
      ugh 'deterpreted-value-type-error',
              input: object.inspect,
              expected: 'Time' \
          unless object.is_a? Time
      return object.xmlschema
    when :json then
      return JSON.generate(object)
    when :pretty_json then
      return JSON.pretty_generate(object)
    when :yaml then
      return YAML.dump(object)
    when :ruby_marshal then
      return Marshal.dump(value)
    when :packed_hex then
      ugh 'deterpreted-value-type-error',
              input: object.inspect,
              expected: 'String' \
          unless object.is_a? String
      return object.unpack('H*').first
    else
      raise 'assertion failed'
  end
end
interpret(interpretation, value) click to toggle source
# File lib/sql-ferret.rb, line 339
def self::interpret interpretation, value
  # If a [[null]] came from the database, we'll interpret it
  # as a [[nil]].
  return nil if value.nil?
  ugh? interpretation: interpretation.to_s,
      input: value.inspect do
    case interpretation
      when nil then
        return value
      when :unix_time, :subsecond_unix_time then
        ugh 'interpreted-value-type-error',
                input: value.inspect,
                expected: 'Numeric' \
            unless value.is_a? Numeric
        return Time.at(value)
      when :iso8601 then
        ugh 'interpreted-value-type-error',
                input: value.inspect,
                expected: 'String' \
            unless value.is_a? String
        return Time.xmlschema(value)
      when :json, :pretty_json then
        ugh 'interpreted-value-type-error',
                input: value.inspect,
                expected: 'String' \
            unless value.is_a? String
        return JSON.parse(value)
      when :yaml then
        ugh 'interpreted-value-type-error',
                input: value.inspect,
                expected: 'String' \
            unless value.is_a? String
        return YAML.load(value)
      when :ruby_marshal then
        ugh 'interpreted-value-type-error',
                input: value.inspect,
                expected: 'String' \
            unless value.is_a? String
        return Marshal.load(value)
      when :packed_hex then
        ugh 'interpreted-value-type-error',
                input: value.inspect,
                expected: 'String' \
            unless value.is_a? String
        ugh 'invalid-hex-data',
                input: value \
            unless value =~ /\A[\dabcdef]*\Z/
        ugh 'odd-length-hex-data',
                input: value \
            unless value.length % 2 == 0
        return [value].pack('H*')
      else
        raise 'assertion failed'
    end
  end
end
new(schema_source, sqlite = nil, use_mutex: true) click to toggle source

If the caller can guarantee that this [[Ferret]] instance is never accessed from multiple threads at once, it can turn off using the internal mutex by passing [[use_mutex: false]] for a small performance increase.

Calls superclass method
# File lib/sql-ferret.rb, line 20
def initialize schema_source, sqlite = nil, use_mutex: true
  raise 'type mismatch' unless schema_source.is_a? String
  super()
  @schema = Ferret::Schema.new(schema_source)
  @sqlite = sqlite
  # Guards access to [[@sqlite]] and
  # [[@sqlite_locked]].
  @sync_mutex = use_mutex ? Mutex.new : nil
  # Are we currently in a transaction?  (This lets us
  # implement [[Ferret#transaction]] reƫntrantly.)
  @sqlite_locked = false
  return
end

Public Instance Methods

change(table_name, **changes) click to toggle source
# File lib/sql-ferret.rb, line 34
def change table_name, **changes
  ugh? attempted: 'ferret-change' do
    table = @schema[table_name] or
        ugh 'unknown-table', table: table_name
    sql = table.sql_to_change changes.keys.map(&:to_s)
    _sync{@sqlite.execute sql, **changes}
  end
  return
end
create_table(name) click to toggle source
# File lib/sql-ferret.rb, line 330
def create_table name
  ugh? attempted: 'ferret-create-table' do
    _sync do
      @sqlite.execute sql_to_create_table(name)
    end
  end
  return
end
go(raw_expr, *inputs, **changes, &thunk) click to toggle source
# File lib/sql-ferret.rb, line 92
def go raw_expr, *inputs, **changes, &thunk
  expr = Ferret::Expression_Parser.new(raw_expr, @schema).expr

  ugh? expr: raw_expr do
    ugh? attempted: 'ferret-go' do
      if inputs.length > expr.exemplars.length then
        ugh 'too-many-exemplars-given',
            expected: expr.exemplars.length,
            given: inputs.length
      elsif inputs.length < expr.exemplars.length then
        ugh 'not-enough-exemplars-given',
            expected: expr.exemplars.length,
            given: inputs.length
      end
    end

    if thunk and ![:select, :select_distinct].
        include? expr.type then
      ugh 'superfluous-thunk-supplied',
          explanation: 'query-not-a-select'
    end

    case expr.type
    when :select, :select_distinct then
      ugh? attempted: 'ferret-select' do
        ugh 'superfluous-changes' \
            unless changes.empty?

        ast = expr.select

        # At least for now, all the parameters behave as
        # simple ANDed filter rules.
        inputs_imply_single_row = false
        coll = Ferret::Parameter_Collector.new
        expr.exemplars.zip(inputs).each_with_index do
            |(exemplar_spec, input), seq_no|
          test, selects_one_p = coll.feed input, exemplar_spec
          inputs_imply_single_row |= selects_one_p
          ast.sql.gsub! /\[test\s+#{seq_no}\]/, test
        end

        # Let's now compose the framework of executing the
        # query from [[proc]]:s.

        # [[tuple_preparer]] takes a tuple of raw values
        # fetched from SQL and prepares it into a deliverable
        # object.
        tuple_preparer = if ast.shape & QSF_MULTICOL == 0 then
          # A single column was requested.  The deliverable
          # object is the piece of data from this column.
          proc do |row|
            Ferret.interpret ast.outputs.values.first,
                row.first
          end
        else
          # Multiple columns were requested (or one column in
          # multicolumn mode).  The deliverable object is an
          # [[OpenStruct]] mapping field names to data from
          # these fields.
          proc do |row|
            output = OpenStruct.new
            raise 'assertion failed' \
                unless row.length == ast.outputs.size
            # Note that we're relying on modern Ruby's
            # [[Hash]]'s retention of key order here.
            ast.outputs.to_a.each_with_index do
                |(name, interpretation), i|
              output[name] =
                  Ferret.interpret interpretation, row[i]
            end
            output
          end
        end

        # [[query_executor]] takes a [[proc]], executes the
        # query, and calls [[proc]] with each tuple prepared
        # by [[tuple_preparer]].
        query_executor = proc do |&result_handler|
          @sqlite.execute ast.sql, **coll do |row|
            result_handler.call tuple_preparer.call(row)
          end
        end

        # [[processor]] executes the query and delivers
        # results either by yielding to [[thunk]] if it has
        # been given or by returning them if not, taking into
        # account the query's shape.
        if thunk then
          # A thunk was supplied -- we'll just pass prepared
          # rows to it.
          processor = proc do
            query_executor.call &thunk
          end
        else
          # Why [[and]] here?  Well, the shape flag tells us
          # whether the query can translate one input to more
          # than one, and [[inputs_imply_single_row]] tells us
          # whether there are more than one input values that
          # thus get translated.  We can only know that the
          # result is a single-row table if both of these
          # preconditions are satisfied.
          if (ast.shape & QSF_MULTIROW == 0) and
              inputs_imply_single_row then
            # A single row was requested (implicitly, by using
            # a unique field as an exemplar).  We'll return
            # this row, or [[nil]] if nothing was found.
            processor = lambda do
              query_executor.call do |output|
                return output
              end
              return nil
            end
          else
            # Many rows were requested.  We'll collect them to
            # a list and return it.
            processor = proc do
              results = []
              query_executor.call do |output|
                results.push output
              end
              return results
            end
          end
        end

        _sync &processor
      end

    when :update then
      ugh? attempted: 'ferret-update' do
        ugh 'missing-changes' \
            if changes.empty?

        changed_table = expr.stages.last.table
        sql = "update #{changed_table.name} set "
        changes.keys.each_with_index do |fn, i|
          field = changed_table[fn.to_s]
          ugh 'unknown-field', field: fn,
                  table: changed_table.name,
                  role: 'changed-field' \
              unless field
          sql << ", " unless i.zero?
          sql << "#{field.name} = :#{fn}"
        end

        if expr.stages.length > 1 then
          ast = expr.select
          sql << " where " <<
              expr.stages.last.stalk.ref.name <<
              " in (#{ast.sql})"
        else
          # Special case: the criteria and the update live in
          # a single table, so we won't need to do any joining
          # or subquerying.
          unless expr.exemplars.empty? then
            sql << " " << expr.where_clause
          end
        end

        # We're going to pass the changes to
        # [[SQLite::Database#execute]] in a [[Hash]].
        # Unfortunately, the Ruby interface of SQLite does not
        # support mixing numbered and named arguments.  As a
        # workaround, we'll pass the etalon as a named
        # argument whose name is a number.  This is also
        # convenient because it avoids clashes with any other
        # named parameters -- those are necessarily column
        # names, and column names can not be numbers.
        coll = Ferret::Parameter_Collector.new
        expr.exemplars.zip(inputs).each_with_index do
            |(exemplar_spec, input), seq_no|
          test, selects_one_p = coll.feed input, exemplar_spec
          sql.gsub! /\[test\s+#{seq_no}\]/, test
        end

        _sync do
          @sqlite.execute sql, **coll, **changes
          return @sqlite.changes
        end
      end

    when :delete then
      ugh? attempted: 'ferret-delete' do
        ugh 'superfluous-changes' \
            unless changes.empty?

        affected_table = expr.stages.last.table
        sql = "delete from #{affected_table.name} "

        if expr.stages.length > 1 then
          ast = expr.select
          sql << " where " <<
              expr.stages.last.stalk.ref.name <<
              " in (#{ast.sql})"
        else
          # Special case: the criteria live in the affected
          # table, so we won't need to do any joining or
          # subquerying.
          unless expr.exemplars.empty? then
            sql << " " << expr.where_clause
          end
        end

        coll = Ferret::Parameter_Collector.new
        expr.exemplars.zip(inputs).each_with_index do
            |(exemplar_spec, input), seq_no|
          test, selects_one_p = coll.feed input, exemplar_spec
          sql.gsub! /\[test\s+#{seq_no}\]/, test
        end

        _sync do
          @sqlite.execute sql, **coll
          return @sqlite.changes
        end
      end

    else
      raise 'assertion failed'
    end
  end
end
insert(table_name, **changes) click to toggle source
# File lib/sql-ferret.rb, line 44
def insert table_name, **changes
  ugh? attempted: 'ferret-insert' do
    table = @schema[table_name] or
        ugh 'unknown-table', table: table_name
    sql = table.sql_to_insert changes.keys.map(&:to_s)
    _sync do
      @sqlite.execute sql, **changes
      return @sqlite.last_insert_row_id
    end
  end
end
pragma_user_version() click to toggle source
# File lib/sql-ferret.rb, line 316
def pragma_user_version
  _sync do
    return @sqlite.get_first_value 'pragma user_version'
  end
end
pragma_user_version=(new_version) click to toggle source
# File lib/sql-ferret.rb, line 322
def pragma_user_version= new_version
  raise 'type mismatch' unless new_version.is_a? Integer
  _sync do
    @sqlite.execute 'pragma user_version = ?', new_version
  end
  return new_version
end
transaction() { || ... } click to toggle source
# File lib/sql-ferret.rb, line 56
def transaction &thunk
  # Note that [[_sync]] is reentrant, too.
  _sync do
    # If we get to this point, the only 'concurrent' access
    # might come from our very own thread -- that is, a
    # subroutine down the execution stack from present.  This
    # means that we can now access [[@sqlite_locked]] as
    # though we were in a single-threading environment, and
    # thus use it as a flag for 'this thread has already
    # acquired the SQLite-level lock so there's no need to
    # engage it again'.  (SQLite's transaction mechanism on
    # its own is not reentrant.)
    if @sqlite_locked then
      return yield
    else
      return @sqlite.transaction do
        begin
          @sqlite_locked = true
          return yield
        ensure
          @sqlite_locked = false
        end
      end
    end
  end
end

Private Instance Methods

_sync() { || ... } click to toggle source
# File lib/sql-ferret.rb, line 83
def _sync &thunk
  if @sync_mutex.nil? or @sync_mutex.owned? then
    return yield
  else
    @sync_mutex.synchronize &thunk
  end
end