@adonis-agora/durable 1.0.0-rc.14 → 1.0.0-rc.16

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 (54) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/providers/durable_provider.d.ts.map +1 -1
  3. package/dist/providers/durable_provider.js +2 -0
  4. package/dist/providers/durable_provider.js.map +1 -1
  5. package/dist/src/dashboard/index.d.ts +1 -1
  6. package/dist/src/dashboard/index.js +1 -1
  7. package/dist/src/define_config.d.ts +8 -0
  8. package/dist/src/define_config.d.ts.map +1 -1
  9. package/dist/src/define_config.js.map +1 -1
  10. package/dist/src/doctor/checks.d.ts +6 -0
  11. package/dist/src/doctor/checks.d.ts.map +1 -1
  12. package/dist/src/doctor/checks.js +91 -0
  13. package/dist/src/doctor/checks.js.map +1 -1
  14. package/dist/src/index.d.ts +2 -2
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +1 -1
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/sql-schema.d.ts +54 -0
  19. package/dist/src/sql-schema.d.ts.map +1 -0
  20. package/dist/src/sql-schema.js +145 -0
  21. package/dist/src/sql-schema.js.map +1 -0
  22. package/dist/src/stores/factory.d.ts +6 -0
  23. package/dist/src/stores/factory.d.ts.map +1 -1
  24. package/dist/src/stores/factory.js +3 -0
  25. package/dist/src/stores/factory.js.map +1 -1
  26. package/dist/src/stores/lucid-schema.d.ts +17 -3
  27. package/dist/src/stores/lucid-schema.d.ts.map +1 -1
  28. package/dist/src/stores/lucid-schema.js +84 -62
  29. package/dist/src/stores/lucid-schema.js.map +1 -1
  30. package/dist/src/stores/lucid.d.ts +27 -0
  31. package/dist/src/stores/lucid.d.ts.map +1 -1
  32. package/dist/src/stores/lucid.js +253 -205
  33. package/dist/src/stores/lucid.js.map +1 -1
  34. package/dist/src/transports/db-schema.d.ts +13 -5
  35. package/dist/src/transports/db-schema.d.ts.map +1 -1
  36. package/dist/src/transports/db-schema.js +52 -27
  37. package/dist/src/transports/db-schema.js.map +1 -1
  38. package/dist/src/transports/db.d.ts +6 -0
  39. package/dist/src/transports/db.d.ts.map +1 -1
  40. package/dist/src/transports/db.js +49 -39
  41. package/dist/src/transports/db.js.map +1 -1
  42. package/dist/src/transports/factory.d.ts +16 -0
  43. package/dist/src/transports/factory.d.ts.map +1 -1
  44. package/dist/src/transports/factory.js +6 -1
  45. package/dist/src/transports/factory.js.map +1 -1
  46. package/dist/src/transports/worker-keys-db.d.ts +5 -0
  47. package/dist/src/transports/worker-keys-db.d.ts.map +1 -1
  48. package/dist/src/transports/worker-keys-db.js +10 -8
  49. package/dist/src/transports/worker-keys-db.js.map +1 -1
  50. package/dist/stubs/database/migrations/create_durable_tables.stub +12 -2
  51. package/dist/stubs/database/migrations/create_durable_transport_messages.stub +34 -5
  52. package/dist/stubs/database/migrations/create_durable_transport_tables.stub +30 -8
  53. package/dist/stubs/database/migrations/create_durable_transport_worker_keys.stub +24 -2
  54. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * The database schema the durable tables live in (`schema` on the store, the db transport and the
4
+ * worker-key store). Every statement names it, so the tables can sit in their own schema while the
5
+ * connection's default (its `search_path`, or its database on MySQL) stays the app's. Never done by
6
+ * moving the connection's `search_path`: behind PgBouncer in transaction mode a `SET search_path`
7
+ * leaks to other clients' sessions.
8
+ *
9
+ * Postgres: a schema. MySQL/MariaDB: a database. Any other dialect (SQLite) has no schemas, and a
10
+ * `schema` there fails instead of being ignored.
11
+ */
12
+ const SCHEMA_NAME = /^[A-Za-z_][A-Za-z0-9_$]*$/;
13
+ const SCHEMA_NAME_MAX = 63;
14
+ /** `schema` when it is a valid schema name, else throws naming `owner` (the option that set it). */
15
+ export function validateSchemaName(schema, owner) {
16
+ if (schema === undefined)
17
+ return undefined;
18
+ if (typeof schema !== 'string' || !SCHEMA_NAME.test(schema) || schema.length > SCHEMA_NAME_MAX) {
19
+ throw new Error(`@adonis-agora/durable: ${owner} schema ${JSON.stringify(schema)} is not a valid schema name: use letters, digits, _ and $, starting with a letter or _, at most ${SCHEMA_NAME_MAX} characters.`);
20
+ }
21
+ return schema;
22
+ }
23
+ /** Whether `client`'s dialect has schemas `schema` can name (Postgres schemas, MySQL databases). */
24
+ function supportsSchemas(client) {
25
+ const name = client.dialect.name;
26
+ return name === 'postgres' || name === 'mysql';
27
+ }
28
+ /**
29
+ * Throw when `schema` is set on a connection whose dialect has no schemas (SQLite), so the tables
30
+ * never silently land in the default one.
31
+ */
32
+ export function assertSchemaSupported(client, schema, owner) {
33
+ if (schema === undefined || supportsSchemas(client))
34
+ return;
35
+ throw new Error(`@adonis-agora/durable: ${owner} sets schema "${schema}", but the ${client.dialect.name} connection${client.connectionName ? ` "${client.connectionName}"` : ''} has no schemas. Remove \`schema\`, or use a Postgres or MySQL connection.`);
36
+ }
37
+ /**
38
+ * `table` qualified with `schema` for Knex (`schema.table`, which Knex quotes part by part as
39
+ * `"schema"."table"`), or `table` alone without one. Also right for a `??` binding in raw SQL.
40
+ */
41
+ export function qualify(schema, table) {
42
+ return schema ? `${schema}.${table}` : table;
43
+ }
44
+ /** Every value of `tables` qualified with `schema` (see {@link qualify}). */
45
+ export function qualifyAll(tables, schema) {
46
+ return Object.fromEntries(Object.entries(tables).map(([key, table]) => [key, qualify(schema, table)]));
47
+ }
48
+ /** `client.schema`, scoped to `schema` when set. A fresh builder per call (Knex's is stateful). */
49
+ export function schemaBuilder(client, schema) {
50
+ return schema ? client.schema.withSchema(schema) : client.schema;
51
+ }
52
+ /** Whether `table` exists in `schema` (the connection's current schema when unset). */
53
+ export async function hasTable(client, schema, table) {
54
+ return schemaBuilder(client, schema).hasTable(table);
55
+ }
56
+ /**
57
+ * Whether `table` in `schema` has `column`. Not `withSchema().hasColumn()`: Knex's MySQL `hasColumn`
58
+ * ignores the schema and looks in the connection's database.
59
+ */
60
+ export async function hasColumn(client, schema, table, column) {
61
+ if (!schema)
62
+ return client.schema.hasColumn(table, column);
63
+ const rows = await client
64
+ .query()
65
+ .from('information_schema.columns')
66
+ .select('column_name')
67
+ .where('table_schema', schema)
68
+ .andWhere('table_name', table)
69
+ .andWhere('column_name', column)
70
+ .limit(1);
71
+ return rows.length > 0;
72
+ }
73
+ /**
74
+ * The `information_schema` filter on `column` for `schema`: the configured one, else the
75
+ * connection's current schema (`current_schema()` on Postgres, `database()` on MySQL).
76
+ */
77
+ export function whereSchema(q, client, column, schema) {
78
+ if (schema) {
79
+ q.where(column, schema);
80
+ return q;
81
+ }
82
+ const current = client.dialect.name === 'mysql' ? 'database()' : 'current_schema()';
83
+ q.whereRaw(`${column} = ${current}`);
84
+ return q;
85
+ }
86
+ /**
87
+ * Create `schema` when it does not exist (`CREATE SCHEMA` on Postgres, `CREATE DATABASE` on MySQL),
88
+ * for the boots that create the tables. Checked first, so a role without the right to create one
89
+ * boots fine against a schema that is already there; when it is missing and cannot be created, the
90
+ * error names the schema and the grant it needs.
91
+ */
92
+ export async function ensureSchemaExists(client, schema) {
93
+ if (!schema)
94
+ return;
95
+ if (await schemaExists(client, schema))
96
+ return;
97
+ const isMysql = client.dialect.name === 'mysql';
98
+ try {
99
+ await client.rawQuery(isMysql ? 'create database if not exists ??' : 'create schema if not exists ??', [schema]);
100
+ }
101
+ catch (error) {
102
+ // Another process created it first (the IF NOT EXISTS race on Postgres).
103
+ if (await schemaExists(client, schema))
104
+ return;
105
+ const grant = isMysql
106
+ ? `the connection's user needs CREATE on \`${schema}\`.* (GRANT CREATE ON \`${schema}\`.* TO <user>), or create it once: CREATE DATABASE \`${schema}\``
107
+ : `the connection's role needs CREATE on the database (GRANT CREATE ON DATABASE <database> TO <role>), or create it once: CREATE SCHEMA "${schema}"`;
108
+ throw new Error(`@adonis-agora/durable: could not create the ${isMysql ? 'database' : 'schema'} "${schema}" for the durable tables: ${error instanceof Error ? error.message : String(error)}. To fix it, ${grant}.`);
109
+ }
110
+ }
111
+ /** Whether `schema` exists (a Postgres schema, a MySQL database). */
112
+ export async function schemaExists(client, schema) {
113
+ const rows = await client
114
+ .query()
115
+ .from('information_schema.schemata')
116
+ .select('schema_name')
117
+ .where('schema_name', schema)
118
+ .limit(1);
119
+ return rows.length > 0;
120
+ }
121
+ /**
122
+ * A lock key for `schema`: `base` without one (today's key, unchanged), else a key derived from both,
123
+ * so two installs in two schemas of one database don't wait on each other. A positive bigint that
124
+ * fits a JS number.
125
+ */
126
+ export function schemaLockKey(base, schema) {
127
+ if (!schema)
128
+ return base;
129
+ const digest = createHash('sha256').update(`${base}:${schema}`).digest();
130
+ return digest.readUIntBE(0, 6);
131
+ }
132
+ /**
133
+ * A MySQL `GET_LOCK` name for `schema` under `prefix`: `prefix` + the schema, or + a hash of it when
134
+ * that would pass MySQL's 64-character limit on lock names.
135
+ */
136
+ export function mysqlLockName(prefix, schema) {
137
+ const name = `${prefix}${schema}`;
138
+ if (name.length <= 64)
139
+ return name;
140
+ return `${prefix}${createHash('sha256')
141
+ .update(schema)
142
+ .digest('hex')
143
+ .slice(0, 64 - prefix.length)}`;
144
+ }
145
+ //# sourceMappingURL=sql-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-schema.js","sourceRoot":"","sources":["../../src/sql-schema.ts"],"names":[],"mappings":"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"}
@@ -28,6 +28,12 @@ export interface LucidStoreConfig {
28
28
  * prepared statements (PgBouncer in transaction mode before 1.21 does not). Default off.
29
29
  */
30
30
  preparedStatements?: boolean;
31
+ /**
32
+ * The database schema the durable tables live in (a database on MySQL): every statement names it,
33
+ * and `autoSchema` creates it. Defaults to the top-level `schema`, else the connection's current
34
+ * schema. Not supported on SQLite.
35
+ */
36
+ schema?: string;
31
37
  }
