@syncular/server 0.26.1 → 0.26.3
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/dist/d1-storage.js +75 -14
- package/dist/handler.js +7 -3
- package/dist/postgres-storage.js +79 -37
- package/dist/pull.d.ts +19 -0
- package/dist/pull.js +87 -33
- package/dist/relational-rows.d.ts +21 -0
- package/dist/relational-rows.js +42 -0
- package/package.json +2 -2
- package/src/d1-storage.ts +93 -33
- package/src/handler.ts +14 -4
- package/src/postgres-storage.ts +112 -50
- package/src/pull.ts +144 -39
- package/src/relational-rows.ts +44 -0
package/src/d1-storage.ts
CHANGED
|
@@ -915,12 +915,29 @@ interface D1MigrationClaim {
|
|
|
915
915
|
state: string;
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
+
/** The reads queued for one `D1ServerStorage#batchedAll` round trip. */
|
|
919
|
+
interface PendingReads {
|
|
920
|
+
readonly db: Pick<D1Database, 'prepare' | 'batch'>;
|
|
921
|
+
readonly entries: {
|
|
922
|
+
readonly statement: D1PreparedStatement;
|
|
923
|
+
readonly resolve: (rows: unknown[]) => void;
|
|
924
|
+
readonly reject: (error: unknown) => void;
|
|
925
|
+
}[];
|
|
926
|
+
}
|
|
927
|
+
|
|
918
928
|
export class D1ServerStorage implements ServerStorage {
|
|
919
929
|
readonly #db: D1Database;
|
|
920
930
|
readonly #pushApplySerialized: boolean;
|
|
921
931
|
/** Set by `ensureSchema`: app-table lookup for the relational row store. */
|
|
922
932
|
#tables: ReadonlyMap<string, CompiledTable> | undefined;
|
|
923
933
|
#schemaVersion: number | undefined;
|
|
934
|
+
/**
|
|
935
|
+
* Page reads waiting for the end of the current microtask turn. A pull
|
|
936
|
+
* starts the window read (or first snapshot page) of every subscription
|
|
937
|
+
* before awaiting any of them, so they leave as ONE `db.batch` round trip
|
|
938
|
+
* instead of one per subscription (SYNCULAR-PULL-ROUNDTRIPS-001).
|
|
939
|
+
*/
|
|
940
|
+
#pendingReads: PendingReads | undefined;
|
|
924
941
|
|
|
925
942
|
constructor(db: D1Database, options: D1ServerStorageOptions = {}) {
|
|
926
943
|
this.#db = db;
|
|
@@ -1400,6 +1417,53 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
1400
1417
|
}
|
|
1401
1418
|
|
|
1402
1419
|
/** Every protected read or write shares a transaction with its schema check. */
|
|
1420
|
+
/** Queue one read into the current turn's `db.batch` (see `#pendingReads`). */
|
|
1421
|
+
#batchedAll<T>(sql: string, params: unknown[]): Promise<T[]> {
|
|
1422
|
+
let pending = this.#pendingReads;
|
|
1423
|
+
if (pending === undefined) {
|
|
1424
|
+
const created: PendingReads = {
|
|
1425
|
+
db: this.#schemaDatabase(),
|
|
1426
|
+
entries: [],
|
|
1427
|
+
};
|
|
1428
|
+
pending = created;
|
|
1429
|
+
this.#pendingReads = created;
|
|
1430
|
+
queueMicrotask(() => {
|
|
1431
|
+
this.#pendingReads = undefined;
|
|
1432
|
+
created.db.batch(created.entries.map((entry) => entry.statement)).then(
|
|
1433
|
+
(results) => {
|
|
1434
|
+
created.entries.forEach((entry, i) => {
|
|
1435
|
+
const result = results[i];
|
|
1436
|
+
if (
|
|
1437
|
+
typeof result !== 'object' ||
|
|
1438
|
+
result === null ||
|
|
1439
|
+
!('results' in result) ||
|
|
1440
|
+
!Array.isArray(result.results)
|
|
1441
|
+
) {
|
|
1442
|
+
entry.reject(
|
|
1443
|
+
new Error('D1 query returned an invalid batch result'),
|
|
1444
|
+
);
|
|
1445
|
+
} else {
|
|
1446
|
+
entry.resolve(result.results);
|
|
1447
|
+
}
|
|
1448
|
+
});
|
|
1449
|
+
},
|
|
1450
|
+
(error: unknown) => {
|
|
1451
|
+
for (const entry of created.entries) entry.reject(error);
|
|
1452
|
+
},
|
|
1453
|
+
);
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
const target = pending;
|
|
1457
|
+
const statement = target.db.prepare(sql).bind(...params);
|
|
1458
|
+
return new Promise<T[]>((resolve, reject) => {
|
|
1459
|
+
target.entries.push({
|
|
1460
|
+
statement,
|
|
1461
|
+
resolve: (rows) => resolve(rows as T[]),
|
|
1462
|
+
reject,
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1403
1467
|
#schemaDatabase(): Pick<D1Database, 'prepare' | 'batch'> {
|
|
1404
1468
|
if (this.#schemaVersion === undefined)
|
|
1405
1469
|
throw new StorageQueryError('sync.storage.schema_changed');
|
|
@@ -2086,37 +2150,34 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
2086
2150
|
partition: string,
|
|
2087
2151
|
query: CommitWindowQuery,
|
|
2088
2152
|
): Promise<StoredCommit[]> {
|
|
2089
|
-
const db = this.#schemaDatabase();
|
|
2090
2153
|
const variables = Object.keys(query.scopeFilter).sort();
|
|
2091
2154
|
const firstVariable = variables[0];
|
|
2092
2155
|
if (firstVariable === undefined) return [];
|
|
2093
2156
|
const firstValues = query.scopeFilter[firstVariable] ?? [];
|
|
2094
2157
|
if (firstValues.length === 0) return [];
|
|
2095
2158
|
// Candidates via the inverted index (one variable) LEFT JOINed to the
|
|
2096
|
-
// commit meta + the table's changes — one
|
|
2097
|
-
//
|
|
2098
|
-
//
|
|
2159
|
+
// commit meta + the table's changes — one statement per page, never two
|
|
2160
|
+
// per candidate (see `commitWindowPageSql`), and concurrent pages share
|
|
2161
|
+
// one D1 round trip. Exact multi-variable verification against the
|
|
2162
|
+
// stored scope map in `collectCommitWindowPage`.
|
|
2099
2163
|
const sql = commitWindowPageSql(firstValues.length, 'sqlite');
|
|
2100
2164
|
const commits: StoredCommit[] = [];
|
|
2101
2165
|
let deliveredChanges = 0;
|
|
2102
2166
|
let afterSeq = query.afterSeq;
|
|
2103
2167
|
const batchSize = Math.max(64, query.limitChanges);
|
|
2104
2168
|
while (deliveredChanges < query.limitChanges) {
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
.
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
query.table,
|
|
2118
|
-
)
|
|
2119
|
-
.all<SqliteCommitWindowRecord>();
|
|
2169
|
+
const records = await this.#batchedAll<SqliteCommitWindowRecord>(sql, [
|
|
2170
|
+
partition,
|
|
2171
|
+
query.table,
|
|
2172
|
+
firstVariable,
|
|
2173
|
+
...firstValues,
|
|
2174
|
+
afterSeq,
|
|
2175
|
+
query.throughSeq,
|
|
2176
|
+
batchSize,
|
|
2177
|
+
partition,
|
|
2178
|
+
partition,
|
|
2179
|
+
query.table,
|
|
2180
|
+
]);
|
|
2120
2181
|
if (records.length === 0) break;
|
|
2121
2182
|
const page = collectCommitWindowPage(
|
|
2122
2183
|
records,
|
|
@@ -2132,12 +2193,12 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
2132
2193
|
}
|
|
2133
2194
|
|
|
2134
2195
|
async scanRows(partition: string, query: RowScanQuery): Promise<StoredRow[]> {
|
|
2135
|
-
const db = this.#schemaDatabase();
|
|
2136
2196
|
const firstVariable = assertScopeIndexedScan(query);
|
|
2137
2197
|
const firstValues = query.scopeFilter[firstVariable] ?? [];
|
|
2138
2198
|
if (firstValues.length === 0) return [];
|
|
2139
2199
|
// Candidates via the inverted index LEFT JOINed to the row table — one
|
|
2140
|
-
//
|
|
2200
|
+
// statement per page, never one per row (see `scanRowPageSql`), and
|
|
2201
|
+
// concurrent pages share one D1 round trip.
|
|
2141
2202
|
// Exact multi-variable verification against the stored scope map below.
|
|
2142
2203
|
const sql = scanRowPageSql(
|
|
2143
2204
|
this.table(query.table),
|
|
@@ -2148,18 +2209,17 @@ export class D1ServerStorage implements ServerStorage {
|
|
|
2148
2209
|
let afterRowId = query.afterRowId ?? '';
|
|
2149
2210
|
const batchSize = Math.max(64, query.limit);
|
|
2150
2211
|
while (rows.length < query.limit) {
|
|
2151
|
-
const
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
.all<SqliteRowRecord & { payload: Uint8Array | null }>();
|
|
2212
|
+
const records = await this.#batchedAll<
|
|
2213
|
+
SqliteRowRecord & { payload: Uint8Array | null }
|
|
2214
|
+
>(sql, [
|
|
2215
|
+
partition,
|
|
2216
|
+
query.table,
|
|
2217
|
+
firstVariable,
|
|
2218
|
+
...firstValues,
|
|
2219
|
+
afterRowId,
|
|
2220
|
+
batchSize,
|
|
2221
|
+
partition,
|
|
2222
|
+
]);
|
|
2163
2223
|
if (records.length === 0) break;
|
|
2164
2224
|
for (const record of records) {
|
|
2165
2225
|
afterRowId = record.row_id;
|
package/src/handler.ts
CHANGED
|
@@ -47,8 +47,9 @@ import {
|
|
|
47
47
|
ACCEPT_INLINE_ROWS,
|
|
48
48
|
clampPullLimits,
|
|
49
49
|
type PullSectionTrace,
|
|
50
|
+
prepareSections,
|
|
51
|
+
preparedSection,
|
|
50
52
|
type SubscriptionPlan,
|
|
51
|
-
subscriptionSection,
|
|
52
53
|
} from './pull';
|
|
53
54
|
import { type ProcessedPushCommit, processPushCommitWithTrace } from './push';
|
|
54
55
|
import { serveNotReadyError } from './readiness';
|
|
@@ -542,16 +543,25 @@ async function* streamResponse(
|
|
|
542
543
|
const limits = clampPullLimits(plan.pull);
|
|
543
544
|
const maxSeq = await ctx.storage.getMaxCommitSeq(ctx.partition);
|
|
544
545
|
const horizonSeq = await ctx.storage.getHorizonSeq(ctx.partition);
|
|
545
|
-
|
|
546
|
+
const prepared = await prepareSections(
|
|
547
|
+
ctx,
|
|
548
|
+
limits,
|
|
549
|
+
plan.subscriptions,
|
|
550
|
+
maxSeq,
|
|
551
|
+
horizonSeq,
|
|
552
|
+
);
|
|
553
|
+
for (const [index, subscription] of plan.subscriptions.entries()) {
|
|
546
554
|
const trace: PullSectionTrace | undefined =
|
|
547
555
|
summaries !== undefined ? { segments: [] } : undefined;
|
|
548
|
-
const
|
|
556
|
+
const reads = prepared[index];
|
|
557
|
+
if (reads === undefined) throw new Error('prepareSections lost a plan');
|
|
558
|
+
const section = preparedSection(
|
|
549
559
|
ctx,
|
|
550
560
|
schema,
|
|
551
561
|
limits,
|
|
552
562
|
subscription,
|
|
553
563
|
maxSeq,
|
|
554
|
-
|
|
564
|
+
reads,
|
|
555
565
|
trace,
|
|
556
566
|
plan.logEpoch,
|
|
557
567
|
);
|
package/src/postgres-storage.ts
CHANGED
|
@@ -53,11 +53,12 @@ import {
|
|
|
53
53
|
asNumber,
|
|
54
54
|
type PgExecutor,
|
|
55
55
|
type PgQueryable,
|
|
56
|
+
type PgRow,
|
|
56
57
|
} from './pg-executor';
|
|
57
58
|
import {
|
|
58
59
|
assertPhysicalColumns,
|
|
59
60
|
assertStoredLayouts,
|
|
60
|
-
|
|
61
|
+
COMMIT_WINDOW_BATCH_SQL,
|
|
61
62
|
deleteRowSql,
|
|
62
63
|
dropTableDdl,
|
|
63
64
|
indexRowPageStatement,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
rewriteValues,
|
|
71
72
|
SCHEMA_META_DDL_POSTGRES,
|
|
72
73
|
type StoredColumnLayout,
|
|
74
|
+
scanRowBatchSql,
|
|
73
75
|
scanRowPageSql,
|
|
74
76
|
schemaDdl,
|
|
75
77
|
selectRowScopesSql,
|
|
@@ -324,28 +326,29 @@ async function readServeGateOn(
|
|
|
324
326
|
partition: string,
|
|
325
327
|
runningSchemaVersion: number,
|
|
326
328
|
): Promise<ServeGate> {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
// One statement: every request reads the gate twice (entry and verify),
|
|
330
|
+
// so its parts share one round trip and one snapshot.
|
|
331
|
+
const { rows } = await client.query<{
|
|
332
|
+
schema_version: unknown;
|
|
333
|
+
log_epoch: string | null;
|
|
334
|
+
pending: unknown;
|
|
335
|
+
}>(
|
|
336
|
+
`SELECT (SELECT schema_version FROM sync_schema_meta WHERE id=1) AS schema_version,
|
|
337
|
+
(SELECT log_epoch FROM sync_partition_registry WHERE partition=$1) AS log_epoch,
|
|
338
|
+
(SELECT json_agg(json_build_object('partition', partition, 'name', name))
|
|
339
|
+
FROM sync_backfill_checkpoints
|
|
340
|
+
WHERE schema_version=$2 AND state<>'activated') AS pending`,
|
|
341
|
+
[partition, runningSchemaVersion],
|
|
338
342
|
);
|
|
343
|
+
const row = rows[0];
|
|
339
344
|
return {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
name: row.name,
|
|
348
|
-
})),
|
|
345
|
+
// asNumber maps the NULL of a missing marker row to 0.
|
|
346
|
+
storedSchemaVersion: asNumber(row?.schema_version),
|
|
347
|
+
logEpoch: row?.log_epoch ?? undefined,
|
|
348
|
+
pending:
|
|
349
|
+
row?.pending === null || row?.pending === undefined
|
|
350
|
+
? []
|
|
351
|
+
: asJson<{ partition: string; name: string }[]>(row.pending),
|
|
349
352
|
};
|
|
350
353
|
}
|
|
351
354
|
|
|
@@ -569,7 +572,7 @@ interface ChangeRecord {
|
|
|
569
572
|
}
|
|
570
573
|
|
|
571
574
|
/**
|
|
572
|
-
* One result row of `
|
|
575
|
+
* One result row of `COMMIT_WINDOW_BATCH_SQL` (candidate LEFT JOIN commit meta
|
|
573
576
|
* LEFT JOIN changes): meta/change columns are NULL when the joined row
|
|
574
577
|
* vanished (LEFT JOIN contract).
|
|
575
578
|
*/
|
|
@@ -1166,16 +1169,76 @@ class RollbackSignal extends Error {
|
|
|
1166
1169
|
}
|
|
1167
1170
|
}
|
|
1168
1171
|
|
|
1172
|
+
/** One page read waiting in `PostgresServerStorage#batchedPage`. */
|
|
1173
|
+
interface PendingPage {
|
|
1174
|
+
readonly spec: Record<string, unknown>;
|
|
1175
|
+
readonly resolve: (rows: PgRow[]) => void;
|
|
1176
|
+
readonly reject: (error: unknown) => void;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1169
1179
|
export class PostgresServerStorage implements ServerStorage {
|
|
1170
1180
|
readonly #exec: PgExecutor;
|
|
1171
1181
|
/** Set by `ensureSchema`: app-table lookup for the relational row store. */
|
|
1172
1182
|
#tables: ReadonlyMap<string, CompiledTable> | undefined;
|
|
1173
1183
|
#schemaVersion: number | undefined;
|
|
1184
|
+
/**
|
|
1185
|
+
* Page reads waiting for the end of the current microtask turn, keyed by
|
|
1186
|
+
* statement and fixed params. A pull starts the window read (or first
|
|
1187
|
+
* snapshot page) of every subscription before awaiting any of them, so the
|
|
1188
|
+
* reads of one table leave as ONE set-based statement instead of one round
|
|
1189
|
+
* trip per subscription (SYNCULAR-PULL-ROUNDTRIPS-001).
|
|
1190
|
+
*/
|
|
1191
|
+
readonly #pageBatches = new Map<string, PendingPage[]>();
|
|
1174
1192
|
|
|
1175
1193
|
constructor(exec: PgExecutor) {
|
|
1176
1194
|
this.#exec = exec;
|
|
1177
1195
|
}
|
|
1178
1196
|
|
|
1197
|
+
/**
|
|
1198
|
+
* Queue one page read into the batch for (`sql`, `partition`, `table`).
|
|
1199
|
+
* The batch runs as a single statement with the queued specs bound as a
|
|
1200
|
+
* JSON array (`$2`); each spec gets its position as `i`, and the result
|
|
1201
|
+
* rows are split back by their `sub` column.
|
|
1202
|
+
*/
|
|
1203
|
+
#batchedPage<Row>(
|
|
1204
|
+
sql: string,
|
|
1205
|
+
partition: string,
|
|
1206
|
+
table: string,
|
|
1207
|
+
spec: Record<string, unknown>,
|
|
1208
|
+
): Promise<Row[]> {
|
|
1209
|
+
const key = `${sql}\u0000${partition}\u0000${table}`;
|
|
1210
|
+
let batch = this.#pageBatches.get(key);
|
|
1211
|
+
if (batch === undefined) {
|
|
1212
|
+
const queued: PendingPage[] = [];
|
|
1213
|
+
batch = queued;
|
|
1214
|
+
this.#pageBatches.set(key, queued);
|
|
1215
|
+
queueMicrotask(() => {
|
|
1216
|
+
this.#pageBatches.delete(key);
|
|
1217
|
+
const specs = JSON.stringify(
|
|
1218
|
+
queued.map((entry, i) => ({ ...entry.spec, i })),
|
|
1219
|
+
);
|
|
1220
|
+
this.#exec.query<PgRow>(sql, [partition, specs, table]).then(
|
|
1221
|
+
({ rows }) => {
|
|
1222
|
+
const grouped = queued.map((): PgRow[] => []);
|
|
1223
|
+
for (const row of rows) grouped[asNumber(row.sub)]?.push(row);
|
|
1224
|
+
queued.forEach((entry, i) => entry.resolve(grouped[i] ?? []));
|
|
1225
|
+
},
|
|
1226
|
+
(error: unknown) => {
|
|
1227
|
+
for (const entry of queued) entry.reject(error);
|
|
1228
|
+
},
|
|
1229
|
+
);
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
const target = batch;
|
|
1233
|
+
return new Promise<Row[]>((resolve, reject) => {
|
|
1234
|
+
target.push({
|
|
1235
|
+
spec,
|
|
1236
|
+
resolve: (rows) => resolve(rows as Row[]),
|
|
1237
|
+
reject,
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1179
1242
|
/** Apply the schema DDL (idempotent). Call once before use. */
|
|
1180
1243
|
async migrate(): Promise<void> {
|
|
1181
1244
|
// Split on the statement boundary so drivers that reject multi-statement
|
|
@@ -2091,27 +2154,26 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
|
|
|
2091
2154
|
// Candidates via the inverted index (one variable): the (partition, tbl,
|
|
2092
2155
|
// var, value, commit_seq) PK makes the candidate subquery an index range
|
|
2093
2156
|
// scan returning commit_seq already ascending (see postgres-explain
|
|
2094
|
-
// .test.ts), LEFT JOINed to the commit meta + the table's changes
|
|
2095
|
-
//
|
|
2096
|
-
// `
|
|
2097
|
-
// stored scope map happens below.
|
|
2098
|
-
const sql = commitWindowPageSql(firstValues.length, 'postgres');
|
|
2157
|
+
// .test.ts), LEFT JOINed to the commit meta + the table's changes. Pages
|
|
2158
|
+
// of concurrent window reads on the same table share one statement (see
|
|
2159
|
+
// `COMMIT_WINDOW_BATCH_SQL`). Exact multi-variable verification against
|
|
2160
|
+
// the stored scope map happens below.
|
|
2099
2161
|
const commits: StoredCommit[] = [];
|
|
2100
2162
|
let deliveredChanges = 0;
|
|
2101
2163
|
let afterSeq = query.afterSeq;
|
|
2102
2164
|
const batchSize = Math.max(64, query.limitChanges);
|
|
2103
2165
|
while (deliveredChanges < query.limitChanges) {
|
|
2104
|
-
const
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
firstVariable,
|
|
2110
|
-
|
|
2111
|
-
afterSeq,
|
|
2112
|
-
query.throughSeq,
|
|
2113
|
-
batchSize,
|
|
2114
|
-
|
|
2166
|
+
const records = await this.#batchedPage<CommitWindowRecord>(
|
|
2167
|
+
COMMIT_WINDOW_BATCH_SQL,
|
|
2168
|
+
partition,
|
|
2169
|
+
query.table,
|
|
2170
|
+
{
|
|
2171
|
+
var: firstVariable,
|
|
2172
|
+
vals: firstValues,
|
|
2173
|
+
after: afterSeq,
|
|
2174
|
+
through: query.throughSeq,
|
|
2175
|
+
lim: batchSize,
|
|
2176
|
+
},
|
|
2115
2177
|
);
|
|
2116
2178
|
if (records.length === 0) break;
|
|
2117
2179
|
// Fold the page: rows arrive ordered (commit_seq, idx); consecutive
|
|
@@ -2170,25 +2232,25 @@ FOR EACH ROW EXECUTE FUNCTION syncular_writer_fence()`);
|
|
|
2170
2232
|
if (firstValues.length === 0) return [];
|
|
2171
2233
|
// Candidates via the inverted index (ordered + LIMITed at the covering
|
|
2172
2234
|
// PK) LEFT JOINed to the row table — one round trip per page, never one
|
|
2173
|
-
// per row
|
|
2235
|
+
// per row, and pages of concurrent scans on the same table share one
|
|
2236
|
+
// statement (see `scanRowBatchSql`). Exact multi-variable verification
|
|
2174
2237
|
// against the stored scope map happens below.
|
|
2175
|
-
const sql =
|
|
2176
|
-
this.table(query.table),
|
|
2177
|
-
firstValues.length,
|
|
2178
|
-
'postgres',
|
|
2179
|
-
);
|
|
2238
|
+
const sql = scanRowBatchSql(this.table(query.table));
|
|
2180
2239
|
const rows: StoredRow[] = [];
|
|
2181
2240
|
let afterRowId = query.afterRowId ?? '';
|
|
2182
2241
|
const batchSize = Math.max(64, query.limit);
|
|
2183
2242
|
while (rows.length < query.limit) {
|
|
2184
|
-
const
|
|
2243
|
+
const records = await this.#batchedPage<RowRecord>(
|
|
2244
|
+
sql,
|
|
2185
2245
|
partition,
|
|
2186
2246
|
query.table,
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2247
|
+
{
|
|
2248
|
+
var: firstVariable,
|
|
2249
|
+
vals: firstValues,
|
|
2250
|
+
after: afterRowId,
|
|
2251
|
+
lim: batchSize,
|
|
2252
|
+
},
|
|
2253
|
+
);
|
|
2192
2254
|
if (records.length === 0) break;
|
|
2193
2255
|
for (const record of records) {
|
|
2194
2256
|
afterRowId = record.row_id;
|