@rocicorp/zero 1.8.0-canary.2 → 1.8.0-canary.4

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.
Files changed (30) hide show
  1. package/out/z2s/src/compiler.d.ts +2 -1
  2. package/out/z2s/src/compiler.d.ts.map +1 -1
  3. package/out/z2s/src/compiler.js +41 -4
  4. package/out/z2s/src/compiler.js.map +1 -1
  5. package/out/zero/package.js +1 -1
  6. package/out/zero/package.js.map +1 -1
  7. package/out/zero-cache/src/observability/metrics.d.ts +4 -0
  8. package/out/zero-cache/src/observability/metrics.d.ts.map +1 -1
  9. package/out/zero-cache/src/observability/metrics.js +16 -6
  10. package/out/zero-cache/src/observability/metrics.js.map +1 -1
  11. package/out/zero-cache/src/services/change-source/pg/initial-sync.d.ts.map +1 -1
  12. package/out/zero-cache/src/services/change-source/pg/initial-sync.js +247 -40
  13. package/out/zero-cache/src/services/change-source/pg/initial-sync.js.map +1 -1
  14. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.d.ts.map +1 -1
  15. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js +22 -3
  16. package/out/zero-cache/src/services/change-streamer/litestream3-backup-monitor.js.map +1 -1
  17. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.d.ts.map +1 -1
  18. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js +21 -2
  19. package/out/zero-cache/src/services/change-streamer/vfs-backup-monitor.js.map +1 -1
  20. package/out/zero-cache/src/services/litestream/commands.d.ts.map +1 -1
  21. package/out/zero-cache/src/services/litestream/commands.js +166 -46
  22. package/out/zero-cache/src/services/litestream/commands.js.map +1 -1
  23. package/out/zero-cache/src/services/litestream/metrics.d.ts +30 -0
  24. package/out/zero-cache/src/services/litestream/metrics.d.ts.map +1 -0
  25. package/out/zero-cache/src/services/litestream/metrics.js +112 -0
  26. package/out/zero-cache/src/services/litestream/metrics.js.map +1 -0
  27. package/out/zero-client/src/client/version.js +1 -1
  28. package/out/zqlite/src/query-builder.js +5 -3
  29. package/out/zqlite/src/query-builder.js.map +1 -1
  30. package/package.json +1 -1
@@ -8,6 +8,7 @@ import { liteTableName } from "../../../types/names.js";
8
8
  import { mapPostgresToLite, mapPostgresToLiteIndex } from "../../../db/pg-to-lite.js";
9
9
  import { ColumnMetadataStore } from "../../replicator/schema/column-metadata.js";
10
10
  import { listIndexes, listTables } from "../../../db/lite-tables.js";
11
+ import { getOrCreateCounter, getOrCreateHistogram } from "../../../observability/metrics.js";
11
12
  import { initReplicationState } from "../../replicator/schema/replication-state.js";
12
13
  import { id } from "../../../types/sql.js";
13
14
  import { connectPgClient, pgClient } from "../../../types/pg.js";
