@xeplr/actions 1.1.13 → 1.1.14
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/README.md +3 -1
- package/lib/uploader/index.js +43 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -189,6 +189,8 @@ await actions.uploader.upload({
|
|
|
189
189
|
// → { movementId, tables: { main, errors }, columns, totalRows, totalBatches, completed, dropped, aborted, durationMs }
|
|
190
190
|
```
|
|
191
191
|
|
|
192
|
+
`onProgress({ rowsRead, batches, rowsWritten, phase? })` fires after each batch is read, with `rowsWritten` from the write queue's own count, so a caller can show both and see whether writing keeps up. After the read, while the queue drains, it fires again each time the written count moves (`phase: 'writing'`), never on a bare timer, so a stuck write goes quiet.
|
|
193
|
+
|
|
192
194
|
Rows are spooled to NDJSON files on disk between source and database. From the first batch it creates, if missing: the target table, `<targetTable>_import_errors` (`movement_id`, `row_num`, `error_description`, `underlying_sql`, `raw_row`, `recorded_at`), a `__xeplr_movement_id__` column, and a unique index on `primaryKeys`. Server dialects also add `__xeplr_id__`; DuckDB does not.
|
|
193
195
|
|
|
194
196
|
Logical types and what each driver creates:
|
|
@@ -310,7 +312,7 @@ node --test test/uploader/reconcile.test.js
|
|
|
310
312
|
|
|
311
313
|
| needs nothing | needs a database |
|
|
312
314
|
|---|---|
|
|
313
|
-
| `test/procedure-call.test.js`, `test/streaming/spool.test.js`, `test/uploader/reconcile.test.js`, `test/formats/csv-gnarly.test.js`, `test/drivers/mysql.test.js`, `test/drivers/mssql.test.js`, `test/drivers/query-columns.test.js`, `test/builtins/email.test.js` (loads the package root, so `@xeplr/db` must be resolvable) | Postgres at `localhost:5435` (`PG_PASSWORD`, default `postgres`; database `xeplr_actions_test`): `test/drivers/postgres.test.js`, `test/builtins/db-push.test.js`, `test/builtins/file-upload.test.js`, `test/uploader/upload.test.js`; plus `@xeplr/db` for `test/uploader/meta-store-knex.test.js` |
|
|
315
|
+
| `test/procedure-call.test.js`, `test/streaming/spool.test.js`, `test/uploader/reconcile.test.js`, `test/write-progress.test.js`, `test/formats/csv-gnarly.test.js`, `test/drivers/mysql.test.js`, `test/drivers/mssql.test.js`, `test/drivers/query-columns.test.js`, `test/builtins/email.test.js` (loads the package root, so `@xeplr/db` must be resolvable) | Postgres at `localhost:5435` (`PG_PASSWORD`, default `postgres`; database `xeplr_actions_test`): `test/drivers/postgres.test.js`, `test/builtins/db-push.test.js`, `test/builtins/file-upload.test.js`, `test/uploader/upload.test.js`; plus `@xeplr/db` for `test/uploader/meta-store-knex.test.js` |
|
|
314
316
|
| `test/drivers/duckdb.test.js`, `test/drivers/duckdb-values.test.js` (needs `@duckdb/node-api`) | MySQL `localhost:3306` (`MYSQL_HOST/PORT/USER/PASSWORD`): `test/drivers/mysql-integration.test.js` |
|
|
315
317
|
| | SQL Server `localhost:1433` (`MSSQL_HOST/PORT/USER/PASSWORD`): `test/drivers/mssql-integration.test.js` |
|
|
316
318
|
| | Postgres + MySQL + SQL Server (`PG_*`, `MYSQL_*`, `MSSQL_*`): `test/builtins/db-replication.test.js` |
|
package/lib/uploader/index.js
CHANGED
|
@@ -154,9 +154,12 @@ async function upload(opts) {
|
|
|
154
154
|
// No total, because for a query source there isn't one to know
|
|
155
155
|
// without running it twice. A caller that wants a percentage has to
|
|
156
156
|
// supply its own denominator.
|
|
157
|
+
//
|
|
158
|
+
// rowsWritten is what HAS landed, from the write queue's own count,
|
|
159
|
+
// so a caller can show both and see whether writing keeps up.
|
|
157
160
|
if (opts.onProgress) {
|
|
158
161
|
try {
|
|
159
|
-
opts.onProgress({ rowsRead: rowsSeen, batches: meta ? meta.batchIndex + 1 : totalBatches });
|
|
162
|
+
opts.onProgress({ rowsRead: rowsSeen, batches: meta ? meta.batchIndex + 1 : totalBatches, rowsWritten: writtenSoFar(opts) });
|
|
160
163
|
} catch (e) { /* a reporting failure must not fail the movement */ }
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -191,7 +194,19 @@ async function upload(opts) {
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
// Wait for the DB writes to finish (spool has finished feeding the queue).
|
|
194
|
-
|
|
197
|
+
//
|
|
198
|
+
// The read is over but the queue can take minutes more to drain, and
|
|
199
|
+
// nothing reported progress while it did — so a caller watching for
|
|
200
|
+
// progress saw a busy movement go silent. Reported here whenever the
|
|
201
|
+
// written count MOVES (never on a bare timer: a stuck write must go quiet).
|
|
202
|
+
var drainWatch = opts.onProgress ? watchWrites(opts, function() {
|
|
203
|
+
return { rowsRead: rowsSeen, batches: totalBatches, rowsWritten: writtenSoFar(opts), phase: 'writing' };
|
|
204
|
+
}) : null;
|
|
205
|
+
try {
|
|
206
|
+
await opts.queue.drain();
|
|
207
|
+
} finally {
|
|
208
|
+
if (drainWatch) drainWatch.stop();
|
|
209
|
+
}
|
|
195
210
|
|
|
196
211
|
var stats = opts.queue.stats(opts.movementId) || {};
|
|
197
212
|
var status = stats.aborted ? 'aborted' : 'completed';
|
|
@@ -332,6 +347,31 @@ async function runBootstrap(driver, pool, targetTable, columns, primaryKeys) {
|
|
|
332
347
|
return reconciled;
|
|
333
348
|
}
|
|
334
349
|
|
|
350
|
+
/** Rows this movement has written so far, from the write queue's own count. */
|
|
351
|
+
function writtenSoFar(opts) {
|
|
352
|
+
try {
|
|
353
|
+
var st = opts.queue && opts.queue.stats ? opts.queue.stats(opts.movementId) : null;
|
|
354
|
+
return st && st.completed != null ? st.completed : null;
|
|
355
|
+
} catch (e) { return null; }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* While the queue drains, report progress each time the written count moves.
|
|
360
|
+
* Checked every `everyMs`; a count that has not moved reports nothing, so a
|
|
361
|
+
* stuck write goes quiet rather than looking alive.
|
|
362
|
+
*/
|
|
363
|
+
function watchWrites(opts, build, everyMs) {
|
|
364
|
+
var last = writtenSoFar(opts);
|
|
365
|
+
var timer = setInterval(function() {
|
|
366
|
+
var now = writtenSoFar(opts);
|
|
367
|
+
if (now == null || now === last) return;
|
|
368
|
+
last = now;
|
|
369
|
+
try { opts.onProgress(build()); } catch (e) { /* reporting must not fail the movement */ }
|
|
370
|
+
}, everyMs || 2000);
|
|
371
|
+
if (timer.unref) timer.unref();
|
|
372
|
+
return { stop: function() { clearInterval(timer); } };
|
|
373
|
+
}
|
|
374
|
+
|
|
335
375
|
async function enqueueBatch(opts, rows, columns, batchRef) {
|
|
336
376
|
var driver = opts.driver;
|
|
337
377
|
var target = opts.targetTable;
|
|
@@ -365,6 +405,7 @@ var { inferColumnType } = require('./infer');
|
|
|
365
405
|
|
|
366
406
|
module.exports = {
|
|
367
407
|
upload: upload,
|
|
408
|
+
watchWrites: watchWrites,
|
|
368
409
|
rollback: rollback,
|
|
369
410
|
inferColumns: inferColumns,
|
|
370
411
|
inferColumnType: inferColumnType,
|
package/package.json
CHANGED