rdo-postgres 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|