Require ResultSet to just implement read, optionally implementing read(T.class). Fixes #5

This commit is contained in:
Ary Borenszweig 2016-06-28 14:02:08 -03:00
parent 038ffef33a
commit 9c88f718e8
8 changed files with 329 additions and 96 deletions

View file

@ -126,3 +126,4 @@ require "./db/driver"
require "./db/connection"
require "./db/statement"
require "./db/result_set"
require "./db/error"

4
src/db/error.cr Normal file
View file

@ -0,0 +1,4 @@
module DB
class Error < Exception
end
end

View file

@ -21,27 +21,190 @@ module DB
# :nodoc:
abstract def prepare(query) : Statement
# Returns a `ResultSet` for the `query`.
# Executes a *query* and returns a `ResultSet` with the results.
# The `ResultSet` must be closed manually.
#
# ```
# result = db.query "select name from contacts where id = ?", 10
# begin
# if result.move_next
# id = result.read(Int32)
# end
# ensure
# result.close
# end
# ```
def query(query, *args)
prepare query, &.query(*args)
end
# Yields a `ResultSet` for the `query`.
# Executes a *query* and yields a `ResultSet` with the results.
# The `ResultSet` is closed automatically.
#
# ```
# db.query("select name from contacts where age > ?", 18) do |rs|
# rs.each do
# name = rs.read(String)
# end
# end
# ```
def query(query, *args)
# CHECK prepare(query).query(*args, &block)
rs = query(query, *args)
yield rs ensure rs.close
end
# Executes a *query* that expects a single row and yields a `ResultSet`
# positioned at that first row.
#
# The given block must not invoke `move_next` on the yielded result set.
#
# Raises `DB::Error` if there were no rows, or if there were more than one row.
#
# ```
# name = db.query_one "select name from contacts where id = ?", 18, &.read(String)
# ```
def query_one(query, *args, &block : ResultSet -> U) : U
query(query, *args) do |rs|
raise DB::Error.new("no rows") unless rs.move_next
value = yield rs
raise DB::Error.new("more than one row") if rs.move_next
return value
end
end
# Executes a *query* that expects a single row and returns it
# as a tuple of the given *types*.
#
# Raises `DB::Error` if there were no rows, or if there were more than one row.
#
# ```
# db.query_one "select name, age from contacts where id = ?", 1, as: {String, Int32}
# ```
def query_one(query, *args, as types : Tuple)
query_one(query, *args) do |rs|
rs.read(*types)
end
end
# Executes a *query* that expects a single row
# and returns the first column's value as the given *type*.
#
# Raises `DB::Error` if there were no rows, or if there were more than one row.
#
# ```
# db.query_one "select name from contacts where id = ?", 1, as: String
# ```
def query_one(query, *args, as type : Class)
query_one(query, *args) do |rs|
rs.read(type)
end
end
# Executes a *query* that expects at most a single row and yields a `ResultSet`
# positioned at that first row.
#
# Returns `nil`, not invoking the block, if there were no rows.
#
# Raises `DB::Error` if there were more than one row
# (this ends up invoking the block once).
#
# ```
# name = db.query_one? "select name from contacts where id = ?", 18, &.read(String)
# typeof(name) # => String | Nil
# ```
def query_one?(query, *args, &block : ResultSet -> U) : U?
query(query, *args) do |rs|
return nil unless rs.move_next
value = yield rs
raise DB::Error.new("more than one row") if rs.move_next
return value
end
end
# Executes a *query* that expects a single row and returns it
# as a tuple of the given *types*.
#
# Returns `nil` if there were no rows.
#
# Raises `DB::Error` if there were more than one row.
#
# ```
# result = db.query_one? "select name, age from contacts where id = ?", 1, as: {String, Int32}
# typeof(result) # => Tuple(String, Int32) | Nil
# ```
def query_one?(query, *args, as types : Tuple)
query_one?(query, *args) do |rs|
rs.read(*types)
end
end
# Executes a *query* that expects a single row
# and returns the first column's value as the given *type*.
#
# Returns `nil` if there were no rows.
#
# Raises `DB::Error` if there were more than one row.
#
# ```
# name = db.query_one? "select name from contacts where id = ?", 1, as: String
# typeof(name) # => String?
# ```
def query_one?(query, *args, as type : Class)
query_one?(query, *args) do |rs|
rs.read(type)
end
end
# Executes a *query* and yield a `ResultSet` positioned at the beginning
# of each row, returning an array of the values of the blocks.
#
# ```
# names = db.query_all "select name from contacts", &.read(String)
# ```
def query_all(query, *args, &block : ResultSet -> U) : Array(U)
ary = [] of U
query(query, *args) do |rs|
rs.each do
ary.push(yield rs)
end
end
ary
end
# Executes a *query* and returns an array where each row is
# read as a tuple of the given *types*.
#
# ```
# contacts = db.query_all "select name, age from contactas", as: {String, Int32}
# ```
def query_all(query, *args, as types : Tuple)
query_all(query, *args) do |rs|
rs.read(*types)
end
end
# Executes a *query* and returns an array where there first
# column's value of each row is read as the given *type*.
#
# ```
# names = db.query_all "select name from contactas", as: String
# ```
def query_all(query, *args, as type : Class)
query_all(query, *args) do |rs|
rs.read(type)
end
end
# Performs the `query` and returns an `ExecResult`
def exec(query, *args)
prepare query, &.exec(*args)
end
# Performs the `query` and returns a single scalar `DB::Any` value
# puts db.scalar("SELECT MAX(name)") as String # => (a String)
# Performs the `query` and returns a single scalar value
# puts db.scalar("SELECT MAX(name)").as(String) # => (a String)
def scalar(query, *args)
prepare query, &.scalar(*args)
end
@ -55,7 +218,5 @@ module DB
raise ex
end
end
# TODO add query_row
end
end

