extralite 1.27 → 2.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.
- 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
|
+
}
|