rdo-sqlite 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,269 @@
1
+ /*
2
+ * RDO SQLite3 Driver.
3
+ * Copyright © 2012 Chris Corbyn.
4
+ *
5
+ * See LICENSE file for details.
6
+ */
7
+
8
+ #include <stdio.h>
9
+ #include <stdlib.h>
10
+ #include "statements.h"
11
+ #include "driver.h"
12
+ #include "macros.h"
13
+
14
+ /** Sruct wrapped by class RDO::SQLite::StatementExecutor */
15
+ typedef struct {
16
+ RDOSQLiteDriver * driver;
17
+ char * cmd;
18
+ sqlite3_stmt * stmt;
19
+ } RDOSQLiteStatementExecutor;
20
+
21
+ /** RDO::SQLite::StatementExecutor class */
22
+ VALUE rdo_sqlite_cStatementExecutor;
23
+
24
+ /** Free memory associated with the statement during GC */
25
+ static void rdo_sqlite_statement_executor_free(RDOSQLiteStatementExecutor * executor) {
26
+ sqlite3_finalize(executor->stmt);
27
+ free(executor);
28
+ }
29
+
30
+ /** Check if the 'tail' of a query points to something other than a comment */
31
+ static int rdo_sqlite_inert_p(char * s) {
32
+ int inslcmt = 0;
33
+ int inmlcmt = 0;
34
+
35
+ for (; *s; ++s) {
36
+ switch (*s) {
37
+ case ' ':
38
+ case '\t':
39
+ break;
40
+
41
+ case '\r':
42
+ case '\n':
43
+ inslcmt = 0;
44
+ break;
45
+
46
+ case '/':
47
+ if (!inslcmt && *(s + 1) == '*') {
48
+ inmlcmt = 1;
49
+ ++s;
50
+ } else if (!inslcmt && !inmlcmt) {
51
+ return 0;
52
+ }
53
+ break;
54
+
55
+ case '*':
56
+ if (inmlcmt && *(s + 1) == '/') {
57
+ inmlcmt = 0;
58
+ ++s;
59
+ } else if (!inslcmt && !inmlcmt) {
60
+ return 0;
61
+ }
62
+ break;
63
+
64
+ case '-':
65
+ if (!inmlcmt && *(s + 1) == '-') {
66
+ inslcmt = 1;
67
+ ++s;
68
+ } else if (!inslcmt && !inmlcmt) {
69
+ return 0;
70
+ }
71
+ break;
72
+
73
+ default:
74
+ if (!inslcmt && !inmlcmt) {
75
+ return 0;
76
+ }
77
+ }
78
+ }
79
+
80
+ return 1;
81
+ }
82
+
83
+ /** Create a new statement executor for the given command */
84
+ VALUE rdo_sqlite_statement_executor_new(VALUE driver, VALUE cmd) {
85
+ Check_Type(cmd, T_STRING);
86
+
87
+ RDOSQLiteStatementExecutor * executor =
88
+ malloc(sizeof(RDOSQLiteStatementExecutor));
89
+
90
+ Data_Get_Struct(driver, RDOSQLiteDriver, executor->driver);
91
+ executor->cmd = strdup(RSTRING_PTR(cmd));
92
+ executor->stmt = NULL;
93
+
94
+ VALUE self = Data_Wrap_Struct(rdo_sqlite_cStatementExecutor, 0,
95
+ rdo_sqlite_statement_executor_free, executor);
96
+
97
+ rb_obj_call_init(self, 1, &driver);
98
+
99
+ return self;
100
+ }
101
+
102
+ /** Initialize the statement (prepare it) */
103
+ static VALUE rdo_sqlite_statement_executor_initialize(VALUE self, VALUE driver) {
104
+ rb_iv_set(self, "driver", driver); // GC safety
105
+ RDOSQLiteStatementExecutor * executor;
106
+ Data_Get_Struct(self, RDOSQLiteStatementExecutor, executor);
107
+
108
+ if (!(executor->driver->is_open)) {
109
+ RDO_ERROR("Cannot execute prepare statement: database is not open");
110
+ }
111
+
112
+ const char * tail;
113
+
114
+ int status = sqlite3_prepare_v2(executor->driver->db,
115
+ executor->cmd,
116
+ (int) strlen(executor->cmd) + 1,
117
+ &(executor->stmt),
118
+ &tail);
119
+
120
+ if ((status != SQLITE_OK) || sqlite3_errcode(executor->driver->db)) {
121
+ RDO_ERROR("Failed to prepare statement: %s",
122
+ sqlite3_errmsg(executor->driver->db));
123
+ }
124
+
125
+ if (!rdo_sqlite_inert_p((char *) tail)) {
126
+ rb_raise(rb_eArgError, "Only one statement can be executed at a time");
127
+ }
128
+
129
+ return self;
130
+ }
131
+
132
+ /** Get the command this statement will execute */
133
+ static VALUE rdo_sqlite_statement_executor_command(VALUE self) {
134
+ RDOSQLiteStatementExecutor * executor;
135
+ Data_Get_Struct(self, RDOSQLiteStatementExecutor, executor);
136
+ return rb_str_new2(executor->cmd);
137
+ }
138
+
139
+ /** Fetch the value from the given column in the result and convert to a Ruby type */
140
+ static VALUE rdo_sqlite_cast_value(sqlite3_stmt * stmt, int col) {
141
+ switch (sqlite3_column_type(stmt, col)) {
142
+ case SQLITE_NULL:
143
+ return Qnil;
144
+
145
+ case SQLITE_INTEGER:
146
+ return LL2NUM(sqlite3_column_int64(stmt, col));
147
+
148
+ case SQLITE_FLOAT:
149
+ return DBL2NUM(sqlite3_column_double(stmt, col));
150
+
151
+ case SQLITE_TEXT:
152
+ return RDO_STRING((const char *) sqlite3_column_text(stmt, col),
153
+ sqlite3_column_bytes(stmt, col), 1);
154
+
155
+ case SQLITE_BLOB:
156
+ return RDO_BINARY_STRING((const char *) sqlite3_column_blob(stmt, col),
157
+ sqlite3_column_bytes(stmt, col));
158
+
159
+ default:
160
+ return RDO_BINARY_STRING((const char *) sqlite3_column_text(stmt, col),
161
+ sqlite3_column_bytes(stmt, col));
162
+ }
163
+ }
164
+
165
+ /** Extract useful result information from the db and the statement */
166
+ static VALUE rdo_sqlite_result_info(sqlite3 * db, sqlite3_stmt * stmt) {
167
+ VALUE info = rb_hash_new();
168
+ rb_hash_aset(info, ID2SYM(rb_intern("insert_id")),
169
+ LL2NUM(sqlite3_last_insert_rowid(db)));
170
+ rb_hash_aset(info, ID2SYM(rb_intern("affected_rows")),
171
+ LL2NUM(sqlite3_changes(db)));
172
+ return info;
173
+ }
174
+
175
+ /** Bind all input values to the statement */
176
+ static void rdo_sqlite_statement_bind_args(sqlite3_stmt * stmt, int argc, VALUE * args) {
177
+ if (sqlite3_bind_parameter_count(stmt) != argc) {
178
+ rb_raise(rb_eArgError,
179
+ "Bind parameter count mismatch: wanted %i, got %i",
180
+ sqlite3_bind_parameter_count(stmt),
181
+ argc);
182
+ }
183
+
184
+ VALUE v;
185
+ int i = 0;
186
+
187
+ for (; i < argc; ++i) {
188
+ v = args[i];
189
+
190
+ if (v == Qnil) {
191
+ sqlite3_bind_null(stmt, i);
192
+ } else {
193
+ if (v == Qtrue) v = INT2NUM(1);
194
+ if (v == Qfalse) v = INT2NUM(0);
195
+
196
+ if ((rb_funcall(v, rb_intern("kind_of?"), 1, rb_cTime) == Qtrue)
197
+ || rb_funcall(v, rb_intern("kind_of?"), 1, rb_path2class("DateTime"))) {
198
+ v = rb_funcall(v, rb_intern("strftime"), 1, rb_str_new2("%F %T"));
199
+ }
200
+
201
+ if (TYPE(v) != T_STRING) v = RDO_OBJ_TO_S(v);
202
+
203
+ sqlite3_bind_text(stmt, i + 1,
204
+ RSTRING_PTR(v), (int) RSTRING_LEN(v), NULL);
205
+ }
206
+ }
207
+ }
208
+
209
+ /** Iterate over all rows in the result and return an Array */
210
+ static VALUE rdo_sqlite_statement_extract_tuples(sqlite3 * db, sqlite3_stmt * stmt) {
211
+ int status;
212
+ int col = 0;
213
+ int ncols = 0;
214
+ VALUE hash;
215
+ VALUE tuples = rb_ary_new();
216
+
217
+ while ((status = sqlite3_step(stmt)) == SQLITE_ROW) {
218
+ hash = rb_hash_new();
219
+ ncols = sqlite3_column_count(stmt);
220
+
221
+ for (col = 0; col < ncols; ++col) {
222
+ rb_hash_aset(hash,
223
+ ID2SYM(rb_intern(sqlite3_column_name(stmt, col))),
224
+ rdo_sqlite_cast_value(stmt, col));
225
+ }
226
+
227
+ rb_ary_push(tuples, hash);
228
+ }
229
+
230
+ if (status != SQLITE_DONE) {
231
+ RDO_ERROR("Failed to execute statement: %s", sqlite3_errmsg(db));
232
+ }
233
+
234
+ return tuples;
235
+ }
236
+
237
+ /** Execute the statement with the given bind parameters and return a Result */
238
+ static VALUE rdo_sqlite_statement_executor_execute(int argc, VALUE * args, VALUE self) {
239
+ RDOSQLiteStatementExecutor * executor;
240
+ Data_Get_Struct(self, RDOSQLiteStatementExecutor, executor);
241
+
242
+ if (!(executor->driver->is_open)) {
243
+ RDO_ERROR("Cannot execute execute statement: database is not open");
244
+ }
245
+
246
+ rdo_sqlite_statement_bind_args(executor->stmt, argc, args);
247
+
248
+ VALUE tuples = rdo_sqlite_statement_extract_tuples(executor->driver->db, executor->stmt);
249
+ VALUE info = rdo_sqlite_result_info(executor->driver->db, executor->stmt);
250
+ sqlite3_reset(executor->stmt);
251
+
252
+ return RDO_RESULT(tuples, info);
253
+ }
254
+
255
+ /** Initialize the statements framework */
256
+ void Init_rdo_sqlite_statements(void) {
257
+ rb_require("date");
258
+
259
+ VALUE mSQLite = rb_path2class("RDO::SQLite");
260
+ rdo_sqlite_cStatementExecutor = rb_define_class_under(
261
+ mSQLite, "StatementExecutor", rb_cObject);
262
+
263
+ rb_define_method(rdo_sqlite_cStatementExecutor,
264
+ "initialize", rdo_sqlite_statement_executor_initialize, 1);
265
+ rb_define_method(rdo_sqlite_cStatementExecutor,
266
+ "command", rdo_sqlite_statement_executor_command, 0);
267
+ rb_define_method(rdo_sqlite_cStatementExecutor,
268
+ "execute", rdo_sqlite_statement_executor_execute, -1);
269
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * RDO SQLite3 Driver.
3
+ * Copyright © 2012 Chris Corbyn.
4
+ *
5
+ * See LICENSE file for details.
6
+ */
7
+
8
+ #include <ruby.h>
9
+ #include <sqlite3.h>
10
+
11
+ /** Create a new prepared statement executor for the given command */
12
+ VALUE rdo_sqlite_statement_executor_new(VALUE driver, VALUE cmd);
13
+
14
+ /** Initialize the prepared statements class */
15
+ void Init_rdo_sqlite_statements(void);
@@ -0,0 +1,23 @@
1
+ ##
2
+ # RDO SQLite3 driver.
3
+ # Copyright © 2012 Chris Corbyn.
4
+ #
5
+ # See LICENSE file for details.
6
+ ##
7
+
8
+ module RDO
9
+ module SQLite
10
+ # Main Driver class to hook into sqlite3 API
11
+ class Driver < RDO::Driver
12
+ def execute(stmt, *args)
13
+ prepare(stmt).execute(*args)
14
+ end
15
+
16
+ private
17
+
18
+ def filename
19
+ options[:path].to_s
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ ##
2
+ # RDO SQLite3 driver.
3
+ # Copyright © 2012 Chris Corbyn.
4
+ #
5
+ # See LICENSE file for details.
6
+ ##
7
+
8
+ module RDO
9
+ module SQLite
10
+ VERSION = "0.0.1"
11
+ end
12
+ end
data/lib/rdo/sqlite.rb ADDED
@@ -0,0 +1,16 @@
1
+ ##
2
+ # RDO SQLite3 driver.
3
+ # Copyright © 2012 Chris Corbyn.
4
+ #
5
+ # See LICENSE file for details.
6
+ ##
7
+
8
+ require "rdo"
9
+ require "rdo/sqlite/version"
10
+ require "rdo/sqlite/driver"
11
+ require "rdo_sqlite/rdo_sqlite" # c extension
12
+
13
+ # Register driver with RDO
14
+ %w[sqlite sqlite3].each do |name|
15
+ RDO::Connection.register_driver(name, RDO::SQLite::Driver)
16
+ end
data/lib/rdo-sqlite.rb ADDED
@@ -0,0 +1 @@
1
+ require "rdo/sqlite"
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/rdo/sqlite/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["d11wtq"]
6
+ gem.email = ["chris@w3style.co.uk"]
7
+ gem.description = "Provides access to SQLite3 using the RDO interface"
8
+ gem.summary = "SQLite3 Driver for RDO"
9
+ gem.homepage = "https://github.com/d11wtq/rdo-sqlite"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "rdo-sqlite"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = RDO::SQLite::VERSION
17
+ gem.extensions = ["ext/rdo_sqlite/extconf.rb"]
18
+
19
+ gem.add_runtime_dependency "rdo", ">= 0.0.1"
20
+
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency "rake-compiler"
23
+ end
@@ -0,0 +1,10 @@
1
+ require "rdo"
2
+ require "rdo/sqlite"
3
+
4
+ ENV["CONNECTION"] ||= "sqlite://memory?encoding=utf-8"
5
+
6
+ RSpec.configure do |config|
7
+ def connection_uri
8
+ ENV["CONNECTION"]
9
+ end
10
+ end
@@ -0,0 +1,316 @@
1
+ require "spec_helper"
2
+ require "bigdecimal"
3
+ require "date"
4
+
5
+ describe RDO::SQLite::Driver, "bind params" do
6
+ let(:options) { "sqlite::memory:" }
7
+ let(:db) { RDO.connect(options) }
8
+ let(:table) { "" }
9
+ let(:tuple) { db.execute("SELECT * FROM test").first }
10
+
11
+ before(:each) { db.execute(table) }
12
+
13
+ describe "nil param" do
14
+ context "against a text field" do
15
+ let(:table) { "CREATE TABLE test (name text)" }
16
+
17
+ before(:each) { db.execute("INSERT INTO test (name) VALUES (?)", nil) }
18
+
19
+ it "is inferred correctly" do
20
+ tuple[:name].should be_nil
21
+ end
22
+ end
23
+
24
+ context "against an integer field" do
25
+ let(:table) { "CREATE TABLE test (age integer)" }
26
+
27
+ before(:each) { db.execute("INSERT INTO test (age) VALUES (?)", nil) }
28
+
29
+ it "is inferred correctly" do
30
+ tuple[:age].should be_nil
31
+ end
32
+ end
33
+
34
+ context "against a float field" do
35
+ let(:table) { "CREATE TABLE test (score float)" }
36
+
37
+ before(:each) { db.execute("INSERT INTO test (score) VALUES (?)", nil) }
38
+
39
+ it "is inferred correctly" do
40
+ tuple[:score].should be_nil
41
+ end
42
+ end
43
+
44
+ context "against a blob field" do
45
+ let(:table) { "CREATE TABLE test (salt blob)" }
46
+
47
+ before(:each) { db.execute("INSERT INTO test (salt) VALUES (?)", nil) }
48
+
49
+ it "is inferred correctly" do
50
+ tuple[:salt].should be_nil
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "String param" do
56
+ context "against a text field" do
57
+ let(:table) { "CREATE TABLE test (name text)" }
58
+
59
+ before(:each) { db.execute("INSERT INTO test (name) VALUES (?)", "jim") }
60
+
61
+ it "is inferred correctly" do
62
+ tuple[:name].should == "jim"
63
+ end
64
+ end
65
+
66
+ context "against an integer field" do
67
+ let(:table) { "CREATE TABLE test (age integer)" }
68
+
69
+ before(:each) { db.execute("INSERT INTO test (age) VALUES (?)", "29") }
70
+
71
+ it "is inferred correctly" do
72
+ tuple[:age].should == 29
73
+ end
74
+ end
75
+
76
+ context "against an float field" do
77
+ let(:table) { "CREATE TABLE test (score float)" }
78
+
79
+ before(:each) { db.execute("INSERT INTO test (score) VALUES (?)", "56.4") }
80
+
81
+ it "is inferred correctly" do
82
+ tuple[:score].should == 56.4
83
+ end
84
+ end
85
+
86
+ context "against an blob field" do
87
+ let(:table) { "CREATE TABLE test (salt blob)" }
88
+
89
+ before(:each) { db.execute("INSERT INTO test (salt) VALUES (?)", "\x00\x11\x22") }
90
+
91
+ it "is inferred correctly" do
92
+ tuple[:salt].should == "\x00\x11\x22"
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "Fixnum param" do
98
+ context "against an integer field" do
99
+ let(:table) { "CREATE TABLE test (age integer)" }
100
+
101
+ before(:each) { db.execute("INSERT INTO test (age) VALUES (?)", 29) }
102
+
103
+ it "is inferred correctly" do
104
+ tuple[:age].should == 29
105
+ end
106
+ end
107
+
108
+ context "against a text field" do
109
+ let(:table) { "CREATE TABLE test (name text)" }
110
+
111
+ before(:each) { db.execute("INSERT INTO test (name) VALUES (?)", 27) }
112
+
113
+ it "is inferred correctly" do
114
+ tuple[:name].should == "27"
115
+ end
116
+ end
117
+
118
+ context "against an float field" do
119
+ let(:table) { "CREATE TABLE test (score float)" }
120
+
121
+ before(:each) { db.execute("INSERT INTO test (score) VALUES (?)", 56) }
122
+
123
+ it "is inferred correctly" do
124
+ tuple[:score].should == 56.0
125
+ end
126
+ end
127
+
128
+ context "against an blob field" do
129
+ let(:table) { "CREATE TABLE test (salt blob)" }
130
+
131
+ before(:each) { db.execute("INSERT INTO test (salt) VALUES (?)", 19) }
132
+
133
+ it "is inferred correctly" do
134
+ tuple[:salt].should == "19"
135
+ end
136
+ end
137
+ end
138
+
139
+ describe "Float param" do
140
+ context "against an float field" do
141
+ let(:table) { "CREATE TABLE test (score float)" }
142
+
143
+ before(:each) { db.execute("INSERT INTO test (score) VALUES (?)", 56.4) }
144
+
145
+ it "is inferred correctly" do
146
+ tuple[:score].should == 56.4
147
+ end
148
+ end
149
+
150
+ context "against an integer field" do
151
+ let(:table) { "CREATE TABLE test (age integer)" }
152
+
153
+ before(:each) { db.execute("INSERT INTO test (age) VALUES (?)", 29.2) }
154
+
155
+ it "is inferred correctly" do
156
+ tuple[:age].should == 29.2 # sqlite type affinity
157
+ end
158
+ end
159
+
160
+ context "against a text field" do
161
+ let(:table) { "CREATE TABLE test (name text)" }
162
+
163
+ before(:each) { db.execute("INSERT INTO test (name) VALUES (?)", 27.4) }
164
+
165
+ it "is inferred correctly" do
166
+ tuple[:name].should == "27.4"
167
+ end
168
+ end
169
+
170
+ context "against an blob field" do
171
+ let(:table) { "CREATE TABLE test (salt blob)" }
172
+
173
+ before(:each) { db.execute("INSERT INTO test (salt) VALUES (?)", 19.2) }
174
+
175
+ it "is inferred correctly" do
176
+ tuple[:salt].should == "19.2"
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "Boolean param" do
182
+ context "against an boolean (i.e. numeric) field" do
183
+ context "when it is true" do
184
+ let(:table) { "CREATE TABLE test (rad boolean)" }
185
+
186
+ before(:each) { db.execute("INSERT INTO test (rad) VALUES (?)", true) }
187
+
188
+ it "is inferred as integer 1" do
189
+ tuple[:rad].should == 1
190
+ end
191
+ end
192
+
193
+ context "when it is false" do
194
+ let(:table) { "CREATE TABLE test (rad boolean)" }
195
+
196
+ before(:each) { db.execute("INSERT INTO test (rad) VALUES (?)", false) }
197
+
198
+ it "is inferred as integer 0" do
199
+ tuple[:rad].should == 0
200
+ end
201
+ end
202
+ end
203
+ end
204
+
205
+ describe "BigDecimal param" do
206
+ context "against an float field" do
207
+ let(:table) { "CREATE TABLE test (score float)" }
208
+
209
+ before(:each) { db.execute("INSERT INTO test (score) VALUES (?)", BigDecimal("56.4")) }
210
+
211
+ it "is inferred correctly" do
212
+ tuple[:score].should == 56.4
213
+ end
214
+ end
215
+
216
+ context "against an integer field" do
217
+ let(:table) { "CREATE TABLE test (age integer)" }
218
+
219
+ before(:each) { db.execute("INSERT INTO test (age) VALUES (?)", BigDecimal("29.2")) }
220
+
221
+ it "is inferred correctly" do
222
+ tuple[:age].should == 29.2 # sqlite type affinity
223
+ end
224
+ end
225
+
226
+ context "against a text field" do
227
+ let(:table) { "CREATE TABLE test (name text)" }
228
+
229
+ before(:each) { db.execute("INSERT INTO test (name) VALUES (?)", BigDecimal("27.4")) }
230
+
231
+ it "is inferred correctly" do
232
+ tuple[:name].should == "0.274E2"
233
+ end
234
+ end
235
+
236
+ context "against an blob field" do
237
+ let(:table) { "CREATE TABLE test (salt blob)" }
238
+
239
+ before(:each) { db.execute("INSERT INTO test (salt) VALUES (?)", BigDecimal("27.4")) }
240
+
241
+ it "is inferred correctly" do
242
+ tuple[:salt].should == "0.274E2"
243
+ end
244
+ end
245
+ end
246
+
247
+ describe "Date param" do
248
+ context "against an date (numeric) field" do
249
+ let(:table) { "CREATE TABLE test (dob date)" }
250
+
251
+ before(:each) { db.execute("INSERT INTO test (dob) VALUES (?)", Date.new(1983, 5, 3)) }
252
+
253
+ it "is inferred correctly" do
254
+ tuple[:dob].should == "1983-05-03"
255
+ end
256
+ end
257
+ end
258
+
259
+ describe "Time param" do
260
+ context "against an datetime (numeric) field" do
261
+ let(:table) { "CREATE TABLE test (created_at datetime)" }
262
+
263
+ before(:each) do
264
+ db.execute(
265
+ "INSERT INTO test (created_at) VALUES (?)",
266
+ Time.new(1983, 5, 3, 7, 15, 43)
267
+ )
268
+ end
269
+
270
+ it "is inferred correctly" do
271
+ tuple[:created_at].should == "1983-05-03 07:15:43"
272
+ end
273
+
274
+ it "is parseable by sqlite" do
275
+ tuple
276
+ db.execute("SELECT date(created_at, '+1 day') FROM test").first_value.should ==
277
+ "1983-05-04"
278
+ end
279
+ end
280
+ end
281
+
282
+ describe "DateTime param" do
283
+ context "against an datetime (numeric) field" do
284
+ let(:table) { "CREATE TABLE test (created_at datetime)" }
285
+
286
+ before(:each) do
287
+ db.execute(
288
+ "INSERT INTO test (created_at) VALUES (?)",
289
+ DateTime.new(1983, 5, 3, 7, 15, 43)
290
+ )
291
+ end
292
+
293
+ it "is inferred correctly" do
294
+ tuple[:created_at].should == "1983-05-03 07:15:43"
295
+ end
296
+
297
+ it "is parseable by sqlite" do
298
+ tuple
299
+ db.execute("SELECT date(created_at, '+1 day') FROM test").first_value.should ==
300
+ "1983-05-04"
301
+ end
302
+ end
303
+ end
304
+
305
+ describe "multiple params" do
306
+ let(:table) { "CREATE TABLE test (name text, age integer)" }
307
+
308
+ before(:each) do
309
+ db.execute("INSERT INTO test (name, age) VALUES (?, ?)", "bob", 27)
310
+ end
311
+
312
+ it "binds them all" do
313
+ tuple.should == {name: "bob", age: 27}
314
+ end
315
+ end
316
+ end