switch to 0-pased positional arguments

add docs, many docs
This commit is contained in:
Brian J. Cardiff 2016-01-31 19:40:02 -03:00
parent a96776e336
commit fd804dd592
9 changed files with 229 additions and 30 deletions

View file

@ -86,7 +86,7 @@ class DummyDriver < DB::Driver
return nil if n == "NULL"
if n == "?"
return @statement.params[1]
return @statement.params[0]
end
if n.starts_with?(":")

View file

@ -13,9 +13,9 @@ describe DB::Statement do
with_dummy do |db|
stmt = db.prepare("the query")
stmt.query "a", 1, nil
stmt.params[1].should eq("a")
stmt.params[2].should eq(1)
stmt.params[3].should eq(nil)
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
stmt.params[2].should eq(nil)
end
end
@ -43,9 +43,9 @@ describe DB::Statement do
with_dummy do |db|
stmt = db.prepare("the query")
stmt.exec "a", 1, nil
stmt.params[1].should eq("a")
stmt.params[2].should eq(1)
stmt.params[3].should eq(nil)
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
stmt.params[2].should eq(nil)
end
end
@ -73,9 +73,9 @@ describe DB::Statement do
with_dummy do |db|
stmt = db.prepare("the query")
stmt.scalar String, "a", 1, nil
stmt.params[1].should eq("a")
stmt.params[2].should eq(1)
stmt.params[3].should eq(nil)
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
stmt.params[2].should eq(nil)
end
end
@ -103,9 +103,9 @@ describe DB::Statement do
with_dummy do |db|
stmt = db.prepare("the query")
stmt.scalar? String, "a", 1, nil
stmt.params[1].should eq("a")
stmt.params[2].should eq(1)
stmt.params[3].should eq(nil)
stmt.params[0].should eq("a")
stmt.params[1].should eq(1)
stmt.params[2].should eq(nil)
end
end

View file

@ -1,4 +1,19 @@
module DB
# Database driver implementors must subclass `Connection`.
#
# Represents one active connection to a database.
#
# Users should never instantiate a `Connection` manually. Use `DB#open` or `Database#connection`.
#
# Refer to `QueryMethods` for documentation about querying the database through this connection.
#
# ### Note to implementors
#
# The connection must be initialized in `#initialize` and closed in `#perform_close`.
#
# To allow quering override `#prepare` method in order to return a prepared `Statement`.
# Also override `#last_insert_id` to allow safe access to the last inserted id through this connection.
#
abstract class Connection
getter connection_string
@ -23,10 +38,12 @@ module DB
# close unless closed?
# end
# Returns an `Statement` with the prepared `query`
abstract def prepare(query) : Statement
include QueryMethods
# Returns the last inserted id through this connection.
abstract def last_insert_id : Int64
protected abstract def perform_close

View file

@ -3,22 +3,29 @@ module DB
# Currently it creates a single connection to the database.
# Eventually a connection pool will be handled.
#
# It should be created from DB module. See `DB.open`.
# It should be created from DB module. See `DB#open`.
#
# Refer to `QueryMethods` for documentation about querying the database.
class Database
# :nodoc:
getter driver_class
# Connection configuration to the database.
getter connection_string
# :nodoc:
def initialize(@driver_class, @connection_string)
@driver = @driver_class.new(@connection_string)
@connection = @driver.build_connection
end
# Closes all connection to the database
# Closes all connection to the database.
def close
@connection.close
end
# Returns a `Connection` to the database
# Returns a `Connection` to the database.
# Useful if you need to ensure the statements are executed in the connection.
def connection
@connection
end

View file

