@jskit-ai/database-runtime-mysql 0.1.198 → 0.1.200

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/database-runtime-mysql",
3
- "version": "0.1.198",
3
+ "version": "0.1.200",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -12,7 +12,7 @@
12
12
  "./shared/dialect": "./src/shared/dialect.js"
13
13
  },
14
14
  "dependencies": {
15
- "@jskit-ai/database-runtime": "0.1.200",
15
+ "@jskit-ai/database-runtime": "0.1.202",
16
16
  "mysql2": "^3.11.2"
17
17
  },
18
18
  "jskit": {
@@ -56,6 +56,6 @@
56
56
  }
57
57
  },
58
58
  "peerDependencies": {
59
- "@jskit-ai/kernel": "0.1.200"
59
+ "@jskit-ai/kernel": "0.1.202"
60
60
  }
61
61
  }
@@ -48,17 +48,24 @@ Knex discovers package-owned migrations from the installed dependency graph.
48
48
  `example/knexfile.js` loads an optional local `.env` and fixes the MySQL dialect.
49
49
  `example/scripts/prepare-database.js` is the portable migrate-then-seed
50
50
  entrypoint for managed sessions and deployments. `example/.env.example` names
51
- the five connection values without a secret.
51
+ the connection values and separate test databases without a secret.
52
52
 
53
53
  `example/tests/browser/database.js` is the application-owned browser preparation
54
- helper. It requires the exact `TEST_DB_NAME`, rejects the ordinary database,
54
+ helper. It requires the exact `BROWSER_TEST_DB_NAME`, rejects the ordinary and
55
+ disposable `TEST_DB_NAME` databases,
55
56
  retains the schema and migration ledger, applies pending migrations, then clears
56
57
  test rows and calls one explicit fixture seed transaction. The seed must restore
57
58
  any migration-owned baseline rows the app needs. Call it once before starting
58
59
  the isolated test server, never in `beforeEach`. It closes its connections without
59
60
  dropping the database. The launcher selects that database only for its own server
60
61
  and identity process, disables external effects, and proves actual server identity
61
- before the suite writes data. It must not edit the normal environment.
62
+ before the suite writes data. It must not edit the normal environment. Provision
63
+ both test databases with exact grants for the application account. A disposable
64
+ integration or migration test must never drop the browser database.
65
+
66
+ Startup reports whether the schema exists, the number of migrations applied,
67
+ and the migration, fixture-reset and seed durations. Use those measurements to
68
+ diagnose a slow start before rerunning a browser case.
62
69
 
63
70
  Keep fixtures small for the selected test scope. Run related cases in one suite
64
71
  invocation. Use the development launcher for ordinary browser checks; test the
@@ -81,7 +88,7 @@ it as `seed` to `prepareDatabaseFromApp()`.
81
88
  - Run `npm run db:migrate:status` and the application verification command.
82
89
  - Exercise one transaction and one negative connection case.
83
90
  - When a seed exists, run `db:prepare` twice and require the second run to be safe.
84
- - For browser preparation, prove two consecutive starts: the second applies no
91
+ - For browser preparation, run a disposable test between two starts: the second applies no
85
92
  old migrations, stale fixture data is removed, baseline rows are restored,
86
93
  foreign-key checks remain enabled, and the normal database is unchanged.
87
94
  Also prove that a new pending migration is applied. Keep this as a regression
@@ -3,3 +3,5 @@ DB_PORT=3306
3
3
  DB_NAME=application
4
4
  DB_USER=application
5
5
  DB_PASSWORD=replace-me
6
+ TEST_DB_NAME=application_test
7
+ BROWSER_TEST_DB_NAME=application_browser_test
@@ -8,6 +8,6 @@
8
8
  "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
9
9
  },
10
10
  "dependencies": {
11
- "@jskit-ai/database-runtime-mysql": "0.1.198"
11
+ "@jskit-ai/database-runtime-mysql": "0.1.200"
12
12
  }
13
13
  }
@@ -2,33 +2,45 @@ import knex from "knex";
2
2
 
3
3
  // Application-owned test setup. Schema migration tests use a separate fresh database.
