diff --git a/spec/custom_drivers_types_spec.cr b/spec/custom_drivers_types_spec.cr index 21d1668..0b25a6a 100644 --- a/spec/custom_drivers_types_spec.cr +++ b/spec/custom_drivers_types_spec.cr @@ -227,14 +227,14 @@ describe DB do db.query "query", 1, "string" { } db.query("query", Bytes.new(4)) { } db.query("query", 1, "string", FooValue.new(5)) { } - db.query "query", [1, "string", FooValue.new(5)] { } + db.query "query", args: [1, "string", FooValue.new(5)] { } db.query("query").close db.query("query", 1).close db.query("query", 1, "string").close db.query("query", Bytes.new(4)).close db.query("query", 1, "string", FooValue.new(5)).close - db.query("query", [1, "string", FooValue.new(5)]).close + db.query("query", args: [1, "string", FooValue.new(5)]).close end DB.open("bar://host") do |db| @@ -244,14 +244,14 @@ describe DB do db.query "query", 1, "string" { } db.query("query", Bytes.new(4)) { } db.query("query", 1, "string", BarValue.new(5)) { } - db.query "query", [1, "string", BarValue.new(5)] { } + db.query "query", args: [1, "string", BarValue.new(5)] { } db.query("query").close db.query("query", 1).close db.query("query", 1, "string").close db.query("query", Bytes.new(4)).close db.query("query", 1, "string", BarValue.new(5)).close - db.query("query", [1, "string", BarValue.new(5)]).close + db.query("query", args: [1, "string", BarValue.new(5)]).close end end @@ -263,7 +263,7 @@ describe DB do db.exec("query", 1, "string") db.exec("query", Bytes.new(4)) db.exec("query", 1, "string", FooValue.new(5)) - db.exec("query", [1, "string", FooValue.new(5)]) + db.exec("query", args: [1, "string", FooValue.new(5)]) end DB.open("bar://host") do |db| @@ -273,20 +273,20 @@ describe DB do db.exec("query", 1, "string") db.exec("query", Bytes.new(4)) db.exec("query", 1, "string", BarValue.new(5)) - db.exec("query", [1, "string", BarValue.new(5)]) + db.exec("query", args: [1, "string", BarValue.new(5)]) end end it "Foo and Bar drivers should not implement each other params" do DB.open("foo://host") do |db| expect_raises Exception, "FooDriver::FooStatement does not support BarValue params" do - db.exec("query", [BarValue.new(5)]) + db.exec("query", args: [BarValue.new(5)]) end end DB.open("bar://host") do |db| expect_raises Exception, "BarDriver::BarStatement does not support FooValue params" do - db.exec("query", [FooValue.new(5)]) + db.exec("query", args: [FooValue.new(5)]) end end end diff --git a/src/db/pool_statement.cr b/src/db/pool_statement.cr index 668ce2b..9df1027 100644 --- a/src/db/pool_statement.cr +++ b/src/db/pool_statement.cr @@ -20,8 +20,8 @@ module DB end # See `QueryMethods#exec` - def exec(args : Array) : ExecResult - statement_with_retry &.exec(args) + def exec(*, args : Array) : ExecResult + statement_with_retry &.exec(args: args) end # See `QueryMethods#query` @@ -35,8 +35,8 @@ module DB end # See `QueryMethods#query` - def query(args : Array) : ResultSet - statement_with_retry &.query(args) + def query(*, args : Array) : ResultSet + statement_with_retry &.query(args: args) end # See `QueryMethods#scalar` diff --git a/src/db/query_methods.cr b/src/db/query_methods.cr index 9676256..93bf993 100644 --- a/src/db/query_methods.cr +++ b/src/db/query_methods.cr @@ -271,5 +271,256 @@ module DB def scalar(query, *args) build(query).scalar(*args) end + + # 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 : Array) + build(query).query(args: args) + end + + # 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 : Array) + # CHECK build(query).query(args: args, &block) + rs = query(query, args: 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 : Array, &block : ResultSet -> U) : U forall U + query(query, args: 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 : Array, as types : Tuple) + query_one(query, args: args) do |rs| + rs.read(*types) + end + end + + # Executes a *query* that expects a single row and returns it + # as a named tuple of the given *types* (the keys of the named tuple + # are not necessarily the column names). + # + # 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: {name: String, age: Int32} + # ``` + def query_one(query, *, args : Array, as types : NamedTuple) + query_one(query, args: 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 : Array, as type : Class) + query_one(query, args: 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 : Array, &block : ResultSet -> U) : U? forall U + query(query, args: 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 : Array, as types : Tuple) + query_one?(query, args: args) do |rs| + rs.read(*types) + end + end + + # Executes a *query* that expects a single row and returns it + # as a named tuple of the given *types* (the keys of the named tuple + # are not necessarily the column names). + # + # 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: {age: String, name: Int32} + # typeof(result) # => NamedTuple(age: String, name: Int32) | Nil + # ``` + def query_one?(query, *, args : Array, as types : NamedTuple) + query_one?(query, args: 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 : Array, as type : Class) + query_one?(query, args: 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 : Array, &block : ResultSet -> U) : Array(U) forall U + ary = [] of U + query_each(query, args: args) do |rs| + ary.push(yield rs) + 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 contacts", as: {String, Int32} + # ``` + def query_all(query, *, args : Array, as types : Tuple) + query_all(query, args: args) do |rs| + rs.read(*types) + end + end + + # Executes a *query* and returns an array where each row is + # read as a named tuple of the given *types* (the keys of the named tuple + # are not necessarily the column names). + # + # ``` + # contacts = db.query_all "select name, age from contacts", as: {name: String, age: Int32} + # ``` + def query_all(query, *, args : Array, as types : NamedTuple) + query_all(query, args: args) do |rs| + rs.read(**types) + end + end + + # Executes a *query* and returns an array where the + # value of each row is read as the given *type*. + # + # ``` + # names = db.query_all "select name from contacts", as: String + # ``` + def query_all(query, *, args : Array, as type : Class) + query_all(query, args: args) do |rs| + rs.read(type) + end + end + + # Executes a *query* and yields the `ResultSet` once per each row. + # The `ResultSet` is closed automatically. + # + # ``` + # db.query_each "select name from contacts" do |rs| + # puts rs.read(String) + # end + # ``` + def query_each(query, *, args : Array) + query(query, args: args) do |rs| + rs.each do + yield rs + end + end + end + + # Performs the `query` and returns an `ExecResult` + def exec(query, *, args : Array) + build(query).exec(args: args) + end + + # Performs the `query` and returns a single scalar value + # + # ``` + # puts db.scalar("SELECT MAX(name)").as(String) # => (a String) + # ``` + def scalar(query, *, args : Array) + build(query).scalar(args: args) + end end end