@iterativeflow/mysql 2.1.1 → 2.2.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/dist/index.d.mts CHANGED
@@ -53,6 +53,11 @@ declare const createMysqlBackend: (sql: Sql, opts?: MysqlBackendOpts) => Backend
53
53
  * MySQL has no implicit rowid). Indexed string columns are `VARCHAR(191)` to stay under the utf8mb4
54
54
  * index-key limit. The idempotency/signal-dedup UNIQUE KEYs need no partial `WHERE`: MySQL treats
55
55
  * NULLs as distinct, so unkeyed rows never collide.
56
+ *
57
+ * Also creates the `pending_work` function, so applying this needs `CREATE ROUTINE` as well as
58
+ * `CREATE TABLE`. MySQL has no `CREATE OR REPLACE FUNCTION`, so that one statement is a `DROP` +
59
+ * `CREATE` rather than `IF NOT EXISTS` — a scaler polling the function across a boot can see one
60
+ * failed read.
56
61
  */
57
62
  declare const ddl: (prefix?: string) => string[];
58
63
  /** Apply the schema DDL (idempotent). Run once before use. */
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { NON_SUCCESS_TERMINAL_STATUSES, RECONCILABLE_STATUSES, TERMINAL_STATUSES, assertSqlIdentifier, createLocalWakeup, newId, orphanedRunsSql, queueDepthOf, statusList, zeroRunStats } from "@iterativeflow/core/backend";
1
+ import { NON_SUCCESS_TERMINAL_STATUSES, RECONCILABLE_STATUSES, TERMINAL_STATUSES, assertSqlIdentifier, createLocalWakeup, isTerminal, newId, orphanedRunsSql, purgeWhereSql, queueDepthOf, statusList, zeroRunStats } from "@iterativeflow/core/backend";
2
2
  //#region src/queue.ts
3
3
  /** @internal */