View file

@ -16,10 +16,9 @@ module DB
# ### Note to implementors
#
# 1. Override `#move_next` to move to the next row.
# 2. Override `#read?(t)` for all `t` in `DB::TYPES` and any other types the driver should handle.
# 3. (Optional) Override `#read(t)` for all `t` in `DB::TYPES` and any other.
# 2. Override `#read` returning the next value in the row.
# 3. (Optional) Override `#read(t)` for some types `t` for which custom logic other than a simple cast is needed.
# 4. Override `#column_count`, `#column_name`.
# 5. Override `#column_type`. It must return a type in `DB::TYPES`.
abstract class ResultSet
include Disposable
@ -55,30 +54,29 @@ module DB
# Returns the name of the column in `index` 0-based position.
abstract def column_name(index : Int32) : String
# Returns the type of the column in `index` 0-based position.
# The result is one of `DB::TYPES`.
abstract def column_type(index : Int32)
# Reads the next column value
abstract def read
def read(t)
read?(t).not_nil!
# Reads the next column value as a **type**
def read(type : T.class) : T
read.as(T)
end
# Reads the next column as a Nil.
def read(t : Nil.class) : Nil
read?(Nil)
# Reads the next columns and returns a tuple of the values.
def read(*types : Class)
internal_read(*types)
end
def read?(t)
raise "read?(t : #{t}) is not implemented in #{self.class}"
private def internal_read(*types : *T)
{% begin %}
Tuple.new(
{% for type in T %}
read({{type.instance}}),
{% end %}
)
{% end %}
end
# list datatypes that must be supported form the driver
# users will call read(String) or read?(String) for nillables
{% for t in DB::TYPES %}
# Reads the next column as a nillable {{t}}.
abstract def read?(t : {{t}}.class) : {{t}}?
{% end %}
# def read_blob
# yield ... io ....
# end

View file

@ -45,7 +45,7 @@ module DB
def scalar(*args)
query(*args) do |rs|
rs.each do
return rs.read?(rs.column_type(0))
return rs.read
end
end