@syncular/server 0.23.0 → 0.25.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.
package/src/d1-storage.ts CHANGED
@@ -45,11 +45,14 @@ import type { CommitPruneQuery, CommitPruneResult } from './storage';
45
45
  import { decodeRow, type RowValue } from '@syncular/core';
46
46
  import {
47
47
  bindAuthoritativePartition,
48
+ type BoundAuthoritativeQuery,
48
49
  prepareAuthoritativeQuery,
49
50
  } from './authoritative-query';
50
51
  import { syncError } from './errors';
51
52
  import {
52
53
  assertAppendOnlyMigration,
54
+ assertPhysicalColumns,
55
+ assertStoredLayouts,
53
56
  commitWindowPageSql,
54
57
  deleteRowSql,
55
58
  deleteSqliteRowScopesSql,
@@ -81,6 +84,7 @@ import {
81
84
  asUint8Array,
82
85
  collectCommitWindowPage,
83
86
  deserializePushResult,
87
+ placeholders,
84
88
  type SqliteCommitWindowRecord,
85
89
  type SqliteRowRecord,
86
90
  serializePushResult,
@@ -202,6 +206,49 @@ function relationalValuesEqual(left: RowValue, right: RowValue): boolean {
202
206
  return Object.is(left, right);
203
207
  }
204
208
 
209
+ /** Run one partition-bound registered query and the cursor read as one batch. */
210
+ async function queryAuthoritativeOn(
211
+ db: Pick<D1Database, 'prepare' | 'batch'>,
212
+ partition: string,
213
+ prepared: BoundAuthoritativeQuery,
214
+ ): Promise<AuthoritativeQueryResult> {
215
+ const results = await db.batch([
216
+ db.prepare(prepared.sql).bind(...prepared.params),
217
+ db
218
+ .prepare('SELECT max_commit_seq FROM sync_partitions WHERE partition=?')
219
+ .bind(partition),
220
+ ]);
221
+ const rowsResult = results[0];
222
+ const cursorResult = results[1];
223
+ if (
224
+ typeof rowsResult !== 'object' ||
225
+ rowsResult === null ||
226
+ !('results' in rowsResult) ||
227
+ !Array.isArray(rowsResult.results) ||
228
+ typeof cursorResult !== 'object' ||
229
+ cursorResult === null ||
230
+ !('results' in cursorResult) ||
231
+ !Array.isArray(cursorResult.results)
232
+ ) {
233
+ throw new Error('D1 registered query returned an invalid batch result');
234
+ }
235
+ const cursor = cursorResult.results[0];
236
+ const maxCommitSeq =
237
+ typeof cursor === 'object' &&
238
+ cursor !== null &&
239
+ 'max_commit_seq' in cursor &&
240
+ typeof cursor.max_commit_seq === 'number'
241
+ ? cursor.max_commit_seq
242
+ : 0;
243
+ return {
244
+ rows: rowsResult.results.filter(
245
+ (row): row is Readonly<Record<string, unknown>> =>
246
+ typeof row === 'object' && row !== null,
247
+ ),
248
+ maxCommitSeq,
249
+ };
250
+ }
251
+
205
252
  /** Read-your-own-writes overlay entry: a buffered upsert, or a deletion. */
206
253
  type PendingRow =
207
254
  | { readonly kind: 'row'; readonly row: StoredRow }
@@ -213,6 +260,10 @@ class D1Transaction implements StorageTransaction {
213
260
  readonly #db: Pick<D1Database, 'prepare' | 'batch'>;
214
261
  readonly #partition: string;
215
262
  readonly #resolveTable: (name: string) => CompiledTable;
263
+ /** Storage-owned relation rewrite, bound to this transaction's partition. */
264
+ readonly #prepareQuery: (
265
+ query: AuthoritativeQueryRequest,
266
+ ) => BoundAuthoritativeQuery;
216
267
  readonly #pushApplySerialized: boolean;
217
268
  readonly #buffer: BufferedStatement[] = [];
218
269
  #open = true;
@@ -241,11 +292,13 @@ class D1Transaction implements StorageTransaction {
241
292
  db: Pick<D1Database, 'prepare' | 'batch'>,
242
293
  partition: string,
243
294
  resolveTable: (name: string) => CompiledTable,
295
+ prepareQuery: (query: AuthoritativeQueryRequest) => BoundAuthoritativeQuery,
244
296
  pushApplySerialized: boolean,
245
297
  ) {
246
298
  this.#db = db;
247
299
  this.#partition = partition;
248
300
  this.#resolveTable = resolveTable;
301
+ this.#prepareQuery = prepareQuery;
249
302
  this.#pushApplySerialized = pushApplySerialized;
250
303
  }
251
304
 
@@ -438,6 +491,25 @@ class D1Transaction implements StorageTransaction {
438
491
  .slice(0, query.limit);
439
492
  }
440
493
 
494
+ async queryAuthoritative(
495
+ query: AuthoritativeQueryRequest,
496
+ ): Promise<AuthoritativeQueryResult> {
497
+ this.#assertOpen();
498
+ // D1 buffers writes until commit() and cannot overlay them on generated
499
+ // SQL, so a read over a written table would return committed state
500
+ // without this transaction's staged rows. Refuse it instead.
501
+ for (const key of this.#pending.keys()) {
502
+ if (query.tables.includes(key.slice(0, key.indexOf('\u0000')))) {
503
+ throw new StorageQueryError('sync.storage.query_over_staged_writes');
504
+ }
505
+ }
506
+ return queryAuthoritativeOn(
507
+ this.#db,
508
+ this.#partition,
509
+ this.#prepareQuery(query),
510
+ );
511
+ }
512
+
441
513
  async lockPartitionForPush(): Promise<void> {
442
514
  this.#assertOpen();
443
515
  if (!this.#pushApplySerialized) {
@@ -805,6 +877,27 @@ export interface D1ServerStorageOptions {
805
877
  readonly commitValidationSerialized?: boolean;
806
878
  }
807
879
 
880
+ /**
881
+ * The core tables `D1ServerStorage` reads or writes outside schema migration.
882
+ * `migrateSchema` creates them in its core phase, which runs only when the
883
+ * version marker advances, so a same-version open refuses a database missing
884
+ * one. `sync_backfill_checkpoints` and `sync_writer_fence` stay out: D1
885
+ * declares no checkpoints and installs no fence.
886
+ */
887
+ const D1_CORE_TABLES = [
888
+ 'sync_partitions',
889
+ 'sync_partition_registry',
890
+ 'sync_row_scopes',
891
+ 'sync_commits',
892
+ 'sync_changes',
893
+ 'sync_change_scopes',
894
+ 'sync_push_results',
895
+ 'sync_reactions',
896
+ 'sync_clients',
897
+ 'sync_blob_refs',
898
+ 'sync_tombstones',
899
+ ] as const;
900
+
808
901
  interface D1MigrationState {
809
902
  phase: 'core' | 'prepare' | 'ddl' | 'rewrite';
810
903
  offset: number;
@@ -946,6 +1039,39 @@ export class D1ServerStorage implements ServerStorage {
946
1039
  this.#db.prepare('SELECT * FROM sync_schema_migration WHERE id=1'),
947
1040
  );
948
1041
  if (claim === null && marker?.schema_version === schema.version) {
1042
+ assertStoredLayouts(schema, marker.layouts);
1043
+ statementsExecuted++;
1044
+ const present = await this.#db
1045
+ .prepare(
1046
+ `SELECT name FROM sqlite_master WHERE type='table' AND name IN (${placeholders(D1_CORE_TABLES.length)})`,
1047
+ )
1048
+ .bind(...D1_CORE_TABLES)
1049
+ .all<{ name: string }>();
1050
+ const presentNames = new Set(present.results.map((row) => row.name));
1051
+ for (const name of D1_CORE_TABLES) {
1052
+ if (!presentNames.has(name)) {
1053
+ throw new StorageQueryError('sync.storage.physical_layout_mismatch', {
1054
+ table: name,
1055
+ reason: 'missing_table',
1056
+ });
1057
+ }
1058
+ }
1059
+ for (const table of tables) {
1060
+ statementsExecuted++;
1061
+ const { results } = await this.#db
1062
+ .prepare(`PRAGMA table_info(${quoteIdent(table.name)})`)
1063
+ .all<{ name: string; type: string; notnull: number; pk: number }>();
1064
+ assertPhysicalColumns(
1065
+ table,
1066
+ results.map((column) => ({
1067
+ name: column.name,
1068
+ type: column.type,
1069
+ notNull: column.notnull === 1,
1070
+ primaryKeyPosition: column.pk,
1071
+ })),
1072
+ 'sqlite',
1073
+ );
1074
+ }
949
1075
  this.#tables = schema.tables;
950
1076
  this.#schemaVersion = schema.version;
951
1077
  return { complete: true, statementsExecuted };
@@ -1448,6 +1574,7 @@ export class D1ServerStorage implements ServerStorage {
1448
1574
  throw new StorageQueryError('sync.storage.schema_changed');
1449
1575
  return table;
1450
1576
  },
1577
+ (query) => this.#prepareAuthoritativeQuery(partition, query),
1451
1578
  this.#pushApplySerialized,
1452
1579
  );
1453
1580
  }
@@ -1469,12 +1596,23 @@ export class D1ServerStorage implements ServerStorage {
1469
1596
  // re-checks the published marker inside this same batch, so condition 2
1470
1597
  // is already evaluated with the query (§2.4 D1 schema readiness).
1471
1598
  const db = this.#schemaDatabase();
1599
+ return queryAuthoritativeOn(
1600
+ db,
1601
+ partition,
1602
+ this.#prepareAuthoritativeQuery(partition, query),
1603
+ );
1604
+ }
1605
+
1606
+ #prepareAuthoritativeQuery(
1607
+ partition: string,
1608
+ query: AuthoritativeQueryRequest,
1609
+ ): BoundAuthoritativeQuery {
1472
1610
  if (this.#tables === undefined) {
1473
1611
  throw new Error(
1474
1612
  'ensureSchema(schema) must run before registered queries',
1475
1613
  );
1476
1614
  }
1477
- const prepared = bindAuthoritativePartition(
1615
+ return bindAuthoritativePartition(
1478
1616
  prepareAuthoritativeQuery(
1479
1617
  query.plan,
1480
1618
  query.params,
@@ -1483,41 +1621,6 @@ export class D1ServerStorage implements ServerStorage {
1483
1621
  ),
1484
1622
  partition,
1485
1623
  );
1486
- const results = await db.batch([
1487
- db.prepare(prepared.sql).bind(...prepared.params),
1488
- db
1489
- .prepare('SELECT max_commit_seq FROM sync_partitions WHERE partition=?')
1490
- .bind(partition),
1491
- ]);
1492
- const rowsResult = results[0];
1493
- const cursorResult = results[1];
1494
- if (
1495
- typeof rowsResult !== 'object' ||
1496
- rowsResult === null ||
1497
- !('results' in rowsResult) ||
1498
- !Array.isArray(rowsResult.results) ||
1499
- typeof cursorResult !== 'object' ||
1500
- cursorResult === null ||
1501
- !('results' in cursorResult) ||
1502
- !Array.isArray(cursorResult.results)
1503
- ) {
1504
- throw new Error('D1 registered query returned an invalid batch result');
1505
- }
1506
- const cursor = cursorResult.results[0];
1507
- const maxCommitSeq =
1508
- typeof cursor === 'object' &&
1509
- cursor !== null &&
1510
- 'max_commit_seq' in cursor &&
1511
- typeof cursor.max_commit_seq === 'number'
1512
- ? cursor.max_commit_seq
1513
- : 0;
1514
- return {
1515
- rows: rowsResult.results.filter(
1516
- (row): row is Readonly<Record<string, unknown>> =>
1517
- typeof row === 'object' && row !== null,
1518
- ),
1519
- maxCommitSeq,
1520
- };
1521
1624
  }
1522
1625
 
1523
1626
  async getPartitionLogEpoch(partition: string): Promise<string | undefined> {
package/src/operations.ts CHANGED
@@ -22,8 +22,13 @@ import { SyncError, syncError } from './errors';
22
22
  import { processPushOperationsWithTrace } from './push';
23
23
  import { compileSchema } from './schema';
24
24
  import { authorizeWrite, type ResolvedScopes } from './scopes';
25
- import type { AuthoritativeQueryValue } from './storage';
25
+ import type {
26
+ AuthoritativeQueryRequest,
27
+ AuthoritativeQueryResult,
28
+ AuthoritativeQueryValue,
29
+ } from './storage';
26
30
  import type { StorageTransaction } from './storage';
31
+ import { transactionQuery } from './storage-query';
27
32
  import { toValidateRow, type ValidateRow } from './validate';
28
33
 
29
34
  export interface RemoteQueryDependency {
@@ -107,6 +112,14 @@ export interface RemoteCommandContext {
107
112
  readonly operationId: string;
108
113
  readonly requestId: string;
109
114
  getRow(table: string, rowId: string): Promise<ValidateRow | undefined>;
115
+ /**
116
+ * Run a generated registered query on the command's push transaction
117
+ * (§6.7). The storage binds every relation to the partition; unlike
118
+ * `getRow`, the rows are not filtered by the actor's scopes.
119
+ */
120
+ queryAuthoritative(
121
+ query: AuthoritativeQueryRequest,
122
+ ): Promise<AuthoritativeQueryResult>;
110
123
  }
111
124
 
112
125
  export interface RemoteCommandOptions<Input> {
@@ -570,6 +583,7 @@ function commandContext(
570
583
  decodeRow(table.columns, stored.payload),
571
584
  );
572
585
  },
586
+ queryAuthoritative: (query) => transactionQuery(tx, query),
573
587
  };
574
588
  }
575
589
 
@@ -43,6 +43,7 @@ import { serveNotReadyError } from './readiness';
43
43
  import type { PushOperationResult } from '@syncular/core';
44
44
  import {
45
45
  bindAuthoritativePartition,
46
+ type BoundAuthoritativeQuery,
46
47
  postgresPlaceholders,
47
48
  prepareAuthoritativeQuery,
48
49
  } from './authoritative-query';
@@ -55,6 +56,7 @@ import {
55
56
  } from './pg-executor';
56
57
  import {
57
58
  assertPhysicalColumns,
59
+ assertStoredLayouts,
58
60
  commitWindowPageSql,
59
61
  deleteRowSql,
60
62
  dropTableDdl,
@@ -730,9 +732,36 @@ async function getPushResultOn(
730
732
  }
731
733
  }
732
734
 
735
+ /** Run one partition-bound registered query and the cursor read on `client`. */
736
+ async function queryAuthoritativeOn(
737
+ client: PgQueryable,
738
+ partition: string,
739
+ prepared: BoundAuthoritativeQuery,
740
+ ): Promise<AuthoritativeQueryResult> {
741
+ const result = await client.query<Readonly<Record<string, unknown>>>(
742
+ postgresPlaceholders(prepared.sql),
743
+ prepared.params,
744
+ );
745
+ const cursor = await client.query<{ max_commit_seq: unknown }>(
746
+ 'SELECT max_commit_seq FROM sync_partitions WHERE partition=$1',
747
+ [partition],
748
+ );
749
+ return {
750
+ rows: result.rows,
751
+ maxCommitSeq:
752
+ cursor.rows[0] === undefined
753
+ ? 0
754
+ : asNumber(cursor.rows[0].max_commit_seq),
755
+ };
756
+ }
757
+
733
758
  class PostgresTransaction implements StorageTransaction {
734
759
  #client: PgQueryable;
735
760
  #partition: string;
761
+ /** Storage-owned relation rewrite, bound to this transaction's partition. */
762
+ readonly #prepareQuery: (
763
+ query: AuthoritativeQueryRequest,
764
+ ) => BoundAuthoritativeQuery;
736
765
  /** Schema version this transaction writes; rides every appended commit row. */
737
766
  #writerVersion: number;
738
767
  #resolveTable: (name: string) => CompiledTable;
@@ -747,6 +776,7 @@ class PostgresTransaction implements StorageTransaction {
747
776
  partition: string,
748
777
  writerVersion: number,
749
778
  resolveTable: (name: string) => CompiledTable,
779
+ prepareQuery: (query: AuthoritativeQueryRequest) => BoundAuthoritativeQuery,
750
780
  resolve: () => void,
751
781
  reject: (error: unknown) => void,
752
782
  ) {
@@ -770,6 +800,7 @@ class PostgresTransaction implements StorageTransaction {
770
800
  this.#partition = partition;
771
801
  this.#writerVersion = writerVersion;
772
802
  this.#resolveTable = resolveTable;
803
+ this.#prepareQuery = prepareQuery;
773
804
  this.#resolve = resolve;
774
805
  this.#reject = reject;
775
806
  }
@@ -871,6 +902,19 @@ class PostgresTransaction implements StorageTransaction {
871
902
  );
872
903
  }
873
904
 
905
+ queryAuthoritative(
906
+ query: AuthoritativeQueryRequest,
907
+ ): Promise<AuthoritativeQueryResult> {
908
+ this.#assertOpen();
909
+ // Runs on this transaction's pinned client, so the read observes every
910
+ // staged row and never waits for a second pool connection.
911
+ return queryAuthoritativeOn(
912
+ this.#client,
913
+ this.#partition,
914
+ this.#prepareQuery(query),
915
+ );
916
+ }
917
+
874
918
  async lockPartitionForPush(): Promise<void> {
875
919
  this.#assertOpen();
876
920
  await lockPartitionOn(this.#client, this.#partition);
@@ -1229,65 +1273,47 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
1229
1273
  );
1230
1274
  }
1231
1275
  if (stored !== undefined && stored === schema.version) {
1232
- // Version equality is not layout equality: a marker written by another
1233
- // build at the same version describes rows the running codec cannot
1234
- // decode. Compare the stored layouts instead of trusting the number.
1235
- const storedLayouts = parseLayouts(
1276
+ assertStoredLayouts(
1277
+ schema,
1236
1278
  typeof marker.rows[0]?.layouts === 'string'
1237
1279
  ? marker.rows[0].layouts
1238
1280
  : undefined,
1239
1281
  );
1240
- const configuredLayouts = parseLayouts(layoutsOf(schema));
1241
- let mismatch: string | undefined;
1242
- for (const [tableName, columns] of Object.entries(configuredLayouts)) {
1243
- const storedTable = storedLayouts[tableName];
1244
- const columnCount = Math.max(columns.length, storedTable?.length ?? 0);
1245
- for (let index = 0; index < columnCount; index++) {
1246
- const expected = columns[index];
1247
- const actual = storedTable?.[index];
1248
- if (
1249
- expected !== undefined &&
1250
- actual !== undefined &&
1251
- actual.name === expected.name &&
1252
- actual.type === expected.type &&
1253
- actual.nullable === expected.nullable
1254
- ) {
1255
- continue;
1256
- }
1257
- mismatch = `table ${JSON.stringify(tableName)} column ${JSON.stringify(expected?.name ?? actual?.name ?? '')}`;
1258
- break;
1259
- }
1260
- if (mismatch !== undefined) break;
1261
- }
1262
- if (mismatch === undefined) {
1263
- for (const tableName of Object.keys(storedLayouts)) {
1264
- if (!(tableName in configuredLayouts)) {
1265
- mismatch = `table ${JSON.stringify(tableName)}`;
1266
- break;
1267
- }
1268
- }
1269
- }
1270
- if (mismatch !== undefined) {
1271
- throw new Error(
1272
- `stored schema layouts disagree with the configured schema at version ${schema.version} (${mismatch}) — refusing to serve a database whose stored rows the running code cannot decode`,
1273
- );
1274
- }
1275
- // The persisted layouts describe the codec's app columns only: read the
1276
- // physical tables so a same-version database missing a
1277
- // storage-internal column is refused at startup instead of failing at
1278
- // the first write. Resolve through `to_regclass`, which follows the
1279
- // session's `search_path` exactly as the unqualified row queries do —
1282
+ // Resolve through `to_regclass`, which follows the session's
1283
+ // `search_path` exactly as the unqualified row queries do:
1280
1284
  // `current_schema()` names only the first existing schema, so it would
1281
1285
  // reject a table that resolves later in the path.
1282
1286
  for (const table of schema.tables.values()) {
1283
- const { rows } = await this.#exec.query<{ column_name: string }>(
1284
- `SELECT attribute.attname AS column_name
1287
+ const { rows } = await this.#exec.query<{
1288
+ name: string;
1289
+ type: string;
1290
+ not_null: boolean;
1291
+ pk: number | null;
1292
+ }>(
1293
+ `SELECT attribute.attname AS name,
1294
+ format_type(attribute.atttypid, attribute.atttypmod) AS type,
1295
+ attribute.attnotnull AS not_null,
1296
+ (SELECT key.position::int
1297
+ FROM pg_index pk,
1298
+ unnest(pk.indkey::int2[]) WITH ORDINALITY
1299
+ AS key(attnum, position)
1300
+ WHERE pk.indrelid = attribute.attrelid AND pk.indisprimary
1301
+ AND key.attnum = attribute.attnum) AS pk
1285
1302
  FROM pg_attribute attribute
1286
1303
  WHERE attribute.attrelid = to_regclass($1)
1287
1304
  AND attribute.attnum > 0 AND NOT attribute.attisdropped`,
1288
1305
  [table.name],
1289
1306
  );
1290
- assertPhysicalColumns(table, new Set(rows.map((r) => r.column_name)));
1307
+ assertPhysicalColumns(
1308
+ table,
1309
+ rows.map((row) => ({
1310
+ name: row.name,
1311
+ type: row.type,
1312
+ notNull: row.not_null,
1313
+ primaryKeyPosition: row.pk ?? 0,
1314
+ })),
1315
+ 'postgres',
1316
+ );
1291
1317
  }
1292
1318
  }
1293
1319
  if (stored === undefined || stored < schema.version) {
@@ -1514,6 +1540,7 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
1514
1540
  partition,
1515
1541
  this.#schemaVersion ?? 0,
1516
1542
  (name) => this.table(name),
1543
+ (query) => this.#prepareAuthoritativeQuery(partition, query),
1517
1544
  resolveScope,
1518
1545
  rejectScope,
1519
1546
  );
@@ -1538,17 +1565,16 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
1538
1565
  return rows[0] === undefined ? 0 : asNumber(rows[0].max_commit_seq);
1539
1566
  }
1540
1567
 
1541
- async queryAuthoritative(
1568
+ #prepareAuthoritativeQuery(
1542
1569
  partition: string,
1543
1570
  query: AuthoritativeQueryRequest,
1544
- checkpoints?: readonly CheckpointDeclaration[],
1545
- ): Promise<AuthoritativeQueryResult> {
1571
+ ): BoundAuthoritativeQuery {
1546
1572
  if (this.#tables === undefined) {
1547
1573
  throw new Error(
1548
1574
  'ensureSchema(schema) must run before registered queries',
1549
1575
  );
1550
1576
  }
1551
- const prepared = bindAuthoritativePartition(
1577
+ return bindAuthoritativePartition(
1552
1578
  prepareAuthoritativeQuery(
1553
1579
  query.plan,
1554
1580
  query.params,
@@ -1557,6 +1583,14 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
1557
1583
  ),
1558
1584
  partition,
1559
1585
  );
1586
+ }
1587
+
1588
+ async queryAuthoritative(
1589
+ partition: string,
1590
+ query: AuthoritativeQueryRequest,
1591
+ checkpoints?: readonly CheckpointDeclaration[],
1592
+ ): Promise<AuthoritativeQueryResult> {
1593
+ const prepared = this.#prepareAuthoritativeQuery(partition, query);
1560
1594
  return this.#exec.transaction(async (client) => {
1561
1595
  await client.query(
1562
1596
  'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY',
@@ -1569,21 +1603,7 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
1569
1603
  const gate = await readServeGateOn(client, partition, runningVersion);
1570
1604
  const refusal = serveGateRefusal(gate, runningVersion, checkpoints);
1571
1605
  if (refusal !== undefined) throw serveNotReadyError(refusal);
1572
- const result = await client.query<Readonly<Record<string, unknown>>>(
1573
- postgresPlaceholders(prepared.sql),
1574
- prepared.params,
1575
- );
1576
- const cursor = await client.query<{ max_commit_seq: unknown }>(
1577
- 'SELECT max_commit_seq FROM sync_partitions WHERE partition=$1',
1578
- [partition],
1579
- );
1580
- return {
1581
- rows: result.rows,
1582
- maxCommitSeq:
1583
- cursor.rows[0] === undefined
1584
- ? 0
1585
- : asNumber(cursor.rows[0].max_commit_seq),
1586
- };
1606
+ return queryAuthoritativeOn(client, partition, prepared);
1587
1607
  });
1588
1608
  }
1589
1609
 
package/src/push.ts CHANGED
@@ -54,6 +54,7 @@ import type {
54
54
  } from './storage';
55
55
  import { serveGateRefusal } from './storage';
56
56
  import { StorageConstraintError } from './storage-errors';
57
+ import { transactionQuery } from './storage-query';
57
58
  import { serveNotReadyError } from './readiness';
58
59
  import type {
59
60
  CommitValidationReader,
@@ -186,6 +187,7 @@ async function runValidator(
186
187
  values: readonly RowValue[] | undefined,
187
188
  storedValues: readonly RowValue[] | undefined,
188
189
  opIndex: number,
190
+ tx: StorageTransaction,
189
191
  partition: string,
190
192
  actorId: string,
191
193
  ): Promise<TerminatingOutcome | undefined> {
@@ -206,7 +208,11 @@ async function runValidator(
206
208
  ? toValidateRow(table.columns, storedValues)
207
209
  : undefined,
208
210
  },
209
- { actorId, partition },
211
+ {
212
+ actorId,
213
+ partition,
214
+ queryAuthoritative: (query) => transactionQuery(tx, query),
215
+ },
210
216
  );
211
217
  } catch (error) {
212
218
  if (error instanceof ValidationRejection) {
@@ -363,6 +369,7 @@ async function applyOperation(
363
369
  undefined,
364
370
  storedValues,
365
371
  opIndex,
372
+ tx,
366
373
  partition,
367
374
  actorId,
368
375
  );
@@ -553,6 +560,7 @@ async function applyOperation(
553
560
  values,
554
561
  storedValues,
555
562
  opIndex,
563
+ tx,
556
564
  partition,
557
565
  actorId,
558
566
  );
@@ -683,6 +691,7 @@ async function applyOperation(
683
691
  values,
684
692
  undefined,
685
693
  opIndex,
694
+ tx,
686
695
  partition,
687
696
  actorId,
688
697
  );
@@ -836,6 +845,7 @@ function commitValidationReader(
836
845
  serverVersion: stored.serverVersion,
837
846
  }));
838
847
  },
848
+ queryAuthoritative: (query) => transactionQuery(tx, query),
839
849
  };
840
850
  }
841
851
 
@@ -1084,6 +1094,7 @@ async function enforceReferences(
1084
1094
  undefined,
1085
1095
  storedValues,
1086
1096
  item.originOpIndex,
1097
+ tx,
1087
1098
  partition,
1088
1099
  actorId,
1089
1100
  );
@@ -1117,6 +1128,7 @@ async function enforceReferences(
1117
1128
  values,
1118
1129
  storedValues,
1119
1130
  item.originOpIndex,
1131
+ tx,
1120
1132
  partition,
1121
1133
  actorId,
1122
1134
  );