#-- # Copyright (c) 2008 Jeremy Hinegardner # All rights reserved. See LICENSE and/or COPYING for details. #++ # require 'date' require 'arrayfields' require 'ostruct' module Amalgalite class Statement include ::Amalgalite::SQLite3::Constants attr_reader :db attr_reader :api class << self # special column names that indicate that indicate the column is a rowid def rowid_column_names @rowid_column_names ||= %w[ ROWID OID _ROWID_ ] end end ## # Initialize a new statement on the database. # def initialize( db, sql ) @db = db #prepare_method = @db.utf16? ? :prepare16 : :prepare prepare_method = :prepare @param_positions = {} @stmt_api = @db.api.send( prepare_method, sql ) @blobs_to_write = [] @rowid_index = nil @result_meta = nil @open = true end ## # is the statement open for business # def open? @open end ## # Is the special column "ROWID", "OID", or "_ROWID_" used? # def using_rowid_column? not @rowid_index.nil? end ## # reset the Statement back to it state right after the constructor returned, # except if any variables have been bound to parameters, those are still # bound. # def reset! @stmt_api.reset! @param_positions = {} @blobs_to_write.clear @rowid_index = nil end ## # reset the Statement back to it state right after the constructor returned, # AND clear all parameter bindings. # def reset_and_clear_bindings! reset! @stmt_api.clear_bindings! end ## # reset the statment in preparation for executing it again # def reset_for_next_execute! @stmt_api.reset! @stmt_api.clear_bindings! @blobs_to_write.clear end ## # Execute the statement with the given parameters # # If a block is given, then yield each returned row to the block. If no # block is given then return all rows from the result. No matter what the # prepared statement should be reset before returning the final time. # def execute( *params ) bind( *params ) begin # save the error state at the beginning of the execution. We only want to # reraise the error if it was raised during this execution. s_before = $! if block_given? then while row = next_row yield row end else all_rows end ensure s = $! begin reset_for_next_execute! rescue => e end raise s if s != s_before end end ## # Bind parameters to the sql statement. # # Bindings in SQLite can have a number of formats: # # ? # ?num # :var # @var # $var # # Where 'num' is an Integer and 'var'is an alphanumerical variable. # They may exist in the SQL for which this Statement was created. # # Amalgalite binds parameters to these variables in the following manner: # # If bind is passed in an Array, either as +bind( "foo", "bar", "baz")+ or # as bind( ["foo", "bar", "baz"] ) then each of the params is assumed to be # positionally bound to the statement( ?, ?num ). # # If bind is passed a Hash, either as +bind( :foo => 1, :bar => 'sqlite' )+ # or as bind( { :foo => 1, 'bar' => 'sqlite' }) then it is assumed that each # parameter should be bound as a named parameter (:var, @var, $var). # # If bind is not passed any parameters, or nil, then nothing happens. # def bind( *params ) if params.nil? or params.empty? then check_parameter_count!( 0 ) return nil end if params.first.instance_of?( Hash ) then bind_named_parameters( params.first ) elsif params.first.instance_of?( Array ) then bind_positional_parameters( *params ) else bind_positional_parameters( params ) end end ## # Bind parameters to the statement based upon named parameters # def bind_named_parameters( params ) check_parameter_count!( params.size ) params.each_pair do | param, value | position = param_position_of( param ) if position > 0 then bind_parameter_to( position, value ) else raise Amalgalite::Error, "Unable to find parameter '#{param}' in SQL statement [#{sql}]" end end end ## # Bind parameters to the statements based upon positions. # def bind_positional_parameters( params ) check_parameter_count!( params.size ) params.each_with_index do |value, index| position = index + 1 bind_parameter_to( position, value ) end end ## # bind a single parameter to a particular position # def bind_parameter_to( position, value ) bind_type = db.type_map.bind_type_of( value ) case bind_type when DataType::FLOAT @stmt_api.bind_double( position, value ) when DataType::INTEGER @stmt_api.bind_int64( position, value ) when DataType::NULL @stmt_api.bind_null( position ) when DataType::TEXT @stmt_api.bind_text( position, value.to_s ) when DataType::BLOB if value.incremental? then @stmt_api.bind_zeroblob( position, value.length ) @blobs_to_write << value else @stmt_api.bind_blob( position, value.source ) end else raise ::Amalgalite::Error, "Unknown binding type of #{bind_type} from #{db.type_map.class.name}.bind_type_of" end end ## # Find and cache the binding parameter indexes # def param_position_of( name ) ns = name.to_s unless pos = @param_positions[ns] pos = @param_positions[ns] = @stmt_api.parameter_index( ns ) end return pos end ## # Check and make sure that the number of parameters aligns with the number # that sqlite expects # def check_parameter_count!( num ) expected = @stmt_api.parameter_count if num != expected then raise Amalgalite::Error, "#{sql} has #{expected} parameters, but #{num} were passed to bind." end return expected end ## # Write any blobs that have been bound to parameters to the database. This # assumes that the blobs go into the last inserted row # def write_blobs unless @blobs_to_write.empty? @blobs_to_write.each do |blob| blob.write_to_column! end end end ## # Iterate over the results of the statement returning each row of results # as a hash by +column_name+. The column names are the value after an # 'AS' in the query or default chosen by sqlite. # def each while row = next_row yield row end return self end ## # Return the next row of data, with type conversion as indicated by the # Database#type_map # def next_row row = [] case rc = @stmt_api.step when ResultCode::ROW result_meta.each_with_index do |col, idx| value = nil column_type = @stmt_api.column_type( idx ) case column_type when DataType::TEXT value = @stmt_api.column_text( idx ) when DataType::FLOAT value = @stmt_api.column_double( idx ) when DataType::INTEGER value = @stmt_api.column_int64( idx ) when DataType::NULL value = nil when DataType::BLOB # if the rowid column is encountered, then we can use an incremental # blob api, otherwise we have to use the all at once version. if using_rowid_column? then value = Amalgalite::Blob.new( :db_blob => SQLite3::Blob.new( db.api, col.schema.db, col.schema.table, col.schema.name, @stmt_api.column_int64( @rowid_index ), "r"), :column => col.schema) else value = Amalgalite::Blob.new( :string => @stmt_api.column_blob( idx ), :column => col.schema ) end else raise ::Amalgalite::Error, "BUG! : Unknown SQLite column type of #{column_type}" end row << db.type_map.result_value_of( col.schema.declared_data_type, value ) end row.fields = result_fields when ResultCode::DONE row = nil write_blobs else self.close # must close so that the error message is guaranteed to be pushed into the database handler # and we can can call last_error_message on it msg = "SQLITE ERROR #{rc} (#{Amalgalite::SQLite3::Constants::ResultCode.name_from_value( rc )}) : #{@db.api.last_error_message}" raise Amalgalite::SQLite3::Error, msg end return row end ## # Return all rows from the statement as one array # def all_rows rows = [] while row = next_row rows << row end return rows end ## # Inspect the statement and gather all the meta information about the # results, include the name of the column result column and the origin # column. The origin column is the original database.table.column the value # comes from. # # The full meta information from teh origin column is also obtained for help # in doing type conversion. # # As iteration over the row meta informatio happens, record if the special # "ROWID", "OID", or "_ROWID_" column is encountered. If that column is # encountered then we make note of it. # def result_meta unless @result_meta meta = [] column_count.times do |idx| column_meta = ::OpenStruct.new column_meta.name = @stmt_api.column_name( idx ) db_name = @stmt_api.column_database_name( idx ) tbl_name = @stmt_api.column_table_name( idx ) col_name = @stmt_api.column_origin_name( idx ) column_meta.schema = ::Amalgalite::Column.new( db_name, tbl_name, col_name, idx ) column_meta.schema.declared_data_type = @stmt_api.column_declared_type( idx ) # only check for rowid if we have a table name and it is not the # sqlite_master table. We could get recursion in those cases. if not using_rowid_column? and tbl_name and tbl_name != 'sqlite_master' and is_column_rowid?( tbl_name, col_name ) then @rowid_index = idx end meta << column_meta end @result_meta = meta end return @result_meta end ## # is the column indicated by the Column a 'rowid' column # def is_column_rowid?( table_name, column_name ) column_schema = @db.schema.tables[table_name].columns[column_name] if column_schema then if column_schema.primary_key? and column_schema.declared_data_type and column_schema.declared_data_type.upcase == "INTEGER" then return true end else return true if Statement.rowid_column_names.include?( column_name.upcase ) end return false end ## # Return the array of field names for the result set, the field names are # all strings # def result_fields @fields ||= result_meta.collect { |m| m.name } end ## # Return any unsued SQL from the statement # def remaining_sql @stmt_api.remaining_sql end ## # return the number of columns in the result of this query # def column_count @stmt_api.column_count end ## # return the raw sql that was originally used to prepare the statement # def sql @stmt_api.sql end ## # Close the statement. The statement is no longer valid for use after it # has been closed. # def close if open? then @stmt_api.close @open = false end end end end