module Swift
# Adapter.
#
# @abstract
# @see Swift::DB See Swift::DB for concrete adapters.
# @todo For the time being all adapters are SQL and DBIC++ centric. It would be super easy to abstract though I
# don't know if you would be better off doing it at the Ruby or DBIC++ level (or both).
#--
# TODO: Extension methods are undocumented.
class Adapter
attr_reader :options
# 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::Scheme] scheme Concrete scheme subclass to load.
# @param [Hash] keys Hash of id(s) {id_name: value}.
# @return [Swift::Scheme, nil]
# @see Swift::Scheme.get
#--
# NOTE: Not significantly shorter than Scheme.db.first(User, 'id = ?', 12)
def get scheme, keys
resource = scheme.new(keys)
prepare_get(scheme).execute(*resource.tuple.values_at(*scheme.header.keys)).first
end
# Select one or more.
#
# @example All.
# Swif.db.all(User)
# @example All with conditions and binds.
# Swift.db.all(User, ':name = ? and :age > ?', 'Apple Arthurton', 32)
# @example Block form iterator.
# Swift.db.all(User, ':age > ?', 32) do |user|
# puts user.name
# end
#
# @param [Swift::Scheme] scheme Concrete scheme subclass to load.
# @param [String] conditions Optional SQL 'where' fragment.
# @param [Object, ...] *binds Optional bind values that accompany conditions SQL fragment.
# @param [Proc] &block Optional 'each' iterator block.
# @return [Swift::Result]
# @see Swift::Scheme.all
def all scheme, conditions = '', *binds, &block
where = "where #{exchange_names(scheme, conditions)}" unless conditions.empty?
prepare(scheme, "select * from #{scheme.store} #{where}").execute(*binds, &block)
end
# Select one.
#
# @example First.
# Swif.db.first(User)
# @example First with conditions and binds.
# Swift.db.first(User, ':name = ? and :age > ?', 'Apple Arthurton', 32)
# @example Block form iterator.
# Swift.db.first(User, ':age > ?', 32) do |user|
# puts user.name
# end
#
# @param [Swift::Scheme] scheme Concrete scheme subclass to load.
# @param [String] conditions Optional SQL 'where' fragment.
# @param [Object, ...] *binds Optional bind values that accompany conditions SQL fragment.
# @param [Proc] &block Optional 'each' iterator block.
# @return [Swift::Scheme, nil]
# @see Swift::Scheme.first
def first scheme, conditions = '', *binds, &block
where = "where #{exchange_names(scheme, conditions)}" unless conditions.empty?
prepare(scheme, "select * from #{scheme.store} #{where} limit 1").execute(*binds, &block).first
end
# Delete one or more.
#
# The SQL condition form of Swift::Adapter.destroy.
#
# @example All.
# Swift.db.delete(User)
# @example All with conditions and binds.
# Swift.db.delete(User, ':name = ? and :age > ?', 'Apple Arthurton', 32)
#
# @param [Swift::Scheme] scheme Concrete scheme subclass
# @param [String] conditions Optional SQL 'where' fragment.
# @param [Object, ...] *binds Optional bind values that accompany conditions SQL fragment.
# @return [Swift::Result]
def delete scheme, conditions = '', *binds
sql = "delete from #{scheme.store}"
sql += " where #{exchange_names(scheme, conditions)}" unless conditions.empty?
execute(sql, *binds)
end
# Create one or more.
#
# @example Scheme.
# user = User.new(name: 'Apply Arthurton', age: 32)
# Swift.db.create(User, user)
# #=> Swift::Scheme
# @example Coerce hash to scheme.
# Swif.db.create(User, name: 'Apple Arthurton', age: 32)
# #=> Swift::Scheme
# @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::Scheme] scheme Concrete scheme subclass to load.
# @param [Swift::Scheme, Hash, Array] resources The resources to be saved.
# @return [Swift::Scheme, Array]
# @note Hashes will be coerced into a Swift::Scheme resource via Swift::Scheme#new
# @note Passing a scalar will result in a scalar.
# @see Swift::Scheme.create
def create scheme, resources
statement = prepare_create(scheme)
result = [resources].flatten.map do |resource|
resource = scheme.new(resource) unless resource.kind_of?(scheme)
result = statement.execute(*resource.tuple.values_at(*scheme.header.insertable))
resource.tuple[scheme.header.serial] = result.insert_id if scheme.header.serial
resource
end
resources.kind_of?(Enumerable) ? result : result.first
end
# Update one or more.
#
# @example Scheme.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swift.db.update(User, user)
# #=> Swift::Scheme
# @example Coerce hash to scheme.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swif.db.update(User, user.tuple)
# #=> Swift::Scheme
# @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::Scheme] scheme Concrete scheme subclass to load.
# @param [Swift::Scheme, Hash, Array] resources The resources to be updated.
# @return [Swift::Scheme, Swift::Result]
# @note Hashes will be coerced into a Swift::Scheme resource via Swift::Scheme#new
# @note Passing a scalar will result in a scalar.
# @see Swift::Scheme#update
def update scheme, resources
statement = prepare_update(scheme)
result = [resources].flatten.map do |resource|
resource = scheme.new(resource) unless resource.kind_of?(scheme)
keys = resource.tuple.values_at(*scheme.header.keys)
# TODO: Name the key field(s) missing.
raise ArgumentError, "#{scheme} resource has incomplete key: #{resource.inspect}" \
unless keys.select(&:nil?).empty?
statement.execute(*resource.tuple.values_at(*scheme.header.updatable), *keys)
resource
end
resources.kind_of?(Enumerable) ? result : result.first
end
# Destroy one or more.
#
# @example Scheme.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swift.db.destroy(User, user)
# @example Coerce hash to scheme.
# user = Swift.db.create(User, name: 'Apply Arthurton', age: 32)
# user.name = 'Arthur Appleton'
# Swif.db.destroy(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.destroy(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.destroy(User, [apple.tuple, benny.tuple])
#
# @param [Swift::Scheme] scheme Concrete scheme subclass to load.
# @param [Swift::Scheme, Hash, Array] resources The resources to be destroyed.
# @return [Swift::Scheme, Array]
# @note Hashes will be coerced into a Swift::Scheme resource via Swift::Scheme#new
# @note Passing a scalar will result in a scalar.
# @see Swift::Scheme#destroy
def destroy scheme, resources
statement = prepare_destroy(scheme)
result = [resources].flatten.map do |resource|
resource = scheme.new(resource) unless resource.kind_of?(scheme)
keys = resource.tuple.values_at(*scheme.header.keys)
# TODO: Name the key field(s) missing.
raise ArgumentError, "#{scheme} resource has incomplete key: #{resource.inspect}" \
unless keys.select(&:nil?).empty?
if result = statement.execute(*keys)
resource.freeze
end
result
end
resources.kind_of?(Enumerable) ? result : result.first
end
def migrate! scheme
keys = scheme.header.keys
fields = scheme.header.map{|p| field_definition(p)}.join(', ')
fields += ", primary key (#{keys.join(', ')})" unless keys.empty?
execute("drop table if exists #{scheme.store} cascade")
execute("create table #{scheme.store} (#{fields})")
end
protected
def exchange_names scheme, query
query.gsub(/:(\w+)/){ scheme.send($1.to_sym).field }
end
def returning?
raise NotImplementedError
end
def prepare_cached scheme, name, &block
@prepared ||= Hash.new{|h,k| h[k] = Hash.new} # Autovivification please Matz!
@prepared[scheme][name] ||= prepare(scheme, yield)
end
def prepare_get scheme
prepare_cached(scheme, :get) do
where = scheme.header.keys.map{|key| "#{key} = ?"}.join(' and ')
"select * from #{scheme.store} where #{where} limit 1"
end
end
def prepare_create scheme
prepare_cached(scheme, :create) do
values = (['?'] * scheme.header.insertable.size).join(', ')
returning = "returning #{scheme.header.serial}" if scheme.header.serial and returning?
"insert into #{scheme.store} (#{scheme.header.insertable.join(', ')}) values (#{values}) #{returning}"
end
end
def prepare_update scheme
prepare_cached(scheme, :update) do
set = scheme.header.updatable.map{|field| "#{field} = ?"}.join(', ')
where = scheme.header.keys.map{|key| "#{key} = ?"}.join(' and ')
"update #{scheme.store} set #{set} where #{where}"
end
end
def prepare_destroy scheme
prepare_cached(scheme, :destroy) do
where = scheme.header.keys.map{|key| "#{key} = ?"}.join(' and ')
"delete from #{scheme.store} where #{where}"
end
end
def field_definition attribute
"#{attribute.field} " + field_type(attribute)
end
def field_type attribute
case attribute
when Type::String then 'text'
when Type::Integer then attribute.serial ? 'serial' : 'integer'
when Type::Float then 'float'
when Type::BigDecimal then 'numeric'
when Type::Time then 'timestamp'
when Type::Date then 'date'
when Type::Boolean then 'boolean'
when Type::IO then 'blob'
else 'text'
end
end
end # Adapter
end # Swift