orez-lite 0.13.0 → 0.13.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.
@@ -18,6 +18,7 @@ const SCHEMA_PROVISIONING_WAIT_MS = 20_000;
18
18
  const SCHEMA_PROVISIONING_MAX_DELAY_MS = 500;
19
19
  const APPLICATION_SQL_TURN_WAIT_MS = 30_000;
20
20
  const WRITE_GRANT_WAIT_SAMPLE_CAPACITY = 4_096;
21
+ const DEFAULT_SQL_TELEMETRY_SAMPLE_RATE = 0.01;
21
22
  const DEFAULT_SNAPSHOT_PAGE_ROWS = 2_000;
22
23
  const MAX_SNAPSHOT_PAGE_ROWS = 10_000;
23
24
  const TRANSACTION_CONTROL_SQL = /^\s*(?:BEGIN|COMMIT|ROLLBACK|SAVEPOINT|RELEASE)(?=\s|;|$)/i;
@@ -61,6 +62,12 @@ function positiveEnvInteger(value, fallback) {
61
62
  const parsed = Number(value);
62
63
  return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
63
64
  }
65
+ function probabilityEnv(value, fallback) {
66
+ if (value === undefined)
67
+ return fallback;
68
+ const parsed = Number(value);
69
+ return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : fallback;
70
+ }
64
71
  function quoteIdent(name) {
65
72
  return `"${name.replace(/"/g, '""')}"`;
66
73
  }