@@ -36,17 +37,22 @@ import { pipeline as pipeline$1 } from "node:stream/promises";
36
37
  async function initialSync(lc, shard, tx, upstreamURI, syncOptions, context) {
37
38
  if (!ALLOWED_APP_ID_CHARACTERS.test(shard.appID)) throw new Error("The App ID may only consist of lower-case letters, numbers, and the underscore character");
38
39
  const { tableCopyWorkers, profileCopy, textCopy = false, replicationSlotFailover = false, shadow } = syncOptions;
39
- const copyProfiler = profileCopy ? await CpuProfiler.connect() : null;
40
- const sql = await connectPgClient(lc, upstreamURI, "initial-sync");
41
- const replicationSession = shadow ? void 0 : pgClient(lc, upstreamURI, "initial-sync-replication-session", {
42
- ["fetch_types"]: false,
43
- connection: { replication: "database" }
44
- });
40
+ const syncMode = shadow ? "shadow" : "initial";
41
+ const copyFormat = textCopy ? "text" : "binary";
42
+ const start = performance.now();
43
+ let sql;
44
+ let replicationSession;
45
45
  const replicaID = Date.now().toString();
46
46
  let slotName;
47
47
  const statusPublisher = ReplicationStatusPublisher.forRunningTransaction(tx, shadow ? async () => {} : void 0).publish(lc, "Initializing");
48
48
  let releaseShadowSnapshot;
49
49
  try {
50
+ const copyProfiler = profileCopy ? await CpuProfiler.connect() : null;
51
+ sql = await connectPgClient(lc, upstreamURI, "initial-sync");
52
+ replicationSession = shadow ? void 0 : pgClient(lc, upstreamURI, "initial-sync-replication-session", {
53
+ ["fetch_types"]: false,
54
+ connection: { replication: "database" }
55
+ });
50
56
  const pgVersion = await checkUpstreamConfig(sql);
51
57
  const { publications } = shadow ? await getInternalShardConfig(sql, shard) : await ensurePublishedTables(lc, sql, shard);
52
58
  lc.info?.(`Upstream is setup with publications [${publications}]`);
@@ -67,7 +73,6 @@ async function initialSync(lc, shard, tx, upstreamURI, syncOptions, context) {
67
73
  }
68
74
  const initialVersion = toStateVersionString(lsn);
69
75
  initReplicationState(tx, publications, initialVersion, context);
70
- const start = performance.now();
71
76
  const published = await runTx(sql, async (tx) => {
72
77
  await tx.unsafe(`SET TRANSACTION SNAPSHOT '${snapshot}'`);
73
78
  return getPublicationInfo(tx, publications);
@@ -89,16 +94,12 @@ async function initialSync(lc, shard, tx, upstreamURI, syncOptions, context) {
89
94
  const downloads = await Promise.all(tables.map((spec) => copiers.processReadTask((db, lc) => getInitialDownloadState(lc, db, spec, shadow !== void 0))));
90
95
  statusPublisher.publish(lc, "Initializing", `Copying ${numTables} upstream tables at version ${initialVersion}`, 5e3, () => ({ downloadStatus: downloads.map(({ status }) => status) }));
91
96
  copyProfiler?.start();
92
- const rowCounts = await Promise.all(downloads.map((table) => copiers.processReadTask((db, lc) => copy(lc, table, copyPool, db, tx, textCopy, sampleRate, maxRowsPerTable))));
97
+ const copyStart = performance.now();
98
+ const copyResults = await Promise.all(downloads.map((table) => copiers.processReadTask((db, lc) => copy(lc, table, copyPool, db, tx, textCopy, syncMode, sampleRate, maxRowsPerTable))));
99
+ const copyElapsed = performance.now() - copyStart;
93
100
  copyProfiler?.stopAndDispose(lc, "initial-copy");
94
101
  copiers.setDone();
95
- const total = rowCounts.reduce((acc, curr) => ({
96
- rows: acc.rows + curr.rows,
97
- flushTime: acc.flushTime + curr.flushTime
98
- }), {
99
- rows: 0,
100
- flushTime: 0
101
- });
102
+ const copySummary = initialSyncCopySummary(copyResults, copyElapsed);
102
103
  statusPublisher.publish(lc, "Indexing", `Creating ${indexes.length} indexes`, 5e3);
103
104
  const indexStart = performance.now();
104
105
  createLiteIndices(lc, tx, indexes);
@@ -108,16 +109,45 @@ async function initialSync(lc, shard, tx, upstreamURI, syncOptions, context) {
108
109
  else {
109
110
  assert(shadow, "expected to be in shadow sync if there is no slotName");
110
111
  const rowsByTable = /* @__PURE__ */ new Map();
111
- for (let i = 0; i < downloads.length; i++) rowsByTable.set(downloads[i].status.table, rowCounts[i].rows);
112
+ for (let i = 0; i < downloads.length; i++) rowsByTable.set(downloads[i].status.table, copyResults[i].rows);
112
113
  verifyShadowReplica(lc, tx, published, rowsByTable);
113
114
  }
114
115
  const elapsed = performance.now() - start;
115
- lc.info?.(`Synced ${total.rows.toLocaleString()} rows of ${numTables} tables in ${publications} up to ${lsn} (flush: ${total.flushTime.toFixed(3)}, index: ${index.toFixed(3)}, total: ${elapsed.toFixed(3)} ms)`);
116
+ const copyOtherMs = Math.max(0, elapsed - copySummary.flushMs - index);
117
+ recordInitialSyncRunMetrics({
118
+ durationMs: elapsed,
119
+ rows: copySummary.rows,
120
+ copyBytes: copySummary.copyBytes,
121
+ copyMs: copySummary.copyMs,
122
+ copyOtherMs,
123
+ flushMs: copySummary.flushMs,
124
+ indexMs: index
125
+ }, {
126
+ result: "success",
127
+ syncMode,
128
+ copyFormat
129
+ });
130
+ lc.info?.(`Synced ${copySummary.rows.toLocaleString()} rows of ${numTables} tables in ${publications} up to ${lsn} (flush: ${copySummary.flushMs.toFixed(3)}, index: ${index.toFixed(3)}, total: ${elapsed.toFixed(3)} ms)`, {
131
+ syncMode,
132
+ copyFormat,
133
+ publications,
134
+ lsn,
135
+ ...copySummary,
136
+ indexes: indexes.length,
137
+ indexMs: index,
138
+ copyOtherMs,
139
+ totalMs: elapsed
140
+ });
116
141
  } finally {
117
142
  copyPool.end().catch((e) => lc.warn?.(`Error closing copyPool`, e));
118
143
  }
119
144
  } catch (e) {
120
- if (slotName) {
145
+ recordInitialSyncRunMetrics({ durationMs: performance.now() - start }, {
146
+ result: "error",
147
+ syncMode,
148
+ copyFormat
149
+ });
150
+ if (slotName && sql) {
121
151
  lc.warn?.(`dropping replication slot ${slotName}`, e);
122
152
  await sql`
123
153
  SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots
@@ -129,7 +159,7 @@ async function initialSync(lc, shard, tx, upstreamURI, syncOptions, context) {
129
159
  statusPublisher.stop();
130
160
  if (releaseShadowSnapshot) await releaseShadowSnapshot().catch((e) => lc.warn?.(`Error releasing shadow snapshot`, e));
131
161
  if (replicationSession) await replicationSession.end();
132
- await sql.end();
162
+ if (sql) await sql.end();
133
163
  }
134
164
  }
135
165
  /**
@@ -368,6 +398,113 @@ function makeDownloadStatements(table, cols, sampleRate, maxRowsPerTable, select
368
398
  getTotalBytes: `SELECT ${totalBytes} AS "totalBytes" ${fromTable}`
369
399
  };
370
400
  }
401
+ var INITIAL_SYNC_DURATION_HISTOGRAM_BOUNDARIES_S = [
402
+ 1,
403
+ 2,
404
+ 5,
405
+ 10,
406
+ 30,
407
+ 60,
408
+ 120,
409
+ 300,
410
+ 600,
411
+ 1200,
412
+ 2400,
413
+ 3600,
414
+ 7200
415
+ ];
416
+ var SLOW_COPY_FLUSH_MS = 1e4;
417
+ function initialSyncRuns() {
418
+ return getOrCreateCounter("replication", "initial_sync_runs", "Initial sync runs, labeled by result.");
419
+ }
420
+ function initialSyncDuration() {
421
+ return initialSyncDurationHistogram("initial_sync_duration", "Wall-clock duration of an initial sync run, labeled by result.");
422
+ }
423
+ function initialSyncCopyDuration() {
424
+ return initialSyncDurationHistogram("initial_sync_copy_duration", "Wall-clock duration of the COPY phase for a successful initial sync run.");
425
+ }
426
+ function initialSyncCopyOtherDuration() {
427
+ return initialSyncDurationHistogram("initial_sync_copy_other_duration", "Initial sync total duration excluding SQLite flush and index time for a successful run.");
428
+ }
429
+ function initialSyncFlushDuration() {
430
+ return initialSyncDurationHistogram("initial_sync_flush_duration", "Total SQLite flush time for a successful initial sync run.");
431
+ }
432
+ function initialSyncIndexDuration() {
433
+ return initialSyncDurationHistogram("initial_sync_index_duration", "SQLite index creation time for a successful initial sync run.");
434
+ }
435
+ function initialSyncRows() {
436
+ return getOrCreateCounter("replication", "initial_sync_rows", "Rows copied during successful initial sync runs.");
437
+ }
438
+ function initialSyncCopyStream() {
439
+ return getOrCreateCounter("replication", "initial_sync_copy_stream", {
440
+ description: "PostgreSQL COPY stream bytes processed during initial sync, including in-progress and failed runs.",
441
+ unit: "bytes"
442
+ });
443
+ }
444
+ function initialSyncCompletedCopyStream() {
445
+ return getOrCreateCounter("replication", "initial_sync_completed_copy_stream", {
446
+ description: "PostgreSQL COPY stream bytes processed during successful initial sync runs.",
447
+ unit: "bytes"
448
+ });
449
+ }
450
+ function initialSyncCopyChunks() {
451
+ return getOrCreateCounter("replication", "initial_sync_copy_chunks", "PostgreSQL COPY stream chunks processed during initial sync.");
452
+ }
453
+ function initialSyncDurationHistogram(name, description) {
454
+ return getOrCreateHistogram("replication", name, {
455
+ description,
456
+ unit: "s",
457
+ bucketBoundaries: INITIAL_SYNC_DURATION_HISTOGRAM_BOUNDARIES_S
458
+ });
459
+ }
460
+ function initialSyncMetricAttrs(attrs) {
461
+ return {
462
+ sync_mode: attrs.syncMode,
463
+ copy_format: attrs.copyFormat
464
+ };
465
+ }
466
+ function initialSyncRunMetricAttrs(attrs) {
467
+ return {
468
+ ...initialSyncMetricAttrs(attrs),
469
+ result: attrs.result
470
+ };
471
+ }
472
+ function recordInitialSyncRunMetrics(stats, attrs) {
473
+ const labels = initialSyncRunMetricAttrs(attrs);
474
+ initialSyncRuns().add(1, labels);
475
+ initialSyncDuration().recordMs(stats.durationMs, labels);
476
+ if (attrs.result === "success") {
477
+ if (stats.copyMs !== void 0) initialSyncCopyDuration().recordMs(stats.copyMs, labels);
478
+ if (stats.copyOtherMs !== void 0) initialSyncCopyOtherDuration().recordMs(stats.copyOtherMs, labels);
479
+ if (stats.flushMs !== void 0) initialSyncFlushDuration().recordMs(stats.flushMs, labels);
480
+ if (stats.indexMs !== void 0) initialSyncIndexDuration().recordMs(stats.indexMs, labels);
481
+ if (stats.rows !== void 0 && stats.rows > 0) initialSyncRows().add(stats.rows, labels);
482
+ if (stats.copyBytes !== void 0 && stats.copyBytes > 0) initialSyncCompletedCopyStream().add(stats.copyBytes, labels);
483
+ }
484
+ }
485
+ function initialSyncCopySummary(results, copyMs) {
486
+ const totals = results.reduce((acc, curr) => {
487
+ acc.rows += curr.rows;
488
+ acc.flushMs += curr.flushMs;
489
+ acc.copyBytes += curr.copyBytes;
490
+ return acc;
491
+ }, {
492
+ rows: 0,
493
+ flushMs: 0,
494
+ copyBytes: 0
495
+ });
496
+ return {
497
+ tables: results.length,
498
+ rows: totals.rows,
499
+ copyMs,
500
+ flushMs: totals.flushMs,
501
+ copyBytes: totals.copyBytes
502
+ };
503
+ }
504
+ function logSlowCopyFlush(lc, details) {
505
+ if (details.elapsedMs < SLOW_COPY_FLUSH_MS || details.flushedRows === 0) return;
506
+ lc.info?.("initial-sync table copy slow flush", details);
507
+ }
371
508
  async function getInitialDownloadState(lc, sql, spec, skipTotals) {
372
509
  const start = performance.now();
373
510
  const table = liteTableName(spec);
@@ -403,13 +540,21 @@ async function getInitialDownloadState(lc, sql, spec, skipTotals) {
403
540
  lc.info?.(`Computed initial download state for ${table} (${elapsed} ms)`, { state: state.status });
404
541
  return state;
405
542
  }
406
- function copy(lc, { spec: table, status }, dbClient, from, to, textCopy, sampleRate, maxRowsPerTable) {
407
- if (textCopy) return copyText(lc, table, status, dbClient, from, to, sampleRate, maxRowsPerTable);
408
- return copyBinary(lc, table, status, from, to, sampleRate, maxRowsPerTable);
543
+ function copy(lc, { spec: table, status }, dbClient, from, to, textCopy, syncMode, sampleRate, maxRowsPerTable) {
544
+ if (textCopy) return copyText(lc, table, status, dbClient, from, to, syncMode, sampleRate, maxRowsPerTable);
545
+ return copyBinary(lc, table, status, from, to, syncMode, sampleRate, maxRowsPerTable);
409
546
  }
410
- async function copyBinary(lc, table, status, from, to, sampleRate, maxRowsPerTable) {
547
+ async function copyBinary(lc, table, status, from, to, syncMode, sampleRate, maxRowsPerTable) {
411
548
  const start = performance.now();
412
- let flushTime = 0;
549
+ const copyFormat = "binary";
550
+ const metricLabels = initialSyncMetricAttrs({
551
+ syncMode,
552
+ copyFormat
553
+ });
554
+ const copyStreamMetric = initialSyncCopyStream();
555
+ const copyChunksMetric = initialSyncCopyChunks();
556
+ let flushMs = 0;
557
+ let copyBytes = 0;
413
558
  const tableName = liteTableName(table);
414
559
  const orderedColumns = Object.entries(table.columns);
415
560
  const columnNames = orderedColumns.map(([c]) => c);
@@ -428,7 +573,7 @@ async function copyBinary(lc, table, status, from, to, sampleRate, maxRowsPerTab
428
573
  let pendingRows = 0;
429
574
  let pendingSize = 0;
430
575
  function flush() {
431
- const start = performance.now();
576
+ const flushStart = performance.now();
432
577
  const flushedRows = pendingRows;
433
578
  const flushedSize = pendingSize;
434
579
  let l = 0;
@@ -438,9 +583,21 @@ async function copyBinary(lc, table, status, from, to, sampleRate, maxRowsPerTab
438
583
  for (let i = 0; i < flushedValues; i++) pendingValues[i] = void 0;
439
584
  pendingSize = 0;
440
585
  status.rows += flushedRows;
441
- const elapsed = performance.now() - start;
442
- flushTime += elapsed;
586
+ const elapsed = performance.now() - flushStart;
587
+ flushMs += elapsed;
443
588
  lc.debug?.(`flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`);
589
+ logSlowCopyFlush(lc, {
590
+ schema: table.schema,
591
+ table: table.name,
592
+ replicaTable: tableName,
593
+ syncMode,
594
+ copyFormat,
595
+ elapsedMs: elapsed,
596
+ flushedRows,
597
+ flushedBytes: flushedSize,
598
+ rows: status.rows,
599
+ copyBytes
600
+ });
444
601
  }
445
602
  const binaryParser = new BinaryCopyParser();
446
603
  let col = 0;
@@ -449,8 +606,12 @@ async function copyBinary(lc, table, status, from, to, sampleRate, maxRowsPerTab
449
606
  highWaterMark: BUFFERED_SIZE_THRESHOLD,
450
607
  write(chunk, _encoding, callback) {
451
608
  try {
609
+ copyBytes += chunk.length;
610
+ copyStreamMetric.add(chunk.length, metricLabels);
611
+ copyChunksMetric.add(1, metricLabels);
452
612
  for (const fieldBuf of binaryParser.parse(chunk)) {
453
- pendingSize += fieldBuf === null ? 4 : fieldBuf.length;
613
+ const fieldSize = fieldBuf === null ? 4 : fieldBuf.length;
614
+ pendingSize += fieldSize;
454
615
  pendingValues[pendingRows * valuesPerRow + col] = fieldBuf === null ? null : decoders[col](fieldBuf);
455
616
  if (++col === decoders.length) {
456
617
  col = 0;
@@ -472,15 +633,34 @@ async function copyBinary(lc, table, status, from, to, sampleRate, maxRowsPerTab
472
633
  }
473
634
  }));
474
635
  const elapsed = performance.now() - start;
475
- lc.info?.(`Finished copying ${status.rows} rows into ${tableName} (flush: ${flushTime.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `);
476
- return {
636
+ const result = {
637
+ schema: table.schema,
638
+ table: table.name,
639
+ replicaTable: tableName,
640
+ syncMode,
641
+ copyFormat,
642
+ columnCount: columnNames.length,
477
643
  rows: status.rows,
478
- flushTime
644
+ estimatedRows: status.totalRows,
645
+ estimatedBytes: status.totalBytes,
646
+ flushMs,
647
+ elapsedMs: elapsed,
648
+ copyBytes
479
649
  };
650
+ lc.info?.(`Finished copying ${status.rows} rows into ${tableName} (flush: ${flushMs.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `, result);
651
+ return result;
480
652
  }
481
- async function copyText(lc, table, status, dbClient, from, to, sampleRate, maxRowsPerTable) {
653
+ async function copyText(lc, table, status, dbClient, from, to, syncMode, sampleRate, maxRowsPerTable) {
482
654
  const start = performance.now();
483
- let flushTime = 0;
655
+ const copyFormat = "text";
656
+ const metricLabels = initialSyncMetricAttrs({
657
+ syncMode,
658
+ copyFormat
659
+ });
660
+ const copyStreamMetric = initialSyncCopyStream();
661
+ const copyChunksMetric = initialSyncCopyChunks();
662
+ let flushMs = 0;
663
+ let copyBytes = 0;
484
664
  const tableName = liteTableName(table);
485
665
  const orderedColumns = Object.entries(table.columns);
486
666
  const columnNames = orderedColumns.map(([c]) => c);
@@ -498,7 +678,7 @@ async function copyText(lc, table, status, dbClient, from, to, sampleRate, maxRo
498
678
  let pendingRows = 0;
499
679
  let pendingSize = 0;
500
680
  function flush() {
501
- const start = performance.now();
681
+ const flushStart = performance.now();
502
682
  const flushedRows = pendingRows;
503
683
  const flushedSize = pendingSize;
504
684
  let l = 0;
@@ -508,9 +688,21 @@ async function copyText(lc, table, status, dbClient, from, to, sampleRate, maxRo
508
688
  for (let i = 0; i < flushedValues; i++) pendingValues[i] = void 0;
509
689
  pendingSize = 0;
510
690
  status.rows += flushedRows;
511
- const elapsed = performance.now() - start;
512
- flushTime += elapsed;
691
+ const elapsed = performance.now() - flushStart;
692
+ flushMs += elapsed;
513
693
  lc.debug?.(`flushed ${flushedRows} ${tableName} rows (${flushedSize} bytes) in ${elapsed.toFixed(3)} ms`);
694
+ logSlowCopyFlush(lc, {
695
+ schema: table.schema,
696
+ table: table.name,
697
+ replicaTable: tableName,
698
+ syncMode,
699
+ copyFormat,
700
+ elapsedMs: elapsed,
701
+ flushedRows,
702
+ flushedBytes: flushedSize,
703
+ rows: status.rows,
704
+ copyBytes
705
+ });
514
706
  }
515
707
  lc.info?.(`Starting text copy stream of ${tableName}:`, select);
516
708
  const pgParsers = await getTypeParsers(dbClient, { returnJsonAsString: true });
@@ -524,8 +716,12 @@ async function copyText(lc, table, status, dbClient, from, to, sampleRate, maxRo
524
716
  highWaterMark: BUFFERED_SIZE_THRESHOLD,
525
717
  write(chunk, _encoding, callback) {
526
718
  try {
719
+ copyBytes += chunk.length;
720
+ copyStreamMetric.add(chunk.length, metricLabels);
721
+ copyChunksMetric.add(1, metricLabels);
527
722
  for (const text of tsvParser.parse(chunk)) {
528
- pendingSize += text === null ? 4 : text.length;
723
+ const fieldSize = text === null ? 4 : text.length;
724
+ pendingSize += fieldSize;
529
725
  pendingValues[pendingRows * valuesPerRow + col] = text === null ? null : parsers[col](text);
530
726
  if (++col === parsers.length) {
531
727
  col = 0;
@@ -547,11 +743,22 @@ async function copyText(lc, table, status, dbClient, from, to, sampleRate, maxRo
547
743
  }
548
744
  }));
549
745
  const elapsed = performance.now() - start;
550
- lc.info?.(`Finished copying ${status.rows} rows into ${tableName} (flush: ${flushTime.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `);
551
- return {
746
+ const result = {
747
+ schema: table.schema,
748
+ table: table.name,
749
+ replicaTable: tableName,
750
+ syncMode,
751
+ copyFormat,
752
+ columnCount: columnNames.length,
552
753
  rows: status.rows,
553
- flushTime
754
+ estimatedRows: status.totalRows,
755
+ estimatedBytes: status.totalBytes,
756
+ flushMs,
757
+ elapsedMs: elapsed,
758
+ copyBytes
554
759
  };
760
+ lc.info?.(`Finished copying ${status.rows} rows into ${tableName} (flush: ${flushMs.toFixed(3)} ms) (total: ${elapsed.toFixed(3)} ms) `, result);
761
+ return result;
555
762
  }
556
763
  //#endregion
557
764
  export { initialSync, makeBinarySelectExprs, makeDownloadStatements, shadowInitialSync };