plume 0.2.0 → 0.3.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: 5233a7cda234897d9b0cca09246777a1cd343d3da52d9fef922a11eddb230195
4
- data.tar.gz: 539669c43ded2e665a44f44f33d43354c050243c02afc7b08de2b4ecb41f0709
3
+ metadata.gz: 94377beb43ecb80343f932d40da7aa323d8229a6fb6dba2d83c6dcaf31385a45
4
+ data.tar.gz: 38f780903f0665b23423653f38dfca576daf28535521917a426daf1dc01b9871
5
5
  SHA512:
6
- metadata.gz: eb601a1c93cfc62614a3188053f20401ccd4213e9b5274f8f71af8ec567b66ecffeb18e796b9bfad4499d55a80be5ddf0a2a646086b633aadc4ba2093bbde46d
7
- data.tar.gz: 79172b07840a82d4a35a201e51fbbe589763cc0160fe7ac2360d9254bf0ca9789e1c9b0ea3cba3e33769d9cb6945cc700e07fe4620f7a0f6514246cb54ac3a04
6
+ metadata.gz: e2402f5627d6cec60c889729e465bcc29ce932beaf836d86ce86bc93e2bc617dbb7b067335a229333f8b2cba28faf2a13764e4443e9e1a22d806deaba28054e7
7
+ data.tar.gz: b1d4dcf873e71476020dcf6c00ee647d5cded80c443f4df6d5f5da04a1b1a5c6436500c1ab2e80e7704c49409304aec29e9a8737e080a8e8c1166d53f8a692d3
data/README.md CHANGED
@@ -12,7 +12,7 @@ Plume is a Ruby library for working with SQLite elegantly. Presently, this means
12
12
 
13
13
  ## Installation
14
14
 
15
- Install the gem and add to the application's Gemfile by executing:
15
+ Plume needs Ruby 3.2 or later. Install the gem and add to the application's Gemfile by executing:
16
16
 
17
17
  ```bash
18
18
  bundle add plume
@@ -26,9 +26,9 @@ gem install plume
26
26
 
27
27
  ## Usage
28
28
 
29
- Parse a SQL query with the `Plume::Parser` class:
29
+ Every statement in SQLite's grammar parses, and the parser is checked against the SQL in SQLite's own test suite - both the SQL written in its test files and the SQL they build as they run. Of the 26,977 fixture files in that corpus, Plume parses 26,707 and rejects 266 that SQLite rejects too. The 4 left are deliberate: 3 that Plume parses but SQLite refuses for reasons other than syntax, and 1 expression nested 1,000 deep, past what Plume's stack allows.
30
30
 
31
- Every statement in SQLite's grammar parses, and the parser is checked against the SQL in SQLite's own test suite: of the 15,836 statements there, the ones it does not parse are those SQLite rejects as well.
31
+ Parse SQL with `Plume.parse`, which returns one node per statement:
32
32
 
33
33
  ```ruby
34
34
  sql = <<~SQL
@@ -177,7 +177,6 @@ Plume::CreateTableStatement(
177
177
  options = [
178
178
  Plume::WithoutRowidTableOption(),
179
179
  ],
180
- select_statement = Object(),
181
180
  strict = false,
182
181
  without_row_id = true,
183
182
  )
@@ -198,14 +197,118 @@ stmt.to_sql
198
197
  # => "SELECT a FROM t WHERE (b = 1) AND (tenant_id = ?)"
