mirror of
https://gitea.invidious.io/iv-org/shard-crystal-db.git
synced 2024-08-15 00:53:32 +00:00
switch to 0-pased positional arguments
add docs, many docs
This commit is contained in:
parent
a96776e336
commit
fd804dd592
9 changed files with 229 additions and 30 deletions
|
@ -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?(":")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
87
src/db/db.cr
87
src/db/db.cr
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue