require 'swift/result'
require 'swift/statement'
module Swift
# Adapter.
#
# @abstract
# @see Swift::DB See Swift::DB for concrete adapters.
class Adapter
attr_reader :db
def initialize db
@db = db
end
# Select by id(s).
#
# @example Single key.
# Swift.db.get(User, id: 12)
# @example Complex primary key.
# Swift.db.get(UserAddress, user_id: 12, address_id: 15)
#
# @param [Swift::Record] record Concrete record subclass to load.
# @param [Hash] keys Hash of id(s) {id_name: value}.
# @return [Swift::Record, nil]
# @see Swift::Record.get
#--
# NOTE: Not significantly shorter than Record.db.first(User, 'id = ?', 12)
def get record, keys
resource = record.new(keys)
execute(record, command_get(record), *resource.tuple.values_at(*record.header.keys)).first
end
# Create one or more.
#
# @example Record.
# user = User.new(name: 'Apply Arthurton', age: 32)
# Swift.db.create(User, user)
# #=> Swift::Record
# @example Coerce hash to record.
# Swif.db.create(User, name: 'Apple Arthurton', age: 32)
# #=> Swift::Record
# @example Multiple resources.
# apple = User.new(name: 'Apple Arthurton', age: 32)
# benny = User.new(name: 'Benny Arthurton', age: 30)
# Swift.db.create(User, [apple, benny])
# #=> Array
# @example Coerce multiple resources.
# Swift.db.create(User, [{name: 'Apple Arthurton', age: 32}, {name: 'Benny Arthurton', age: 30}])
# #=> Array
#
# @param [Swift::Record] record Concrete record subclass to load.
# @param [Swift::Record, Hash, Array] resources The resources to be saved.
# @return [Swift::Record, Array]
# @note Hashes will be coerced into a Swift::Record resource via Swift::Record#new
# @note Passing a scalar will result in a scalar.
# @see Swift::Record.create
def create record, resources
result = [resources].flatten.map do |resource|
resource = record.new(resource) unless resource.kind_of?(record)
result = execute(command_create(record), *resource.tuple.values_at(*record.header.insertable))
resource.tuple[record.header.serial] = result.insert_id if record.header.serial
resource
end
resources.kind_of?(Array) ? result : result.first
end
# Update one or more.
#
# @example Record.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swift.db.update(User, user)
# #=> Swift::Record
# @example Coerce hash to record.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swif.db.update(User, user.tuple)
# #=> Swift::Record
# @example Multiple resources.
# apple = Swift.db.create(User, name: 'Apple Arthurton', age: 32)
# benny = Swift.db.create(User, name: 'Benny Arthurton', age: 30)
# Swift.db.update(User, [apple, benny])
# #=> Array
# @example Coerce multiple resources.
# apple = Swift.db.create(User, name: 'Apple Arthurton', age: 32)
# benny = Swift.db.create(User, name: 'Benny Arthurton', age: 30)
# Swift.db.update(User, [apple.tuple, benny.tuple])
# #=> Array
#
# @param [Swift::Record] record Concrete record subclass to load.
# @param [Swift::Record, Hash, Array] resources The resources to be updated.
# @return [Swift::Record, Swift::Result]
# @note Hashes will be coerced into a Swift::Record resource via Swift::Record#new
# @note Passing a scalar will result in a scalar.
# @see Swift::Record#update
def update record, resources
result = [resources].flatten.map do |resource|
resource = record.new(resource) unless resource.kind_of?(record)
keys = resource.tuple.values_at(*record.header.keys)
# TODO: Name the key field(s) missing.
raise ArgumentError, "#{record} resource has incomplete key: #{resource.inspect}" \
unless keys.select(&:nil?).empty?
execute(command_update(record), *resource.tuple.values_at(*record.header.updatable), *keys)
resource
end
resources.kind_of?(Array) ? result : result.first
end
# Delete one or more.
#
# @example Record.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swift.db.delete(User, user)
# @example Coerce hash to record.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swif.db.delete(User, user.tuple)
# @example Multiple resources.
# apple = Swift.db.create(User, name: 'Apple Arthurton', age: 32)
# benny = Swift.db.create(User, name: 'Benny Arthurton', age: 30)
# Swift.db.delete(User, [apple, benny])
# @example Coerce multiple resources.
# apple = Swift.db.create(User, name: 'Apple Arthurton', age: 32)
# benny = Swift.db.create(User, name: 'Benny Arthurton', age: 30)
# Swift.db.delete(User, [apple.tuple, benny.tuple])
#
# @param [Swift::Record] record Concrete record subclass to load.
# @param [Swift::Record, Hash, Array] resources The resources to be deleteed.
# @return [Swift::Record, Array]
# @note Hashes will be coerced into a Swift::Record resource via Swift::Record#new
# @note Passing a scalar will result in a scalar.
# @see Swift::Record#delete
def delete record, resources
result = [resources].flatten.map do |resource|
resource = record.new(resource) unless resource.kind_of?(record)
keys = resource.tuple.values_at(*record.header.keys)
# TODO: Name the key field(s) missing.
raise ArgumentError, "#{record} resource has incomplete key: #{resource.inspect}" \
unless keys.select(&:nil?).empty?
if result = execute(command_delete(record), *keys)
resource.freeze
end
result
end
resources.kind_of?(Array) ? result : result.first
end
# Create a server side prepared statement
#
# @example
# finder = Swift.db.prepare(User, "select * from users where id > ?")
# user = finder.execute(1).first
# user.id
#
# @overload prepare(record, command)
# @param [Swift::Record] record Concrete record subclass to load.
# @param [String] command Command to be prepared by the underlying concrete adapter.
# @overload prepare(command)
# @param [String] command Command to be prepared by the underlying concrete adapter.
#
# @return [Swift::Statement, Swift::DB::Mysql::Statement, Swift::DB::Sqlite3::Statement, ...]
def prepare record = nil, command
record ? Statement.new(record, command) : db.prepare(command)
end
# Trace commands being executed.
#
# @example
# Swift.db.trace { Swift.db.execute("select * from users") }
# @example
# Swift.db.trace(StringIO.new) { Swift.db.execute("select * from users") }
# @example
# Swift.db.trace(File.open('command.log', 'w')) { Swift.db.execute("select * from users") }
#
# @param [IO] io An optional IO object to log commands
# @return [Object] result Result from the block yielded to
def trace io = $stdout
@trace = io
result = yield
@trace = false
result
end
# Check if the adapter commands are being traced.
#
# @return [TrueClass, FalseClass]
def trace?
!!@trace
end
# Execute a command using the underlying concrete adapter.
#
# @example
# Swift.db.execute("select * from users")
# @example
# Swift.db.execute(User, "select * from users where id = ?", 1)
#
# @overload execute(record, command, *bind)
# @param [Swift::Record] record Concrete record subclass to load.
# @param [String] command Command to be executed by the adapter.
# @param [*Object] bind Bind values.
# @overload execute(command, *bind)
# @param [String] command Command to be executed by the adapter.
# @param [*Object] bind Bind values.
#
# @return [Swift::Result, Swift::DB::Mysql::Result, Swift::DB::Sqlite3::Result, ...]
def execute command, *bind
start = Time.now
record, command = command, bind.shift if command.kind_of?(Class) && command < Record
record ? Result.new(record, db.execute(command, *bind)) : db.execute(command, *bind)
ensure
log_command(start, command, bind) if @trace
end
private
def log_command start, command, bind
@trace.print Time.now.strftime('%F %T.%N'), ' - ', (Time.now - start).to_f, ' - ', command, ' ', bind, $/
end
end # Adapter
end # Swift