32
38
  /**
33
39
  * The store factory namespace used in `config/durable.ts`:
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/stores/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE5C;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtE,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM;IACjB,4EAA4E;IAC5E,KAAK,UAAS,gBAAgB,GAAQ,YAAY;CAanD,CAAC"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/stores/factory.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEjE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE5C;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEtE,qDAAqD;AACrD,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,MAAM;IACjB,4EAA4E;IAC5E,KAAK,UAAS,gBAAgB,GAAQ,YAAY;CAgBnD,CAAC"}
@@ -29,6 +29,9 @@ export const stores = {
29
29
  ...(config.preparedStatements !== undefined
30
30
  ? { preparedStatements: config.preparedStatements }
31
31
  : {}),
32
+ ...((config.schema ?? ctx.schema) !== undefined
33
+ ? { schema: config.schema ?? ctx.schema }
34
+ : {}),
32
35
  });
33
36
  };
34
37
  },
@@ -1 +1 @@
1
- {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/stores/factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAmC9D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,4EAA4E;IAC5E,KAAK,CAAC,MAAM,GAAqB,EAAE;QACjC,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;YACnB,MAAM,EAAE,GAAG,CAAC,MAAM,oBAAoB,CAAC,GAAG,CAAC,CAAa,CAAC;YACzD,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YACvD,OAAO,IAAI,eAAe,CAAC,EAAE,EAAE;gBAC7B,GAAG,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjF,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1E,GAAG,CAAC,MAAM,CAAC,kBAAkB,KAAK,SAAS;oBACzC,CAAC,CAAC,EAAE,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,EAAE;oBACnD,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC;CACF,CAAC"}
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/stores/factory.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAyC9D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,4EAA4E;IAC5E,KAAK,CAAC,MAAM,GAAqB,EAAE;QACjC,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;YACnB,MAAM,EAAE,GAAG,CAAC,MAAM,oBAAoB,CAAC,GAAG,CAAC,CAAa,CAAC;YACzD,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;YACvD,OAAO,IAAI,eAAe,CAAC,EAAE,EAAE;gBAC7B,GAAG,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjF,GAAG,CAAC,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1E,GAAG,CAAC,MAAM,CAAC,kBAAkB,KAAK,SAAS;oBACzC,CAAC,CAAC,EAAE,kBAAkB,EAAE,MAAM,CAAC,kBAAkB,EAAE;oBACnD,CAAC,CAAC,EAAE,CAAC;gBACP,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,SAAS;oBAC7C,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE;oBACzC,CAAC,CAAC,EAAE,CAAC;aACR,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -39,6 +39,12 @@ export interface CreateDurableTablesOptions {
39
39
  * {@link DURABLE_SCHEMA_META_TABLE}). For a database whose durable tables were changed by hand.
40
40
  */
