From d55a34e85155bafac4336e82f4065525d35bf91d Mon Sep 17 00:00:00 2001 From: Arthur Poulet Date: Fri, 29 Dec 2017 23:32:25 +0100 Subject: [PATCH] Allow query results to be read as named tuples directly (#56) --- spec/dummy_driver_spec.cr | 21 ++++++++++++++++++ src/db/query_methods.cr | 46 +++++++++++++++++++++++++++++++++++++++ src/db/result_set.cr | 15 +++++++++++++ 3 files changed, 82 insertions(+) diff --git a/spec/dummy_driver_spec.cr b/spec/dummy_driver_spec.cr index f82f12e..a853786 100644 --- a/spec/dummy_driver_spec.cr +++ b/spec/dummy_driver_spec.cr @@ -138,6 +138,12 @@ describe DummyDriver do end end + it "with a named tuple" do + with_dummy do |db| + db.query_one("3,4", as: {a: Int64, b: Int64}).should eq({a: 3i64, b: 4i64}) + end + end + it "with as, just one" do with_dummy do |db| db.query_one("3", as: Int64).should eq(3i64) @@ -176,6 +182,14 @@ describe DummyDriver do end end + it "with as" do + with_dummy do |db| + value = db.query_one?("3,4", as: {a: Int64, b: Int64}) + value.should be_a(NamedTuple(a: Int64, b: Int64)?) + value.should eq({a: 3i64, b: 4i64}) + end + end + it "with as, just one" do with_dummy do |db| value = db.query_one?("3", as: Int64) @@ -200,6 +214,13 @@ describe DummyDriver do end end + it "queries with a named tuple" do + with_dummy do |db| + ary = db.query_all "3,4 1,2", as: {a: Int64, b: Int64} + ary.should eq([{a: 3, b: 4}, {a: 1, b: 2}]) + end + end + it "queries with as, just one" do with_dummy do |db| ary = db.query_all "3 1", as: Int64 diff --git a/src/db/query_methods.cr b/src/db/query_methods.cr index f2f160a..fa1c5df 100644 --- a/src/db/query_methods.cr +++ b/src/db/query_methods.cr @@ -88,6 +88,21 @@ module DB 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, as types : NamedTuple) + query_one(query, *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*. # @@ -141,6 +156,24 @@ module DB 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, as types : NamedTuple) + query_one(query, *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*. # @@ -184,6 +217,19 @@ module DB 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, as types : NamedTuple) + query_all(query, *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*. # diff --git a/src/db/result_set.cr b/src/db/result_set.cr index 0bbffcf..b2bd722 100644 --- a/src/db/result_set.cr +++ b/src/db/result_set.cr @@ -89,6 +89,11 @@ module DB internal_read(*types) end + # Reads the next columns and returns a named tuple of the values. + def read(**types : Class) + internal_read(**types) + end + private def internal_read(*types : *T) forall T {% begin %} Tuple.new( @@ -99,6 +104,16 @@ module DB {% end %} end + private def internal_read(**types : **T) forall T + {% begin %} + NamedTuple.new( + {% for name, type in T %} + {{ name }}: read({{type.instance}}), + {% end %} + ) + {% end %} + end + # def read_blob # yield ... io .... # end