extralite 2.15 → 3.0.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4394001511dda6eca48bbcf1cc6904e8bb0fc8b9951915bc0cf7192b2ea31c9a
4
- data.tar.gz: f0466fc3d8cda8fd367aa159cf89a98fc667c9a55e9bdd589afc7cb8446c58a9
3
+ metadata.gz: eb66a5b2636ccad51ab5a86ff0b7802d25574a564a5c4395c838a2d26f2599b9
4
+ data.tar.gz: d00921f287ba1c602c8669ff85740108a7bafb4f8111722f14055612db7afc3f
5
5
  SHA512:
6
- metadata.gz: 2051da94afe02e5facb094de6a03727c6215f1fb115d34e7541f52b5999e8750972fd697e05ed3e61c6f49103a43d0be1f73fe06d638415cf494acae99be839e
7
- data.tar.gz: 3ed1815f50ff89b5e1d37f06afd17dc17737c68e99cb0a9a415d3f319c4a9fc926abab0452e9a4b27222e04440564c100f791a4ab668587954aedba73d44d78b
6
+ metadata.gz: be12c6a8629f83db24107fc6a6d06f1b94beff46622b9d6c0c47ed55575069cee2529389ba709a7f8dec4f31f11cb3738e3df25f17667264031014fc5e3dc7c8
7
+ data.tar.gz: 43be64a94f7e29cbafb25a2b32271e1b260d2a7f5b6f69eb214c0e069faaca1973e0bb92fb6d62f1bcba05e55136369c6841d28dc22c06b3290188da1a84bba2
@@ -12,7 +12,7 @@ jobs:
12
12
  fail-fast: false
13
13
  matrix:
14
14
  os: [ubuntu-latest, macos-latest]
15
- ruby: ['3.2', '3.3', '3.4', 'head']
15
+ ruby: ['3.4', '4.0', 'head']
16
16
 
17
17
  name: ${{matrix.os}}, ${{matrix.ruby}}
18
18
 
@@ -12,7 +12,7 @@ jobs:
12
12
  fail-fast: false
13
13
  matrix:
14
14
  os: [ubuntu-latest, macos-latest]
15
- ruby: ['3.2', '3.3', '3.4', 'head']
15
+ ruby: ['3.4', '4.0', 'head']
16
16
 
17
17
  name: ${{matrix.os}}, ${{matrix.ruby}}
18
18
 
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 3.0.0 2026-07-02
2
+
3
+ - Update benchmark results in README
4
+ - Prevent extension loading using `load_extension` SQL function
5
+ - Remove `wal: true` option, add `legacy: true` option for
6
+ - Change default behaviour to set WAL journal mode + synchronous + foreign keys
7
+ - Add transform DSL
8
+ - Add support for type coercion in transforms
9
+ - Add `Extralite::Transform` class for transforming rows into an object graph
10
+
1
11
  ## 2.15 2026-06-28
2
12
 
3
13
  - Update bundled SQLite to 3.53.3
data/README.md CHANGED
@@ -37,12 +37,14 @@ latest features and enhancements.
37
37
 
38
38
  ## Features
39
39
 