199
198
  ```
200
199
 
201
- Tokenize a SQL query with the `Plume::Lexer` class:
200
+ `walk` yields every node with its parent, and `walk_with_slots` also gives the prop the node sits in, plus its index when that prop is a list. `transform` builds a changed copy and leaves the original alone. Return a node from the block to keep it, or another node to put in its place:
201
+
202
+ ```ruby
203
+ stmt = Plume.parse("SELECT a FROM t WHERE b IN (1, 2) ORDER BY 1").first
204
+ stmt.transform { |node, parent| Plume::LiteralExpression === node && !(Plume::OrderingTerm === parent) ? Plume::Variable.new(name: "?") : node }.to_sql
205
+ # => "SELECT a FROM t WHERE b IN (?, ?) ORDER BY 1"
206
+ ```
207
+
208
+ Only the nodes on the way to a replacement are copied, and the rest is shared with the original. Neither method recurses, so trees as deep as SQLite allows are fine. This works on shareable trees as well.
209
+
210
+ `Plume.split` finds the statements in a script by their tokens alone, so it also works on SQL that doesn't parse. A trigger's body stays in one piece:
211
+
212
+ ```ruby
213
+ Plume.split("SELECT 1; CREATE TRIGGER t AFTER INSERT ON x BEGIN DELETE FROM y; END; SELEC oops")
214
+ # => ["SELECT 1", "CREATE TRIGGER t AFTER INSERT ON x BEGIN DELETE FROM y; END", "SELEC oops"]
215
+ ```
216
+ `to_sql(pretty: true)` lays the SQL out over lines. Each clause starts a line. A clause too long for `max_line_length` (80 by default) puts its items one per line, indented by `indent_size` (4), and a long `AND` or `OR` chain puts each operand on its own line. Subqueries are indented inside their parentheses. `commas_start_of_line: true` puts the commas at the start of the lines:
217
+
218
+ ```ruby
219
+ Plume.parse("select id, title from posts p join users u on u.id = p.user_id where u.active and p.draft = 0").first.to_sql(pretty: true)
220
+ # => SELECT id, title
221
+ # FROM posts AS p
222
+ # JOIN users AS u ON u.id = p.user_id
223
+ # WHERE u.active AND (p.draft = 0)
224
+ ```
225
+
226
+ Pretty SQL parses back into the same tree, which is checked across the whole fixture corpus.
227
+
228
+ Tokenize SQL with `Plume.tokenize`:
202
229
 
203
230
  ```ruby
204
231
  tokens = Plume.tokenize("SELECT * FROM users WHERE id = 1")
205
232
  # => [:SELECT, :STAR, :FROM, :ID, :WHERE, :ID, :EQ, :INTEGER]