4
4
  async function prepareBrowserTestDatabase({ knexConfig, environment = process.env, seed }) {
5
+ const started = performance.now();
5
6
  if (environment.NODE_ENV === "production") throw new Error("Browser tests cannot prepare a production runtime.");
6
7
  if (typeof seed !== "function") throw new TypeError("An explicit browser fixture seed is required.");
7
8
  if (knexConfig?.client !== "mysql2" || !knexConfig.connection || typeof knexConfig.connection !== "object") {
8
9
  throw new Error("Browser preparation requires a resolved MySQL Knex configuration.");
9
10
  }
10
- const database = String(environment.TEST_DB_NAME || "").trim();
11
- if (!/^[a-zA-Z0-9_]{1,64}$/.test(database)) throw new Error("TEST_DB_NAME must name the exact disposable database.");
12
- const protectedNames = [knexConfig.connection.database, environment.DB_NAME];
11
+ const database = String(environment.BROWSER_TEST_DB_NAME || "").trim();
12
+ if (!/^[a-zA-Z0-9_]{1,64}$/.test(database)) throw new Error("BROWSER_TEST_DB_NAME must name the exact browser database.");
13
+ const protectedNames = [knexConfig.connection.database, environment.DB_NAME, environment.TEST_DB_NAME];
13
14
  if (environment.DATABASE_URL) {
14
15
  try { protectedNames.push(decodeURIComponent(new URL(environment.DATABASE_URL).pathname.slice(1))); }
15
16
  catch { throw new Error("DATABASE_URL must be a valid database URL."); }
16
17
  }
17
18
  if (!knexConfig.connection.database || protectedNames.some(name => String(name || "").trim().toLowerCase() === database.toLowerCase())) {
18
- throw new Error("TEST_DB_NAME must differ from every configured application database.");
19
+ throw new Error("BROWSER_TEST_DB_NAME must differ from the application and disposable TEST_DB_NAME databases.");
19
20
  }
20
21
  const adminConnection = { ...knexConfig.connection };
21
22
  delete adminConnection.database;
22
23
  const admin = knex({ ...knexConfig, connection: adminConnection, pool: { min: 0, max: 1 } });
23
- try { await admin.raw("CREATE DATABASE IF NOT EXISTS ??", [database]); }
24
+ let schemaExisted;
25
+ try {
26
+ const [[row]] = await admin.raw("SELECT COUNT(*) AS count FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", [database]);
27
+ schemaExisted = Number(row.count) === 1;
28
+ await admin.raw("CREATE DATABASE IF NOT EXISTS ??", [database]);
29
+ }
24
30
  finally { await admin.destroy(); }
31
+ console.info(`Browser database ${database}: ${schemaExisted ? "existing schema" : "new schema"}.`);
25
32
  const db = knex({ ...knexConfig, connection: { ...knexConfig.connection, database } });
26
33
  try {
34
+ const migrationStarted = performance.now();
27
35
  const [, migrations] = await db.migrate.latest();
36
+ const migrationMs = Math.round(performance.now() - migrationStarted);
37
+ console.info(`Browser migrations: ${migrations.length} applied in ${migrationMs}ms.`);
38
+ const resetStarted = performance.now();
39
+ let resetTables = 0;
28
40
  const connection = await db.client.acquireConnection();
29
41
  try {
30
42
  const [[actual]] = await db.raw("SELECT DATABASE() AS databaseName").connection(connection);
31
- if (actual.databaseName !== database) throw new Error("Browser reset requires the exact TEST_DB_NAME connection.");
43
+ if (actual.databaseName !== database) throw new Error("Browser reset requires the exact BROWSER_TEST_DB_NAME connection.");
32
44
  const [tables] = await db.raw(
33
45
  "SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE'", [database]
34
46
  ).connection(connection);
@@ -38,11 +50,18 @@ async function prepareBrowserTestDatabase({ knexConfig, environment = process.en
38
50
  for (const { name } of tables) {
39
51
  if (name === ledger || name === `${ledger}_lock`) continue;
40
52
  await db.raw("TRUNCATE TABLE ??", [name]).connection(connection);
53
+ resetTables += 1;
41
54
  }
42
55
  } finally { await db.raw("SET FOREIGN_KEY_CHECKS = 1").connection(connection); }
43
56
  } finally { await db.client.releaseConnection(connection); }
57
+ const resetMs = Math.round(performance.now() - resetStarted);
58
+ console.info(`Browser fixture reset: ${resetTables} tables in ${resetMs}ms.`);
59
+ const seedStarted = performance.now();
44
60
  await db.transaction(seed);
45
- return { database, migrations };
61
+ const seedMs = Math.round(performance.now() - seedStarted);
62
+ const totalMs = Math.round(performance.now() - started);
63
+ console.info(`Browser fixture seed: ${seedMs}ms; database preparation total: ${totalMs}ms.`);
64
+ return { database, migrations, schemaExisted, timings: { migrationMs, resetMs, seedMs, totalMs } };
46
65
  } finally { await db.destroy(); }
47
66
  }
48
67
 
@@ -8,27 +8,28 @@ const seed = async (db) => db("fixtures").insert({ id: 1, label: "baseline" });
8
8
  test("browser preparation rejects missing, ordinary and production databases before connecting", async () => {
9
9
  const knexConfig = { client: "mysql2", connection: { database: "application" } };
10
10
  for (const environment of [
11
- {}, { TEST_DB_NAME: "application" }, { TEST_DB_NAME: "APPLICATION" },
12
- { TEST_DB_NAME: "other", DB_NAME: "other" },
13
- { TEST_DB_NAME: "other", DATABASE_URL: "mysql://localhost/other" },
14
- { TEST_DB_NAME: "tests", NODE_ENV: "production" }
11
+ {}, { BROWSER_TEST_DB_NAME: "application" }, { BROWSER_TEST_DB_NAME: "APPLICATION" },
12
+ { BROWSER_TEST_DB_NAME: "other", DB_NAME: "other" },
13
+ { BROWSER_TEST_DB_NAME: "other", DATABASE_URL: "mysql://localhost/other" },
14
+ { BROWSER_TEST_DB_NAME: "tests", TEST_DB_NAME: "TESTS" },
15
+ { BROWSER_TEST_DB_NAME: "tests", NODE_ENV: "production" }
15
16
  ]) {
16
17
  await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment, seed }), /TEST_DB_NAME|production/u);
17
18
  }
18
- await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment: { TEST_DB_NAME: "tests" } }), /seed/u);
19
+ await assert.rejects(prepareBrowserTestDatabase({ knexConfig, environment: { BROWSER_TEST_DB_NAME: "tests" } }), /seed/u);
19
20
  });
20
21
 
21
22
  test("native browser preparation retains schema, clears data and applies only new migrations", {
22
- skip: !process.env.TEST_DB_NAME || !process.env.DB_HOST
23
+ skip: !process.env.BROWSER_TEST_DB_NAME || !process.env.TEST_DB_NAME || !process.env.DB_HOST
23
24
  }, async () => {
24
25
  const environment = process.env;
25
26
  const connection = { host: environment.DB_HOST, port: Number(environment.DB_PORT), user: environment.DB_USER,
26
27
  password: environment.DB_PASSWORD, database: environment.DB_NAME };
27
28
  const admin = knex({ client: "mysql2", connection: { ...connection, database: undefined } });
28
- const [[existing]] = await admin.raw("SELECT COUNT(*) AS n FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?", [environment.TEST_DB_NAME]);
29
+ const [[existing]] = await admin.raw("SELECT COUNT(*) AS n FROM information_schema.SCHEMATA WHERE SCHEMA_NAME IN (?, ?)", [environment.BROWSER_TEST_DB_NAME, environment.TEST_DB_NAME]);
29
30
  if (Number(existing.n)) {
30
31
  await admin.destroy();
31
- throw new Error("The native pattern regression requires a fresh, explicitly supplied TEST_DB_NAME.");
32
+ throw new Error("The native pattern regression requires fresh, explicitly supplied test database names.");
32
33
  }
33
34
  let db;
34
35
  let applied = 0;
@@ -44,11 +45,17 @@ test("native browser preparation retains schema, clears data and applies only ne
44
45
  const [normalBefore] = await admin.raw("SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?", [environment.DB_NAME]);
45
46
  const first = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
46
47
  assert.deepEqual(first.migrations, ["001_fixture.js"]);
47
- db = knex({ client: "mysql2", connection: { ...connection, database: environment.TEST_DB_NAME } });
48
+ assert.equal(first.schemaExisted, false);
49
+ db = knex({ client: "mysql2", connection: { ...connection, database: environment.BROWSER_TEST_DB_NAME } });
48
50
  const ledger = await db("knex_migrations").select();
49
51
  await db("fixtures").insert({ id: 2, label: "stale" });
50
52
  await db("children").insert({ parent_id: 2 });
53
+ // A disposable integration test between browser starts must not erase its schema.
54
+ await admin.raw("CREATE DATABASE ??", [environment.TEST_DB_NAME]);
55
+ await admin.raw("CREATE TABLE ??.proof (id INT PRIMARY KEY)", [environment.TEST_DB_NAME]);
56
+ await admin.raw("DROP DATABASE ??", [environment.TEST_DB_NAME]);
51
57
  const second = await prepareBrowserTestDatabase({ knexConfig, environment, seed });
58
+ assert.equal(second.schemaExisted, true);
52
59
  assert.deepEqual(second.migrations, []);
53
60
  assert.equal(applied, 1);
54
61
  assert.deepEqual(await db("knex_migrations").select(), ledger);
@@ -68,6 +75,7 @@ test("native browser preparation retains schema, clears data and applies only ne
68
75
  assert.deepEqual(normalAfter, normalBefore);
69
76
  } finally {
70
77
  await db?.destroy();
78
+ await admin.raw("DROP DATABASE IF EXISTS ??", [environment.BROWSER_TEST_DB_NAME]);
71
79
  await admin.raw("DROP DATABASE IF EXISTS ??", [environment.TEST_DB_NAME]);
72
80
  await admin.destroy();
73
81
  }