introduce unprepared statements

* rename QueryMethods#prepare to QueryMethods#build
* rename Connection#build_statement to Connection#build_prepared_statement
* add Connection#build_unprepared_statement
* add Connection #prepared and #unprepared dsl methods
This commit is contained in:
Brian J. Cardiff 2016-11-29 20:14:07 -03:00
parent 73108c169e
commit fe0ed55ef9
8 changed files with 118 additions and 39 deletions

View file

@ -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

View file

@ -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::PoolStatement)
db.prepare("query2").should be_a(DB::PoolStatement) db.build("query2").should be_a(DB::PoolStatement)
db.prepare("query3").should be_a(DB::PoolStatement) db.build("query3").should be_a(DB::PoolStatement)
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

View file

@ -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

View file

@ -1,15 +1,25 @@
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 do |cnn|
prepared = cnn.unprepared("the query")
prepared.should be_a(DB::Statement)
prepared.as(DummyDriver::DummyStatement).prepared?.should be_false
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 +29,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 +39,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 +49,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 +59,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 +69,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 +78,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 +86,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 +95,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 +104,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 +112,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 +120,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

View file

@ -11,7 +11,7 @@ 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.
# 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
@ -29,16 +29,69 @@ module DB
end end
# :nodoc: # :nodoc:
def prepare(query) : Statement def build(query) : Statement
@statements_cache.fetch(query) { build_statement(query) } # TODO add flag for default statements kind.
# configured in database overridable by connection
fetch_or_build_prepared_statement(query)
end end
abstract def build_statement(query) : Statement # :nodoc:
def fetch_or_build_prepared_statement(query)
@statements_cache.fetch(query) { build_prepared_statement(query) }
end
abstract def build_prepared_statement(query) : Statement
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
@statements_cache.clear @statements_cache.clear
@database.pool.delete self @database.pool.delete self
end end
# dsl helper to build prepared statements
# returns a value that includes `QueryMethods`
def prepared
PreparedQuery.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.new(self)
end
# Returns an unprepared `Statement` that has not been executed yet.
def unprepared(query)
unprepared.build(query)
end
struct PreparedQuery
include QueryMethods
def initialize(@connection : Connection)
end
def build(query)
@connection.fetch_or_build_prepared_statement(query)
end
end
struct UnpreparedQuery
include QueryMethods
def initialize(@connection : Connection)
end
def build(query)
@connection.build_unprepared_statement(query)
end
end
end end
end end

View file

@ -59,7 +59,7 @@ module DB
end end
# :nodoc: # :nodoc:
def prepare(query) def build(query)
@statements_cache.fetch(query) { PoolStatement.new(self, query) } @statements_cache.fetch(query) { PoolStatement.new(self, query) }
end end

View file

@ -64,7 +64,7 @@ module DB
clean_connections clean_connections
conn, existing = @db.checkout_some(@connections) conn, existing = @db.checkout_some(@connections)
@connections << WeakRef.new(conn) unless existing @connections << WeakRef.new(conn) unless existing
conn.prepare(@query) conn.build(@query)
end end
private def clean_connections private def clean_connections

View file

@ -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