mirror of
https://gitea.invidious.io/iv-org/shard-crystal-db.git
synced 2024-08-15 00:53:32 +00:00
Merge pull request #25 from crystal-lang/feature/unprepared
Support non prepared statements
This commit is contained in:
commit
825046e556
14 changed files with 376 additions and 85 deletions
|
@ -48,9 +48,13 @@ class FooDriver < DB::Driver
|
||||||
end
|
end
|
||||||
|
|
||||||
class FooConnection < DB::Connection
|
class FooConnection < DB::Connection
|
||||||
def build_statement(query)
|
def build_prepared_statement(query)
|
||||||
FooStatement.new(self)
|
FooStatement.new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_unprepared_statement(query)
|
||||||
|
raise "not implemented"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class FooStatement < DB::Statement
|
class FooStatement < DB::Statement
|
||||||
|
@ -107,9 +111,13 @@ class BarDriver < DB::Driver
|
||||||
end
|
end
|
||||||
|
|
||||||
class BarConnection < DB::Connection
|
class BarConnection < DB::Connection
|
||||||
def build_statement(query)
|
def build_prepared_statement(query)
|
||||||
BarStatement.new(self)
|
BarStatement.new(self)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_unprepared_statement(query)
|
||||||
|
raise "not implemented"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class BarStatement < DB::Statement
|
class BarStatement < DB::Statement
|
||||||
|
|
|
@ -42,24 +42,24 @@ describe DB::Database do
|
||||||
|
|
||||||
it "should allow creation of more statements than pool connections" do
|
it "should allow creation of more statements than pool connections" do
|
||||||
DB.open "dummy://localhost:1027?initial_pool_size=1&max_pool_size=2" do |db|
|
DB.open "dummy://localhost:1027?initial_pool_size=1&max_pool_size=2" do |db|
|
||||||
db.prepare("query1").should be_a(DB::PoolStatement)
|
db.build("query1").should be_a(DB::PoolPreparedStatement)
|
||||||
db.prepare("query2").should be_a(DB::PoolStatement)
|
db.build("query2").should be_a(DB::PoolPreparedStatement)
|
||||||
db.prepare("query3").should be_a(DB::PoolStatement)
|
db.build("query3").should be_a(DB::PoolPreparedStatement)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should return same statement in pool per query" do
|
it "should return same statement in pool per query" do
|
||||||
with_dummy do |db|
|
with_dummy do |db|
|
||||||
stmt = db.prepare("query1")
|
stmt = db.build("query1")
|
||||||
db.prepare("query2").should_not eq(stmt)
|
db.build("query2").should_not eq(stmt)
|
||||||
db.prepare("query1").should eq(stmt)
|
db.build("query1").should eq(stmt)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should close pool statements when closing db" do
|
it "should close pool statements when closing db" do
|
||||||
stmt = uninitialized DB::PoolStatement
|
stmt = uninitialized DB::PoolStatement
|
||||||
with_dummy do |db|
|
with_dummy do |db|
|
||||||
stmt = db.prepare("query1")
|
stmt = db.build("query1")
|
||||||
end
|
end
|
||||||
stmt.closed?.should be_true
|
stmt.closed?.should be_true
|
||||||
end
|
end
|
||||||
|
@ -97,4 +97,86 @@ describe DB::Database do
|
||||||
DummyDriver::DummyConnection.connections.size.should eq(2)
|
DummyDriver::DummyConnection.connections.size.should eq(2)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "prepared_statements connection option" do
|
||||||
|
it "defaults to true" do
|
||||||
|
with_dummy "dummy://localhost:1027" do |db|
|
||||||
|
db.prepared_statements?.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "can be set to false" do
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=false" do |db|
|
||||||
|
db.prepared_statements?.should be_false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is copied to connections (false)" do
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=false&initial_pool_size=1" do |db|
|
||||||
|
connection = DummyDriver::DummyConnection.connections.first
|
||||||
|
connection.prepared_statements?.should be_false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "is copied to connections (true)" do
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=true&initial_pool_size=1" do |db|
|
||||||
|
connection = DummyDriver::DummyConnection.connections.first
|
||||||
|
connection.prepared_statements?.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should build prepared statements if true" do
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=true" do |db|
|
||||||
|
db.build("the query").should be_a(DB::PoolPreparedStatement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should build unprepared statements if false" do
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=false" do |db|
|
||||||
|
db.build("the query").should be_a(DB::PoolUnpreparedStatement)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should be overrided by dsl" do
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=true" do |db|
|
||||||
|
stmt = db.unprepared.query("the query").statement.as(DummyDriver::DummyStatement)
|
||||||
|
stmt.prepared?.should be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
with_dummy "dummy://localhost:1027?prepared_statements=false" do |db|
|
||||||
|
stmt = db.prepared.query("the query").statement.as(DummyDriver::DummyStatement)
|
||||||
|
stmt.prepared?.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "unprepared statements in pool" do
|
||||||
|
it "creating statements should not create new connections" do
|
||||||
|
with_dummy "dummy://localhost:1027?initial_pool_size=1" do |db|
|
||||||
|
stmt1 = db.unprepared.build("query1")
|
||||||
|
stmt2 = db.unprepared.build("query2")
|
||||||
|
DummyDriver::DummyConnection.connections.size.should eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "simultaneous statements should go to different connections" do
|
||||||
|
with_dummy "dummy://localhost:1027?initial_pool_size=1" do |db|
|
||||||
|
rs1 = db.unprepared.query("query1")
|
||||||
|
rs2 = db.unprepared.query("query2")
|
||||||
|
rs1.statement.connection.should_not eq(rs2.statement.connection)
|
||||||
|
DummyDriver::DummyConnection.connections.size.should eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sequential statements should go to different connections" do
|
||||||
|
with_dummy "dummy://localhost:1027?initial_pool_size=1" do |db|
|
||||||
|
rs1 = db.unprepared.query("query1")
|
||||||
|
rs1.close
|
||||||
|
rs2 = db.unprepared.query("query2")
|
||||||
|
rs2.close
|
||||||
|
rs1.statement.connection.should eq(rs2.statement.connection)
|
||||||
|
DummyDriver::DummyConnection.connections.size.should eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -97,4 +97,19 @@ describe DB do
|
||||||
DB.open "foobar://baz"
|
DB.open "foobar://baz"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should parse boolean query string params" do
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("foo=true"), "foo", false).should be_true
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("foo=True"), "foo", false).should be_true
|
||||||
|
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("foo=false"), "foo", true).should be_false
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("foo=False"), "foo", true).should be_false
|
||||||
|
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("bar=true"), "foo", false).should be_false
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("bar=true"), "foo", true).should be_true
|
||||||
|
|
||||||
|
expect_raises(ArgumentError, %(invalid "other" value for option "foo")) do
|
||||||
|
DB.fetch_bool(HTTP::Params.parse("foo=other"), "foo", true)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,8 +22,12 @@ class DummyDriver < DB::Driver
|
||||||
@@connections.try &.clear
|
@@connections.try &.clear
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_statement(query)
|
def build_prepared_statement(query)
|
||||||
DummyStatement.new(self, query)
|
DummyStatement.new(self, query, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_unprepared_statement(query)
|
||||||
|
DummyStatement.new(self, query, false)
|
||||||
end
|
end
|
||||||
|
|
||||||
def last_insert_id : Int64
|
def last_insert_id : Int64
|
||||||
|
@ -46,7 +50,7 @@ class DummyDriver < DB::Driver
|
||||||
class DummyStatement < DB::Statement
|
class DummyStatement < DB::Statement
|
||||||
property params
|
property params
|
||||||
|
|
||||||
def initialize(connection, @query : String)
|
def initialize(connection, @query : String, @prepared : Bool)
|
||||||
@params = Hash(Int32 | String, DB::Any).new
|
@params = Hash(Int32 | String, DB::Any).new
|
||||||
super(connection)
|
super(connection)
|
||||||
end
|
end
|
||||||
|
@ -79,6 +83,10 @@ class DummyDriver < DB::Driver
|
||||||
raise "not implemented for #{value.class}"
|
raise "not implemented for #{value.class}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def prepared?
|
||||||
|
@prepared
|
||||||
|
end
|
||||||
|
|
||||||
protected def do_close
|
protected def do_close
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
@ -204,8 +212,8 @@ def with_dummy(uri : String = "dummy://host?checkout_timeout=0.5")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_dummy_connection
|
def with_dummy_connection(options = "")
|
||||||
with_dummy do |db|
|
with_dummy("dummy://host?checkout_timeout=0.5&#{options}") do |db|
|
||||||
db.using_connection do |cnn|
|
db.using_connection do |cnn|
|
||||||
yield cnn.as(DummyDriver::DummyConnection)
|
yield cnn.as(DummyDriver::DummyConnection)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,15 +1,41 @@
|
||||||
require "./spec_helper"
|
require "./spec_helper"
|
||||||
|
|
||||||
describe DB::Statement do
|
describe DB::Statement do
|
||||||
it "should prepare statements" do
|
it "should build prepared statements" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
cnn.prepare("the query").should be_a(DB::Statement)
|
prepared = cnn.prepared("the query")
|
||||||
|
prepared.should be_a(DB::Statement)
|
||||||
|
prepared.as(DummyDriver::DummyStatement).prepared?.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should build unprepared statements" do
|
||||||
|
with_dummy_connection("prepared_statements=false") do |cnn|
|
||||||
|
prepared = cnn.unprepared("the query")
|
||||||
|
prepared.should be_a(DB::Statement)
|
||||||
|
prepared.as(DummyDriver::DummyStatement).prepared?.should be_false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "prepared_statements flag" do
|
||||||
|
it "should build prepared statements if true" do
|
||||||
|
with_dummy_connection("prepared_statements=true") do |cnn|
|
||||||
|
stmt = cnn.query("the query").statement
|
||||||
|
stmt.as(DummyDriver::DummyStatement).prepared?.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should build unprepared statements if false" do
|
||||||
|
with_dummy_connection("prepared_statements=false") do |cnn|
|
||||||
|
stmt = cnn.query("the query").statement
|
||||||
|
stmt.as(DummyDriver::DummyStatement).prepared?.should be_false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should initialize positional params in query" do
|
it "should initialize positional params in query" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
|
stmt = cnn.prepared("the query").as(DummyDriver::DummyStatement)
|
||||||
stmt.query "a", 1, nil
|
stmt.query "a", 1, nil
|
||||||
stmt.params[0].should eq("a")
|
stmt.params[0].should eq("a")
|
||||||
stmt.params[1].should eq(1)
|
stmt.params[1].should eq(1)
|
||||||
|
@ -19,7 +45,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "should initialize positional params in query with array" do
|
it "should initialize positional params in query with array" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
|
stmt = cnn.prepared("the query").as(DummyDriver::DummyStatement)
|
||||||
stmt.query ["a", 1, nil]
|
stmt.query ["a", 1, nil]
|
||||||
stmt.params[0].should eq("a")
|
stmt.params[0].should eq("a")
|
||||||
stmt.params[1].should eq(1)
|
stmt.params[1].should eq(1)
|
||||||
|
@ -29,7 +55,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "should initialize positional params in exec" do
|
it "should initialize positional params in exec" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
|
stmt = cnn.prepared("the query").as(DummyDriver::DummyStatement)
|
||||||
stmt.exec "a", 1, nil
|
stmt.exec "a", 1, nil
|
||||||
stmt.params[0].should eq("a")
|
stmt.params[0].should eq("a")
|
||||||
stmt.params[1].should eq(1)
|
stmt.params[1].should eq(1)
|
||||||
|
@ -39,7 +65,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "should initialize positional params in exec with array" do
|
it "should initialize positional params in exec with array" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
|
stmt = cnn.prepared("the query").as(DummyDriver::DummyStatement)
|
||||||
stmt.exec ["a", 1, nil]
|
stmt.exec ["a", 1, nil]
|
||||||
stmt.params[0].should eq("a")
|
stmt.params[0].should eq("a")
|
||||||
stmt.params[1].should eq(1)
|
stmt.params[1].should eq(1)
|
||||||
|
@ -49,7 +75,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "should initialize positional params in scalar" do
|
it "should initialize positional params in scalar" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare("the query").as(DummyDriver::DummyStatement)
|
stmt = cnn.prepared("the query").as(DummyDriver::DummyStatement)
|
||||||
stmt.scalar "a", 1, nil
|
stmt.scalar "a", 1, nil
|
||||||
stmt.params[0].should eq("a")
|
stmt.params[0].should eq("a")
|
||||||
stmt.params[1].should eq(1)
|
stmt.params[1].should eq(1)
|
||||||
|
@ -59,7 +85,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "query with block should not close statement" do
|
it "query with block should not close statement" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare "3,4 1,2"
|
stmt = cnn.prepared "3,4 1,2"
|
||||||
stmt.query
|
stmt.query
|
||||||
stmt.closed?.should be_false
|
stmt.closed?.should be_false
|
||||||
end
|
end
|
||||||
|
@ -68,7 +94,7 @@ describe DB::Statement do
|
||||||
it "closing connection should close statement" do
|
it "closing connection should close statement" do
|
||||||
stmt = uninitialized DB::Statement
|
stmt = uninitialized DB::Statement
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare "3,4 1,2"
|
stmt = cnn.prepared "3,4 1,2"
|
||||||
stmt.query
|
stmt.query
|
||||||
end
|
end
|
||||||
stmt.closed?.should be_true
|
stmt.closed?.should be_true
|
||||||
|
@ -76,7 +102,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "query with block should not close statement" do
|
it "query with block should not close statement" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare "3,4 1,2"
|
stmt = cnn.prepared "3,4 1,2"
|
||||||
stmt.query do |rs|
|
stmt.query do |rs|
|
||||||
end
|
end
|
||||||
stmt.closed?.should be_false
|
stmt.closed?.should be_false
|
||||||
|
@ -85,7 +111,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "query should not close statement" do
|
it "query should not close statement" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare "3,4 1,2"
|
stmt = cnn.prepared "3,4 1,2"
|
||||||
stmt.query do |rs|
|
stmt.query do |rs|
|
||||||
end
|
end
|
||||||
stmt.closed?.should be_false
|
stmt.closed?.should be_false
|
||||||
|
@ -94,7 +120,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "scalar should not close statement" do
|
it "scalar should not close statement" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare "3,4 1,2"
|
stmt = cnn.prepared "3,4 1,2"
|
||||||
stmt.scalar
|
stmt.scalar
|
||||||
stmt.closed?.should be_false
|
stmt.closed?.should be_false
|
||||||
end
|
end
|
||||||
|
@ -102,7 +128,7 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "exec should not close statement" do
|
it "exec should not close statement" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
stmt = cnn.prepare "3,4 1,2"
|
stmt = cnn.prepared "3,4 1,2"
|
||||||
stmt.exec
|
stmt.exec
|
||||||
stmt.closed?.should be_false
|
stmt.closed?.should be_false
|
||||||
end
|
end
|
||||||
|
@ -110,11 +136,11 @@ describe DB::Statement do
|
||||||
|
|
||||||
it "connection should cache statements by query" do
|
it "connection should cache statements by query" do
|
||||||
with_dummy_connection do |cnn|
|
with_dummy_connection do |cnn|
|
||||||
rs = cnn.query "1, ?", 2
|
rs = cnn.prepared.query "1, ?", 2
|
||||||
stmt = rs.statement
|
stmt = rs.statement
|
||||||
rs.close
|
rs.close
|
||||||
|
|
||||||
rs = cnn.query "1, ?", 4
|
rs = cnn.prepared.query "1, ?", 4
|
||||||
rs.statement.should be(stmt)
|
rs.statement.should be(stmt)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
21
src/db.cr
21
src/db.cr
|
@ -120,17 +120,34 @@ module DB
|
||||||
private def self.build_database(uri : URI)
|
private def self.build_database(uri : URI)
|
||||||
Database.new(driver_class(uri.scheme).new, uri)
|
Database.new(driver_class(uri.scheme).new, uri)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def self.fetch_bool(params : HTTP::Params, name, default : Bool)
|
||||||
|
case (value = params[name]?).try &.downcase
|
||||||
|
when nil
|
||||||
|
default
|
||||||
|
when "true"
|
||||||
|
true
|
||||||
|
when "false"
|
||||||
|
false
|
||||||
|
else
|
||||||
|
raise ArgumentError.new(%(invalid "#{value}" value for option "#{name}"))
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "./db/pool"
|
require "./db/pool"
|
||||||
require "./db/string_key_cache"
|
require "./db/string_key_cache"
|
||||||
require "./db/query_methods"
|
require "./db/query_methods"
|
||||||
|
require "./db/session_methods"
|
||||||
require "./db/disposable"
|
require "./db/disposable"
|
||||||
require "./db/database"
|
|
||||||
require "./db/driver"
|
require "./db/driver"
|
||||||
require "./db/connection"
|
|
||||||
require "./db/statement"
|
require "./db/statement"
|
||||||
|
require "./db/connection"
|
||||||
require "./db/pool_statement"
|
require "./db/pool_statement"
|
||||||
|
require "./db/database"
|
||||||
|
require "./db/pool_prepared_statement"
|
||||||
|
require "./db/pool_unprepared_statement"
|
||||||
require "./db/result_set"
|
require "./db/result_set"
|
||||||
require "./db/error"
|
require "./db/error"
|
||||||
require "./db/mapping"
|
require "./db/mapping"
|
||||||
|
|
|
@ -11,7 +11,8 @@ module DB
|
||||||
#
|
#
|
||||||
# The connection must be initialized in `#initialize` and closed in `#do_close`.
|
# The connection must be initialized in `#initialize` and closed in `#do_close`.
|
||||||
#
|
#
|
||||||
# Override `#build_statement` method in order to return a prepared `Statement` to allow querying.
|
# Override `#build_prepared_statement` method in order to return a prepared `Statement` to allow querying.
|
||||||
|
# Override `#build_unprepared_statement` method in order to return a unprepared `Statement` to allow querying.
|
||||||
# See also `Statement` to define how the statements are executed.
|
# See also `Statement` to define how the statements are executed.
|
||||||
#
|
#
|
||||||
# If at any give moment the connection is lost a DB::ConnectionLost should be raised. This will
|
# If at any give moment the connection is lost a DB::ConnectionLost should be raised. This will
|
||||||
|
@ -19,21 +20,27 @@ module DB
|
||||||
#
|
#
|
||||||
abstract class Connection
|
abstract class Connection
|
||||||
include Disposable
|
include Disposable
|
||||||
include QueryMethods
|
include SessionMethods(Connection, Statement)
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
getter database
|
getter database
|
||||||
@statements_cache = StringKeyCache(Statement).new
|
@statements_cache = StringKeyCache(Statement).new
|
||||||
|
getter? prepared_statements : Bool
|
||||||
|
|
||||||
def initialize(@database : Database)
|
def initialize(@database : Database)
|
||||||
|
@prepared_statements = @database.prepared_statements?
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def prepare(query) : Statement
|
def fetch_or_build_prepared_statement(query)
|
||||||
@statements_cache.fetch(query) { build_statement(query) }
|
@statements_cache.fetch(query) { build_prepared_statement(query) }
|
||||||
end
|
end
|
||||||
|
|
||||||
abstract def build_statement(query) : Statement
|
# :nodoc:
|
||||||
|
abstract def build_prepared_statement(query) : Statement
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
abstract def build_unprepared_statement(query) : Statement
|
||||||
|
|
||||||
protected def do_close
|
protected def do_close
|
||||||
@statements_cache.each_value &.close
|
@statements_cache.each_value &.close
|
||||||
|
|
|
@ -13,10 +13,17 @@ module DB
|
||||||
# - retry_attempts (default 1)
|
# - retry_attempts (default 1)
|
||||||
# - retry_delay (in seconds, default 1.0)
|
# - retry_delay (in seconds, default 1.0)
|
||||||
#
|
#
|
||||||
|
# When querying a database prepared statements are used by default.
|
||||||
|
# This can be changed from the `prepared_statements` URI parameter:
|
||||||
|
#
|
||||||
|
# - prepared_statements = `true`|`false` (default `true`)
|
||||||
|
#
|
||||||
# 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.
|
# Refer to `QueryMethods` and `SessionMethods` for documentation about querying the database.
|
||||||
class Database
|
class Database
|
||||||
|
include SessionMethods(Database, PoolStatement)
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
getter driver
|
getter driver
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
|
@ -25,13 +32,16 @@ module DB
|
||||||
# Returns the uri with the connection settings to the database
|
# Returns the uri with the connection settings to the database
|
||||||
getter uri
|
getter uri
|
||||||
|
|
||||||
|
getter? prepared_statements : Bool
|
||||||
|
|
||||||
@pool : Pool(Connection)
|
@pool : Pool(Connection)
|
||||||
@setup_connection : Connection -> Nil
|
@setup_connection : Connection -> Nil
|
||||||
@statements_cache = StringKeyCache(PoolStatement).new
|
@statements_cache = StringKeyCache(PoolPreparedStatement).new
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def initialize(@driver : Driver, @uri : URI)
|
def initialize(@driver : Driver, @uri : URI)
|
||||||
params = HTTP::Params.parse(uri.query || "")
|
params = HTTP::Params.parse(uri.query || "")
|
||||||
|
@prepared_statements = DB.fetch_bool(params, "prepared_statements", true)
|
||||||
pool_options = @driver.connection_pool_options(params)
|
pool_options = @driver.connection_pool_options(params)
|
||||||
|
|
||||||
@setup_connection = ->(conn : Connection) {}
|
@setup_connection = ->(conn : Connection) {}
|
||||||
|
@ -59,8 +69,18 @@ module DB
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
def prepare(query)
|
def fetch_or_build_prepared_statement(query)
|
||||||
@statements_cache.fetch(query) { PoolStatement.new(self, query) }
|
@statements_cache.fetch(query) { build_prepared_statement(query) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def build_prepared_statement(query)
|
||||||
|
PoolPreparedStatement.new(self, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def build_unprepared_statement(query)
|
||||||
|
PoolUnpreparedStatement.new(self, query)
|
||||||
end
|
end
|
||||||
|
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
|
@ -91,7 +111,5 @@ module DB
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
include QueryMethods
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
50
src/db/pool_prepared_statement.cr
Normal file
50
src/db/pool_prepared_statement.cr
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
module DB
|
||||||
|
# Represents a statement to be executed in any of the connections
|
||||||
|
# of the pool. The statement is not be executed in a prepared fashion.
|
||||||
|
# The execution of the statement is retried according to the pool configuration.
|
||||||
|
#
|
||||||
|
# See `PoolStatement`
|
||||||
|
class PoolPreparedStatement < PoolStatement
|
||||||
|
# connections where the statement was prepared
|
||||||
|
@connections = Set(WeakRef(Connection)).new
|
||||||
|
|
||||||
|
def initialize(db : Database, query : String)
|
||||||
|
super
|
||||||
|
# Prepares a statement on some connection
|
||||||
|
# otherwise the preparation is delayed until the first execution.
|
||||||
|
# After the first initialization the connection must be released
|
||||||
|
# it will be checked out when executing it.
|
||||||
|
statement_with_retry &.release_connection
|
||||||
|
# TODO use a round-robin selection in the pool so multiple sequentially
|
||||||
|
# initialized statements are assigned to different connections.
|
||||||
|
end
|
||||||
|
|
||||||
|
protected def do_close
|
||||||
|
# TODO close all statements on all connections.
|
||||||
|
# currently statements are closed when the connection is closed.
|
||||||
|
|
||||||
|
# WHAT-IF the connection is busy? Should each statement be able to
|
||||||
|
# deallocate itself when the connection is free.
|
||||||
|
@connections.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
# builds a statement over a real connection
|
||||||
|
# the conneciton is registered in `@connections`
|
||||||
|
private def build_statement
|
||||||
|
clean_connections
|
||||||
|
conn, existing = @db.checkout_some(@connections)
|
||||||
|
@connections << WeakRef.new(conn) unless existing
|
||||||
|
conn.prepared.build(@query)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def clean_connections
|
||||||
|
# remove disposed or closed connections
|
||||||
|
@connections.each do |ref|
|
||||||
|
conn = ref.target
|
||||||
|
if !conn || conn.closed?
|
||||||
|
@connections.delete ref
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -3,29 +3,10 @@ module DB
|
||||||
# a statement from the DB needs to be able to represent a statement in any
|
# a statement from the DB needs to be able to represent a statement in any
|
||||||
# of the connections of the pool. Otherwise the user will need to deal with
|
# of the connections of the pool. Otherwise the user will need to deal with
|
||||||
# actual connections in some point.
|
# actual connections in some point.
|
||||||
class PoolStatement
|
abstract class PoolStatement
|
||||||
include StatementMethods
|
include StatementMethods
|
||||||
|
|
||||||
# connections where the statement was prepared
|
|
||||||
@connections = Set(WeakRef(Connection)).new
|
|
||||||
|
|
||||||
def initialize(@db : Database, @query : String)
|
def initialize(@db : Database, @query : String)
|
||||||
# Prepares a statement on some connection
|
|
||||||
# otherwise the preparation is delayed until the first execution.
|
|
||||||
# After the first initialization the connection must be released
|
|
||||||
# it will be checked out when executing it.
|
|
||||||
statement_with_retry &.release_connection
|
|
||||||
# TODO use a round-robin selection in the pool so multiple sequentially
|
|
||||||
# initialized statements are assigned to different connections.
|
|
||||||
end
|
|
||||||
|
|
||||||
protected def do_close
|
|
||||||
# TODO close all statements on all connections.
|
|
||||||
# currently statements are closed when the connection is closed.
|
|
||||||
|
|
||||||
# WHAT-IF the connection is busy? Should each statement be able to
|
|
||||||
# deallocate itself when the connection is free.
|
|
||||||
@connections.clear
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# See `QueryMethods#exec`
|
# See `QueryMethods#exec`
|
||||||
|
@ -60,22 +41,7 @@ module DB
|
||||||
|
|
||||||
# builds a statement over a real connection
|
# builds a statement over a real connection
|
||||||
# the conneciton is registered in `@connections`
|
# the conneciton is registered in `@connections`
|
||||||
private def build_statement
|
private abstract def build_statement : Statement
|
||||||
clean_connections
|
|
||||||
conn, existing = @db.checkout_some(@connections)
|
|
||||||
@connections << WeakRef.new(conn) unless existing
|
|
||||||
conn.prepare(@query)
|
|
||||||
end
|
|
||||||
|
|
||||||
private def clean_connections
|
|
||||||
# remove disposed or closed connections
|
|
||||||
@connections.each do |ref|
|
|
||||||
conn = ref.target
|
|
||||||
if !conn || conn.closed?
|
|
||||||
@connections.delete ref
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private def statement_with_retry
|
private def statement_with_retry
|
||||||
@db.retry do
|
@db.retry do
|
||||||
|
|
21
src/db/pool_unprepared_statement.cr
Normal file
21
src/db/pool_unprepared_statement.cr
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
module DB
|
||||||
|
# Represents a statement to be executed in any of the connections
|
||||||
|
# of the pool. The statement is not be executed in a non prepared fashion.
|
||||||
|
# The execution of the statement is retried according to the pool configuration.
|
||||||
|
#
|
||||||
|
# See `PoolStatement`
|
||||||
|
class PoolUnpreparedStatement < PoolStatement
|
||||||
|
def initialize(db : Database, query : String)
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
protected def do_close
|
||||||
|
# unprepared statements do not need to be release in each connection
|
||||||
|
end
|
||||||
|
|
||||||
|
# builds a statement over a real connection
|
||||||
|
private def build_statement
|
||||||
|
@db.pool.checkout.unprepared.build(@query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,11 +15,11 @@ module DB
|
||||||
#
|
#
|
||||||
# Convention of mapping how arguments are mapped to the query depends on each driver.
|
# Convention of mapping how arguments are mapped to the query depends on each driver.
|
||||||
#
|
#
|
||||||
# Including `QueryMethods` requires a `prepare(query) : Statement` method that is not expected
|
# Including `QueryMethods` requires a `build(query) : Statement` method that is not expected
|
||||||
# to be called directly.
|
# to be called directly.
|
||||||
module QueryMethods
|
module QueryMethods
|
||||||
# :nodoc:
|
# :nodoc:
|
||||||
abstract def prepare(query) : Statement
|
abstract def build(query) : Statement
|
||||||
|
|
||||||
# Executes a *query* and returns a `ResultSet` with the results.
|
# Executes a *query* and returns a `ResultSet` with the results.
|
||||||
# The `ResultSet` must be closed manually.
|
# The `ResultSet` must be closed manually.
|
||||||
|
@ -35,7 +35,7 @@ module DB
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
def query(query, *args)
|
def query(query, *args)
|
||||||
prepare(query).query(*args)
|
build(query).query(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Executes a *query* and yields a `ResultSet` with the results.
|
# Executes a *query* and yields a `ResultSet` with the results.
|
||||||
|
@ -49,7 +49,7 @@ module DB
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
def query(query, *args)
|
def query(query, *args)
|
||||||
# CHECK prepare(query).query(*args, &block)
|
# CHECK build(query).query(*args, &block)
|
||||||
rs = query(query, *args)
|
rs = query(query, *args)
|
||||||
yield rs ensure rs.close
|
yield rs ensure rs.close
|
||||||
end
|
end
|
||||||
|
@ -200,13 +200,13 @@ module DB
|
||||||
|
|
||||||
# Performs the `query` and returns an `ExecResult`
|
# Performs the `query` and returns an `ExecResult`
|
||||||
def exec(query, *args)
|
def exec(query, *args)
|
||||||
prepare(query).exec(*args)
|
build(query).exec(*args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Performs the `query` and returns a single scalar value
|
# Performs the `query` and returns a single scalar value
|
||||||
# puts db.scalar("SELECT MAX(name)").as(String) # => (a String)
|
# puts db.scalar("SELECT MAX(name)").as(String) # => (a String)
|
||||||
def scalar(query, *args)
|
def scalar(query, *args)
|
||||||
prepare(query).scalar(*args)
|
build(query).scalar(*args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
73
src/db/session_methods.cr
Normal file
73
src/db/session_methods.cr
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
module DB
|
||||||
|
# Methods that are shared accross session like objects:
|
||||||
|
# - Database
|
||||||
|
# - Connection
|
||||||
|
#
|
||||||
|
# Classes that includes this module are able to execute
|
||||||
|
# queries and statements in both prepared and unprepared fashion.
|
||||||
|
#
|
||||||
|
# This module serves for dsl reuse over session like objects.
|
||||||
|
module SessionMethods(Session, Stmt)
|
||||||
|
include QueryMethods
|
||||||
|
|
||||||
|
# Returns whether by default the statements should
|
||||||
|
# be prepared or not.
|
||||||
|
abstract def prepared_statements? : Bool
|
||||||
|
|
||||||
|
abstract def fetch_or_build_prepared_statement(query) : Stmt
|
||||||
|
|
||||||
|
abstract def build_unprepared_statement(query) : Stmt
|
||||||
|
|
||||||
|
def build(query) : Stmt
|
||||||
|
if prepared_statements?
|
||||||
|
fetch_or_build_prepared_statement(query)
|
||||||
|
else
|
||||||
|
build_unprepared_statement(query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# dsl helper to build prepared statements
|
||||||
|
# returns a value that includes `QueryMethods`
|
||||||
|
def prepared
|
||||||
|
PreparedQuery(Session, Stmt).new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a prepared `Statement` that has not been executed yet.
|
||||||
|
def prepared(query)
|
||||||
|
prepared.build(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
# dsl helper to build unprepared statements
|
||||||
|
# returns a value that includes `QueryMethods`
|
||||||
|
def unprepared
|
||||||
|
UnpreparedQuery(Session, Stmt).new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns an unprepared `Statement` that has not been executed yet.
|
||||||
|
def unprepared(query)
|
||||||
|
unprepared.build(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
struct PreparedQuery(Session, Stmt)
|
||||||
|
include QueryMethods
|
||||||
|
|
||||||
|
def initialize(@session : Session)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(query) : Stmt
|
||||||
|
@session.fetch_or_build_prepared_statement(query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
struct UnpreparedQuery(Session, Stmt)
|
||||||
|
include QueryMethods
|
||||||
|
|
||||||
|
def initialize(@session : Session)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build(query) : Stmt
|
||||||
|
@session.build_unprepared_statement(query)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -39,7 +39,7 @@ module DB
|
||||||
abstract def query(args : Array) : ResultSet
|
abstract def query(args : Array) : ResultSet
|
||||||
end
|
end
|
||||||
|
|
||||||
# Represents a prepared query in a `Connection`.
|
# Represents a query in a `Connection`.
|
||||||
# It should be created by `QueryMethods`.
|
# It should be created by `QueryMethods`.
|
||||||
#
|
#
|
||||||
# ### Note to implementors
|
# ### Note to implementors
|
||||||
|
|
Loading…
Reference in a new issue