@ -1,23 +1,94 @@
# The DB module is a unified interface to database access.
# Database dialects is supported by custom database driver shards.
# Check [manastech/crystal-sqlite3](https://github.com/manastech/crystal-sqlite3) for example.
#
# Drivers implementors check `Driver` class.
#
# Currently a *single connection* to the database is stablished.
# In the future a connection pool and transaction support will be available.
#
# ### Usage
#
# Assuming `crystal-sqlite3` is included a sqlite3 database can be opened with `#open`.
#
# ```
# db = DB.open "sqlite3", ":memory:" # or the sqlite3 file path
# db.close
# ```
#
# If a block is given to `#open` the database is closed automatically
#
# ```
# DB.open "sqlite3", ":memory:" do |db|
# # work with db
# end # db is closed
# ```
#
# Three kind of statements can be performed:
# 1. `Database#exec` waits no response from the database.
# 2. `Database#scalar` reads a single value of the response.
# 3. `Database#query` returns a ResultSet that allows iteration over the rows in the response and column information.
#
# All of the above methods allows parametrised query. Either positional or named arguments.
#
# Check a full working version:
#
# ```
# require "db"
# require "sqlite3"
#
# DB.open "sqlite3", ":memory:" do |db|
# db.exec "create table contacts (name string, age integer)"
# db.exec "insert into contacts values (?, ?)", "John Doe", 30
# db.exec "insert into contacts values (:name, :age)", {name: "Sarah", age: 33}
#
# puts "max age:"
# puts db.scalar "select max(age) from contacts" # => 33
#
# puts "contacts:"
# db.query "select name, age from contacts order by age desc" do |rs|
# puts "#{rs.column_name(0)} (#{rs.column_name(1)})"
# # => name (age)
# rs.each do
# puts "#{rs.read(String)} (#{rs.read(Int32)})"
# # => Sarah (33)
# # => John Doe (30)
# end
# end
# end
# ```
#
module DB
# Types supported to interface with database driver.
# These can be used in any `ResultSet#read` or any `Database#query` related
# method to be used as query parameters
TYPES = [String, Int32, Int64, Float32, Float64, Slice(UInt8)]
# See `DB::TYPES` in `DB`
alias Any = String | Int32 | Int64 | Float32 | Float64 | Slice(UInt8)
# :nodoc:
def self.driver_class(name) # : Driver.class
@@drivers.not_nil![name]
def self.driver_class(driver_name) # : Driver.class
@@drivers.not_nil![driver_name]
end
def self.register_driver(name, klass : Driver.class)
# Registers a driver class for a given `driver_name`.
# Should be called by drivers implementors only.
def self.register_driver(driver_name, driver_class : Driver.class)
@@drivers ||= {} of String => Driver.class
@@drivers.not_nil![name] = klass
@@drivers.not_nil![driver_name] = driver_class
end
def self.open(name, connection_string)
Database.new(driver_class(name), connection_string)
# Opens a database using the `driver_name` registered driver.
# Uses `connection_string` for connection configuration.
# Returned database must be closed by `Database#close`.
def self.open(driver_name, connection_string)
Database.new(driver_class(driver_name), connection_string)
end
def self.open(name, connection_string, &block)
open(name, connection_string).tap do |db|
# Same as `#open` but the database is yielded and closed automatically.
def self.open(driver_name, connection_string, &block)
open(driver_name, connection_string).tap do |db|
yield db
db.close
end

View file

@ -1,4 +1,30 @@
module DB
# Database driver implementors must subclass `Driver`,
# register with a driver_name using `DB#register_driver` and
# override the factory method `#build_connection`.
#
# ```
# require "db"
#
# class FakeDriver < Driver
# def build_connection
# FakeConnection.new connection_string
# end
# end
#
# DB.register_driver "fake", FakeDriver
# ```
#
# Access to this fake datbase will be available with
#
# ```
# DB.open "fake", "..." do |db|
# # ... use db ...
# end
# ```
#
# Refer to `Connection`, `Statement` and `ResultSet` for further
# driver implementation instructions.
abstract class Driver
getter connection_string

View file

@ -1,9 +1,29 @@
module DB
# Methods to allow querying a database.
# All methods accepts a `query : String` and a set arguments.
#
# Three kind of statements can be performed:
# 1. `#exec` waits no response from the database.
# 2. `#scalar` reads a single value of the response. Use `#scalar?` if the response is nillable.
# 3. `#query` returns a ResultSet that allows iteration over the rows in the response and column information.
#
# Arguments can be passed:
# * by position: `db.query("SELECT name FROM ... WHERE age > ?", age)`
# * by symbol: `db.query("SELECT name FROM ... WHERE age > :age", {age: age})`
# * by string: `db.query("SELECT name FROM ... WHERE age > :age", {"age": age})`
#
# Convention of mapping how arguments are mapped to the query depends on each driver.
#
# Including `QueryMethods` requires a `prepare(query) : Statement` method.
module QueryMethods
# Returns a `ResultSet` for the `query`.
# The `ResultSet` must be closed manually.
def query(query, *args)
prepare(query).query(*args)
end
# Yields a `ResultSet` for the `query`.
# The `ResultSet` is closed automatically.
def query(query, *args)
# CHECK prepare(query).query(*args, &block)
query(query, *args).tap do |rs|
@ -15,22 +35,37 @@ module DB
end
end
# Performs the `query` discarding any response
def exec(query, *args)
prepare(query).exec(*args)
end
# Performs the `query` and returns a single scalar `Int32` value
def scalar(query, *args)
prepare(query).scalar(*args)
end
# Performs the `query` and returns a single scalar value of type `t`.
# `t` must be any of the allowed `DB::Any` types.
#
# ```
# puts db.scalar(String, "SELECT MAX(name)") # => (a String)
# ```
def scalar(t, query, *args)
prepare(query).scalar(t, *args)
end
# Performs the `query` and returns a single scalar `Int32 | Nil` value
def scalar?(query, *args)
prepare(query).scalar?(*args)
end
# Performs the `query` and returns a single scalar value of type `t` or `Nil`.
# `t` must be any of the allowed `DB::Any` types.
#
# ```
# puts db.scalar?(String, "SELECT MAX(name)") # => (a String | Nil)
# ```
def scalar?(t, query, *args)
prepare(query).scalar?(t, *args)
end

View file

@ -1,39 +1,66 @@
module DB
# The response of a query performed on a `Database`.
#
# See `DB` for a complete sample.
#
# Each `#read` call consumes the result and moves to the next column.
#
# ### Note to implementors
#
# 1. Override `#move_next` to move to the next row.
# 2. Override `#read?(t)` for all `t` in `DB::TYPES`.
# 3. (Optional) Override `#read(t)` for all `t` in `DB::TYPES`.
# 4. Override `#column_count`, `#column_name`.
# 5. Override `#column_type`. It must return a type in `DB::TYPES`.
abstract class ResultSet
# :nodoc:
getter statement
def initialize(@statement : Statement)
end
# Iterates over all the rows
def each
while move_next
yield
end
end
# Closes the result set.
def close
@statement.close
end
# :nodoc:
# Ensures it executes the query
def exec
move_next
end
# Move the next row in the result.
# Return `false` if no more rows are available.
# See `#each`
abstract def move_next : Bool
# TODO def empty? : Bool, handle internally with move_next (?)
# Returns the number of columns in the result
abstract def column_count : Int32
# 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)
# 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}}?
# Reads the next column as a {{t}}.
def read(t : {{t}}.class) : {{t}}
read?({{t}}).not_nil!
end

View file

@ -1,4 +1,15 @@
module DB
# Represents a prepared query in a `Connection`.
# It should be created by `QueryMethods`.
#
# ### Note to implementors
#
# 1. Subclass `Statements`
# 2. `Statements` are created from a custom driver `Connection#prepare` method.
# 3. `#begin_parameters` is called before the parameters are set.
# 4. `#add_parameter` methods helps to support 0-based positional arguments and named arguments
# 5. After parameters are set `#perform` is called to return a `ResultSet`
# 6. `#on_close` is called to release the statement resources.
abstract class Statement
getter connection
@ -6,17 +17,19 @@ module DB
@closed = false
end
# See `QueryMethods#exec`
def exec(*args)
query(*args) do |rs|
rs.exec
end
end
# See `QueryMethods#scalar`
def scalar(*args)
scalar(Int32, *args)
end
# t in DB::TYPES
# See `QueryMethods#scalar`. `t` must be in DB::TYPES
def scalar(t, *args)
query(*args) do |rs|
rs.each do
@ -27,11 +40,12 @@ module DB
raise "no results"
end
# See `QueryMethods#scalar?`
def scalar?(*args)
scalar?(Int32, *args)
end
# t in DB::TYPES
# See `QueryMethods#scalar?`. `t` must be in DB::TYPES
def scalar?(t, *args)
query(*args) do |rs|
rs.each do
@ -42,10 +56,12 @@ module DB
raise "no results"
end
# See `QueryMethods#query`
def query(*args)
execute *args
end
# See `QueryMethods#query`
def query(*args)
execute(*args).tap do |rs|
begin
@ -62,13 +78,13 @@ module DB
private def execute(arg : Slice(UInt8))
begin_parameters
add_parameter 1, arg
add_parameter 0, arg
perform
end
private def execute(args : Enumerable)
begin_parameters
args.each_with_index(1) do |arg, index|
args.each_with_index do |arg, index|
if arg.is_a?(Hash)
arg.each do |key, value|
add_parameter key.to_s, value
@ -97,7 +113,7 @@ module DB
# close unless closed?
# end
# 1-based positional arguments
# 0-based positional arguments
protected def begin_parameters
end
protected abstract def add_parameter(index : Int32, value)