40
- - Best-in-class [performance](#performance) (up to 4.5X the performance of the
40
+ - Best-in-class [performance](#performance) (up to 2.35X the performance of the
41
41
  [sqlite3](https://github.com/sparklemotion/sqlite3-ruby) gem).
42
- - Support for [concurrency](#concurrency) out of the box for multi-threaded
43
- and multi-fibered apps.
42
+ - Support for [concurrency](#concurrency) out of the box for multi-threaded and
43
+ multi-fibered apps.
44
44
  - A variety of ways to [retrieve data](#query-modes) - hashes, arrays, single
45
- columns, single rows, [transforms](#value-transforms).
45
+ columns, single rows.
46
+ - Support for retrieving records as structured objects using
47
+ [transforms](#transforms).
46
48
  - Support for [external iteration](#iterating-over-records-in-a-prepared-query),
47
49
  allowing iterating through single records or batches of records.
48
50
  - [Prepared queries](#prepared-queries).
@@ -53,7 +55,7 @@ latest features and enhancements.
53
55
  [backups](#creating-backups), retrieve [status
54
56
  information](#retrieving-status-information), work with
55
57
  [changesets](#working-with-changesets), interrogate [database
56
- limits](#working-with-database-limits), [trace](#tracing-sql-statements)
58
+ limits](#working-with-database-limits), [trace](#tracing-sql-statements)
57
59
  queries.
58
60
  - [Sequel](#usage-with-sequel) adapter.
59
61
 
@@ -63,7 +65,7 @@ latest features and enhancements.
63
65
  - [Getting Started](#getting-started)
64
66
  - [Query Modes](#query-modes)
65
67
  - [Parameter binding](#parameter-binding)
66
- - [Value Transforms](#value-transforms)
68
+ - [Transforms](#transforms)
67
69
  - [Data Types](#data-types)
68
70
  - [Prepared Queries](#prepared-queries)
69
71
  - [Batch Execution of Queries](#batch-execution-of-queries)
@@ -286,18 +288,20 @@ db.execute(sql, Extralite::Blob.new('Hello, 世界!'))
286
288
  db.execute(sql, 'Hello, 世界!'.force_encoding(Encoding::ASCII_8BIT))
287
289
  ```
288
290
 
289
- ## Value Transforms
291
+ ## Transforms
290
292
 
291
293
  Extralite allows you to transform rows to any value your application may need by
292
- providing a transform proc that takes the raw row values and returns the
293
- transformed data. The transform proc is passed each resulting row either as a
294
- hash or as a list of values.
294
+ providing a transform proc or transform object that takes the raw row values and
295
+ returns the transformed data.
295
296
 
296
297
  Transforms are useful when you need to transform rows into ORM model instances,
297
298
  or when you need to do some other transformation on the values retrieved from
298
- the database.
299
+ the database. With transforms, you can create nested, structured Ruby objects
300
+ that represents different entities. This is especially useful for representing
301
+ one-to-one, one-to-many or many-to-many relationships when doing joins.
299
302
 
300
- To transform results, pass a transform proc as the first parameter to one of the
303
+ A transform proc is expressed as a lambda taking one or more values. To
304
+ transform results, pass a transform proc as the first parameter to one of the
301
305
  `#query_xxx` methods:
302
306
 
303
307
  ```ruby
@@ -315,8 +319,50 @@ db.query_splat(transform, 'select a, b, c from foo')
315
319
  #=> transformed rows
316
320
  ```
317
321
 
318
- Value transforms can also be done with [prepared
319
- queries](#value-transforms-in-prepared-queries).
322
+ ### Structured Transforms
323
+
324
+ To transform rows into an object graph containing entities, you can use the
325
+ `Extralite::Transform` class, which lets you express data as an object graph.
326
+ This is particularly useful when doing `JOIN` queries. For example, a query may
327
+ express a many-to-many relationship between posts and tags:
328
+
329
+ ```sql
330
+ select
331
+ posts.id, posts.content,
332
+ tags.id, tags.name
333
+ from posts
334
+ left outer join posts_tags
335
+ on posts_tags.post_id = posts.id
336
+ left outer join tags
337
+ on posts_tags.tag_id = tags.id
338
+ ```
339
+
340
+ The result rows will contain information about both post and tag entities, and
341
+ each entity (be it a post or a tag) may be repeated in multiple rows. With a
342
+ transform you can tell Extralite to eliminate the duplicate entities by using an
343
+ identity map, and to include a list of tags for each post:
344
+
345
+ ```ruby
346
+ transform = Extralite::Transform do
347
+ {
348
+ id: integer.identity, # posts.id
349
+ content: text, # posts.content
350
+ tags: [{
351
+ id: integer.identity, # tags.id
352
+ name: text # tags.name
353
+ }]
354
+ }
355
+ end
356
+ ```
357
+
358
+ To use the transform, pass it along with the SQL string to `Database#query`:
359
+
360
+ ```ruby
361
+ db.qurey(transform, sql) #=> [...]
362
+ ```
363
+
364
+ Transforms can also be used with [prepared
365
+ queries](#transforms-in-prepared-queries).
320
366
 
321
367
  ## Prepared Queries
322
368
 
@@ -470,12 +516,13 @@ iterator = Extralite::Iterator.new(query)
470
516
  iterator.each { |r| ... }
471
517
  ```
472
518
 
473
- ### Value Transforms in Prepared Queries
519
+ ### Transforms in Prepared Queries
474
520
 
475
521
  Prepared queries can automatically transform their result sets by setting a
476
- transform block. The transform block receives values according to the query mode
477
- (hash, array or splat). To set a transform you can pass a block to one of the
478
- `Database#prepare_xxx` methods, or use `Query#transform`:
522
+ transform proc or object. If a transform proc is provided, the proc will be
523
+ invoked according to the query mode (hash, array or splat). To set a transform
524
+ proc you can pass a block to one of the `Database#prepare_xxx` methods, or use
525
+ `Query#transform=`:
479
526
 
480
527
  ```ruby
481
528
  q = db.prepare('select * from items where id = ?') { |h| Item.new(h) }
@@ -483,7 +530,7 @@ q.bind(42).next #=> Item instance
483
530
 
484
531
  # An equivalent
485
532
  q = db.prepare('select * from items where id = ?')
486
- q.transform { |h| Item.new(h) }
533
+ q.transform = ->(h) { Item.new(h) }
487
534
  ```
488
535
 
489
536
  The same can be done for queries in `splat` or `array` mode:
@@ -494,6 +541,26 @@ db.prepare_splat('select * from foo') { |a, b, c| a + b + c }
494
541
  db.prepare_array('select * from foo') { |a| a.map(&:to_s).join }
495
542
  ```
496
543
 
544
+ You can also use structured transforms with prepared queries:
545
+
546
+ ```ruby
547
+ q = db.prepare <<~SQL
548
+ select posts.id, posts.content, author.id, author.name
549
+ from posts left join authors
550
+ on posts.author_id = authors.id
551
+ SQL
552
+ q.transform = Extralite::Transform.new do
553
+ {
554
+ id: integer.identity,
555
+ content: text,
556
+ author: {
557
+ id: integer.identity,
558
+ name: text
559
+ }
560
+ }
561
+ end
562
+ ```
563
+
497
564
  ## Batch Execution of Queries
498
565
 
499
566
  Extralite provides methods for batch execution of queries, with multiple sets of
@@ -764,16 +831,13 @@ Extralite provides a comprehensive set of tools for dealing with concurrency
764
831
  issues, and for making sure that running queries on SQLite databases does not
765
832
  cause the app to freeze.
766
833
 
767
- **Note**: In order to allow concurrent access your the database, it is highly
768
- recommended that you set your database to use [WAL journaling
769
- mode](https://www.sqlite.org/wal.html) for *all* database connections.
770
- Otherwise, you risking running into performance problems and having queries fail
771
- with `BusyError` exceptions. You can easily open your database in WAL journaling
772
- mode by passing a `wal: true` option:
834
+ **Note**: By default Extralite automatically configures SQLite to [WAL
835
+ journaling mode](https://www.sqlite.org/wal.html) for *all* database
836
+ connections. To open a legacy database without WAL journaling, you can open the
837
+ database and pass the `legacy: true` option:
773
838
 
774
839
  ```ruby
775
- # This will set PRAGMA journal_mode=1 and PRAGMA synchronous=1
776
- db = Extralite::Database.new('path/to/db', wal: true)
840
+ db = Extralite::Database.new('path/to/db', legacy: true)
777
841
  ```
778
842
 
779
843
  ### The Ruby GVL
@@ -1248,7 +1312,7 @@ p articles.to_a
1248
1312
 
1249
1313
  A benchmark script is included, creating a table of various row counts, then
1250
1314
  fetching the entire table using either `sqlite3` or `extralite`. This benchmark
1251
- shows Extralite to be up to ~4.5 times faster than `sqlite3` when fetching a
1315
+ shows Extralite to be up to ~2.35 times faster than `sqlite3` when fetching a
1252
1316
  large number of rows.
1253
1317
 
1254
1318
  ### Rows as Hashes
@@ -1256,36 +1320,36 @@ large number of rows.
1256
1320
  [Benchmark source
1257
1321
  code](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash.rb)
1258
1322
 
1259
- |Row count|sqlite3 2.6.0|Extralite 2.12|Advantage|
1323
+ |Row count|sqlite3 2.9.5|Extralite 2.15|Advantage|
1260
1324
  |-:|-:|-:|-:|
1261
- |10|629.0K rows/s|950.4K rows/s|__1.51x__|
1262
- |1K|1770.5K rows/s|4321.5K rows/s|__2.44x__|
1263
- |100K|1028.8K rows/s|4088.7K rows/s|__3.97x__|
1325
+ |10|111.6K rows/s|177.3K rows/s|__1.59x__|
1326
+ |1K|2732.8K rows/s|5863.0K rows/s|__2.15x__|
1327
+ |100K|2129.0K rows/s|4863.1K rows/s|__2.28x__|
1264
1328
 
1265
1329
  ### Rows as Arrays
1266
1330
 
1267
1331
  [Benchmark source
1268
1332
  code](https://github.com/digital-fabric/extralite/blob/main/test/perf_array.rb)
1269
1333
 
1270
- |Row count|sqlite3 2.6.0|Extralite 2.12|Advantage|
1334
+ |Row count|sqlite3 2.9.5|Extralite 2.15|Advantage|
1271
1335
  |-:|-:|-:|-:|
1272
- |10|889.4K rows/s|1000.1K rows/s|__1.13x__|
1273
- |1K|4518.1K rows/s|5381.5K rows/s|__1.19x__|
1274
- |100K|4454.0K rows/s|5083.8K rows/s|__1.14x__|
1336
+ |10|1635.0K rows/s|1892.6K rows/s|__1.16x__|
1337
+ |1K|6197.4K rows/s|6691.5K rows/s|__1.08x__|
1338
+ |100K|5845.3K rows/s|5560.3K rows/s|__0.95x__|
1275
1339
 
1276
1340
  ### Prepared Queries (Prepared Statements)
1277
1341
 
1278
1342
  [Benchmark source
1279
1343
  code](https://github.com/digital-fabric/extralite/blob/main/test/perf_hash_prepared.rb)
1280
1344
 
1281
- |Row count|sqlite3 2.6.0|Extralite 2.12|Advantage|
1345
+ |Row count|sqlite3 2.9.5|Extralite 2.15|Advantage|
1282
1346
  |-:|-:|-:|-:|
1283
- |10|783.1K rows/s|1115.1K rows/s|__1.42x__|
1284
- |1K|1782.5K rows/s|4635.5K rows/s|__2.60x__|
1285
- |100K|1018.1K rows/s|4599.4K rows/s|__4.52x__|
1347
+ |10|1584.5K rows/s|2420.4K rows/s|__1.53x__|
1348
+ |1K|2738.2K rows/s|6124.3K rows/s|__2.24x__|
1349
+ |100K|2137.6K rows/s|5015.1K rows/s|__2.35x__|
1286
1350
 
1287
- As those benchmarks show, Extralite is capabale of reading up to 4.5M rows per
1288
- second, and can be more than 4 times faster than the `sqlite3` gem.
1351
+ As those benchmarks show, Extralite is capabale of reading up to 6.7M rows per
1352
+ second, and can be more than 2 times faster than the `sqlite3` gem.
1289
1353
 
1290
1354
  Note that the benchmarks above were performed on synthetic data, in a
1291
1355
  single-threaded environment, with the GVL release threshold set to -1, which
data/TODO.md CHANGED
@@ -1,16 +1,2 @@
1
- - Transform objects:
2
-
3
-
4
-
5
-
6
- - More database methods:
7
-
8
- - `Database#quote`
9
- - `Database#cache_flush` https://sqlite.org/c3ref/db_cacheflush.html
10
- - `Database#release_memory` https://sqlite.org/c3ref/db_release_memory.html
11
-
12
- - Security
13
-
14
- - Enable extension loading by using
15
- [SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION](https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigenableloadextension)
16
- in order to prevent usage of `load_extension()` SQL function.
1
+ - [ ] Version 3.0
2
+ - [ ] Run benchmarks again against latest version of sqlite3 gem
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ gem 'extralite', path: '.'
7
+ end
8
+
9
+ db = Extralite::Database.new(':memory:')
10
+ db.pragma('foreign_keys' => 1)
11
+ db.execute <<~SQL
12
+ create table posts (
13
+ id integer primary key,
14
+ title text,
15
+ content text
16
+ );
17
+ create table tags (
18
+ id integer primary key,
19
+ name text
20
+ );
21
+ create table posts_tags (
22
+ post_id integer references posts(id) on delete cascade,
23
+ tag_id integer references tags(id) on delete cascade
24
+ );
25
+
26
+ insert into posts (title, content) values ('T1', 'C1');
27
+ insert into posts (title, content) values ('T2', 'C2');
28
+ insert into tags (name) values ('tag1');
29
+ insert into tags (name) values ('tag2');
30
+ insert into tags (name) values ('tag3');
31
+
32
+ insert into posts_tags(post_id, tag_id) values (1, 1);
33
+ insert into posts_tags(post_id, tag_id) values (1, 2);
34
+ insert into posts_tags(post_id, tag_id) values (2, 2);
35
+ insert into posts_tags(post_id, tag_id) values (2, 3);
36
+ SQL
37
+
38
+ sql = <<~SQL
39
+ select
40
+ posts.id, posts.title, posts.content,
41
+ tags.id, tags.name
42
+ from posts
43
+ left outer join posts_tags on posts_tags.post_id = posts.id
44
+ left outer join tags on posts_tags.tag_id = tags.id
45
+ order by posts.id, tags.id
46
+ SQL
47
+
48
+ transform = Extralite::Transform.new do
49
+ {
50
+ id: integer.identity,
51
+ title: text,
52
+ content: text,
53
+ tags: [{
54
+ id: integer.identity,
55
+ name: text
56
+ }]
57
+ }
58
+ end
59
+
60
+ require 'pp'
61
+ PP.pp db.query(transform, sql), $stdout, 40
@@ -117,7 +117,7 @@ VALUE cleanup_track(struct track_ctx *ctx) {
117
117
  /* Tracks changes in the given block and collects them into the changeset.
118
118
  * Changes are tracked only for the given tables. If nil is supplied as the
119
119
  * given tables, changes are tracked for all tables.
120
- *
120
+ *
121
121
  * # track changes for the foo and bar tables
122
122
  * changeset.track(db, [:foo, :bar]) do
123
123
  * run_some_queries
@@ -207,7 +207,7 @@ VALUE changeset_iter_info(sqlite3_changeset_iter *iter) {
207
207
  VALUE new_values = Qnil;
208
208
  VALUE converted = Qnil;
209
209
  VALUE row = rb_ary_new2(4);
210
-
210
+
211
211
  const char *tbl_name;
212
212
  int column_count;
213
213
  int op_int;
@@ -298,7 +298,7 @@ inline void verify_changeset(Changeset_t *changeset) {
298
298
  * Each change entry is an array containing the operation (:insert / :update /
299
299
  * :delete), the table name, an array containing the old values, and an array
300
300
  * containing the new values.
301
- *
301
+ *
302
302
  * changeset.each do |(op, table, old_values, new_values)|
303
303
  * ...
304
304
  * end
@@ -322,7 +322,7 @@ VALUE Changeset_each(VALUE self) {
322
322
  * is an array containing the operation (:insert / :update / :delete), the table
323
323
  * name, an array containing the old values, and an array containing the new
324
324
  * values.
325
- *
325
+ *
326
326
  * @return [Array<Array>] changes in the changeset
327
327
  */
328
328
  VALUE Changeset_to_a(VALUE self) {
@@ -344,7 +344,7 @@ static int xConflict(void *pCtx, int eConflict, sqlite3_changeset_iter *pIter){
344
344
  }
345
345
 
346
346
  /* Applies the changeset to the given database.
347
- *
347
+ *
348
348
  * @param db [Extralite::Database] database to apply changes to
349
349
  * @return [Extralite::Changeset] changeset
350
350
  */
@@ -374,7 +374,7 @@ VALUE Changeset_apply(VALUE self, VALUE db) {
374
374
  *
375
375
  * # undo changes
376
376
  * changeset.invert.apply(db)
377
- *
377
+ *
378
378
  * @return [Extralite::Changeset] inverted changeset
379
379
  */
380
380
  VALUE Changeset_invert(VALUE self) {
@@ -399,7 +399,7 @@ VALUE Changeset_invert(VALUE self) {
399
399
  * changeset BLOB can be stored to file for later retrieval.
400
400
  *
401
401
  * File.open('my.changes', 'w+') { |f| f << changeset.to_blob }
402
- *
402
+ *
403
403
  * @return [String] changeset BLOB
404
404
  */
405
405
  VALUE Changeset_to_blob(VALUE self) {
@@ -417,7 +417,7 @@ VALUE Changeset_to_blob(VALUE self) {
417
417
  * changeset = Extralite::Changeset.new
418
418
  * changeset.load(IO.read('my.changes'))
419
419
  * changeset.apply(db)
420
- *
420
+ *
421
421
  * @param blob [String] changeset BLOB
422
422
  * @return [Extralite::Changeset] changeset
423
423
  */
@@ -452,9 +452,9 @@ void Init_ExtraliteChangeset(void) {
452
452
  rb_define_method(cChangeset, "to_blob", Changeset_to_blob, 0);
453
453
  rb_define_method(cChangeset, "track", Changeset_track, 2);
454
454
 
455
- SYM_delete = ID2SYM(rb_intern("delete"));
456
- SYM_insert = ID2SYM(rb_intern("insert"));
457
- SYM_update = ID2SYM(rb_intern("update"));
455
+ SYM_delete = ID2SYM(rb_intern_const("delete"));
456
+ SYM_insert = ID2SYM(rb_intern_const("insert"));
457
+ SYM_update = ID2SYM(rb_intern_const("update"));
458
458
 
459
459
  rb_gc_register_mark_object(SYM_delete);
460
460
  rb_gc_register_mark_object(SYM_insert);