rdo-postgres 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +20 -0
- data/.rspec +1 -0
- data/Gemfile +9 -0
- data/LICENSE +22 -0
- data/README.md +94 -0
- data/Rakefile +14 -0
- data/ext/rdo_postgres/casts.c +134 -0
- data/ext/rdo_postgres/casts.h +16 -0
- data/ext/rdo_postgres/driver.c +169 -0
- data/ext/rdo_postgres/driver.h +20 -0
- data/ext/rdo_postgres/extconf.rb +39 -0
- data/ext/rdo_postgres/macros.h +180 -0
- data/ext/rdo_postgres/params.c +106 -0
- data/ext/rdo_postgres/params.h +15 -0
- data/ext/rdo_postgres/rdo_postgres.c +18 -0
- data/ext/rdo_postgres/statements.c +237 -0
- data/ext/rdo_postgres/statements.h +15 -0
- data/ext/rdo_postgres/tuples.c +85 -0
- data/ext/rdo_postgres/tuples.h +20 -0
- data/lib/rdo-postgres.rb +1 -0
- data/lib/rdo/postgres.rb +16 -0
- data/lib/rdo/postgres/driver.rb +68 -0
- data/lib/rdo/postgres/version.rb +12 -0
- data/rdo-postgres.gemspec +23 -0
- data/spec/postgres/bind_params_spec.rb +902 -0
- data/spec/postgres/driver_spec.rb +285 -0
- data/spec/postgres/type_cast_spec.rb +272 -0
- data/spec/spec_helper.rb +10 -0
- metadata +126 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright © 2012 Chris Corbyn.
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
# RDO PostgreSQL Driver
|
2
|
+
|
3
|
+
This is the PostgreSQL driver for [RDO—Ruby Data Objects]
|
4
|
+
(https://github.com/d11wtq/rdo).
|
5
|
+
|
6
|
+
Refer to the RDO project [README](https://github.com/d11wtq/rdo) for usage
|
7
|
+
information.
|
8
|
+
|
9
|
+
This driver cannot be used with Postgres versions older than 7.4, since the
|
10
|
+
protocol has changed and this driver takes advantage of newer protocol (3.0)
|
11
|
+
features.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Via rubygems:
|
16
|
+
|
17
|
+
$ gem install rdo-postgres
|
18
|
+
|
19
|
+
Or add the following line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem "rdo-postgres"
|
22
|
+
|
23
|
+
And install with Bundler:
|
24
|
+
|
25
|
+
$ bundle install
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
The registered URI schemes are postgres:// and postgresql://
|
30
|
+
|
31
|
+
``` ruby
|
32
|
+
require "rdo"
|
33
|
+
require "rdo-postgres"
|
34
|
+
|
35
|
+
conn = RDO.connect("postgres://user:pass@localhost/dbname?encoding=utf-8")
|
36
|
+
```
|
37
|
+
|
38
|
+
Alternatively, give the driver name as "postgres" or "postgresql" in an
|
39
|
+
options Hash.
|
40
|
+
|
41
|
+
``` ruby
|
42
|
+
conn = RDO.connect(
|
43
|
+
driver: "postgresql",
|
44
|
+
host: "localhost",
|
45
|
+
user: "user",
|
46
|
+
passsword: "pass",
|
47
|
+
database: "dbname",
|
48
|
+
encoding: "utf-8"
|
49
|
+
)
|
50
|
+
```
|
51
|
+
|
52
|
+
### Bind parameters support
|
53
|
+
|
54
|
+
PostgreSQL uses $1, $2 etc for bind parameters. RDO uses '?'. You can use
|
55
|
+
either, but you **cannot** mix both styles in the same query, or you will
|
56
|
+
get errors.
|
57
|
+
|
58
|
+
These are ok:
|
59
|
+
|
60
|
+
``` ruby
|
61
|
+
conn.execute("SELECT * FROM users WHERE banned = ? AND created_at > ?", true, 1.week.ago)
|
62
|
+
conn.execute("SELECT * FROM users WHERE banned = $1 AND created_at > $2", true, 1.week.ago)
|
63
|
+
```
|
64
|
+
|
65
|
+
This is **not ok**:
|
66
|
+
|
67
|
+
``` ruby
|
68
|
+
conn.execute("SELECT * FROM users WHERE banned = $1 AND created_at > ?", true, 1.week.ago)
|
69
|
+
```
|
70
|
+
|
71
|
+
## Contributing
|
72
|
+
|
73
|
+
Contributions to support older versions of PostgreSQL (< 7.4) welcomed,
|
74
|
+
though if the changes required break the native support for newer versions
|
75
|
+
pull requests will not be accepted. It is possible to write a separate
|
76
|
+
driver for older versions of PostgreSQL and register it under the driver
|
77
|
+
name 'postgresql' instead of this one if that is preferable. Alternatively,
|
78
|
+
use an explicit name that indicates legacy compatibility, such as
|
79
|
+
'postgres73'.
|
80
|
+
|
81
|
+
If you find any bugs, please send a pull request if you think you can
|
82
|
+
fix it, or file in an issue in the issue tracker.
|
83
|
+
|
84
|
+
When sending pull requests, please use topic branches—don't send a pull
|
85
|
+
request from the master branch of your fork, as that may change
|
86
|
+
unintentionally.
|
87
|
+
|
88
|
+
Contributors will be credited in this README.
|
89
|
+
|
90
|
+
## Copyright & Licensing
|
91
|
+
|
92
|
+
Written by Chris Corbyn.
|
93
|
+
|
94
|
+
Licensed under the MIT license. See the LICENSE file for full details.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require "rake/extensiontask"
|
4
|
+
|
5
|
+
Rake::ExtensionTask.new('rdo_postgres') do |ext|
|
6
|
+
ext.lib_dir = File.join('lib', 'rdo')
|
7
|
+
end
|
8
|
+
|
9
|
+
desc "Run the full RSpec suite"
|
10
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
11
|
+
t.pattern = 'spec/'
|
12
|
+
end
|
13
|
+
|
14
|
+
Rake::Task['spec'].prerequisites << :compile
|
@@ -0,0 +1,134 @@
|
|
1
|
+
/*
|
2
|
+
* RDO Postgres Driver.
|
3
|
+
* Copyright © 2012 Chris Corbyn.
|
4
|
+
*
|
5
|
+
* See LICENSE file for details.
|
6
|
+
*/
|
7
|
+
|
8
|
+
#include "casts.h"
|
9
|
+
#include <stdlib.h>
|
10
|
+
#include <postgres.h>
|
11
|
+
#include <catalog/pg_type.h>
|
12
|
+
#include "macros.h"
|
13
|
+
|
14
|
+
/** Predicate test if the given string is formatted as \x0afe... */
|
15
|
+
#define RDO_PG_NEW_HEX_P(s, len) (len >= 2 && s[0] == '\\' && s[1] == 'x')
|
16
|
+
|
17
|
+
/** Lookup table for fast conversion of bytea hex strings to binary data */
|
18
|
+
static char * RDOPostgres_HexLookup;
|
19
|
+
|
20
|
+
/** Cast from a bytea to a String according to the new (PG 9.0) hex format */
|
21
|
+
static VALUE rdo_postgres_cast_bytea_hex(char * hex, size_t len) {
|
22
|
+
if ((len % 2) != 0) {
|
23
|
+
rb_raise(rb_eRuntimeError,
|
24
|
+
"Bad hex value provided for bytea (length not divisible by 2)");
|
25
|
+
}
|
26
|
+
|
27
|
+
size_t buflen = (len - 2) / 2;
|
28
|
+
char * buffer = malloc(sizeof(char) * buflen);
|
29
|
+
char * s = hex + 2;
|
30
|
+
char * b = buffer;
|
31
|
+
|
32
|
+
if (buffer == NULL) {
|
33
|
+
rb_raise(rb_eRuntimeError,
|
34
|
+
"Failed to allocate %ld bytes for bytea conversion", buflen);
|
35
|
+
}
|
36
|
+
|
37
|
+
for (; *s; s += 2, ++b)
|
38
|
+
*b = (RDOPostgres_HexLookup[*s] << 4) + (RDOPostgres_HexLookup[*(s + 1)]);
|
39
|
+
|
40
|
+
VALUE str = rb_str_new(buffer, buflen);
|
41
|
+
free(buffer);
|
42
|
+
|
43
|
+
return str;
|
44
|
+
}
|
45
|
+
|
46
|
+
/** Cast from a bytea to a String according to a regular escape format */
|
47
|
+
static VALUE rdo_postgres_cast_bytea_escape(char * escaped, size_t len) {
|
48
|
+
unsigned char * buffer = PQunescapeBytea(escaped, &len);
|
49
|
+
|
50
|
+
if (buffer == NULL) {
|
51
|
+
rb_raise(rb_eRuntimeError,
|
52
|
+
"Failed to allocate memory for PQunescapeBytea() conversion");
|
53
|
+
}
|
54
|
+
|
55
|
+
VALUE str = rb_str_new(buffer, len);
|
56
|
+
PQfreemem(buffer);
|
57
|
+
|
58
|
+
return str;
|
59
|
+
}
|
60
|
+
|
61
|
+
/** Get the value as a ruby type */
|
62
|
+
VALUE rdo_postgres_cast_value(PGresult * res, int row, int col, int enc) {
|
63
|
+
if (PQgetisnull(res, row, col)) {
|
64
|
+
return Qnil;
|
65
|
+
}
|
66
|
+
|
67
|
+
char * value = PQgetvalue(res, row, col);
|
68
|
+
int length = PQgetlength(res, row, col);
|
69
|
+
|
70
|
+
switch (PQftype(res, col)) {
|
71
|
+
case INT2OID:
|
72
|
+
case INT4OID:
|
73
|
+
case INT8OID:
|
74
|
+
return RDO_FIXNUM(value);
|
75
|
+
|
76
|
+
case FLOAT4OID:
|
77
|
+
case FLOAT8OID:
|
78
|
+
return RDO_FLOAT(value);
|
79
|
+
|
80
|
+
case NUMERICOID:
|
81
|
+
return RDO_DECIMAL(value);
|
82
|
+
|
83
|
+
case BOOLOID:
|
84
|
+
return RDO_BOOL(value);
|
85
|
+
|
86
|
+
case BYTEAOID:
|
87
|
+
if (RDO_PG_NEW_HEX_P(value, length))
|
88
|
+
return rdo_postgres_cast_bytea_hex(value, length);
|
89
|
+
else
|
90
|
+
return rdo_postgres_cast_bytea_escape(value, length);
|
91
|
+
|
92
|
+
case DATEOID:
|
93
|
+
return RDO_DATE(value);
|
94
|
+
|
95
|
+
case TIMESTAMPOID:
|
96
|
+
return RDO_DATE_TIME_WITHOUT_ZONE(value);
|
97
|
+
|
98
|
+
case TIMESTAMPTZOID:
|
99
|
+
return RDO_DATE_TIME_WITH_ZONE(value);
|
100
|
+
|
101
|
+
case TEXTOID:
|
102
|
+
case CHAROID:
|
103
|
+
case VARCHAROID:
|
104
|
+
case BPCHAROID:
|
105
|
+
return RDO_STRING(value, length, enc);
|
106
|
+
|
107
|
+
default:
|
108
|
+
return RDO_BINARY_STRING(value, length);
|
109
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
/* Initialize hex decoding lookup table */
|
113
|
+
void Init_rdo_postgres_casts(void) {
|
114
|
+
RDOPostgres_HexLookup = malloc(sizeof(char) * 128);
|
115
|
+
|
116
|
+
if (RDOPostgres_HexLookup == NULL) {
|
117
|
+
rb_raise(rb_eRuntimeError,
|
118
|
+
"Failed to allocate 128 bytes for internal lookup table");
|
119
|
+
}
|
120
|
+
|
121
|
+
char c;
|
122
|
+
|
123
|
+
for (c = '\0'; c < '\x7f'; ++c)
|
124
|
+
RDOPostgres_HexLookup[c] = 0;
|
125
|
+
|
126
|
+
for (c = '0'; c <= '9'; ++c)
|
127
|
+
RDOPostgres_HexLookup[c] = c - '0';
|
128
|
+
|
129
|
+
for (c = 'a'; c <= 'f'; ++c)
|
130
|
+
RDOPostgres_HexLookup[c] = 10 + c - 'a';
|
131
|
+
|
132
|
+
for (c = 'A'; c <= 'F'; ++c)
|
133
|
+
RDOPostgres_HexLookup[c] = 10 + c - 'A';
|
134
|
+
}
|
@@ -0,0 +1,16 @@
|
|
1
|
+
/*
|
2
|
+
* RDO Postgres Driver.
|
3
|
+
* Copyright © 2012 Chris Corbyn.
|
4
|
+
*
|
5
|
+
* See LICENSE file for details.
|
6
|
+
*/
|
7
|
+
|
8
|
+
#include <stdio.h>
|
9
|
+
#include <ruby.h>
|
10
|
+
#include <libpq-fe.h>
|
11
|
+
|
12
|
+
/** Cast the given value from the result to a ruby type */
|
13
|
+
VALUE rdo_postgres_cast_value(PGresult * res, int row, int col, int enc);
|
14
|
+
|
15
|
+
/** Initialize the casting framework */
|
16
|
+
void Init_rdo_postgres_casts(void);
|
@@ -0,0 +1,169 @@
|
|
1
|
+
/*
|
2
|
+
* RDO Postgres Driver.
|
3
|
+
* Copyright © 2012 Chris Corbyn.
|
4
|
+
*
|
5
|
+
* See LICENSE file for details.
|
6
|
+
*/
|
7
|
+
|
8
|
+
#include "driver.h"
|
9
|
+
#include "statements.h"
|
10
|
+
#include "macros.h"
|
11
|
+
#include <ruby.h>
|
12
|
+
#include <stdlib.h>
|
13
|
+
#include <postgres.h>
|
14
|
+
|
15
|
+
/** During GC, free any stranded connection */
|
16
|
+
static void rdo_postgres_driver_free(RDOPostgresDriver * driver) {
|
17
|
+
PQfinish(driver->conn_ptr);
|
18
|
+
free(driver);
|
19
|
+
}
|
20
|
+
|
21
|
+
/** Postgres outputs notices (e.g. auto-generating sequence...) unless overridden */
|
22
|
+
static void rdo_postgres_driver_notice_processor(void * arg, const char * msg) {}
|
23
|
+
|
24
|
+
/** Allocate memory for the connection struct */
|
25
|
+
static VALUE rdo_postgres_driver_allocate(VALUE klass) {
|
26
|
+
RDOPostgresDriver * driver = malloc(sizeof(RDOPostgresDriver));
|
27
|
+
|
28
|
+
driver->conn_ptr = NULL;
|
29
|
+
driver->is_open = 0;
|
30
|
+
driver->stmt_count = 0;
|
31
|
+
driver->encoding = -1;
|
32
|
+
|
33
|
+
VALUE self = Data_Wrap_Struct(klass, 0,
|
34
|
+
rdo_postgres_driver_free, driver);
|
35
|
+
|
36
|
+
return self;
|
37
|
+
}
|
38
|
+
|
39
|
+
/** Connect to the postgres server */
|
40
|
+
static VALUE rdo_postgres_driver_open(VALUE self) {
|
41
|
+
RDOPostgresDriver * driver;
|
42
|
+
Data_Get_Struct(self, RDOPostgresDriver, driver);
|
43
|
+
|
44
|
+
if (driver->is_open) {
|
45
|
+
return Qtrue;
|
46
|
+
}
|
47
|
+
|
48
|
+
driver->conn_ptr = PQconnectdb(
|
49
|
+
RSTRING_PTR(rb_funcall(self, rb_intern("connect_db_string"), 0)));
|
50
|
+
|
51
|
+
if (driver->conn_ptr == NULL || PQstatus(driver->conn_ptr) == CONNECTION_BAD) {
|
52
|
+
rb_raise(rb_path2class("RDO::Exception"),
|
53
|
+
"PostgreSQL connection failed: %s",
|
54
|
+
PQerrorMessage(driver->conn_ptr));
|
55
|
+
} else if (PQprotocolVersion(driver->conn_ptr) < 3) {
|
56
|
+
rb_raise(rb_path2class("RDO::Exception"),
|
57
|
+
"rdo-postgres requires PostgreSQL protocol version >= 3 (using %u). "
|
58
|
+
"PostgreSQL >= 7.4 required.",
|
59
|
+
PQprotocolVersion(driver->conn_ptr));
|
60
|
+
} else {
|
61
|
+
PQsetNoticeProcessor(driver->conn_ptr, &rdo_postgres_driver_notice_processor, NULL);
|
62
|
+
driver->is_open = 1;
|
63
|
+
driver->stmt_count = 0;
|
64
|
+
driver->encoding = rb_enc_find_index(
|
65
|
+
RSTRING_PTR(rb_funcall(self, rb_intern("encoding"), 0)));
|
66
|
+
rb_funcall(self, rb_intern("after_open"), 0);
|
67
|
+
}
|
68
|
+
|
69
|
+
return Qtrue;
|
70
|
+
}
|
71
|
+
|
72
|
+
/** Disconnect from the postgres server, and release memory */
|
73
|
+
static VALUE rdo_postgres_driver_close(VALUE self) {
|
74
|
+
RDOPostgresDriver * driver;
|
75
|
+
Data_Get_Struct(self, RDOPostgresDriver, driver);
|
76
|
+
|
77
|
+
PQfinish(driver->conn_ptr);
|
78
|
+
driver->conn_ptr = NULL;
|
79
|
+
driver->is_open = 0;
|
80
|
+
driver->stmt_count = 0;
|
81
|
+
driver->encoding = -1;
|
82
|
+
|
83
|
+
return Qtrue;
|
84
|
+
}
|
85
|
+
|
86
|
+
/** Preciate test if connection is open */
|
87
|
+
static VALUE rdo_postgres_driver_open_p(VALUE self) {
|
88
|
+
RDOPostgresDriver * driver;
|
89
|
+
Data_Get_Struct(self, RDOPostgresDriver, driver);
|
90
|
+
return driver->is_open ? Qtrue : Qfalse;
|
91
|
+
}
|
92
|
+
|
93
|
+
/** Prepare a statement for execution */
|
94
|
+
static VALUE rdo_postgres_driver_prepare(VALUE self, VALUE cmd) {
|
95
|
+
Check_Type(cmd, T_STRING);
|
96
|
+
|
97
|
+
RDOPostgresDriver * driver;
|
98
|
+
Data_Get_Struct(self, RDOPostgresDriver, driver);
|
99
|
+
|
100
|
+
if (!(driver->is_open)) {
|
101
|
+
rb_raise(rb_path2class("RDO::Exception"),
|
102
|
+
"Unable to prepare statement: connection is not open");
|
103
|
+
}
|
104
|
+
|
105
|
+
char name[32];
|
106
|
+
sprintf(name, "rdo_stmt_%i", ++driver->stmt_count);
|
107
|
+
|
108
|
+
return RDO_STATEMENT(rdo_postgres_statement_executor_new(
|
109
|
+
self, cmd, rb_str_new2(name)));
|
110
|
+
}
|
111
|
+
|
112
|
+
/** Quote a string literal for safe insertion in a statement */
|
113
|
+
static VALUE rdo_postgres_driver_quote(VALUE self, VALUE str) {
|
114
|
+
if (TYPE(str) == T_NIL) {
|
115
|
+
return rb_str_new2("NULL");
|
116
|
+
} else if (TYPE(str) != T_STRING) {
|
117
|
+
str = RDO_OBJ_TO_S(str);
|
118
|
+
}
|
119
|
+
|
120
|
+
RDOPostgresDriver * driver;
|
121
|
+
Data_Get_Struct(self, RDOPostgresDriver, driver);
|
122
|
+
|
123
|
+
if (!(driver->is_open)) {
|
124
|
+
rb_raise(rb_path2class("RDO::Exception"),
|
125
|
+
"Unable to quote string: connection is not open");
|
126
|
+
}
|
127
|
+
|
128
|
+
char * quoted = malloc(sizeof(char) * RSTRING_LEN(str) * 4);
|
129
|
+
PQescapeStringConn(driver->conn_ptr, quoted,
|
130
|
+
RSTRING_PTR(str), RSTRING_LEN(str), NULL);
|
131
|
+
|
132
|
+
VALUE newstr = rb_str_new2(quoted);
|
133
|
+
|
134
|
+
free(quoted);
|
135
|
+
|
136
|
+
return newstr;
|
137
|
+
}
|
138
|
+
|
139
|
+
void Init_rdo_postgres_driver(void) {
|
140
|
+
rb_require("rdo/postgres/driver");
|
141
|
+
|
142
|
+
VALUE cPostgresConnection = rb_path2class("RDO::Postgres::Driver");
|
143
|
+
|
144
|
+
rb_define_alloc_func(
|
145
|
+
cPostgresConnection,
|
146
|
+
rdo_postgres_driver_allocate);
|
147
|
+
|
148
|
+
rb_define_method(
|
149
|
+
cPostgresConnection,
|
150
|
+
"open", rdo_postgres_driver_open, 0);
|
151
|
+
|
152
|
+
rb_define_method(
|
153
|
+
cPostgresConnection,
|
154
|
+
"close", rdo_postgres_driver_close, 0);
|
155
|
+
|
156
|
+
rb_define_method(
|
157
|
+
cPostgresConnection,
|
158
|
+
"open?", rdo_postgres_driver_open_p, 0);
|
159
|
+
|
160
|
+
rb_define_method(
|
161
|
+
cPostgresConnection,
|
162
|
+
"prepare", rdo_postgres_driver_prepare, 1);
|
163
|
+
|
164
|
+
rb_define_method(
|
165
|
+
cPostgresConnection,
|
166
|
+
"quote", rdo_postgres_driver_quote, 1);
|
167
|
+
|
168
|
+
Init_rdo_postgres_statements();
|
169
|
+
}
|