@@ -137,14 +144,17 @@ class ApplicationSqlSessionTarget extends RpcTarget {
137
144
  sessionID;
138
145
  readOnly;
139
146
  priority;
147
+ telemetry;
140
148
  state = 'created';
141
149
  mutated = false;
142
- constructor(owner, sessionID, readOnly, priority) {
150
+ telemetryFinished = false;
151
+ constructor(owner, sessionID, readOnly, priority, telemetry) {
143
152
  super();
144
153
  this.owner = owner;
145
154
  this.sessionID = sessionID;
146
155
  this.readOnly = readOnly;
147
156
  this.priority = priority;
157
+ this.telemetry = telemetry;
148
158
  }
149
159
  begin() {
150
160
  return this.owner[APPLICATION_SQL_ACQUIRE](this);
@@ -182,6 +192,7 @@ export class ZeroDO extends DurableObject {
182
192
  sqlStatements: 0,
183
193
  };
184
194
  sqlBillingSinceBoot = { rowsRead: 0, rowsWritten: 0 };
195
+ sqlTelemetrySampleRate;
185
196
  writeGrantWaitMs = new RecentLatencySamples(WRITE_GRANT_WAIT_SAMPLE_CAPACITY);
186
197
  sql;
187
198
  watermarks;
@@ -205,6 +216,56 @@ export class ZeroDO extends DurableObject {
205
216
  objectName: typeof this.ctx.id.name === 'string' ? this.ctx.id.name : null,
206
217
  };
207
218
  }
219
+ startSqlTelemetrySample() {
220
+ if (this.sqlTelemetrySampleRate <= 0 ||
221
+ (this.sqlTelemetrySampleRate < 1 && Math.random() >= this.sqlTelemetrySampleRate)) {
222
+ return null;
223
+ }
224
+ return {
225
+ startedAt: performance.now(),
226
+ admittedAt: null,
227
+ rowsReturned: 0,
228
+ rowsChanged: 0,
229
+ statements: 0,
230
+ };
231
+ }
232
+ recordSqlTelemetry(sample, result) {
233
+ sample.rowsReturned += result.rows.length;
234
+ sample.rowsChanged += result.changes;
235
+ }
236
+ emitSqlTelemetry(event, name, outcome, sample, error) {
237
+ if (!sample)
238
+ return;
239
+ try {
240
+ console.log(JSON.stringify({
241
+ event,
242
+ name: name.slice(0, 200),
243
+ outcome,
244
+ durationMs: Math.round((performance.now() - sample.startedAt) * 1_000) / 1_000,
245
+ ...(sample.admittedAt === null
246
+ ? null
247
+ : {
248
+ queueMs: Math.round((sample.admittedAt - sample.startedAt) * 1_000) / 1_000,
249
+ }),
250
+ rowsReturned: sample.rowsReturned,
251
+ rowsChanged: sample.rowsChanged,
252
+ statements: sample.statements,
253
+ sampleRate: this.sqlTelemetrySampleRate,
254
+ ...(error
255
+ ? {
256
+ errorName: (error instanceof Error ? error.name : typeof error).slice(0, 100),
257
+ }
258
+ : null),
259
+ }));
260
+ }
261
+ catch { }
262
+ }
263
+ finishApplicationSqlTelemetry(session, outcome, error) {
264
+ if (session.telemetryFinished)
265
+ return;
266
+ session.telemetryFinished = true;
267
+ this.emitSqlTelemetry('orez_sql_transaction_sample', session.readOnly ? 'application_sql_read' : 'application_sql_write', outcome, session.telemetry, error);
268
+ }
208
269
  recordWriteBudgetRows(rows, statement) {
209
270
  const wasTripped = this.writeBudget.status().tripped;
210
271
  try {
@@ -291,6 +352,7 @@ export class ZeroDO extends DurableObject {
291
352
  this.sql = ctx.storage.sql;
292
353
  this.writeBudgetDisabled = /^(?:1|true)$/i.test(env.OREZ_DO_WRITE_BUDGET_DISABLED ?? '');
293
354
  this.writeBudgetAdminToken = env.OREZ_DO_WRITE_BUDGET_ADMIN_TOKEN;
355
+ this.sqlTelemetrySampleRate = probabilityEnv(env.OREZ_SQL_TELEMETRY_SAMPLE_RATE, DEFAULT_SQL_TELEMETRY_SAMPLE_RATE);
294
356
  this.writeBudget = new RollingRowWriteBudget({
295
357
  budgetRows: positiveEnvInteger(env.OREZ_DO_WRITE_BUDGET_ROWS, DEFAULT_WRITE_BUDGET_ROWS),
296
358
  windowMs: positiveEnvInteger(env.OREZ_DO_WRITE_BUDGET_WINDOW_MS, DEFAULT_WRITE_BUDGET_WINDOW_MS),
@@ -339,6 +401,10 @@ export class ZeroDO extends DurableObject {
339
401
  });
340
402
  if (recovered.length)
341
403
  this.invalidateSchemaCaches();
404
+ // after recovery, which may still restore journal rows or re-create the
405
+ // residual triggers from a pre-cleanup schema snapshot, and before the
406
+ // trip restore, so the one-time drops run against a fresh write budget.
407
+ this.dropZeroHttpJournalResidue();
342
408
  if (!this.writeBudgetDisabled) {
343
409
  const persisted = await ctx.storage.get(WRITE_BUDGET_TRIPPED_KEY);
344
410
  if (persisted) {
@@ -349,6 +415,66 @@ export class ZeroDO extends DurableObject {
349
415
  }
350
416
  });
351
417
  }
418
+ /**
419
+ * Drop what the retired zero-http full-projection engine left in this
420
+ * database. That engine's mount installed `_zsync_tr_<table>_{i,u,d}` AFTER
421
+ * triggers on every synced table, appending a change envelope to
422
+ * `_zsync_changes` per write plus a `_zsync_meta` marker row per UPDATE. The
423
+ * engine was deleted on 2026-07-29, its drop logic with it, and nothing has
424
+ * read either table since the packed ledger replaced that pull path. The
425
+ * triggers stayed installed, though, so every synced write on a namespace
426
+ * mounted before the deletion still paid one or two billable journal rows
427
+ * forever (86,082 rows and ~2,600/day measured on the largest production
428
+ * namespace).
429
+ *
430
+ * Order matters for crash safety, and every step is idempotent:
431
+ *
432
+ * 1. preserve the journal's high watermark in `_zsync_watermark`, which
433
+ * `initializePackedLedger` already consults, so a namespace whose packed
434
+ * ledger has not initialized yet still seeds above every cookie it ever
435
+ * issued after the journal is gone;
436
+ * 2. drop every trigger whose body references `_zsync_changes`; `_zsync_*`
437
+ * is orez's reserved namespace, so no application trigger can
438
+ * legitimately write it;
439
+ * 3. drop both tables and the journal's rollback-capture registration.
440
+ *
441
+ * Steady state costs one sqlite_master read per boot.
442
+ */
443
+ dropZeroHttpJournalResidue() {
444
+ const residue = this.sql
445
+ .exec('SELECT name, type FROM sqlite_master ' +
446
+ "WHERE (type = 'trigger' AND sql LIKE '%\\_zsync\\_changes%' ESCAPE '\\') " +
447
+ `OR (type = 'table' AND name IN ('${ZSYNC_CHANGES_TABLE}', '_zsync_meta'))`)
448
+ .toArray()
449
+ .map((row) => ({ name: String(row.name), type: String(row.type) }));
450
+ if (residue.length === 0)
451
+ return;
452
+ if (residue.some((row) => row.type === 'table' && row.name === ZSYNC_CHANGES_TABLE)) {
453
+ const high = Number(this.sql
454
+ .exec(`SELECT COALESCE(MAX(watermark), 0) AS high FROM ${quoteIdent(ZSYNC_CHANGES_TABLE)}`)
455
+ .toArray()[0]?.high ?? 0);
456
+ if (high > 0) {
457
+ this.sql.exec('CREATE TABLE IF NOT EXISTS _zsync_watermark (' +
458
+ 'lock INTEGER PRIMARY KEY CHECK (lock = 1), high INTEGER NOT NULL)');
459
+ this.sql.exec('INSERT INTO _zsync_watermark (lock, high) VALUES (1, ?) ' +
460
+ 'ON CONFLICT (lock) DO UPDATE SET high = MAX(high, excluded.high)', high);
461
+ }
462
+ }
463
+ for (const row of residue) {
464
+ if (row.type === 'trigger') {
465
+ this.sql.exec(`DROP TRIGGER IF EXISTS ${quoteIdent(row.name)}`);
466
+ }
467
+ }
468
+ this.sql.exec(`DROP TABLE IF EXISTS ${quoteIdent(ZSYNC_CHANGES_TABLE)}`);
469
+ this.sql.exec('DROP TABLE IF EXISTS _zsync_meta');
470
+ const cdcRegistry = this.sql
471
+ .exec("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = '_orez_cdc_tables' LIMIT 1")
472
+ .toArray();
473
+ if (cdcRegistry.length > 0) {
474
+ this.sql.exec('DELETE FROM _orez_cdc_tables WHERE physical_table = ?', ZSYNC_CHANGES_TABLE);
475
+ this.cdc.reload();
476
+ }
477
+ }
352
478
  async fetch(request) {
353
479
  this.requestsSinceBoot.fetch++;
354
480
  const url = new URL(request.url);
@@ -1138,6 +1264,9 @@ export class ZeroDO extends DurableObject {
1138
1264
  this.applicationSqlQueue.shift();
1139
1265
  clearTimeout(waiter.timer);
1140
1266
  waiter.session.state = 'active';
1267
+ if (waiter.session.telemetry) {
1268
+ waiter.session.telemetry.admittedAt = performance.now();
1269
+ }
1141
1270
  if (waiter.session.readOnly)
1142
1271
  this.applicationSqlReaders.add(waiter.session);
1143
1272
  else {
@@ -1326,7 +1455,7 @@ export class ZeroDO extends DurableObject {
1326
1455
  this.requestsSinceBoot.applicationSqlReadSessions++;
1327
1456
  else
1328
1457
  this.requestsSinceBoot.applicationSqlWriteSessions++;
1329
- return new ApplicationSqlSessionTarget(this, sessionID, options.readOnly === true, priority);
1458
+ return new ApplicationSqlSessionTarget(this, sessionID, options.readOnly === true, priority, this.startSqlTelemetrySample());
1330
1459
  }
1331
1460
  /**
1332
1461
  * Run one read statement end to end in a single round trip.
@@ -1385,7 +1514,7 @@ export class ZeroDO extends DurableObject {
1385
1514
  if (this.prepareApplicationSqlMutation(session.sessionID, sql)) {
1386
1515
  session.mutated = true;
1387
1516
  }
1388
- return this.executeSQL(sql, [...params], undefined, session.sessionID).rows;
1517
+ return this.executeSQL(sql, [...params], undefined, session.sessionID, session.telemetry).rows;
1389
1518
  });
1390
1519
  }
1391
1520
  async [APPLICATION_SQL_EXEC](session, sql, params = [], metadata) {
@@ -1394,16 +1523,26 @@ export class ZeroDO extends DurableObject {
1394
1523
  if (this.prepareApplicationSqlMutation(session.sessionID, sql)) {
1395
1524
  session.mutated = true;
1396
1525
  }
1397
- const result = this.executeSQL(sql, [...params], applicationSqlTrack(metadata), session.sessionID);
1526
+ const result = this.executeSQL(sql, [...params], applicationSqlTrack(metadata), session.sessionID, session.telemetry);
1398
1527
  return { changes: result.changes };
1399
1528
  });
1400
1529
  }
1401
1530
  async [APPLICATION_SQL_QUERY_PLAN](session, plan, queryName, queryBudget) {
1402
1531
  this.assertApplicationSqlSession(session);
1403
- return this.atomically(() => executeTransactionQueryPlan(plan, (sql, params) => {
1404
- this.assertApplicationSqlStatement(session, sql);
1405
- return this.executeSQL(sql, params, undefined, session.sessionID).rows;
1406
- }, { queryName, budget: queryBudget }));
1532
+ const sample = this.startSqlTelemetrySample();
1533
+ const name = queryName?.trim() || `${plan.rootTable}:${plan.planHash}`;
1534
+ try {
1535
+ const result = await this.atomically(() => executeTransactionQueryPlan(plan, (sql, params) => {
1536
+ this.assertApplicationSqlStatement(session, sql);
1537
+ return this.executeSQL(sql, params, undefined, session.sessionID, session.telemetry, sample).rows;
1538
+ }, { queryName, budget: queryBudget }));
1539
+ this.emitSqlTelemetry('orez_sql_query_sample', name, 'success', sample);
1540
+ return result;
1541
+ }
1542
+ catch (error) {
1543
+ this.emitSqlTelemetry('orez_sql_query_sample', name, 'error', sample, error);
1544
+ throw error;
1545
+ }
1407
1546
  }
1408
1547
  async [APPLICATION_SQL_REGISTER_TABLES](session, tables) {
1409
1548
  this.assertApplicationSqlSession(session);
@@ -1424,10 +1563,21 @@ export class ZeroDO extends DurableObject {
1424
1563
  });
1425
1564
  }
1426
1565
  }
1566
+ catch (error) {
1567
+ this.finishApplicationSqlTelemetry(session, 'error', error);
1568
+ throw error;
1569
+ }
1427
1570
  finally {
1428
1571
  this.releaseApplicationSqlTurn(session);
1429
1572
  }
1430
- this.applicationSqlDidCommit(published, session.mutated);
1573
+ try {
1574
+ this.applicationSqlDidCommit(published, session.mutated);
1575
+ this.finishApplicationSqlTelemetry(session, 'committed');
1576
+ }
1577
+ catch (error) {
1578
+ this.finishApplicationSqlTelemetry(session, 'error', error);
1579
+ throw error;
1580
+ }
1431
1581
  }
1432
1582
  /**
1433
1583
  * Undo whatever this session wrote and hand its turn on.
@@ -1454,10 +1604,24 @@ export class ZeroDO extends DurableObject {
1454
1604
  this.releaseApplicationSqlTurn(session);
1455
1605
  }
1456
1606
  async [APPLICATION_SQL_ROLLBACK](session) {
1457
- this.closeApplicationSqlSession(session);
1607
+ try {
1608
+ this.closeApplicationSqlSession(session);
1609
+ this.finishApplicationSqlTelemetry(session, 'rolled_back');
1610
+ }
1611
+ catch (error) {
1612
+ this.finishApplicationSqlTelemetry(session, 'error', error);
1613
+ throw error;
1614
+ }
1458
1615
  }
1459
1616
  [APPLICATION_SQL_DISPOSE](session) {
1460
- this.closeApplicationSqlSession(session);
1617
+ try {
1618
+ this.closeApplicationSqlSession(session);
1619
+ this.finishApplicationSqlTelemetry(session, 'rolled_back');
1620
+ }
1621
+ catch (error) {
1622
+ this.finishApplicationSqlTelemetry(session, 'error', error);
1623
+ throw error;
1624
+ }
1461
1625
  }
1462
1626
  atomicallySync(work) {
1463
1627
  try {
@@ -1541,7 +1705,12 @@ export class ZeroDO extends DurableObject {
1541
1705
  return false;
1542
1706
  }
1543
1707
  }
1544
- executeSQL(sql, params = [], track, transactionID) {
1708
+ executeSQL(sql, params = [], track, transactionID, transactionTelemetry = null, queryTelemetry = null) {
1709
+ if (transactionTelemetry)
1710
+ transactionTelemetry.statements++;
1711
+ if (queryTelemetry && queryTelemetry !== transactionTelemetry) {
1712
+ queryTelemetry.statements++;
1713
+ }
1545
1714
  let capturesTrackedTable = false;
1546
1715
  if (track?.physicalTableName) {
1547
1716
  capturesTrackedTable = this.cdc.ensureTable({
@@ -1579,16 +1748,6 @@ export class ZeroDO extends DurableObject {
1579
1748
  }
1580
1749
  if (track && trackedTransactionID) {
1581
1750
  const physicalTableName = track.physicalTableName || stripPublicPrefix(track.tableName);
1582
- // the `_zsync_changes` journal is the one side-effect target orez owns,
1583
- // so this path may register it directly: its identity is fixed and it
1584
- // never publishes. registering it before the DML that fires the trigger,
1585
- // in the same storage transaction, keeps the ordering safe. otherwise
1586
- // `coversRowUndo` says no and the journal is snapshotted as before.
1587
- this.cdc.ensureTable({
1588
- physicalTableName: ZSYNC_CHANGES_TABLE,
1589
- tableName: ZSYNC_CHANGES_TABLE,
1590
- publish: false,
1591
- });
1592
1751
  for (const table of snapshotSideEffectWriteTables(this.sql, trackedTransactionID, physicalTableName, track.operation, (table) => this.cdc.coversRowUndo(table))) {
1593
1752
  snapshotted.add(table);
1594
1753
  }
@@ -1657,16 +1816,22 @@ export class ZeroDO extends DurableObject {
1657
1816
  this.appendDerivedTrackedChanges(track, rows);
1658
1817
  }
1659
1818
  const publishedCaptured = captured.filter((change) => change.publish !== false).length;
1660
- if (!track)
1661
- return { rows, columns, changes, capturedChanges: publishedCaptured };
1662
- return {
1663
- rows: track.returnRows ? rows : [],
1664
- columns: track.returnRows ? columns : [],
1665
- changes,
1666
- affectedRows: rows.length,
1667
- capturedChanges: publishedCaptured ||
1668
- (capturesTrackedTable || track.publish === false ? 0 : rows.length),
1669
- };
1819
+ const result = !track
1820
+ ? { rows, columns, changes, capturedChanges: publishedCaptured }
1821
+ : {
1822
+ rows: track.returnRows ? rows : [],
1823
+ columns: track.returnRows ? columns : [],
1824
+ changes,
1825
+ affectedRows: rows.length,
1826
+ capturedChanges: publishedCaptured ||
1827
+ (capturesTrackedTable || track.publish === false ? 0 : rows.length),
1828
+ };
1829
+ if (transactionTelemetry)
1830
+ this.recordSqlTelemetry(transactionTelemetry, result);
1831
+ if (queryTelemetry && queryTelemetry !== transactionTelemetry) {
1832
+ this.recordSqlTelemetry(queryTelemetry, result);
1833
+ }
1834
+ return result;
1670
1835
  }
1671
1836
  cursorRows(cursor, columns) {
1672
1837
  const cols = Array.isArray(columns) && columns.length > 0 ? columns : null;