# frozen-string-literal: true require_relative 'utils/unmodified_identifiers' module Sequel module Mock class Connection # Sequel::Mock::Database object that created this connection attr_reader :db # Shard this connection operates on, when using Sequel's # sharding support (always :default for databases not using # sharding). attr_reader :server # The specific database options for this connection. attr_reader :opts # Store the db, server, and opts. def initialize(db, server, opts) @db = db @server = server @opts = opts end # Delegate to the db's #_execute method. def execute(sql) @db.send(:_execute, self, sql, :log=>false) end end class Database < Sequel::Database set_adapter_scheme :mock # Set the autogenerated primary key integer # to be returned when running an insert query. # Argument types supported: # # nil :: Return nil for all inserts # Integer :: Starting integer for next insert, with # futher inserts getting an incremented # value # Array :: First insert gets the first value in the # array, second gets the second value, etc. # Proc :: Called with the insert SQL query, uses # the value returned # Class :: Should be an Exception subclass, will create a new # instance an raise it wrapped in a DatabaseError. def autoid=(v) @autoid = case v when Integer i = v - 1 proc{@mutex.synchronize{i+=1}} else v end end # Set the columns to set in the dataset when the dataset fetches # rows. Argument types supported: # nil :: Set no columns # Array of Symbols :: Used for all datasets # Array (otherwise) :: First retrieval gets the first value in the # array, second gets the second value, etc. # Proc :: Called with the select SQL query, uses the value # returned, which should be an array of symbols attr_writer :columns # Set the hashes to yield by execute when retrieving rows. # Argument types supported: # # nil :: Yield no rows # Hash :: Always yield a single row with this hash # Array of Hashes :: Yield separately for each hash in this array # Array (otherwise) :: First retrieval gets the first value # in the array, second gets the second value, etc. # Proc :: Called with the select SQL query, uses # the value returned, which should be a hash or # array of hashes. # Class :: Should be an Exception subclass, will create a new # instance an raise it wrapped in a DatabaseError. attr_writer :fetch # Set the number of rows to return from update or delete. # Argument types supported: # # nil :: Return 0 for all updates and deletes # Integer :: Used for all updates and deletes # Array :: First update/delete gets the first value in the # array, second gets the second value, etc. # Proc :: Called with the update/delete SQL query, uses # the value returned. # Class :: Should be an Exception subclass, will create a new # instance an raise it wrapped in a DatabaseError. attr_writer :numrows # Mock the server version, useful when using the shared adapters attr_accessor :server_version # Return a related Connection option connecting to the given shard. def connect(server) Connection.new(self, server, server_opts(server)) end def disconnect_connection(c) end # Store the sql used for later retrieval with #sqls, and return # the appropriate value using either the #autoid, #fetch, or # #numrows methods. def execute(sql, opts=OPTS, &block) synchronize(opts[:server]){|c| _execute(c, sql, opts, &block)} end alias execute_ddl execute # Store the sql used, and return the value of the #numrows method. def execute_dui(sql, opts=OPTS) execute(sql, opts.merge(:meth=>:numrows)) end # Store the sql used, and return the value of the #autoid method. def execute_insert(sql, opts=OPTS) execute(sql, opts.merge(:meth=>:autoid)) end # Return all stored SQL queries, and clear the cache # of SQL queries. def sqls @mutex.synchronize do s = @sqls.dup @sqls.clear s end end # Enable use of savepoints. def supports_savepoints? shared_adapter? ? super : true end private def _execute(c, sql, opts=OPTS, &block) sql += " -- args: #{opts[:arguments].inspect}" if opts[:arguments] sql += " -- #{@opts[:append]}" if @opts[:append] sql += " -- #{c.server.is_a?(Symbol) ? c.server : c.server.inspect}" if c.server != :default log_connection_yield(sql, c){} unless opts[:log] == false @mutex.synchronize{@sqls << sql} ds = opts[:dataset] begin if block columns(ds, sql) if ds _fetch(sql, (ds._fetch if ds) || @fetch, &block) elsif meth = opts[:meth] if meth == :numrows _numrows(sql, (ds.numrows if ds) || @numrows) else if ds @mutex.synchronize do v = ds.autoid if v.is_a?(Integer) ds.send(:cache_set, :_autoid, v + 1) end v end end || _nextres(@autoid, sql, nil) end end rescue => e raise_error(e) end end def _fetch(sql, f, &block) case f when Hash yield f.dup when Array if f.all?{|h| h.is_a?(Hash)} f.each{|h| yield h.dup} else _fetch(sql, @mutex.synchronize{f.shift}, &block) end when Proc h = f.call(sql) if h.is_a?(Hash) yield h.dup elsif h h.each{|h1| yield h1.dup} end when Class if f < Exception raise f else raise Error, "Invalid @fetch attribute: #{v.inspect}" end when nil # nothing else raise Error, "Invalid @fetch attribute: #{f.inspect}" end end def _nextres(v, sql, default) case v when Integer v when Array v.empty? ? default : _nextres(@mutex.synchronize{v.shift}, sql, default) when Proc v.call(sql) when Class if v < Exception raise v else raise Error, "Invalid @autoid/@numrows attribute: #{v.inspect}" end when nil default else raise Error, "Invalid @autoid/@numrows attribute: #{v.inspect}" end end def _numrows(sql, v) _nextres(v, sql, 0) end # Additional options supported: # # :autoid :: Call #autoid= with the value # :columns :: Call #columns= with the value # :fetch :: Call #fetch= with the value # :numrows :: Call #numrows= with the value # :extend :: A module the object is extended with. # :sqls :: The array to store the SQL queries in. def adapter_initialize opts = @opts @mutex = Mutex.new @sqls = opts[:sqls] || [] @shared_adapter = false case db_type = opts[:host] when String, Symbol db_type = db_type.to_sym unless mod = Sequel.synchronize{SHARED_ADAPTER_MAP[db_type]} begin require "sequel/adapters/shared/#{db_type}" rescue LoadError else mod = Sequel.synchronize{SHARED_ADAPTER_MAP[db_type]} end end if mod @shared_adapter = true extend(mod::DatabaseMethods) extend_datasets(mod::DatasetMethods) if mod.respond_to?(:mock_adapter_setup) mod.mock_adapter_setup(self) end end end unless @shared_adapter extend UnmodifiedIdentifiers::DatabaseMethods extend_datasets UnmodifiedIdentifiers::DatasetMethods end self.autoid = opts[:autoid] self.columns = opts[:columns] self.fetch = opts[:fetch] self.numrows = opts[:numrows] extend(opts[:extend]) if opts[:extend] sqls end def columns(ds, sql, cs=@columns) case cs when Array unless cs.empty? if cs.all?{|c| c.is_a?(Symbol)} ds.columns(*cs) else columns(ds, sql, @mutex.synchronize{cs.shift}) end end when Proc ds.columns(*cs.call(sql)) when nil # nothing else raise Error, "Invalid @columns attribute: #{cs.inspect}" end end def dataset_class_default Dataset end def quote_identifiers_default shared_adapter? ? super : false end def shared_adapter? @shared_adapter end end class Dataset < Sequel::Dataset # The autoid setting for this dataset, if it has been overridden def autoid cache_get(:_autoid) || @opts[:autoid] end # The fetch setting for this dataset, if it has been overridden def _fetch cache_get(:_fetch) || @opts[:fetch] end # The numrows setting for this dataset, if it has been overridden def numrows cache_get(:_numrows) || @opts[:numrows] end # If arguments are provided, use them to set the columns # for this dataset and return self. Otherwise, use the # default Sequel behavior and return the columns. def columns(*cs) if cs.empty? super else self.columns = cs self end end def fetch_rows(sql, &block) execute(sql, &block) end def quote_identifiers? @opts.fetch(:quote_identifiers, db.send(:quote_identifiers_default)) end # Return cloned dataset with the autoid setting modified def with_autoid(autoid) clone(:autoid=>autoid) end # Return cloned dataset with the fetch setting modified def with_fetch(fetch) clone(:fetch=>fetch) end # Return cloned dataset with the numrows setting modified def with_numrows(numrows) clone(:numrows=>numrows) end private def execute(sql, opts=OPTS, &block) super(sql, opts.merge(:dataset=>self), &block) end def execute_dui(sql, opts=OPTS, &block) super(sql, opts.merge(:dataset=>self), &block) end def execute_insert(sql, opts=OPTS, &block) super(sql, opts.merge(:dataset=>self), &block) end def non_sql_option?(key) super || key == :fetch || key == :numrows || key == :autoid end end end end