4
4
  const createMysqlQueue = (sql, t, id) => {
@@ -80,7 +80,7 @@ const createMysqlQueue = (sql, t, id) => {
80
80
  async depth(now, names) {
81
81
  if (names && names.length === 0) return queueDepthOf([], now.getTime());
82
82
  const nowMs = now.getTime();
83
- const namePredicate = names ? ` WHERE r.name IN (${names.map(() => "?").join(",")})` : "";
83
+ const namePredicate = names ? ` WHERE r.name IS NULL OR r.name IN (${names.map(() => "?").join(",")})` : "";
84
84
  const r = (await sql.query(`SELECT SUM(claimable) AS claimable, SUM(leased) AS leased,
85
85
  MIN(CASE WHEN claimable = 1 THEN run_at END) AS oldest
86
86
  FROM (SELECT j.run_at AS run_at,
@@ -121,6 +121,11 @@ const tables = (prefix) => {
121
121
  * MySQL has no implicit rowid). Indexed string columns are `VARCHAR(191)` to stay under the utf8mb4
122
122
  * index-key limit. The idempotency/signal-dedup UNIQUE KEYs need no partial `WHERE`: MySQL treats
123
123
  * NULLs as distinct, so unkeyed rows never collide.
124
+ *
125
+ * Also creates the `pending_work` function, so applying this needs `CREATE ROUTINE` as well as
126
+ * `CREATE TABLE`. MySQL has no `CREATE OR REPLACE FUNCTION`, so that one statement is a `DROP` +
127
+ * `CREATE` rather than `IF NOT EXISTS` — a scaler polling the function across a boot can see one
128
+ * failed read.
124
129
  */
125
130
  const ddl = (prefix = "") => {
126
131
  const t = tables(prefix);
@@ -196,11 +201,16 @@ const ddl = (prefix = "") => {
196
201
  `CREATE FUNCTION \`${prefix}pending_work\`(flow_names JSON, as_of BIGINT)
197
202
  RETURNS BIGINT READS SQL DATA
198
203
  RETURN (
199
- (SELECT COUNT(*) FROM ${t.job} j LEFT JOIN ${t.run} r ON r.id = j.run_id
204
+ (SELECT COUNT(*) FROM ${t.job} j
200
205
  WHERE j.run_at <= as_of AND (j.lease_expires IS NULL OR j.lease_expires <= as_of)
201
- AND (flow_names IS NULL OR JSON_CONTAINS(flow_names, JSON_QUOTE(r.name))))
202
- + (SELECT COUNT(*) FROM ${t.timer} tm LEFT JOIN ${t.run} r ON r.id = tm.run_id
203
- WHERE tm.fire_at <= as_of AND (flow_names IS NULL OR JSON_CONTAINS(flow_names, JSON_QUOTE(r.name))))
206
+ AND (flow_names IS NULL OR (JSON_LENGTH(flow_names) > 0 AND NOT EXISTS (
207
+ SELECT 1 FROM ${t.run} r
208
+ WHERE r.id = j.run_id AND NOT JSON_CONTAINS(flow_names, JSON_QUOTE(r.name))))))
209
+ + (SELECT COUNT(*) FROM ${t.timer} tm
210
+ WHERE tm.fire_at <= as_of
211
+ AND (flow_names IS NULL OR (JSON_LENGTH(flow_names) > 0 AND NOT EXISTS (
212
+ SELECT 1 FROM ${t.run} r
213
+ WHERE r.id = tm.run_id AND NOT JSON_CONTAINS(flow_names, JSON_QUOTE(r.name))))))
204
214
  + (SELECT COUNT(*) FROM ${t.cron} c
205
215
  WHERE c.next_run_at <= as_of AND (flow_names IS NULL OR JSON_CONTAINS(flow_names, JSON_QUOTE(c.flow_name))))
206
216
  )`
@@ -299,6 +309,10 @@ const sqlTuple = (statuses) => `(${statuses.map((s) => `'${s}'`).join(",")})`;
299
309
  const TERMINAL = sqlTuple(TERMINAL_STATUSES);
300
310
  const RECONCILABLE = sqlTuple(RECONCILABLE_STATUSES);
301
311
  const NON_SUCCESS_TERMINAL = sqlTuple(NON_SUCCESS_TERMINAL_STATUSES);
312
+ const MS_PURGE = {
313
+ placeholder: () => "?",
314
+ time: (at) => at.getTime()
315
+ };
302
316
  const inList = (n) => `(${Array.from({ length: n }, () => "?").join(",")})`;
303
317
  /** @internal */
304
318
  const createMysqlStore = (sql, t, id) => {
@@ -358,6 +372,24 @@ const createMysqlStore = (sql, t, id) => {
358
372
  status: "pending"
359
373
  };
360
374
  };
375
+ const deleteRuns = async (filter, limit) => {
376
+ const q = purgeWhereSql(filter, MS_PURGE);
377
+ if (!q) return 0;
378
+ const params = [...q.params, limit];
379
+ return sql.tx(async (tx) => {
380
+ const ids = (await tx.query(`SELECT id FROM ${t.run}
381
+ WHERE ${q.where}
382
+ ORDER BY seq LIMIT ?`, params)).map((r) => r.id);
383
+ if (ids.length === 0) return 0;
384
+ const inIds = inList(ids.length);
385
+ await tx.exec(`DELETE FROM ${t.step} WHERE run_id IN ${inIds}`, ids);
386
+ await tx.exec(`DELETE FROM ${t.signal} WHERE run_id IN ${inIds}`, ids);
387
+ await tx.exec(`DELETE FROM ${t.job} WHERE run_id IN ${inIds}`, ids);
388
+ await tx.exec(`DELETE FROM ${t.timer} WHERE run_id IN ${inIds}`, ids);
389
+ await tx.exec(`DELETE FROM ${t.run} WHERE id IN ${inIds}`, ids);
390
+ return ids.length;
391
+ });
392
+ };
361
393
  return {
362
394
  startRun(spec) {
363
395
  return startOne(sql, spec);
@@ -418,7 +450,7 @@ const createMysqlStore = (sql, t, id) => {
418
450
  const cur = await tx.query(`SELECT status, attempts FROM ${t.run} WHERE id = ? FOR UPDATE`, [runId]);
419
451
  if (!cur[0]) throw new Error(`markRunning: run ${runId} not found`);
420
452
  const attempts = Number(cur[0].attempts);
421
- if (TERMINAL_STATUSES.includes(cur[0].status)) return attempts;
453
+ if (isTerminal(cur[0].status)) return attempts;
422
454
  await tx.exec(`UPDATE ${t.run} SET status = 'running', attempts = attempts + 1 WHERE id = ?`, [runId]);
423
455
  return attempts + 1;
424
456
  });
@@ -519,21 +551,8 @@ const createMysqlStore = (sql, t, id) => {
519
551
  limit: "?"
520
552
  }), [limit])).map((r) => r.id);
521
553
  },
522
- deleteRunsOlderThan(before, limit) {
523
- return sql.tx(async (tx) => {
524
- const ids = (await tx.query(`SELECT id FROM ${t.run}
525
- WHERE status IN ${TERMINAL} AND created_at < ?
526
- ORDER BY seq LIMIT ?`, [before.getTime(), limit])).map((r) => r.id);
527
- if (ids.length === 0) return 0;
528
- const inIds = inList(ids.length);
529
- await tx.exec(`DELETE FROM ${t.step} WHERE run_id IN ${inIds}`, ids);
530
- await tx.exec(`DELETE FROM ${t.signal} WHERE run_id IN ${inIds}`, ids);
531
- await tx.exec(`DELETE FROM ${t.job} WHERE run_id IN ${inIds}`, ids);
532
- await tx.exec(`DELETE FROM ${t.timer} WHERE run_id IN ${inIds}`, ids);
533
- await tx.exec(`DELETE FROM ${t.run} WHERE id IN ${inIds}`, ids);
534
- return ids.length;
535
- });
536
- },
554
+ deleteRuns,
555
+ deleteRunsOlderThan: (before, limit) => deleteRuns({ before }, limit),
537
556
  retryRun(runId) {
538
557
  return sql.tx(async (tx) => {
539
558
  if ((await tx.exec(`UPDATE ${t.run} SET status = 'pending', error = NULL WHERE id = ? AND status = 'failed'`, [runId])).affectedRows !== 1) return { retried: false };
@@ -603,7 +622,7 @@ const createMysqlTimer = (sql, t) => {
603
622
  },
604
623
  async dueCount(now, names) {
605
624
  if (names && names.length === 0) return 0;
606
- const namePredicate = names ? ` AND r.name IN (${names.map(() => "?").join(",")})` : "";
625
+ const namePredicate = names ? ` AND (r.name IS NULL OR r.name IN (${names.map(() => "?").join(",")}))` : "";
607
626
  const params = names ? [now.getTime(), ...names] : [now.getTime()];
608
627
  const rows = await sql.query(`SELECT count(*) AS n FROM ${t.timer} tm LEFT JOIN ${t.run} r ON r.id = tm.run_id
609
628
  WHERE tm.fire_at <= ?${namePredicate}`, params);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iterativeflow/mysql",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "access": "public"
26
26
  },
27
27
  "dependencies": {
28
- "@iterativeflow/core": "2.1.1"
28
+ "@iterativeflow/core": "2.2.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "mysql2": ">=3.0.0"