extralite 1.27 → 2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/LICENSE +1 -1
- data/README.md +40 -14
- data/TODO.md +21 -0
- data/ext/extralite/common.c +80 -58
- data/ext/extralite/database.c +138 -78
- data/ext/extralite/extconf.rb +16 -16
- data/ext/extralite/extralite.h +63 -17
- data/ext/extralite/extralite_ext.c +4 -2
- data/ext/extralite/iterator.c +208 -0
- data/ext/extralite/query.c +534 -0
- data/lib/extralite/sqlite3_constants.rb +1 -1
- data/lib/extralite/version.rb +1 -1
- data/lib/extralite.rb +0 -2
- data/lib/sequel/adapters/extralite.rb +104 -106
- data/test/perf_prepared.rb +2 -2
- data/test/test_database.rb +35 -9
- data/test/test_extralite.rb +1 -1
- data/test/test_iterator.rb +104 -0
- data/test/test_query.rb +519 -0
- data/test/test_sequel.rb +23 -4
- metadata +6 -4
- data/ext/extralite/prepared_statement.c +0 -333
- data/test/test_prepared_statement.rb +0 -225
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd8cf56eff701ddf57586d6f7510d5959cdf81ff15e6a64c6ed8ff698f93b43b
|
4
|
+
data.tar.gz: 42000f8c83849577fd1672996969961357c5bbadb82d89d2168c74be7acfa303
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1159c965d8ce9bfade2c81de97b6ef311dcfdb84ce65eb85238aa6a23b29b786d31de2bb8f30f87a24a800b88d01475a7cb400b97245401d587bef365b140087
|
7
|
+
data.tar.gz: 74f975529a9efb7291524eaa4c285fadc6976377a607cd28e72cbd3010cbc9fd061264aabb9a5df1ce864f097ca330c9acd9dded62b8de7cc63c1217be88412a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,17 @@
|
|
1
|
+
# 2.1 2023-07-11
|
2
|
+
|
3
|
+
- Implement `Database#execute`, `Query#execute` for data-manipulation queries
|
4
|
+
- Add option for opening databases for read only access
|
5
|
+
|
6
|
+
# 2.0 2023-07-08
|
7
|
+
|
8
|
+
- Fix Sequel migrations (#8)
|
9
|
+
- Redesign prepared statement functionality (#24)
|
10
|
+
- Rewrite `Extralite::PreparedStatement` into `Extralite::Query` with breaking API changes
|
11
|
+
- Add `Extralite::Iterator` class for external iteration
|
12
|
+
- Add `Query#each_xxx`, `Query#to_a_xxx` method
|
13
|
+
- Add `Query#eof?` method
|
14
|
+
|
1
15
|
# 1.27 2023-06-12
|
2
16
|
|
3
17
|
- Fix execution of prepared statements in Sequel adapter (#23 @gschlager)
|
data/Gemfile.lock
CHANGED
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
|
10
10
|
Extralite is a super fast, extra-lightweight (about 1300 lines of C-code)
|
11
11
|
SQLite3 wrapper for Ruby. It provides a minimal set of methods for interacting
|
12
|
-
with an SQLite3 database, as well as prepared statements.
|
12
|
+
with an SQLite3 database, as well as prepared queries (prepared statements).
|
13
13
|
|
14
14
|
Extralite comes in two flavors: the `extralite` gem which uses the
|
15
15
|
system-installed sqlite3 library, and the `extralite-bundle` gem which bundles
|
@@ -26,11 +26,11 @@ latest features and enhancements.
|
|
26
26
|
as arrays, single row, single column, single value.
|
27
27
|
- Prepared statements.
|
28
28
|
- Parameter binding.
|
29
|
+
- External iteration - get single records or batches of records.
|
29
30
|
- Use system-installed sqlite3, or the [bundled latest version of
|
30
31
|
SQLite3](#installing-the-extralite-sqlite3-bundle).
|
31
32
|
- Improved [concurrency](#concurrency) for multithreaded apps: the Ruby GVL is
|
32
33
|
released while preparing SQL statements and while iterating over results.
|
33
|
-
- Iterate over records with a block, or collect records into an array.
|
34
34
|
- Automatically execute SQL strings containing multiple semicolon-separated
|
35
35
|
queries (handy for creating/modifying schemas).
|
36
36
|
- Execute the same query with multiple parameter lists (useful for inserting records).
|
@@ -111,11 +111,37 @@ db.query('select * from foo where bar = :bar', ':bar' => 42)
|
|
111
111
|
db.execute_multi('insert into foo values (?)', ['bar', 'baz'])
|
112
112
|
db.execute_multi('insert into foo values (?, ?)', [[1, 2], [3, 4]])
|
113
113
|
|
114
|
-
# prepared
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
114
|
+
# prepared queries
|
115
|
+
query = db.prepare('select ? as foo, ? as bar') #=> Extralite::Query
|
116
|
+
query.bind(1, 2) #=> [{ :foo => 1, :bar => 2 }]
|
117
|
+
|
118
|
+
query.next #=> next row in result_set (as hash)
|
119
|
+
query.next_hash #=> next row in result_set (as hash)
|
120
|
+
query.next_ary #=> next row in result_set (as array)
|
121
|
+
query.next_single_column #=> next row in result_set (as single value)
|
122
|
+
|
123
|
+
query.next(10) #=> next 10 rows in result_set (as hash)
|
124
|
+
query.next_hash(10) #=> next 10 rows in result_set (as hash)
|
125
|
+
query.next_ary(10) #=> next 10 rows in result_set (as array)
|
126
|
+
query.next_single_column(10) #=> next 10 rows in result_set (as single value)
|
127
|
+
|
128
|
+
query.to_a #=> all rows as array of hashes
|
129
|
+
query.to_a_hash #=> all rows as array of hashes
|
130
|
+
query.to_a_ary #=> all rows as array of arrays
|
131
|
+
query.to_a_single_column #=> all rows as array of single values
|
132
|
+
|
133
|
+
query.each { |r| ... } #=> iterate over all rows as hashes
|
134
|
+
query.each_hash { |r| ... } #=> iterate over all rows as hashes
|
135
|
+
query.each_ary { |r| ... } #=> iterate over all rows as arrays
|
136
|
+
query.each_single_column { |r| ... } #=> iterate over all rows as single columns
|
137
|
+
|
138
|
+
iterator = query.each #=> create enumerable iterator
|
139
|
+
iterator.next #=> next row
|
140
|
+
iterator.each { |r| ... } #=> iterate over all rows
|
141
|
+
values = iterator.map { |r| r[:foo] * 10 } #=> map all rows
|
142
|
+
|
143
|
+
iterator = query.each_ary #=> create enumerable iterator with rows as arrays
|
144
|
+
iterator = query.each_single_column #=> create enumerable iterator with single values
|
119
145
|
|
120
146
|
# get last insert rowid
|
121
147
|
rowid = db.last_insert_rowid
|
@@ -195,10 +221,10 @@ end
|
|
195
221
|
|
196
222
|
Extralite provides methods for retrieving status information about the sqlite
|
197
223
|
runtime, database-specific status and prepared statement-specific status,
|
198
|
-
`Extralite.runtime_status`, `Database#status` and `
|
199
|
-
|
200
|
-
|
201
|
-
|
224
|
+
`Extralite.runtime_status`, `Database#status` and `Query#status` respectively.
|
225
|
+
You can also reset the high water mark for the specific status code by providing
|
226
|
+
true as the reset argument. The status codes mirror those defined by the SQLite
|
227
|
+
API. Some examples:
|
202
228
|
|
203
229
|
```ruby
|
204
230
|
# The Extralite.runtime_status returns a tuple consisting of the current value
|
@@ -212,9 +238,9 @@ current, high_watermark = Extralite.runtime_status(Extralite::SQLITE_STATUS_MEMO
|
|
212
238
|
# argument in order to reset the high watermark):
|
213
239
|
current, high_watermark = db.status(Extralite::SQLITE_DBSTATUS_CACHE_USED)
|
214
240
|
|
215
|
-
# The
|
241
|
+
# The Query#status method returns a single value (pass true as a
|
216
242
|
# second argument in order to reset the high watermark):
|
217
|
-
value =
|
243
|
+
value = query.status(Extralite::SQLITE_STMTSTATUS_RUN)
|
218
244
|
```
|
219
245
|
|
220
246
|
### Working with Database Limits
|
@@ -302,7 +328,7 @@ large number of rows.
|
|
302
328
|
|1K|502.1K rows/s|2.065M rows/s|__4.11x__|
|
303
329
|
|100K|455.7K rows/s|2.511M rows/s|__5.51x__|
|
304
330
|
|
305
|
-
### Prepared Statements
|
331
|
+
### Prepared Queries (Prepared Statements)
|
306
332
|
|
307
333
|
[Benchmark source code](https://github.com/digital-fabric/extralite/blob/main/test/perf_prepared.rb)
|
308
334
|
|
data/TODO.md
CHANGED
@@ -0,0 +1,21 @@
|
|
1
|
+
- Improve tracing
|
2
|
+
- Transactions and savepoints:
|
3
|
+
|
4
|
+
- `DB#transaction {}` - does a `BEGIN..COMMIT` - non-reentrant!
|
5
|
+
- `DB#savepoint(name)` - creates a savepoint
|
6
|
+
- `DB#release(name)` - releases a savepoint
|
7
|
+
- `DB#rollback` - raises `Extralite::Rollback`, which is rescued by `DB#transaction`
|
8
|
+
- `DB#rollback_to(name)` - rolls back to a savepoint
|
9
|
+
|
10
|
+
- More database methods:
|
11
|
+
|
12
|
+
- `Database#quote`
|
13
|
+
- `Database#busy_timeout=` https://sqlite.org/c3ref/busy_timeout.html
|
14
|
+
- `Database#cache_flush` https://sqlite.org/c3ref/db_cacheflush.html
|
15
|
+
- `Database#release_memory` https://sqlite.org/c3ref/db_release_memory.html
|
16
|
+
|
17
|
+
- Security
|
18
|
+
|
19
|
+
- Enable extension loading by using
|
20
|
+
[SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION](https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension)
|
21
|
+
in order to prevent usage of `load_extension()` SQL function.
|
data/ext/extralite/common.c
CHANGED
@@ -75,13 +75,11 @@ inline void bind_parameter_value(sqlite3_stmt *stmt, int pos, VALUE value) {
|
|
75
75
|
}
|
76
76
|
}
|
77
77
|
|
78
|
-
void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv) {
|
79
|
-
for (int i = 0; i < argc; i++)
|
80
|
-
bind_parameter_value(stmt, i + 1, argv[i]);
|
81
|
-
}
|
78
|
+
inline void bind_all_parameters(sqlite3_stmt *stmt, int argc, VALUE *argv) {
|
79
|
+
for (int i = 0; i < argc; i++) bind_parameter_value(stmt, i + 1, argv[i]);
|
82
80
|
}
|
83
81
|
|
84
|
-
void bind_all_parameters_from_object(sqlite3_stmt *stmt, VALUE obj) {
|
82
|
+
inline void bind_all_parameters_from_object(sqlite3_stmt *stmt, VALUE obj) {
|
85
83
|
if (TYPE(obj) == T_ARRAY) {
|
86
84
|
int count = RARRAY_LEN(obj);
|
87
85
|
for (int i = 0; i < count; i++)
|
@@ -233,22 +231,23 @@ void *stmt_iterate_without_gvl(void *ptr) {
|
|
233
231
|
return NULL;
|
234
232
|
}
|
235
233
|
|
236
|
-
int stmt_iterate(
|
237
|
-
struct step_ctx
|
238
|
-
rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&
|
239
|
-
switch (
|
234
|
+
inline int stmt_iterate(query_ctx *ctx) {
|
235
|
+
struct step_ctx step_ctx = {ctx->stmt, 0};
|
236
|
+
rb_thread_call_without_gvl(stmt_iterate_without_gvl, (void *)&step_ctx, RUBY_UBF_IO, 0);
|
237
|
+
switch (step_ctx.rc) {
|
240
238
|
case SQLITE_ROW:
|
241
239
|
return 1;
|
242
240
|
case SQLITE_DONE:
|
241
|
+
ctx->eof = 1;
|
243
242
|
return 0;
|
244
243
|
case SQLITE_BUSY:
|
245
244
|
rb_raise(cBusyError, "Database is busy");
|
246
245
|
case SQLITE_INTERRUPT:
|
247
246
|
rb_raise(cInterruptError, "Query was interrupted");
|
248
247
|
case SQLITE_ERROR:
|
249
|
-
rb_raise(cSQLError, "%s", sqlite3_errmsg(
|
248
|
+
rb_raise(cSQLError, "%s", sqlite3_errmsg(ctx->sqlite3_db));
|
250
249
|
default:
|
251
|
-
rb_raise(cError, "%s", sqlite3_errmsg(
|
250
|
+
rb_raise(cError, "%s", sqlite3_errmsg(ctx->sqlite3_db));
|
252
251
|
}
|
253
252
|
|
254
253
|
return 0;
|
@@ -260,50 +259,61 @@ VALUE cleanup_stmt(query_ctx *ctx) {
|
|
260
259
|
}
|
261
260
|
|
262
261
|
VALUE safe_query_hash(query_ctx *ctx) {
|
263
|
-
VALUE
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
column_count = sqlite3_column_count(ctx->stmt);
|
270
|
-
column_names = get_column_names(ctx->stmt, column_count);
|
271
|
-
|
272
|
-
// block not given, so prepare the array of records to be returned
|
273
|
-
if (!yield_to_block) result = rb_ary_new();
|
262
|
+
VALUE array = MULTI_ROW_P(ctx->mode) ? rb_ary_new() : Qnil;
|
263
|
+
VALUE row = Qnil;
|
264
|
+
int column_count = sqlite3_column_count(ctx->stmt);
|
265
|
+
VALUE column_names = get_column_names(ctx->stmt, column_count);
|
266
|
+
int row_count = 0;
|
274
267
|
|
275
|
-
while (stmt_iterate(ctx
|
268
|
+
while (stmt_iterate(ctx)) {
|
276
269
|
row = row_to_hash(ctx->stmt, column_count, column_names);
|
277
|
-
|
278
|
-
|
270
|
+
row_count++;
|
271
|
+
switch (ctx->mode) {
|
272
|
+
case QUERY_YIELD:
|
273
|
+
rb_yield(row);
|
274
|
+
break;
|
275
|
+
case QUERY_MULTI_ROW:
|
276
|
+
rb_ary_push(array, row);
|
277
|
+
break;
|
278
|
+
case QUERY_SINGLE_ROW:
|
279
|
+
return row;
|
280
|
+
}
|
281
|
+
if (ctx->max_rows != ALL_ROWS && row_count >= ctx->max_rows)
|
282
|
+
return MULTI_ROW_P(ctx->mode) ? array : ctx->self;
|
279
283
|
}
|
280
284
|
|
281
285
|
RB_GC_GUARD(column_names);
|
282
286
|
RB_GC_GUARD(row);
|
283
|
-
RB_GC_GUARD(
|
284
|
-
return
|
287
|
+
RB_GC_GUARD(array);
|
288
|
+
return MULTI_ROW_P(ctx->mode) ? array : Qnil;
|
285
289
|
}
|
286
290
|
|
287
291
|
VALUE safe_query_ary(query_ctx *ctx) {
|
288
|
-
|
289
|
-
VALUE
|
290
|
-
int
|
291
|
-
|
292
|
-
|
293
|
-
column_count = sqlite3_column_count(ctx->stmt);
|
294
|
-
|
295
|
-
// block not given, so prepare the array of records to be returned
|
296
|
-
if (!yield_to_block) result = rb_ary_new();
|
292
|
+
VALUE array = MULTI_ROW_P(ctx->mode) ? rb_ary_new() : Qnil;
|
293
|
+
VALUE row = Qnil;
|
294
|
+
int column_count = sqlite3_column_count(ctx->stmt);
|
295
|
+
int row_count = 0;
|
297
296
|
|
298
|
-
while (stmt_iterate(ctx
|
297
|
+
while (stmt_iterate(ctx)) {
|
299
298
|
row = row_to_ary(ctx->stmt, column_count);
|
300
|
-
|
301
|
-
|
299
|
+
row_count++;
|
300
|
+
switch (ctx->mode) {
|
301
|
+
case QUERY_YIELD:
|
302
|
+
rb_yield(row);
|
303
|
+
break;
|
304
|
+
case QUERY_MULTI_ROW:
|
305
|
+
rb_ary_push(array, row);
|
306
|
+
break;
|
307
|
+
case QUERY_SINGLE_ROW:
|
308
|
+
return row;
|
309
|
+
}
|
310
|
+
if (ctx->max_rows != ALL_ROWS && row_count >= ctx->max_rows)
|
311
|
+
return MULTI_ROW_P(ctx->mode) ? array : ctx->self;
|
302
312
|
}
|
303
313
|
|
304
314
|
RB_GC_GUARD(row);
|
305
|
-
RB_GC_GUARD(
|
306
|
-
return
|
315
|
+
RB_GC_GUARD(array);
|
316
|
+
return MULTI_ROW_P(ctx->mode) ? array : Qnil;
|
307
317
|
}
|
308
318
|
|
309
319
|
VALUE safe_query_single_row(query_ctx *ctx) {
|
@@ -314,7 +324,7 @@ VALUE safe_query_single_row(query_ctx *ctx) {
|
|
314
324
|
column_count = sqlite3_column_count(ctx->stmt);
|
315
325
|
column_names = get_column_names(ctx->stmt, column_count);
|
316
326
|
|
317
|
-
if (stmt_iterate(ctx
|
327
|
+
if (stmt_iterate(ctx))
|
318
328
|
row = row_to_hash(ctx->stmt, column_count, column_names);
|
319
329
|
|
320
330
|
RB_GC_GUARD(row);
|
@@ -323,26 +333,33 @@ VALUE safe_query_single_row(query_ctx *ctx) {
|
|
323
333
|
}
|
324
334
|
|
325
335
|
VALUE safe_query_single_column(query_ctx *ctx) {
|
326
|
-
|
327
|
-
VALUE
|
328
|
-
int
|
329
|
-
|
330
|
-
|
331
|
-
column_count = sqlite3_column_count(ctx->stmt);
|
332
|
-
if (column_count != 1)
|
333
|
-
rb_raise(cError, "Expected query result to have 1 column");
|
336
|
+
VALUE array = MULTI_ROW_P(ctx->mode) ? rb_ary_new() : Qnil;
|
337
|
+
VALUE value = Qnil;
|
338
|
+
int column_count = sqlite3_column_count(ctx->stmt);
|
339
|
+
int row_count = 0;
|
334
340
|
|
335
|
-
|
336
|
-
if (!yield_to_block) result = rb_ary_new();
|
341
|
+
if (column_count != 1) rb_raise(cError, "Expected query result to have 1 column");
|
337
342
|
|
338
|
-
while (stmt_iterate(ctx
|
343
|
+
while (stmt_iterate(ctx)) {
|
339
344
|
value = get_column_value(ctx->stmt, 0, sqlite3_column_type(ctx->stmt, 0));
|
340
|
-
|
345
|
+
row_count++;
|
346
|
+
switch (ctx->mode) {
|
347
|
+
case QUERY_YIELD:
|
348
|
+
rb_yield(value);
|
349
|
+
break;
|
350
|
+
case QUERY_MULTI_ROW:
|
351
|
+
rb_ary_push(array, value);
|
352
|
+
break;
|
353
|
+
case QUERY_SINGLE_ROW:
|
354
|
+
return value;
|
355
|
+
}
|
356
|
+
if (ctx->max_rows != ALL_ROWS && row_count >= ctx->max_rows)
|
357
|
+
return MULTI_ROW_P(ctx->mode) ? array : ctx->self;
|
341
358
|
}
|
342
359
|
|
343
360
|
RB_GC_GUARD(value);
|
344
|
-
RB_GC_GUARD(
|
345
|
-
return
|
361
|
+
RB_GC_GUARD(array);
|
362
|
+
return MULTI_ROW_P(ctx->mode) ? array : Qnil;
|
346
363
|
}
|
347
364
|
|
348
365
|
VALUE safe_query_single_value(query_ctx *ctx) {
|
@@ -353,7 +370,7 @@ VALUE safe_query_single_value(query_ctx *ctx) {
|
|
353
370
|
if (column_count != 1)
|
354
371
|
rb_raise(cError, "Expected query result to have 1 column");
|
355
372
|
|
356
|
-
if (stmt_iterate(ctx
|
373
|
+
if (stmt_iterate(ctx))
|
357
374
|
value = get_column_value(ctx->stmt, 0, sqlite3_column_type(ctx->stmt, 0));
|
358
375
|
|
359
376
|
RB_GC_GUARD(value);
|
@@ -369,7 +386,7 @@ VALUE safe_execute_multi(query_ctx *ctx) {
|
|
369
386
|
sqlite3_clear_bindings(ctx->stmt);
|
370
387
|
bind_all_parameters_from_object(ctx->stmt, RARRAY_AREF(ctx->params, i));
|
371
388
|
|
372
|
-
while (stmt_iterate(ctx
|
389
|
+
while (stmt_iterate(ctx));
|
373
390
|
changes += sqlite3_changes(ctx->sqlite3_db);
|
374
391
|
}
|
375
392
|
|
@@ -379,3 +396,8 @@ VALUE safe_execute_multi(query_ctx *ctx) {
|
|
379
396
|
VALUE safe_query_columns(query_ctx *ctx) {
|
380
397
|
return get_column_names(ctx->stmt, sqlite3_column_count(ctx->stmt));
|
381
398
|
}
|
399
|
+
|
400
|
+
VALUE safe_query_changes(query_ctx *ctx) {
|
401
|
+
while (stmt_iterate(ctx));
|
402
|
+
return INT2FIX(sqlite3_changes(ctx->sqlite3_db));
|
403
|
+
}
|