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