206
233
  ```
207
234
 
208
- SQLite works with a grammar of _167_ token types, _136_ of which are keywords. The remaining _31_ are punctuation, operators, and literals.
235
+ `with_values: true` adds each token's text, and `with_positions: true` its byte range in the SQL - for highlighting, or for finding a token's place in the original:
236
+
237
+ ```ruby
238
+ Plume.tokenize("SELECT a FROM t", with_values: true, with_positions: true)
239
+ # => [[:SELECT, "SELECT", 0, 6], [:ID, "a", 7, 8], [:FROM, "FROM", 9, 13], [:ID, "t", 14, 15]]
240
+ ```
241
+
242
+ `dump` turns a tree into a plain hash that JSON can carry, and `Plume.load` turns that hash back into the same tree. Positions, keywords as written and the source all come through, so the loaded tree prints, locates and compares as the original did. That makes it possible to cache parsed trees, or to hand them to another process:
243
+
244
+ ```ruby
245
+ json = JSON.generate(Plume.parse("SELECT DISTINCT a FROM t").first.dump)
246
+ Plume.load(JSON.parse(json)).to_sql
247
+ # => "SELECT DISTINCT a FROM t"
248
+ ```
249
+
250
+ Neither `dump` nor `load` recurses, but JSON's own generator and parser do, so a very deep tree needs `max_nesting: false` and enough stack.
251
+
252
+ The lexer knows _183_ token types: _153_ keywords, and _30_ more for punctuation, operators, literals and names. Words like `rowid`, `strict` and `stored`, which SQLite reads as plain names and checks by their text, are keywords here that fall back to names wherever SQLite would take a name.
253
+ The lexer knows _184_ token types: _153_ keywords, and _31_ more for punctuation, operators, literals, names and comments. Words like `rowid`, `strict` and `stored`, which SQLite reads as plain names and checks by their text, are keywords here that fall back to names wherever SQLite would take a name.
254
+
255
+ ### Comments
256
+
257
+ Comments are tokens of their own (`:COMMENT`), which the parser skips the same way SQLite does. `Plume.comments` lists them with their byte ranges. `tags` reads [sqlcommenter](https://google.github.io/sqlcommenter/) comments, including the ones Rails' `query_log_tags` adds, in its current format and the older `key:value` one:
258
+
259
+ ```ruby
260
+ sql = "SELECT * FROM posts /*controller='posts',action='index'*/"
261
+ Plume.comments(sql).first.tags # => {"controller" => "posts", "action" => "index"}
262
+ ```
263
+
264
+ `to_sql(comments: true)` keeps the comments directly before and after a statement, such as a header line or the sqlcommenter tags. Comments inside the statement are left out. A trailing `--` comment is printed as `/* */`, so a semicolon after it doesn't become part of the comment:
265
+
266
+ ```ruby
267
+ Plume.parse("-- report
268
+ select a from t /*app='shop'*/").first.to_sql(comments: true)
269
+ # => "-- report
270
+ SELECT a FROM t /*app='shop'*/"
271
+ ```
272
+
273
+ `Plume.parameters` numbers a statement's bind parameters the way SQLite does - `?` takes the next number, `?N` takes N, and a name keeps the number it first got - so you know what to bind where before preparing anything:
274
+
275
+ ```ruby
276
+ parameters = Plume.parameters("SELECT * FROM posts WHERE user_id = :user AND (title = ? OR body = ?) LIMIT :user")
277
+ parameters.count # => 3
278
+ parameters.names # => [":user", nil, nil]
279
+ parameters.index(":user") # => 1
280
+ parameters.map(&:number) # => [1, 2, 3, 1]
281
+ ```
282
+
283
+ `count`, `names` and `index` answer what `sqlite3_bind_parameter_count`, `_name` and `_index` would. Each parameter also carries its `node`, and with it the `location` it was written at.
284
+
285
+ ### Truncating
286
+
287
+ `Plume.truncate(sql, max_length)` shortens SQL to fit a log line or an error tracker. It replaces the parts you need least to recognise the query with `...`: result columns, `WHERE` conditions, `VALUES` rows, CTE bodies, `INSERT` column lists and `UPDATE` assignments. The most deeply nested parts go first, and the longest first among those. Only if that is still not short enough is the end cut off:
288
+
289
+ ```ruby
290
+ sql = "WITH recent AS (SELECT id, title FROM posts WHERE created_at > ?) SELECT r.id, r.title, u.name FROM recent r JOIN users u ON u.id = r.user_id WHERE u.active"
291
+ Plume.truncate(sql, 100)
292
+ # => "WITH recent AS (...) SELECT ... FROM recent r JOIN users u ON u.id = r.user_id WHERE u.active"
293
+ ```
294
+
295
+ ### Normalizing
296
+
297
+ `Plume.normalize` replaces a query's constants with `?` and leaves everything else as written, so the same query with different values comes out the same. That lets you group a log the way `pg_stat_statements` does, or keep values out of it:
298
+
299
+ ```ruby
300
+ Plume.normalize("SELECT * FROM posts /* app */ WHERE user_id = 42 AND title LIKE 'Re: %' ORDER BY 1 LIMIT 10")
301
+ # => "SELECT * FROM posts /* app */ WHERE user_id = ? AND title LIKE ? ORDER BY 1 LIMIT ?"
302
+ ```
303
+
304
+ Numbers (including their sign), strings and blobs are replaced. `NULL`, `TRUE`, `FALSE` and `CURRENT_*` are left alone, and so is an integer on its own in `ORDER BY` or `GROUP BY`, where SQLite reads it as a column position. Only `SELECT`, `INSERT`, `UPDATE` and `DELETE` are normalized. Pass a parsed statement to get a tree back with `Variable`s in place of the constants. The tests bind the constants back into the normalized SQL and check that SQLite returns the same rows.
305
+
306
+ `Plume.fingerprint` goes one step further and gives the same short hash for every spelling of a query: whatever its values, parameter names, spacing, keyword and name case, or parentheses, and however many constants an `IN` list or `VALUES` holds. It is stable across processes and Ruby versions, so it can be stored:
307
+
308
+ ```ruby
309
+ Plume.fingerprint("SELECT a FROM t WHERE b IN (1, 2, 3)") # => "07b540b0c5ed57e1"
310
+ Plume.fingerprint("select A from T where b in (?)") # => "07b540b0c5ed57e1"
311
+ ```
209
312
 
210
313
  ### What does this query read?
211
314
 
@@ -227,9 +330,66 @@ reads.tables # => #<Set: {"posts", "users"}>
227
330
  reads.columns # => {"posts" => #<Set: {"id", "user_id", "title"}>, "users" => #<Set: {"id", "name", "email"}>}
228
331
  reads.projected # columns whose values can end up in the result rows
229
332
  reads.referenced # columns only used to filter, join, sort or group
333
+ reads.calls # => #<Set: {"like"}>
230
334
  ```
231
335
 
232
- `Plume.access` does the same for `INSERT`, `UPDATE` and `DELETE`, and also tells you what they write:
336
+ `reads.calls` lists the functions the statement calls, including the ones inside the views it reads. It also covers what `LIKE`, `GLOB`, `REGEXP`, `MATCH`, `->`, `->>` and `CURRENT_*` call, because SQLite's authorizer counts those as calls too. Table-valued functions in `FROM`, like `json_each`, are listed separately in `reads.functions`.
337
+
338
+ `reads.ctes` lists the names the statement's `WITH` clauses define, and `reads.aliases` maps each table alias to what it stands for, spelled as written: `{"r" => "recent", "u" => "users"}` above.
339
+
340
+ `Plume.reads` also takes what you already parsed - a statement node, or the array `Plume.parse` returns - so the SQL is not parsed twice, and a tree you changed after parsing is analyzed as changed. So do `Plume.writes` and `Plume.access` below.
341
+
342
+ The catalog can also come from a live database with `Plume::Catalog.from_sqlite(db)`, or be left out - then `SELECT *` is reported as a read of the column `"*"` and unqualified columns only resolve when there is a single table in scope.
343
+
344
+ Anything that cannot be attributed (an unknown table or column, an ambiguous name) raises `Plume::Analysis::Error` with a pointer into the SQL. Pass `strict: false` to get the partial result and a list in `reads.unresolved` instead. Each entry of `reads.references` carries the AST node it came from, so you can point at it too - see [examples/read_guard.rb](examples/read_guard.rb) for a small column-level access check built on top.
345
+
346
+ The results are tested against SQLite's own [authorizer callback](https://www.sqlite.org/c3ref/set_authorizer.html), which is also the place to enforce access rules at runtime when you have a connection. `Plume.reads` is for when you want to vet SQL ahead of time, without a database, or with good error messages. It reports one thing the authorizer does not: the right-hand join columns of `USING` and `NATURAL` joins.
347
+
348
+ ### What does this query write?
349
+
350
+ `Plume.writes` works out which tables and columns an `INSERT`, `UPDATE` or `DELETE` would change. It takes the same `catalog:` and `strict:` as `Plume.reads`:
351
+
352
+ ```ruby
353
+ writes = Plume.writes(<<~SQL, catalog: catalog)
354
+ INSERT INTO posts (user_id, title) VALUES (?, ?)
355
+ ON CONFLICT (id) DO UPDATE SET title = excluded.title
356
+ SQL
357
+
358
+ writes.tables # => #<Set: {"posts"}>
359
+ writes.columns # => {"posts" => #<Set: {"user_id", "title"}>}
360
+ writes.inserted # => {"posts" => #<Set: {"user_id", "title"}>}
361
+ writes.updated # => {"posts" => #<Set: {"title"}>}, from the DO UPDATE
362
+ ```
363
+
364
+ A `DELETE` removes whole rows, so it names a table and no columns:
365
+
366
+ ```ruby
367
+ writes = Plume.writes("DELETE FROM posts WHERE user_id = ?", catalog: catalog)
368
+
369
+ writes.deleted # => #<Set: {"posts"}>
370
+ writes.tables # => #<Set: {"posts"}>
371
+ writes.columns # => {}
372
+ ```
373
+
374
+ Around the edges:
375
+
376
+ - An `INSERT` without a column list writes every column of the table except generated ones, or `"*"` without a catalog. `DEFAULT VALUES` writes the table but names no columns.
377
+ - Setting `rowid`, `oid` or `_rowid_` writes `"ROWID"`, even when an `INTEGER PRIMARY KEY` stands in for it - unless the table has a real column by that name.
378
+ - `RETURNING` and CTEs only read. A CTE never stands in for the table being written, even when it has the same name.
379
+ - Writing to a view reports the view. The triggers a statement fires are not looked into, so what an `INSTEAD OF` trigger or any other trigger writes is not reported, and neither are the rows a `REPLACE` deletes to make room. Pass the `CREATE TRIGGER` itself to find out what a trigger does - see below.
380
+ - A `SELECT` writes nothing.
381
+ - Tables outside `main` are keyed with their schema, as in `"aux.users"`.
382
+
383
+ With `strict: false`, a column the catalog does not have is left out and listed in `writes.unresolved`:
384
+
385
+ ```ruby
386
+ writes = Plume.writes("UPDATE posts SET title = ?, body = ? WHERE id = ?", catalog: catalog, strict: false)
387
+
388
+ writes.updated # => {"posts" => #<Set: {"title"}>}
389
+ writes.unresolved.map(&:name) # => ["body"]
390
+ ```
391
+
392
+ `Plume.access` gives you both the reads and the writes, from one pass over the statement:
233
393
 
234
394
  ```ruby
235
395
  access = Plume.access(<<~SQL, catalog: catalog)
@@ -243,13 +403,78 @@ access.reads.referenced # => {"users" => #<Set: {"id"}>, "posts" => #<Set: {"use
243
403
 
244
404
  A value copied into a column being written counts as projected, just like one returned in the result rows.
245
405
 
246
- Both also take what you already parsed - a statement node, or the array `Plume.parse` returns - so the SQL is not parsed twice, and a tree you changed after parsing is analyzed as changed.
406
+ Writes are checked against the authorizer as well, and must match it exactly - except for the columns of an `INSERT`, which the authorizer does not name.
247
407
 
248
- The catalog can also come from a live database with `Plume::Catalog.from_sqlite(db)`, or be left out - then `SELECT *` is reported as a read of the column `"*"` and unqualified columns only resolve when there is a single table in scope.
408
+ `Plume.access`, `reads` and `writes` also take a `CREATE TRIGGER`. They report what its `WHEN` clause and body read, write and call each time the trigger fires. `NEW` and `OLD` stand for the row of the table the trigger is on, so reading `new.total` reads that table, as SQLite's authorizer reports it:
249
409
 
250
- Anything that cannot be attributed (an unknown table or column, an ambiguous name) raises `Plume::Analysis::Error` with a pointer into the SQL. Pass `strict: false` to get the partial result and a list in `reads.unresolved` instead. Each entry of `reads.references` carries the AST node it came from, so you can point at it too - see [examples/read_guard.rb](examples/read_guard.rb) for a small column-level access check built on top.
410
+ ```ruby
411
+ access = Plume.access(<<~SQL, catalog: catalog)
412
+ CREATE TRIGGER audit AFTER UPDATE ON posts WHEN new.title <> old.title BEGIN
413
+ UPDATE users SET name = upper(name) WHERE id = new.user_id;
414
+ END
415
+ SQL
416
+ access.reads.columns # => {"posts" => #<Set: {"title", "user_id"}>, "users" => #<Set: {"name", "id"}>}
417
+ access.writes.updated # => {"users" => #<Set: {"name"}>}
418
+ ```
251
419
 
252
- The results are tested against SQLite's own [authorizer callback](https://www.sqlite.org/c3ref/set_authorizer.html), which is also the place to enforce access rules at runtime when you have a connection. `Plume.reads` is for when you want to vet SQL ahead of time, without a database, or with good error messages. It reports one thing the authorizer does not: the right-hand join columns of `USING` and `NATURAL` joins.
420
+ ### What else does a statement do?
421
+
422
+ `Plume.access` takes every other kind of statement too, and reports in `actions` what it does besides reading and writing rows. The names follow SQLite's authorizer action codes, and the actions are checked against it:
423
+
424
+ ```ruby
425
+ catalog = Plume::Catalog.from_sql(<<~SQL)
426
+ CREATE TABLE posts (id INTEGER PRIMARY KEY, title TEXT UNIQUE);
427
+ CREATE TRIGGER touch AFTER UPDATE ON posts BEGIN SELECT 1; END;
428
+ SQL
429
+
430
+ Plume.access("DROP TABLE posts", catalog:).actions.map { |a| [a.action, a.name, a.detail] }
431
+ # => [[:drop_table, "posts", nil], [:drop_trigger, "touch", "posts"]]
432
+ Plume.access("CREATE TEMP TABLE drafts (slug PRIMARY KEY)").actions.map { |a| [a.action, a.name] }
433
+ # => [[:create_temp_table, "drafts"], [:create_temp_index, "sqlite_autoindex_drafts_1"]]
434
+ ```
435
+
436
+ Each action has an `action` (`:create_table`, `:drop_temp_index`, `:alter_table`, `:pragma`, `:transaction`, `:savepoint`, `:attach`, `:reindex` and the rest), a `name`, a `detail` (the table an index or trigger is on, a pragma's value, the column `ALTER TABLE` drops) and a `schema`.
437
+
438
+ As with SQLite, a `CREATE TABLE … AS` reads what its `SELECT` reads, a `CREATE INDEX` reads the columns it indexes, and `DROP TABLE` deletes the table's rows. A `CREATE VIEW` reads nothing, because SQLite only checks a view's body when the view is used. `EXPLAIN` reports what the statement it explains would do. The catalog also keeps track of indexes and triggers, which is how a `DROP INDEX` knows its table.
439
+
440
+ ### What does a whole script touch?
441
+
442
+ `reads`, `writes` and `access` each take exactly one statement. `Plume.summary` takes any number of statements, such as a migration or a batch from a log, and merges what they touch:
443
+
444
+ ```ruby
445
+ summary = Plume.summary(<<~SQL)
446
+ CREATE TABLE archive (id INTEGER PRIMARY KEY, title TEXT);
447
+ INSERT INTO archive SELECT id, upper(title) FROM posts WHERE id < 100;
448
+ DELETE FROM posts WHERE id < 100;
449
+ SQL
450
+
451
+ summary.statement_types # => ["CreateTableStatement", "InsertStatement", "DeleteStatement"]
452
+ summary.tables # => #<Set: {"posts", "archive"}>
453
+ summary.tables(:delete) # => #<Set: {"posts"}>
454
+ summary.calls # => #<Set: {"upper"}>
455
+ summary.actions.map(&:action) # => [:create_table]
456
+ summary.accesses # => one Plume::Analysis::Access per statement
457
+ ```
458
+
459
+ It takes the same `catalog:` and `strict:` arguments. Each statement sees the schema as the statements before it left it, so a script that creates a table and then fills it checks out against a catalog that has never heard of the table. The changes go to a copy of the catalog, and the one you pass in stays as it was. `Catalog#apply(statement)` makes the same changes to a catalog of your own.
460
+
461
+ ### Ractors
462
+
463
+ Plume loads its parts lazily, and Ractors other than the main one cannot read the class-level state that leaves behind. Call `Plume.eager_load!` once from the main Ractor before starting any, and freeze the catalog with `Ractor.make_shareable` so it can be passed in:
464
+
465
+ ```ruby
466
+ Plume.eager_load!
467
+ catalog = Ractor.make_shareable(Plume::Catalog.from_sql(schema_sql))
468
+
469
+ ractors = queries.each_slice(100).map do |batch|
470
+ Ractor.new(batch, catalog) do |sqls, cat|
471
+ sqls.map { |sql| Plume.reads(sql, catalog: cat, strict: false).columns }
472
+ end
473
+ end
474
+ ractors.flat_map(&:value) # `take` before Ruby 4.0
475
+ ```
476
+
477
+ `tokenize`, `parse`, `to_sql`, `walk`, `reads`, `writes` and `access` all work inside a Ractor. Trees and results come back out like any other object, and a tree passed through `Ractor.make_shareable` can still be read, printed, walked and analyzed - it just can no longer be edited. After `eager_load!` the node classes cannot be given new props.
253
478
 
254
479
  ## Roadmap
255
480
 
@@ -283,20 +508,20 @@ The results are tested against SQLite's own [authorizer callback](https://www.sq
283
508
 
284
509
  ## Performance
285
510
 
286
- `bin/benchmark` runs the whole fixture corpus (15,817 statements, 4.2 MB of SQL). On an Apple M-series laptop with Ruby 3.4:
511
+ `bin/benchmark` runs every fixture that parses (26,707 files, 5.5 MB of SQL). On an Apple M5 Pro with Ruby 4.0:
287
512
 
288
- | Step | Statements/s | MB/s |
289
- | --- | ---: | ---: |
290
- | `Plume.tokenize` | ~13,500 | 3.7 |
291
- | `Plume.parse` | ~1,400 | 0.38 |
292
- | `Node#to_sql` | ~6,000 | 1.7 |
293
- | `Plume.reads` (no catalog) | ~1,000 | 0.21 |
513
+ | Step | Fixtures/s | MB/s | MB/s with YJIT |
514
+ | --- | ---: | ---: | ---: |
515
+ | `Plume.tokenize` | ~30,000 | 6.4 | 18.5 |
516
+ | `Plume.parse` | ~2,900 | 0.62 | 0.66 |
517
+ | `Node#to_sql` | ~12,000 | 2.5 | 2.3 |
518
+ | `Plume.reads` (no catalog, `SELECT`s only) | ~2,200 | 0.38 | 0.46 |
294
519
 
295
520
  The corpus leans on very large generated expressions; a typical application query parses in well under a millisecond. About half of the parse time goes to Literal's runtime type checks on every node property.
296
521
 
297
522
  ## Development
298
523
 
299
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. `bin/fixture-report` shows how much of SQLite's own test SQL parses (pass `--write` to record it as the new baseline the tests hold us to), and `bin/benchmark` times the whole pipeline. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
524
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. `bin/fixture-report` shows how much of SQLite's own test SQL parses (pass `--write` to record it as the new baseline the tests hold us to), and `bin/benchmark` times the whole pipeline. `bin/trace-fixtures` runs SQLite's Tcl test suite and adds a fixture for each shape of statement it finds that the corpus lacks, and for each one where Plume and SQLite disagree. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
300
525
 
301
526
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
302
527