Skip to content

Commit

Permalink
Adds typed exec
Browse files Browse the repository at this point in the history
  • Loading branch information
will committed May 16, 2015
1 parent cafe76a commit cafe8d5
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 36 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.crystal
test*.cr
26 changes: 26 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,34 @@
A Postgres driver for Crystal [![Build Status](https://travis-ci.org/will/crystal-pg.svg?branch=master)](https://travis-ci.org/will/crystal-pg)

## usage

### connecting

``` crystal
DB = PG.connect("postgres://...")
```

### typed querying

The preferred way to send queries is to send a tuple of the types you expect back along with the query. `#rows` will then be an array of tuples with each element properly casted. You can also use parameterized queries for injection-safe server-side interpolation.

``` crystal
result = DB.exec({Int32, String}, "select id, email from users")
result.fields #=> [PG::Result::Field, PG::Result::Field]
result.rows #=> [{1, "will@example.com"}], …]
result.to_hash #=> [{"field1" => value, …}, …]
result = DB.exec({String}, "select $1::text || ' ' || $2::text", ["hello", "world"])
result.rows #=> [{"hello world"}]
```

Out of the box, crystal-pg supports 1-32 types. If you need more, you can reopen `PG::Result` and use the `generate_gather_rows` macro. If your field can return nil, you should use `PG::Nilable{{Type}}` for the type, which is a union of the type and `Nil`.

### untyped querying

If you do not know the types beforehand you can omit them. However you will get back an array of arrays of PGValue. Since it is a union type of amost every type, you will probably have to manually cast later on in your program.

``` crystal
result = DB.exec("select * from table")
result.fields #=> [PG::Result::Field, …]
result.rows #=> [[value, …], …]
Expand Down
32 changes: 27 additions & 5 deletions spec/pg/connection_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ describe PG::Connection, "#initialize" do
end
end

describe PG::Connection, "#exec" do
describe PG::Connection, "#exec untyped" do
it "returns a Result" do
res = DB.exec("select 1")
res.class.should eq(PG::Result)
res.class.should eq(PG::Result(Array(PG::PGValue)))
end

it "raises on bad queries" do
Expand All @@ -20,15 +20,37 @@ describe PG::Connection, "#exec" do

it "returns a Result when create table" do
res = DB.exec("create table if not exists test()")
res.class.should eq(PG::Result)
res.class.should eq(PG::Result(Array(PG::PGValue)))
DB.exec("drop table test")
end
end

describe PG::Connection, "#exec with params" do
describe PG::Connection, "#exec typed" do
it "returns a Result" do
res = DB.exec({Int32}, "select 1")
res.class.should eq( PG::Result({Int32.class}) )
end

it "raises on bad queries" do
expect_raises(PG::ResultError) { DB.exec({Int32}, "select nocolumn from notable") }
end
end

describe PG::Connection, "#exec typed with params" do
it "returns a Result" do
res = DB.exec({Float64}, "select $1::float * $2::float ", [3.4, -2])
res.class.should eq( PG::Result({Float64.class}) )
end

it "raises on bad queries" do
expect_raises(PG::ResultError) { DB.exec("select $1::text from notable", ["hello"]) }
end
end

describe PG::Connection, "#exec untyped with params" do
it "returns a Result" do
res = DB.exec("select $1::text, $2::text, $3::text", ["hello", "", "world"])
res.class.should eq(PG::Result)
res.class.should eq(PG::Result(Array(PG::PGValue)))
end

it "raises on bad queries" do
Expand Down
11 changes: 9 additions & 2 deletions spec/pg/result_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,15 @@ describe PG::Result, "#rows" do
end

it "can handle several types and several rows" do
rows = DB.exec("select 'a', 'b', true union all select '', null, false").rows
rows.should eq([["a", "b", true], ["", nil, false]])
rows = DB.exec(
{String, PG::NilableString, Bool, Int32},
"select 'a', 'b', true, 22 union all select '', null, false, 53"
).rows
rows.should eq([{"a", "b", true, 22},
{"", nil, false, 53}])
[rows[0][0], rows[1][0]].map(&.length).sum.should eq(1)
(rows[0][2] && !rows[1][2]).should be_true
(rows[0][3] < rows[1][3]).should be_true
end

# name, sql, result
Expand Down
6 changes: 6 additions & 0 deletions src/core_ext/class.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Class
macro def as_cast(other) : self
other as self
end
end

1 change: 1 addition & 0 deletions src/pg.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require "./core_ext/*"
require "./pg/*"

module PG
Expand Down
48 changes: 30 additions & 18 deletions src/pg/connection.cr
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,37 @@ module PG
end
end

def exec(query)
exec(query, [] of PG::PGValue)
def exec(query : String)
exec([] of PG::PGValue, query, [] of PG::PGValue)
end

def exec(query, params)
#res = LibPQ.exec(raw, query)
def exec(query : String, params)
exec([] of PG::PGValue, query, params)
end

def exec(types, query : String)
exec(types, query, [] of PG::PGValue)
end

def exec(types, query : String, params)
Result.new(types, libpq_exec(query, params))
end

def finish
LibPQ.finish(raw)
@raw = nil
end

def version
query = "SELECT ver[1]::int AS major, ver[2]::int AS minor, ver[3]::int AS patch
FROM regexp_matches(version(), 'PostgreSQL (\\d+)\\.(\\d+)\\.(\\d+)') ver"
version = exec({Int32, Int32, Int32}, query).rows.first
{major: version[0], minor: version[1], patch: version[2]}
end

private getter raw

private def libpq_exec(query, params)
n_params = params.size
param_types = Pointer(LibPQ::Int).null # have server infer types
param_values = params.map { |v| simple_encode(v) }
Expand All @@ -34,22 +59,9 @@ module PG
result_format
)
check_status(res)
Result.new(res)
end

def finish
LibPQ.finish(raw)
@raw = nil
res
end

def version
query = "SELECT ver[1]::int AS major, ver[2]::int AS minor, ver[3]::int AS patch
FROM regexp_matches(version(), 'PostgreSQL (\\d+)\\.(\\d+)\\.(\\d+)') ver"
version = exec(query).rows.first.map {|i| i as Int32 }
{:major => version[0], :minor => version[1], :patch => version[2]}
end

private getter raw

private def check_status(res)
status = LibPQ.result_status(res)
Expand Down
13 changes: 13 additions & 0 deletions src/pg/nillable_classes.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module PG
module Nilable
macro generate(list)
{% for k in list %}
alias Nilable{{k}} = Nil | {{k}}
{% end %}
end

generate [String, Int32, Float64, Bool, Time]
end

include Nilable
end
30 changes: 19 additions & 11 deletions src/pg/result.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module PG
class Result
class Result(T)

struct Field
property name
Expand All @@ -20,9 +20,9 @@ module PG

getter fields
getter rows
def initialize(@res)
def initialize(types : T, @res)
@fields = gather_fields
@rows = gather_rows
@rows = gather_rows(types)
clear_res
end

Expand Down Expand Up @@ -60,18 +60,26 @@ module PG
fds
end

private def gather_rows
rws = Array( Array(PGValue) ).new(ntuples)
ntuples.times do |i|
rws << Array(PGValue).new(nfields)
nfields.times do |j|
val = decode_value(res, i, j)
rws[i] << val
private def gather_rows(types : Array(PGValue))
Array.new(ntuples) do |i|
Array.new(nfields) do |j|
decode_value(res, i, j)
end
end
rws
end

macro generate_gather_rows(from, to)
{% for n in (from..to) %}
private def gather_rows(types : Tuple({% for i in (1...n) %}Class, {%end%} Class))
Array.new(ntuples) do |i|
{ {% for j in (0...n) %} types[{{j}}].as_cast( decode_value(res,i,{{j}}) ), {% end %} }
end
end
{% end %}
end

generate_gather_rows(1,32)

private def decode_value(res, row, col)
val_ptr = LibPQ.getvalue(res, row, col)
if val_ptr.value == 0 && LibPQ.getisnull(res, row, col)
Expand Down

0 comments on commit cafe8d5

Please sign in to comment.