41
41
  force?: boolean;
42
+ /**
43
+ * The database schema the tables live in (a database on MySQL), created when missing. Every
44
+ * statement names it, so the connection's own default schema is left alone. Omit for the
45
+ * connection's current schema. Not supported on SQLite.
46
+ */
47
+ schema?: string | undefined;
42
48
  }
43
49
  /**
44
50
  * The table where {@link createDurableTables} records which schema it last applied: one row
@@ -85,6 +91,9 @@ export declare const DURABLE_JSON_COLUMNS: Record<string, {
85
91
  * prefer the published migration (`node ace configure @adonis-agora/durable`); this helper is for standalone
86
92
  * use, tests, and `ensureSchema`.
87
93
  *
94
+ * Pass `options.schema` to put the tables in their own database schema (created when missing): every
95
+ * statement names it, on any connection. It must match the store's `schema`.
96
+ *
88
97
  * Pass `connectionName` to provision the tables on a dedicated Lucid connection — it must match the
89
98
  * connection the store reads/writes on, or `ensureSchema` would create the tables where the store
90
99
  * never looks. Omit it to use the `Database` default connection.
@@ -103,7 +112,12 @@ export declare function createDurableTables(db: Database, connectionName?: strin
103
112
  export declare function widenMysqlLongtextColumns(client: QueryClientContract, spec: Record<string, {
104
113
  columns: string[];
105
114
  notNull?: string[];
106
- }>): Promise<string[]>;
107
- /** Drop every durable table (reverse FK order). Used by tests and migration `down`. */
108
- export declare function dropDurableTables(db: Database, connectionName?: string): Promise<void>;
115
+ }>, schema?: string | undefined): Promise<string[]>;
116
+ /**
117
+ * Drop every durable table (reverse FK order), in `options.schema` when set (the schema itself is
118
+ * left). Used by tests and migration `down`.
119
+ */
120
+ export declare function dropDurableTables(db: Database, connectionName?: string, options?: {
121
+ schema?: string | undefined;
122
+ }): Promise<void>;
109
123
  //# sourceMappingURL=lucid-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"lucid-schema.d.ts","sourceRoot":"","sources":["../../../src/stores/lucid-schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,cAAc;aACzB,IAAI,EAAE,uBAAuB;aAC7B,WAAW,EAAE,0BAA0B;aACvC,UAAU,EAAE,wBAAwB;aACpC,aAAa,EAAE,wBAAwB;aACvC,eAAe,EAAE,0BAA0B;aAC3C,cAAc,EAAE,yBAAyB;aACzC,SAAS,EAAE,mBAAmB;CACtB,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAUzC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAQ1F,CAAC;AAsLF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,QAAQ,EACZ,cAAc,CAAC,EAAE,MAAM,EACvB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAcf;AAgWD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,GAC9D,OAAO,CAAC,MAAM,EAAE,CAAC,CAyBnB;AAuCD,uFAAuF;AACvF,wBAAsB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAU5F"}
1
+ {"version":3,"file":"lucid-schema.d.ts","sourceRoot":"","sources":["../../../src/stores/lucid-schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAa1E;;;;GAIG;AACH,eAAO,MAAM,cAAc;aACzB,IAAI,EAAE,uBAAuB;aAC7B,WAAW,EAAE,0BAA0B;aACvC,UAAU,EAAE,wBAAwB;aACpC,aAAa,EAAE,wBAAwB;aACvC,eAAe,EAAE,0BAA0B;aAC3C,cAAc,EAAE,yBAAyB;aACzC,SAAS,EAAE,mBAAmB;CACtB,CAAC;AAEX;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAUzC;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAQ1F,CAAC;AAgMF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,QAAQ,EACZ,cAAc,CAAC,EAAE,MAAM,EACvB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAkBf;AAsWD;;;;GAIG;AACH,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAC/D,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,GAC1B,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BnB;AA4CD;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,QAAQ,EACZ,cAAc,CAAC,EAAE,MAAM,EACvB,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GAC5C,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { assertSchemaSupported, ensureSchemaExists, hasColumn, mysqlLockName, qualify, schemaBuilder, schemaLockKey, validateSchemaName, whereSchema, } from '../sql-schema.js';
2
3
  /**
3
4
  * The canonical durable table names. They match the cross-adapter snake_case contract the other
4
5
  * Agora/aviary stores use (Drizzle is the reference), so a dashboard or migration can be pointed at
@@ -65,12 +66,12 @@ function schemaFingerprint(collation) {
65
66
  .update(`rev=${DURABLE_SCHEMA_REVISION}|collation=${collation ?? ''}`)
66
67
  .digest('hex');
67
68
  }
68
- async function readSchemaFingerprint(client) {
69
- if (!(await client.schema.hasTable(DURABLE_SCHEMA_META_TABLE)))
69
+ async function readSchemaFingerprint(client, schema) {
70
+ if (!(await schemaBuilder(client, schema).hasTable(DURABLE_SCHEMA_META_TABLE)))
70
71
  return undefined;
71
72
  const q = client
72
73
  .query()
73
- .from(DURABLE_SCHEMA_META_TABLE)
74
+ .from(qualify(schema, DURABLE_SCHEMA_META_TABLE))
74
75
  .select('fingerprint')
75
76
  .where('id', SCHEMA_META_ROW);
76
77
  // A locking read on MySQL: under REPEATABLE READ a plain read could answer from a snapshot older
@@ -80,9 +81,9 @@ async function readSchemaFingerprint(client) {
80
81
  const row = (await q.first());
81
82
  return row?.fingerprint ?? undefined;
82
83
  }
83
- async function writeSchemaFingerprint(client, fingerprint) {
84
- if (!(await client.schema.hasTable(DURABLE_SCHEMA_META_TABLE))) {
85
- await client.schema.createTable(DURABLE_SCHEMA_META_TABLE, (table) => {
84
+ async function writeSchemaFingerprint(client, schema, fingerprint) {
85
+ if (!(await schemaBuilder(client, schema).hasTable(DURABLE_SCHEMA_META_TABLE))) {
86
+ await schemaBuilder(client, schema).createTable(DURABLE_SCHEMA_META_TABLE, (table) => {
86
87
  table.string('id', 32).primary();
87
88
  table.string('fingerprint', 64).notNullable();
88
89
  table.bigInteger('applied_at').notNullable();
@@ -90,7 +91,7 @@ async function writeSchemaFingerprint(client, fingerprint) {
90
91
  }
91
92
  await client
92
93
  .insertQuery()
93
- .table(DURABLE_SCHEMA_META_TABLE)
94
+ .table(qualify(schema, DURABLE_SCHEMA_META_TABLE))
94
95
  .knexQuery.insert({ id: SCHEMA_META_ROW, fingerprint, applied_at: Date.now() })
95
96
  .onConflict('id')
96
97
  .merge();
@@ -102,17 +103,29 @@ async function writeSchemaFingerprint(client, fingerprint) {
102
103
  * (its DDL commits implicitly, the lock is the session's) and releases it after. SQLite, and any
103
104
  * other dialect, runs `work` without a lock.
104
105
  */
105
- async function withSchemaLock(connection, logger, work) {
106
+ async function withSchemaLock(connection, schema, logger, work) {
107
+ // With a schema, the lock is that schema's: two installs in two schemas of one database heal
108
+ // independently. Without one, the keys are unchanged.
106
109
  switch (connection.dialect.name) {
107
110
  case 'postgres':
108
111
  await connection.transaction(async (trx) => {
109
- await trx.rawQuery('select pg_advisory_xact_lock(?)', [PG_SCHEMA_LOCK_KEY]);
112
+ await trx.rawQuery('select pg_advisory_xact_lock(?)', [
113
+ schemaLockKey(PG_SCHEMA_LOCK_KEY, schema),
114
+ ]);
110
115
  await work(trx);
111
116
  });
112
117
  return;
113
118
  case 'mysql':
114
119
  await connection.transaction(async (trx) => {
115
- const result = (await trx.rawQuery("select get_lock(concat('durable_schema:', database()), ?) as got", [MYSQL_SCHEMA_LOCK_TIMEOUT_S]));
120
+ // The lock is named after the database the tables are in: the configured one, else the
121
+ // connection's.
122
+ const lockName = schema
123
+ ? { sql: '?', bindings: [mysqlLockName('durable_schema:', schema)] }
124
+ : { sql: "concat('durable_schema:', database())", bindings: [] };
125
+ const result = (await trx.rawQuery(`select get_lock(${lockName.sql}, ?) as got`, [
126
+ ...lockName.bindings,
127
+ MYSQL_SCHEMA_LOCK_TIMEOUT_S,
128
+ ]));
116
129
  const got = Number(result[0]?.[0]?.got) === 1;
117
130
  if (!got) {
118
131
  logger.warn(`@adonis-agora/durable: waited ${MYSQL_SCHEMA_LOCK_TIMEOUT_S}s for the durable schema lock; checking the schema without it.`);
@@ -123,7 +136,7 @@ async function withSchemaLock(connection, logger, work) {
123
136
  finally {
124
137
  if (got) {
125
138
  await trx
126
- .rawQuery("select release_lock(concat('durable_schema:', database()))")
139
+ .rawQuery(`select release_lock(${lockName.sql})`, lockName.bindings)
127
140
  .catch(() => undefined);
128
141
  }
129
142
  }
@@ -155,7 +168,7 @@ function repairWarning(repairs) {
155
168
  ].join(' ');
156
169
  }
157
170
  /**
158
- * Does an index with this name exist in the connection's current schema? Returns `null` for
171
+ * Does an index with this name exist in `schema` (the connection's current schema when unset)? Returns `null` for
159
172
  * dialects outside the store's three declared targets (SQLite / Postgres / MySQL), which the
160
173
  * caller handles by falling back to the legacy best-effort create.
161
174
  *
@@ -167,15 +180,11 @@ function repairWarning(repairs) {
167
180
  * catalog `SELECT` can never fail that way, so the repair never touches the database unless the
168
181
  * index is genuinely missing.
169
182
  */
170
- async function indexExists(connection, indexName) {
183
+ async function indexExists(connection, schema, indexName) {
171
184
  switch (connection.dialect.name) {
172
185
  case 'postgres': {
173
- const rows = await connection
174
- .query()
175
- .from('pg_indexes')
176
- .where('indexname', indexName)
177
- .andWhereRaw('schemaname = current_schema()')
178
- .limit(1);
186
+ const q = connection.query().from('pg_indexes').where('indexname', indexName);
187
+ const rows = await whereSchema(q, connection, 'schemaname', schema).limit(1);
179
188
  return rows.length > 0;
180
189
  }
181
190
  case 'sqlite3':
@@ -190,12 +199,11 @@ async function indexExists(connection, indexName) {
190
199
  return rows.length > 0;
191
200
  }
192
201
  case 'mysql': {
193
- const rows = await connection
202
+ const q = connection
194
203
  .query()
195
204
  .from('information_schema.statistics')
196
- .where('index_name', indexName)
197
- .andWhereRaw('table_schema = database()')
198
- .limit(1);
205
+ .where('index_name', indexName);
206
+ const rows = await whereSchema(q, connection, 'table_schema', schema).limit(1);
199
207
  return rows.length > 0;
200
208
  }
201
209
  default:
@@ -222,6 +230,9 @@ async function indexExists(connection, indexName) {
222
230
  * prefer the published migration (`node ace configure @adonis-agora/durable`); this helper is for standalone
223
231
  * use, tests, and `ensureSchema`.
224
232
  *
233
+ * Pass `options.schema` to put the tables in their own database schema (created when missing): every
234
+ * statement names it, on any connection. It must match the store's `schema`.
235
+ *
225
236
  * Pass `connectionName` to provision the tables on a dedicated Lucid connection — it must match the
226
237
  * connection the store reads/writes on, or `ensureSchema` would create the tables where the store
227
238
  * never looks. Omit it to use the `Database` default connection.
@@ -233,26 +244,31 @@ async function indexExists(connection, indexName) {
233
244
  */
234
245
  export async function createDurableTables(db, connectionName, options = {}) {
235
246
  const connection = db.connection(connectionName);
247
+ const schema = validateSchemaName(options.schema, 'createDurableTables');
248
+ assertSchemaSupported(connection, schema, 'createDurableTables');
236
249
  const logger = options.logger ?? console;
237
250
  const collation = connection.dialect.name === 'mysql' ? configuredCollation(connection, options) : undefined;
238
251
  const expected = schemaFingerprint(collation);
239
252
  // The steady-state boot: one catalog probe and one PK read, no per-table checks.
240
- if (!options.force && (await readSchemaFingerprint(connection)) === expected)
253
+ if (!options.force && (await readSchemaFingerprint(connection, schema)) === expected)
241
254
  return;
242
- await withSchemaLock(connection, logger, async (client) => {
255
+ // Outside the lock's transaction: a failed CREATE SCHEMA would abort it.
256
+ await ensureSchemaExists(connection, schema);
257
+ await withSchemaLock(connection, schema, logger, async (client) => {
243
258
  // A sibling may have healed while this process waited for the lock.
244
- if (!options.force && (await readSchemaFingerprint(client)) === expected)
259
+ if (!options.force && (await readSchemaFingerprint(client, schema)) === expected)
245
260
  return;
246
- await healDurableTables(client, collation, logger);
247
- await writeSchemaFingerprint(client, expected);
261
+ await healDurableTables(client, schema, collation, logger);
262
+ await writeSchemaFingerprint(client, schema, expected);
248
263
  });
249
264
  }
250
265
  /** The additive check-and-repair behind {@link createDurableTables}, on one client (or transaction). */
251
- async function healDurableTables(client, collation, logger) {
266
+ async function healDurableTables(client, schema, collation, logger) {
252
267
  // Knex's schema builder is stateful — operations chained on one instance run together. Take a FRESH
253
268
  // `client.schema` for every hasTable/createTable so each DDL statement executes exactly once, all on
254
- // the store's own connection (or the lock-holding transaction).
255
- const conn = () => client.schema;
269
+ // the store's own connection (or the lock-holding transaction), scoped to the configured schema.
270
+ const conn = () => schemaBuilder(client, schema);
271
+ const columnExists = (table, column) => hasColumn(client, schema, table, column);
256
272
  const isMysql = client.dialect.name === 'mysql';
257
273
  // MySQL: create new tables with the configured collation (CONVERT below aligns existing ones).
258
274
  const charset = collation?.split('_')[0];
@@ -271,7 +287,7 @@ async function healDurableTables(client, collation, logger) {
271
287
  // best-effort path stays — still transaction-unsafe, but only outside the store's declared
272
288
  // targets.
273
289
  const ensureIndex = async (table, columns, indexName) => {
274
- const known = await indexExists(client, indexName);
290
+ const known = await indexExists(client, schema, indexName);
275
291
  if (known === true)
276
292
  return;
277
293
  if (known === null) {
@@ -327,14 +343,14 @@ async function healDurableTables(client, collation, logger) {
327
343
  // Auto-migrate an older runs table by adding any columns introduced after its creation. Each is
328
344
  // applied independently so a table missing several catches up. Mirrors the in-place pattern other
329
345
  // adapters use; new columns are nullable / carry a DEFAULT so existing rows read back unchanged.
330
- if (!(await conn().hasColumn(DURABLE_TABLES.runs, 'priority'))) {
346
+ if (!(await columnExists(DURABLE_TABLES.runs, 'priority'))) {
331
347
  // Nullable (no default) so existing rows read back as "unprioritised" and the FIFO path is unchanged.
332
348
  await conn().alterTable(DURABLE_TABLES.runs, (table) => {
333
349
  table.integer('priority');
334
350
  });
335
351
  repairs.push(`${DURABLE_TABLES.runs}.priority`);
336
352
  }
337
- if (!(await conn().hasColumn(DURABLE_TABLES.runs, 'namespace'))) {
353
+ if (!(await columnExists(DURABLE_TABLES.runs, 'namespace'))) {
338
354
  // DEFAULT 'default' so every pre-namespace row reads back in the reserved 'default' partition —
339
355
  // byte-identical behavior for a single-pool deploy that adds the column.
340
356
  await conn().alterTable(DURABLE_TABLES.runs, (table) => {
@@ -343,14 +359,14 @@ async function healDurableTables(client, collation, logger) {
343
359
  });
344
360
  repairs.push(`${DURABLE_TABLES.runs}.namespace`);
345
361
  }
346
- if (!(await conn().hasColumn(DURABLE_TABLES.runs, 'origin'))) {
362
+ if (!(await columnExists(DURABLE_TABLES.runs, 'origin'))) {
347
363
  // Origin attribution wave: nullable, so an app-own/legacy run reads back with no origin.
348
364
  await conn().alterTable(DURABLE_TABLES.runs, (table) => {
349
365
  table.string('origin');
350
366
  });
351
367
  repairs.push(`${DURABLE_TABLES.runs}.origin`);
352
368
  }
353
- if (!(await conn().hasColumn(DURABLE_TABLES.runs, 'awaiting_decision_task_id'))) {
369
+ if (!(await columnExists(DURABLE_TABLES.runs, 'awaiting_decision_task_id'))) {
354
370
  // Remote-turn decision marker (multi-instance decisions): nullable, so an existing run reads back
355
371
  // as "not awaiting a dispatched turn". TEXT: it embeds the run id plus a UUID.
356
372
  await conn().alterTable(DURABLE_TABLES.runs, (table) => {
@@ -358,7 +374,7 @@ async function healDurableTables(client, collation, logger) {
358
374
  });
359
375
  repairs.push(`${DURABLE_TABLES.runs}.awaiting_decision_task_id`);
360
376
  }
361
- if (!(await conn().hasColumn(DURABLE_TABLES.runs, 'wake_requested_at'))) {
377
+ if (!(await columnExists(DURABLE_TABLES.runs, 'wake_requested_at'))) {
362
378
  // Refused-wake marker (a wake that found another instance's lease): nullable, so an existing
363
379
  // run reads back as "no turn requested".
364
380
  await conn().alterTable(DURABLE_TABLES.runs, (table) => {
@@ -367,7 +383,7 @@ async function healDurableTables(client, collation, logger) {
367
383
  });
368
384
  repairs.push(`${DURABLE_TABLES.runs}.wake_requested_at`);
369
385
  }
370
- if (!(await conn().hasColumn(DURABLE_TABLES.runs, 'tenant_id'))) {
386
+ if (!(await columnExists(DURABLE_TABLES.runs, 'tenant_id'))) {
371
387
  // Tenancy: nullable, so an existing run reads back with no tenant.
372
388
  await conn().alterTable(DURABLE_TABLES.runs, (table) => {
373
389
  table.string('tenant_id');
@@ -410,7 +426,7 @@ async function healDurableTables(client, collation, logger) {
410
426
  });
411
427
  }
412
428
  else {
413
- if (!(await conn().hasColumn(DURABLE_TABLES.checkpoints, 'parallel_group'))) {
429
+ if (!(await columnExists(DURABLE_TABLES.checkpoints, 'parallel_group'))) {
414
430
  // Auto-migrate an older checkpoints table: add the nullable `parallel_group` column in place.
415
431
  // Nullable (no default) so a legacy (non-parallel) checkpoint reads back untagged.
416
432
  await conn().alterTable(DURABLE_TABLES.checkpoints, (table) => {
@@ -418,7 +434,7 @@ async function healDurableTables(client, collation, logger) {
418
434
  });
419
435
  repairs.push(`${DURABLE_TABLES.checkpoints}.parallel_group`);
420
436
  }
421
- if (!(await conn().hasColumn(DURABLE_TABLES.checkpoints, 'last_heartbeat_at'))) {
437
+ if (!(await columnExists(DURABLE_TABLES.checkpoints, 'last_heartbeat_at'))) {
422
438
  // Heartbeat-persistence wave: nullable, so a step whose handler never beats reads back with
423
439
  // no liveness claim (absent ≠ silent-since-epoch).
424
440
  await conn().alterTable(DURABLE_TABLES.checkpoints, (table) => {
@@ -429,7 +445,7 @@ async function healDurableTables(client, collation, logger) {
429
445
  // not the statement count.
430
446
  repairs.push(`${DURABLE_TABLES.checkpoints}.last_heartbeat_at`, `${DURABLE_TABLES.checkpoints}.heartbeat_progress`);
431
447
  }
432
- if (!(await conn().hasColumn(DURABLE_TABLES.checkpoints, 'queue'))) {
448
+ if (!(await columnExists(DURABLE_TABLES.checkpoints, 'queue'))) {
433
449
  // Durable flow-control release: the admitted queue is persisted on the pending checkpoint so
434
450
  // ANY instance receiving the result can free the slot. Nullable — a non-queued step reads back
435
451
  // with no slot to release.
@@ -469,7 +485,7 @@ async function healDurableTables(client, collation, logger) {
469
485
  });
470
486
  }
471
487
  else {
472
- if (!(await conn().hasColumn(DURABLE_TABLES.signalWaiters, 'parallel_group'))) {
488
+ if (!(await columnExists(DURABLE_TABLES.signalWaiters, 'parallel_group'))) {
473
489
  // Auto-migrate an older signal_waiters table: add the nullable `parallel_group` column in
474
490
  // place. Nullable (no default) so a legacy (non-fan) waiter reads back untagged and the await
475
491
  // is unchanged.
@@ -499,11 +515,12 @@ async function healDurableTables(client, collation, logger) {
499
515
  [DURABLE_TABLES.signalWaiters, 'durable_signal_waiters_token_prefix_idx'],
500
516
  [DURABLE_TABLES.bufferedSignals, 'durable_buffered_signals_token_prefix_idx'],
501
517
  ]) {
502
- if (await indexExists(client, indexName))
518
+ if (await indexExists(client, schema, indexName))
503
519
  continue;
520
+ // The index lands in its table's schema: `create index "i" on "s"."t"`.
504
521
  await client.rawQuery('create index ?? on ?? (?? varchar_pattern_ops)', [
505
522
  indexName,
506
- table,
523
+ qualify(schema, table),
507
524
  'token',
508
525
  ]);
509
526
  if (!created.has(table))
@@ -546,7 +563,7 @@ async function healDurableTables(client, collation, logger) {
546
563
  table.index(['paused', 'next_fire_at'], 'durable_schedules_due_idx');
547
564
  });
548
565
  }
549
- else if (!(await conn().hasColumn(DURABLE_TABLES.schedules, 'tenant_id'))) {
566
+ else if (!(await columnExists(DURABLE_TABLES.schedules, 'tenant_id'))) {
550
567
  // Tenancy: nullable, so an existing schedule (and every run it fires) has no tenant.
551
568
  await conn().alterTable(DURABLE_TABLES.schedules, (table) => {
552
569
  table.string('tenant_id');
@@ -558,9 +575,9 @@ async function healDurableTables(client, collation, logger) {
558
575
  // an operator needs to know their schema was behind and by what, and a burst of lines is easier to
559
576
  // lose than a single one.
560
577
  if (isMysql) {
561
- repairs.push(...(await widenMysqlJsonColumns(client)));
578
+ repairs.push(...(await widenMysqlJsonColumns(client, schema)));
562
579
  if (collation && charset) {
563
- repairs.push(...(await alignMysqlCollation(client, charset, collation, logger)));
580
+ repairs.push(...(await alignMysqlCollation(client, schema, charset, collation, logger)));
564
581
  }
565
582
  }
566
583
  if (repairs.length > 0) {
@@ -573,22 +590,22 @@ async function healDurableTables(client, collation, logger) {
573
590
  * since MySQL rebuilds the table for each; a table already widened is not touched. Returns the
574
591
  * columns it changed.
575
592
  */
576
- async function widenMysqlJsonColumns(client) {
577
- return widenMysqlLongtextColumns(client, DURABLE_JSON_COLUMNS);
593
+ async function widenMysqlJsonColumns(client, schema) {
594
+ return widenMysqlLongtextColumns(client, DURABLE_JSON_COLUMNS, schema);
578
595
  }
579
596
  /**
580
597
  * MySQL: `MODIFY` to `longtext` every column of `spec` that exists and isn't `longtext` yet, one
581
598
  * `ALTER` per table. Shared by the store schema and the db transport's (`createDurableTransportTables`).
582
599
  * Returns the columns it changed.
583
600
  */
584
- export async function widenMysqlLongtextColumns(client, spec) {
601
+ export async function widenMysqlLongtextColumns(client, spec, schema) {
585
602
  const tables = Object.keys(spec);
586
- const rows = (await client
603
+ const q = client
587
604
  .query()
588
605
  .from('information_schema.columns')
589
606
  .select('table_name as t', 'column_name as c', 'data_type as d')
590
- .whereRaw('table_schema = database()')
591
- .whereIn('table_name', tables));
607
+ .whereIn('table_name', tables);
608
+ const rows = (await whereSchema(q, client, 'table_schema', schema));
592
609
  const widened = [];
593
610
  for (const [table, columns] of Object.entries(spec)) {
594
611
  const narrow = columns.columns.filter((column) => {
@@ -600,7 +617,7 @@ export async function widenMysqlLongtextColumns(client, spec) {
600
617
  const modify = narrow
601
618
  .map((column) => `modify \`${column}\` longtext ${columns.notNull?.includes(column) ? 'not null' : 'null'}`)
602
619
  .join(', ');
603
- await client.rawQuery(`alter table \`${table}\` ${modify}`);
620
+ await client.rawQuery(`alter table ?? ${modify}`, [qualify(schema, table)]);
604
621
  widened.push(...narrow.map((column) => `${table}.${column} (longtext)`));
605
622
  }
606
623
  return widened;
@@ -610,20 +627,20 @@ export async function widenMysqlLongtextColumns(client, spec) {
610
627
  * version, or before the option was set, carry the server default). A failed `CONVERT` is warned and
611
628
  * left for the next forced check rather than failing the boot. Returns the tables it converted.
612
629
  */
613
- async function alignMysqlCollation(client, charset, collation, logger) {
630
+ async function alignMysqlCollation(client, schema, charset, collation, logger) {
614
631
  const tables = [...Object.values(DURABLE_TABLES), DURABLE_SCHEMA_META_TABLE];
615
- const rows = (await client
632
+ const q = client
616
633
  .query()
617
634
  .from('information_schema.tables')
618
635
  .select('table_name as t', 'table_collation as c')
619
- .whereRaw('table_schema = database()')
620
- .whereIn('table_name', tables));
636
+ .whereIn('table_name', tables);
637
+ const rows = (await whereSchema(q, client, 'table_schema', schema));
621
638
  const converted = [];
622
639
  for (const row of rows) {
623
640
  if (row.c == null || row.c === collation)
624
641
  continue;
625
642
  try {
626
- await client.rawQuery(`alter table \`${row.t}\` convert to character set ${charset} collate ${collation}`);
643
+ await client.rawQuery(`alter table ?? convert to character set ${charset} collate ${collation}`, [qualify(schema, row.t)]);
627
644
  converted.push(`${row.t} (collation ${collation})`);
628
645
  }
629
646
  catch (err) {
@@ -632,9 +649,14 @@ async function alignMysqlCollation(client, charset, collation, logger) {
632
649
  }
633
650
  return converted;
634
651
  }
635
- /** Drop every durable table (reverse FK order). Used by tests and migration `down`. */
636
- export async function dropDurableTables(db, connectionName) {
637
- const conn = () => db.connection(connectionName).schema;
652
+ /**
653
+ * Drop every durable table (reverse FK order), in `options.schema` when set (the schema itself is
654
+ * left). Used by tests and migration `down`.
655
+ */
656
+ export async function dropDurableTables(db, connectionName, options = {}) {
657
+ const schema = validateSchemaName(options.schema, 'dropDurableTables');
658
+ assertSchemaSupported(db.connection(connectionName), schema, 'dropDurableTables');
659
+ const conn = () => schemaBuilder(db.connection(connectionName), schema);
638
660
  await conn().dropTableIfExists(DURABLE_SCHEMA_META_TABLE);
639
661
  await conn().dropTableIfExists(DURABLE_TABLES.schedules);
640
662
  await conn().dropTableIfExists(DURABLE_TABLES.bufferedEvents);