@rvoh/dream 2.13.1 → 2.14.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.
Files changed (212) hide show
  1. package/dist/cjs/src/db/DreamDbConnection.js +9 -3
  2. package/dist/cjs/src/db/dbConnectionLeakDiagnostics.js +0 -14
  3. package/dist/cjs/src/db/migration-helpers/DreamMigrationHelpers.js +131 -0
  4. package/dist/cjs/src/db/testDatabasePool.js +182 -0
  5. package/dist/cjs/src/dream/QueryDriver/Base.js +29 -0
  6. package/dist/cjs/src/dream/QueryDriver/Postgres.js +76 -1
  7. package/dist/cjs/src/dream/QueryDriver/helpers/pg/dropDb.js +3 -1
  8. package/dist/cjs/src/dream-app/index.js +113 -6
  9. package/dist/cjs/src/helpers/areEqual.js +1 -1
  10. package/dist/cjs/src/helpers/loadRepl.js +4 -0
  11. package/dist/esm/src/db/DreamDbConnection.js +9 -3
  12. package/dist/esm/src/db/dbConnectionLeakDiagnostics.js +0 -14
  13. package/dist/esm/src/db/migration-helpers/DreamMigrationHelpers.js +131 -0
  14. package/dist/esm/src/db/testDatabasePool.js +182 -0
  15. package/dist/esm/src/dream/QueryDriver/Base.js +29 -0
  16. package/dist/esm/src/dream/QueryDriver/Postgres.js +76 -1
  17. package/dist/esm/src/dream/QueryDriver/helpers/pg/dropDb.js +3 -1
  18. package/dist/esm/src/dream-app/index.js +113 -6
  19. package/dist/esm/src/helpers/areEqual.js +1 -1
  20. package/dist/esm/src/helpers/loadRepl.js +4 -0
  21. package/dist/types/src/db/migration-helpers/DreamMigrationHelpers.d.ts +105 -1
  22. package/dist/types/src/db/testDatabasePool.d.ts +42 -0
  23. package/dist/types/src/dream/QueryDriver/Base.d.ts +47 -0
  24. package/dist/types/src/dream/QueryDriver/Postgres.d.ts +19 -0
  25. package/dist/types/src/dream-app/index.d.ts +64 -1
  26. package/dist/types/src/helpers/areEqual.d.ts +1 -1
  27. package/dist/types/src/helpers/range.d.ts +8 -4
  28. package/docs/assets/hierarchy.js +1 -1
  29. package/docs/assets/search.js +1 -1
  30. package/docs/classes/db.DreamMigrationHelpers.html +65 -9
  31. package/docs/classes/db.KyselyQueryDriver.html +47 -32
  32. package/docs/classes/db.PostgresQueryDriver.html +52 -33
  33. package/docs/classes/db.QueryDriverBase.html +46 -31
  34. package/docs/classes/errors.CheckConstraintViolation.html +3 -3
  35. package/docs/classes/errors.ColumnOverflow.html +3 -3
  36. package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
  37. package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
  38. package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
  39. package/docs/classes/errors.DecryptionError.html +2 -2
  40. package/docs/classes/errors.DecryptionParseError.html +2 -2
  41. package/docs/classes/errors.DecryptionRotationError.html +3 -3
  42. package/docs/classes/errors.GlobalNameNotSet.html +3 -3
  43. package/docs/classes/errors.InvalidCalendarDate.html +2 -2
  44. package/docs/classes/errors.InvalidClockTime.html +2 -2
  45. package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
  46. package/docs/classes/errors.InvalidDateTime.html +2 -2
  47. package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
  48. package/docs/classes/errors.NonLoadedAssociation.html +3 -3
  49. package/docs/classes/errors.NotNullViolation.html +3 -3
  50. package/docs/classes/errors.RecordNotFound.html +3 -3
  51. package/docs/classes/errors.ValidationError.html +3 -3
  52. package/docs/classes/index.CalendarDate.html +33 -33
  53. package/docs/classes/index.ClockTime.html +32 -32
  54. package/docs/classes/index.ClockTimeTz.html +35 -35
  55. package/docs/classes/index.DateTime.html +86 -86
  56. package/docs/classes/index.Decorators.html +19 -19
  57. package/docs/classes/index.Dream.html +118 -118
  58. package/docs/classes/index.DreamApp.html +42 -6
  59. package/docs/classes/index.DreamTransaction.html +2 -2
  60. package/docs/classes/index.Env.html +2 -2
  61. package/docs/classes/index.Query.html +57 -57
  62. package/docs/classes/system.CliFileWriter.html +4 -4
  63. package/docs/classes/system.DreamBin.html +2 -2
  64. package/docs/classes/system.DreamCLI.html +7 -7
  65. package/docs/classes/system.DreamImporter.html +2 -2
  66. package/docs/classes/system.DreamLogos.html +2 -2
  67. package/docs/classes/system.DreamSerializerBuilder.html +11 -11
  68. package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
  69. package/docs/classes/system.PathHelpers.html +3 -3
  70. package/docs/classes/utils.Encrypt.html +3 -3
  71. package/docs/classes/utils.Range.html +2 -2
  72. package/docs/functions/db.closeAllDbConnections.html +1 -1
  73. package/docs/functions/db.dreamDbConnections.html +1 -1
  74. package/docs/functions/db.untypedDb.html +1 -1
  75. package/docs/functions/db.validateColumn.html +1 -1
  76. package/docs/functions/db.validateTable.html +1 -1
  77. package/docs/functions/errors.pgErrorType.html +1 -1
  78. package/docs/functions/index.DreamSerializer.html +1 -1
  79. package/docs/functions/index.ObjectSerializer.html +1 -1
  80. package/docs/functions/index.ReplicaSafe.html +1 -1
  81. package/docs/functions/index.STI.html +1 -1
  82. package/docs/functions/index.SoftDelete.html +1 -1
  83. package/docs/functions/utils.camelize.html +1 -1
  84. package/docs/functions/utils.capitalize.html +1 -1
  85. package/docs/functions/utils.cloneDeepSafe.html +1 -1
  86. package/docs/functions/utils.compact.html +1 -1
  87. package/docs/functions/utils.groupBy.html +1 -1
  88. package/docs/functions/utils.hyphenize.html +1 -1
  89. package/docs/functions/utils.intersection.html +1 -1
  90. package/docs/functions/utils.isEmpty.html +1 -1
  91. package/docs/functions/utils.normalizeUnicode.html +1 -1
  92. package/docs/functions/utils.pascalize.html +1 -1
  93. package/docs/functions/utils.percent.html +1 -1
  94. package/docs/functions/utils.range.html +1 -1
  95. package/docs/functions/utils.round.html +1 -1
  96. package/docs/functions/utils.sanitizeString.html +1 -1
  97. package/docs/functions/utils.snakeify.html +1 -1
  98. package/docs/functions/utils.sort.html +1 -1
  99. package/docs/functions/utils.sortBy.html +1 -1
  100. package/docs/functions/utils.sortObjectByKey.html +1 -1
  101. package/docs/functions/utils.sortObjectByValue.html +1 -1
  102. package/docs/functions/utils.uncapitalize.html +1 -1
  103. package/docs/functions/utils.uniq.html +1 -1
  104. package/docs/hierarchy.html +1 -1
  105. package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
  106. package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
  107. package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
  108. package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
  109. package/docs/interfaces/types.BelongsToStatement.html +2 -2
  110. package/docs/interfaces/types.DecoratorContext.html +2 -2
  111. package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
  112. package/docs/interfaces/types.DreamAppOpts.html +2 -2
  113. package/docs/interfaces/types.DreamDbConfig.html +5 -5
  114. package/docs/interfaces/types.DurationObject.html +2 -2
  115. package/docs/interfaces/types.EncryptOptions.html +2 -2
  116. package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
  117. package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
  118. package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
  119. package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
  120. package/docs/types/openapi.OpenapiAllTypes.html +1 -1
  121. package/docs/types/openapi.OpenapiFormats.html +1 -1
  122. package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
  123. package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
  124. package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
  125. package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
  126. package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
  127. package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
  128. package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
  129. package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
  130. package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
  131. package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
  132. package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
  133. package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
  134. package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
  135. package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
  136. package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
  137. package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
  138. package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
  139. package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
  140. package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
  141. package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
  142. package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
  143. package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
  144. package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
  145. package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
  146. package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
  147. package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
  148. package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
  149. package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
  150. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
  151. package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
  152. package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
  153. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
  154. package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
  155. package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
  156. package/docs/types/openapi.OpenapiSchemaString.html +1 -1
  157. package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
  158. package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
  159. package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
  160. package/docs/types/openapi.OpenapiTypeField.html +1 -1
  161. package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
  162. package/docs/types/types.CalendarDateDurationUnit.html +1 -1
  163. package/docs/types/types.CalendarDateObject.html +1 -1
  164. package/docs/types/types.Camelized.html +1 -1
  165. package/docs/types/types.ClockTimeObject.html +1 -1
  166. package/docs/types/types.DbConnectionType.html +1 -1
  167. package/docs/types/types.DbTypes.html +1 -1
  168. package/docs/types/types.DreamAssociationMetadata.html +1 -1
  169. package/docs/types/types.DreamAttributes.html +1 -1
  170. package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
  171. package/docs/types/types.DreamClassColumn.html +1 -1
  172. package/docs/types/types.DreamColumn.html +1 -1
  173. package/docs/types/types.DreamColumnNames.html +1 -1
  174. package/docs/types/types.DreamLogLevel.html +1 -1
  175. package/docs/types/types.DreamLogger.html +2 -2
  176. package/docs/types/types.DreamModelSerializerType.html +1 -1
  177. package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
  178. package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
  179. package/docs/types/types.DreamParamSafeAttributes.html +1 -1
  180. package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
  181. package/docs/types/types.DreamSerializable.html +1 -1
  182. package/docs/types/types.DreamSerializableArray.html +1 -1
  183. package/docs/types/types.DreamSerializerKey.html +1 -1
  184. package/docs/types/types.DreamSerializers.html +1 -1
  185. package/docs/types/types.DreamVirtualColumns.html +1 -1
  186. package/docs/types/types.DurationUnit.html +1 -1
  187. package/docs/types/types.EncryptAlgorithm.html +1 -1
  188. package/docs/types/types.HasManyStatement.html +1 -1
  189. package/docs/types/types.HasOneStatement.html +1 -1
  190. package/docs/types/types.Hyphenized.html +1 -1
  191. package/docs/types/types.Pascalized.html +1 -1
  192. package/docs/types/types.PrimaryKeyType.html +1 -1
  193. package/docs/types/types.RoundingPrecision.html +1 -1
  194. package/docs/types/types.SerializerCasing.html +1 -1
  195. package/docs/types/types.SimpleObjectSerializerType.html +1 -1
  196. package/docs/types/types.Snakeified.html +1 -1
  197. package/docs/types/types.StrictInterface.html +1 -1
  198. package/docs/types/types.UpdateableAssociationProperties.html +1 -1
  199. package/docs/types/types.UpdateableProperties.html +1 -1
  200. package/docs/types/types.ValidationType.html +1 -1
  201. package/docs/types/types.ViewModel.html +2 -2
  202. package/docs/types/types.ViewModelClass.html +1 -1
  203. package/docs/types/types.WeekdayName.html +1 -1
  204. package/docs/types/types.WhereStatementForDream.html +1 -1
  205. package/docs/types/types.WhereStatementForDreamClass.html +1 -1
  206. package/docs/variables/index.DreamConst.html +1 -1
  207. package/docs/variables/index.ops.html +1 -1
  208. package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
  209. package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
  210. package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
  211. package/docs/variables/system.primaryKeyTypes.html +1 -1
  212. package/package.json +2 -2
@@ -13,12 +13,35 @@ import { isPrimitiveDataType } from '../../db/dataTypes.js';
13
13
  import DreamApp from '../../dream-app/index.js';
14
14
  import camelize from '../../helpers/camelize.js';
15
15
  import { findCitextArrayOid, findCorrespondingArrayOid, findEnumArrayOids, parsePostgresBigint, parsePostgresDate, parsePostgresDatetime, parsePostgresDatetimeTz, parsePostgresDecimal, parsePostgresTime, parsePostgresTimeTz, } from '../../helpers/customPgParsers.js';
16
+ import { testDatabasePoolSize } from '../../db/testDatabasePool.js';
16
17
  import EnvInternal from '../../helpers/EnvInternal.js';
17
18
  import createDb from './helpers/pg/createDb.js';
18
19
  import _dropDb from './helpers/pg/dropDb.js';
19
20
  import loadPgClient from './helpers/pg/loadPgClient.js';
20
21
  import KyselyQueryDriver from './Kysely.js';
21
22
  const pgTypes = pg.types;
23
+ // Fixed dream-pool namespace constant for the advisory-lock `key1`. Chosen to
24
+ // sit high in the int4 range and be recognizable in `pg_locks` (classid/objid)
25
+ // when debugging. XORed with a per-namespace hash below so two apps sharing one
26
+ // Postgres cluster reserve disjoint index spaces.
27
+ const DREAM_TEST_POOL_LOCK_NAMESPACE = 0x44_52_4d_00 | 0; // "DRM\0"
28
+ // Aggressive TCP keepalive on the dedicated lock-holding connection. Insurance
29
+ // for remote / CI Postgres and abrupt kills where a FIN might be lost: it lets
30
+ // Postgres reap the dead backend (and release the lock) even when the OS never
31
+ // delivered a clean socket close. For a local kill the lock releases on EOF
32
+ // regardless of this.
33
+ const LOCK_CONNECTION_KEEPALIVE_INITIAL_DELAY_MS = 1_000;
34
+ function int32Hash(value) {
35
+ let hash = 0;
36
+ for (let i = 0; i < value.length; i++) {
37
+ hash = (Math.imul(hash, 31) + value.charCodeAt(i)) | 0;
38
+ }
39
+ return hash;
40
+ }
41
+ function advisoryLockKey1(namespace) {
42
+ // Stays within int4 via the `| 0` coercions.
43
+ return (DREAM_TEST_POOL_LOCK_NAMESPACE ^ int32Hash(namespace)) | 0;
44
+ }
22
45
  export default class PostgresQueryDriver extends KyselyQueryDriver {
23
46
  /**
24
47
  * create the database. Must respond to the NODE_ENV value.
@@ -161,7 +184,12 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
161
184
  await client.query(`DROP DATABASE IF EXISTS ${replicaTestWorkerDatabaseName};`);
162
185
  await client.query(`CREATE DATABASE ${replicaTestWorkerDatabaseName} TEMPLATE ${dbConf.name};`);
163
186
  }
164
- for (let i = 2; i <= parallelTests; i++) {
187
+ // The pool spans `<base>` (the migration template, also claimable) plus
188
+ // `<base>_2 .. <base>_K`. Each live test worker claims one member for its
189
+ // lifetime via an advisory lock, so the pool must be wide enough to cover
190
+ // active + still-terminating workers — see src/db/testDatabasePool.ts.
191
+ const poolSize = testDatabasePoolSize(parallelTests);
192
+ for (let i = 2; i <= poolSize; i++) {
165
193
  const workerDatabaseName = `${dbConf.name}_${i}`;
166
194
  DreamCLI.logger.logContinueProgress(`creating duplicate test database ${workerDatabaseName} for concurrent tests...`, { logPrefix: ' ├ [db]', logPrefixColor: 'greenBright' });
167
195
  await client.query(`DROP DATABASE IF EXISTS ${workerDatabaseName};`);
@@ -170,4 +198,51 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
170
198
  await client.end();
171
199
  DreamCLI.logger.logEndProgress();
172
200
  }
201
+ static supportsParallelTestDatabases = true;
202
+ /**
203
+ * @internal
204
+ *
205
+ * Postgres implementation of the per-worker test-database claim seam (see
206
+ * {@link TestDatabaseLockSession} and `src/db/testDatabasePool.ts`).
207
+ *
208
+ * Postgres advisory locks are global to the cluster (not scoped to a single
209
+ * database), so one dedicated connection — to the `postgres` maintenance
210
+ * database — can reserve a pool index for the whole cluster. We use the
211
+ * two-`int4` form `pg_try_advisory_lock(key1, key2)`, whose lock space is
212
+ * distinct from the single-`bigint` form `pg_advisory_lock(bigint)` that
213
+ * application code typically uses, so a claim can never clash with an
214
+ * app-level advisory lock. `key1` folds a hash of the lock namespace (the
215
+ * base database name) into a fixed dream-pool constant; `key2` is the pool
216
+ * index. The lock auto-releases when the connection drops on process exit.
217
+ */
218
+ static async openTestDatabaseLockSession(connectionName) {
219
+ const dreamApp = DreamApp.getOrFail();
220
+ const creds = dreamApp.dbCredentialsFor(connectionName)?.primary ?? dreamApp.dbCredentialsFor(connectionName)?.replica;
221
+ if (!creds?.name)
222
+ throw new Error(`[dream] cannot open a test-database lock session: the "${connectionName}" connection has no resolvable primary db name`);
223
+ const client = new pg.Client({
224
+ host: creds.host || 'localhost',
225
+ port: creds.port,
226
+ // The maintenance database always exists and we never lock it —
227
+ // advisory locks are cluster-wide.
228
+ database: 'postgres',
229
+ user: creds.user,
230
+ password: creds.password,
231
+ keepAlive: true,
232
+ keepAliveInitialDelayMillis: LOCK_CONNECTION_KEEPALIVE_INITIAL_DELAY_MS,
233
+ });
234
+ await client.connect();
235
+ return {
236
+ async tryAcquire(namespace, index) {
237
+ const result = await client.query('SELECT pg_try_advisory_lock($1::int4, $2::int4) AS locked', [
238
+ advisoryLockKey1(namespace),
239
+ index,
240
+ ]);
241
+ return result.rows[0]?.locked === true;
242
+ },
243
+ async release() {
244
+ await client.end().catch(() => undefined);
245
+ },
246
+ };
247
+ }
173
248
  }
@@ -1,4 +1,5 @@
1
1
  import DreamCLI from '../../../../cli/index.js';
2
+ import { testDatabasePoolSize } from '../../../../db/testDatabasePool.js';
2
3
  import DreamApp from '../../../../dream-app/index.js';
3
4
  import EnvInternal from '../../../../helpers/EnvInternal.js';
4
5
  import loadPgClient from './loadPgClient.js';
@@ -25,7 +26,8 @@ async function maybeDropDuplicateDatabases(client, dbName) {
25
26
  DreamCLI.logger.logContinueProgress(`dropping fake replica test database ${replicaTestWorkerDatabaseName}`, { logPrefix: ' ├ [db]', logPrefixColor: 'greenBright' });
26
27
  await client.query(`DROP DATABASE IF EXISTS ${replicaTestWorkerDatabaseName};`);
27
28
  }
28
- for (let i = 2; i <= parallelTests; i++) {
29
+ const poolSize = testDatabasePoolSize(parallelTests);
30
+ for (let i = 2; i <= poolSize; i++) {
29
31
  const workerDatabaseName = `${dbName}_${i}`;
30
32
  DreamCLI.logger.logContinueProgress(`dropping duplicate test database ${workerDatabaseName}`, {
31
33
  logPrefix: ' ├ [db]',
@@ -1,4 +1,5 @@
1
1
  import * as util from 'node:util';
2
+ import { claimedTestDatabaseIndexOrNull, claimTestDatabase, testDatabaseNameForIndex, } from '../db/testDatabasePool.js';
2
3
  import validateTable from '../db/validators/validateTable.js';
3
4
  import Query from '../dream/Query.js';
4
5
  import PostgresQueryDriver from '../dream/QueryDriver/Postgres.js';
@@ -47,8 +48,12 @@ export default class DreamApp {
47
48
  if (!dreamApp.serializers)
48
49
  setCachedSerializers({});
49
50
  cacheDreamApp(dreamApp);
50
- if (!opts.bypassDbConnectionsDuringInit)
51
+ if (!opts.bypassDbConnectionsDuringInit) {
52
+ // Claim this worker's pool database before any connection is built, so
53
+ // the synchronous dbName() always resolves to the claimed database.
54
+ await dreamApp.ensureTestDatabaseClaimed();
51
55
  await this.setDatabaseTypeParsers(dreamApp);
56
+ }
52
57
  await deferCb?.(dreamApp);
53
58
  for (const plugin of dreamApp.plugins) {
54
59
  await plugin(dreamApp);
@@ -289,9 +294,56 @@ A new key can also be generated from the CLI:
289
294
  get serializers() {
290
295
  return getSerializersOrFail();
291
296
  }
297
+ /**
298
+ * The database name to connect to for a given connection. In dev/prod this is
299
+ * simply the configured name. Under vitest (test env) it is the per-worker
300
+ * database claimed from the pool — see {@link testDatabaseClaimEnabled} and
301
+ * `src/db/testDatabasePool.ts`.
302
+ *
303
+ * This is synchronous because Kysely builds its `pg.Pool` synchronously. It
304
+ * reads the already-claimed pool index; the claim is awaited earlier in the
305
+ * worker — `DreamApp.init()` calls {@link claimTestDatabase} before the first
306
+ * connection is built, and `dream-spec-helpers`' `truncate` awaits
307
+ * {@link testDatabaseName} — so the index is always populated by the time any
308
+ * connection resolves its name.
309
+ */
292
310
  dbName(connectionName, connection) {
293
311
  const conf = this.dbConnectionConfig(connectionName, connection);
294
- return this.parallelDatabasesEnabled ? `${conf.name}_${process.env.VITEST_POOL_ID}` : conf.name;
312
+ if (!this.runtimeNeedsTestDatabaseClaim)
313
+ return conf.name;
314
+ this.requireTestDatabaseClaimSupport();
315
+ const index = claimedTestDatabaseIndexOrNull();
316
+ if (index === null)
317
+ throw new Error(`[dream] dbName("${connectionName}", "${connection}") was resolved before this worker claimed ` +
318
+ `a test database. The pool claim must be awaited first (DreamApp.init / truncate do this). ` +
319
+ `This indicates a database connection was opened before app initialization completed.`);
320
+ return testDatabaseNameForIndex(conf.name, index);
321
+ }
322
+ /**
323
+ * Single source of truth for the claimed test-database name, used both by
324
+ * dream's connection resolution and by `dream-spec-helpers`' `truncate`
325
+ * (which opens its own `pg.Client` and so must read — not recompute — the
326
+ * claim). Awaits the pool claim if it has not happened yet in this worker.
327
+ */
328
+ async testDatabaseName(connectionName, connection) {
329
+ const conf = this.dbConnectionConfig(connectionName, connection);
330
+ if (!this.runtimeNeedsTestDatabaseClaim)
331
+ return conf.name;
332
+ this.requireTestDatabaseClaimSupport();
333
+ const index = await claimTestDatabase(this);
334
+ return testDatabaseNameForIndex(conf.name, index);
335
+ }
336
+ /**
337
+ * Claim this worker's pool database if it hasn't been claimed yet. Idempotent
338
+ * and a no-op outside the test-under-vitest path. Called by `DreamApp.init()`
339
+ * before any connection is built so the synchronous {@link dbName} always
340
+ * sees a populated index.
341
+ */
342
+ async ensureTestDatabaseClaimed() {
343
+ if (!this.runtimeNeedsTestDatabaseClaim)
344
+ return;
345
+ this.requireTestDatabaseClaimSupport();
346
+ await claimTestDatabase(this);
295
347
  }
296
348
  dbConnectionConfig(connectionName, connection) {
297
349
  const conf = this.dbCredentialsFor(connectionName)?.[connection] || this.dbCredentialsFor(connectionName)?.primary;
@@ -322,10 +374,57 @@ A new key can also be generated from the CLI:
322
374
  hasReplicaConfig(connectionName) {
323
375
  return !!this.dbCredentials[connectionName]?.replica;
324
376
  }
325
- get parallelDatabasesEnabled() {
326
- return (!!this.parallelTests &&
327
- !Number.isNaN(Number(process.env.VITEST_POOL_ID)) &&
328
- Number(process.env.VITEST_POOL_ID) > 1);
377
+ /**
378
+ * Whether the runtime *requires* a per-worker test-database claim. Gated on
379
+ * test env *under vitest* (not on `parallelTests > 1`): the slot-reuse
380
+ * collision reproduces even serially, because vitest respawns a process per
381
+ * file and the lingering process overlaps the new one on the same database.
382
+ * CLI db tasks (`db:migrate`, `db:reset`, …) do not run under vitest, so they
383
+ * keep the unsuffixed name and operate on the whole pool explicitly.
384
+ *
385
+ * This is independent of whether the default driver can actually claim — when
386
+ * the need is present but the capability is absent, the claim path fails loud
387
+ * (see {@link requireTestDatabaseClaimSupport}) rather than silently sharing
388
+ * one database.
389
+ */
390
+ get runtimeNeedsTestDatabaseClaim() {
391
+ return EnvInternal.isTest && runningUnderVitest();
392
+ }
393
+ /**
394
+ * Whether the default connection's query driver implements the per-worker
395
+ * test-database claim seam (`openTestDatabaseLockSession`). The claim is keyed
396
+ * off the **default** connection's capability so a Postgres-default app with a
397
+ * MySQL secondary keeps working (the secondary borrows the Postgres-claimed
398
+ * index for its database name).
399
+ */
400
+ get defaultConnectionSupportsParallelTestDatabases() {
401
+ return this.dbConnectionQueryDriverClass('default').supportsParallelTestDatabases;
402
+ }
403
+ /**
404
+ * Whether the per-worker test-database pool claim is active: needed by the
405
+ * runtime *and* supported by the default connection's driver. Consumers that
406
+ * key behavior off the claimed index (connection-cache suffixing) use this;
407
+ * the claim entry points additionally fail loud when the need is present but
408
+ * support is absent.
409
+ */
410
+ get testDatabaseClaimEnabled() {
411
+ return this.runtimeNeedsTestDatabaseClaim && this.defaultConnectionSupportsParallelTestDatabases;
412
+ }
413
+ /**
414
+ * Throw an actionable error when the runtime needs a per-worker test-database
415
+ * claim but the default connection's driver cannot provide one — rather than
416
+ * silently sharing a single database across overlapping vitest workers (which
417
+ * reintroduces the flake this whole mechanism exists to fix).
418
+ */
419
+ requireTestDatabaseClaimSupport() {
420
+ if (this.defaultConnectionSupportsParallelTestDatabases)
421
+ return;
422
+ const driverName = this.dbConnectionQueryDriverClass('default').name;
423
+ throw new Error(`[dream] running under vitest requires a per-worker test database, but the "default" connection's ` +
424
+ `query driver (${driverName}) does not support parallel test databases. Implement the claim seam ` +
425
+ `on that driver — set \`supportsParallelTestDatabases = true\` and override ` +
426
+ `\`openTestDatabaseLockSession\` (see PostgresQueryDriver / the test-app MysqlQueryDriver for ` +
427
+ `reference) — or do not run this adapter under vitest.`);
329
428
  }
330
429
  async load(resourceType, resourcePath, importCb) {
331
430
  switch (resourceType) {
@@ -436,6 +535,14 @@ function loggerArgToString(arg) {
436
535
  }
437
536
  return JSON.stringify(util.inspect(arg, { depth: 6 }));
438
537
  }
538
+ /**
539
+ * Whether the current process is a vitest worker. vitest sets `VITEST=true` in
540
+ * every worker, and also exposes the per-worker `VITEST_POOL_ID` /
541
+ * `VITEST_WORKER_ID`. CLI db tasks run outside vitest, so this is false there.
542
+ */
543
+ function runningUnderVitest() {
544
+ return process.env.VITEST === 'true' || process.env.VITEST_POOL_ID != null;
545
+ }
439
546
  export const DreamAppAllowedPackageManagersEnumValues = ['pnpm', 'yarn', 'npm', 'bun', 'deno'];
440
547
  // GeneratorImportStyles are used by CLI generators to determine how
441
548
  // to style import suffixes. When integrating with other apps, this
@@ -4,7 +4,7 @@ import CalendarDate from '../utils/datetime/CalendarDate.js';
4
4
  import { DateTime } from '../utils/datetime/DateTime.js';
5
5
  /**
6
6
  * Performs a deep equality check between two values, supporting primitives, arrays,
7
- * Dream instances, CalendarDate, DateTime, and objects.
7
+ * Dream instances, CalendarDate, DateTime, ClockTime, ClockTimeTz, and objects.
8
8
  *
9
9
  * Examples:
10
10
  * areEqual(undefined, undefined) // true
@@ -2,6 +2,8 @@ import DreamApp from '../dream-app/index.js';
2
2
  import Encrypt from '../encrypt/index.js';
3
3
  import ops from '../ops/index.js';
4
4
  import CalendarDate from '../utils/datetime/CalendarDate.js';
5
+ import ClockTime from '../utils/datetime/ClockTime.js';
6
+ import ClockTimeTz from '../utils/datetime/ClockTimeTz.js';
5
7
  import { DateTime } from '../utils/datetime/DateTime.js';
6
8
  import camelize from './camelize.js';
7
9
  import capitalize from './capitalize.js';
@@ -19,6 +21,8 @@ import uniq from './uniq.js';
19
21
  export default async function loadRepl(context) {
20
22
  const dreamApp = DreamApp.getOrFail();
21
23
  context.CalendarDate = CalendarDate;
24
+ context.ClockTime = ClockTime;
25
+ context.ClockTimeTz = ClockTimeTz;
22
26
  context.DateTime = DateTime;
23
27
  context.Encrypt = Encrypt;
24
28
  context.camelize = camelize;
@@ -1,4 +1,4 @@
1
- import { Kysely } from 'kysely';
1
+ import { ColumnDataType, Kysely } from 'kysely';
2
2
  export default class DreamMigrationHelpers {
3
3
  /**
4
4
  * Rename a table and its associated primary key index and sequence.
@@ -141,6 +141,95 @@ export default class DreamMigrationHelpers {
141
141
  * @param __namedParameters.replacements - Details about which table and column to change and which value to replace the dropped value with (or remove it if the column is an array)
142
142
  */
143
143
  static dropEnumValue(db: Kysely<any>, { enumName, value, replacements }: DropValueFromEnumOpts): Promise<void>;
144
+ /**
145
+ * Convert an existing plaintext column into the encrypted-backed form expected by
146
+ * the `@Encrypted` decorator.
147
+ *
148
+ * This renames `column` to `encrypted_<column>`, widens it to `text`, and rewrites
149
+ * every non-null value with the AES-GCM ciphertext produced by the exact same code
150
+ * path the decorator's setter uses (`InternalEncrypt.encryptColumn`). After it runs,
151
+ * the column holds real ciphertext that the decorator's getter can decrypt — which is
152
+ * what a bare column rename does **not** do (renaming a plaintext column to
153
+ * `encrypted_<column>` and decorating the property leaves plaintext in the column, so
154
+ * the getter throws `DecryptionError`).
155
+ *
156
+ * The encryption key and algorithm come from the application's encryption config
157
+ * (`DreamApp` `encryption.columns.current`); if encryption is not configured this
158
+ * throws `MissingColumnEncryptionOpts`. Null values are left null.
159
+ *
160
+ * Intended for text/string columns (the common case is text -> encrypted text). The
161
+ * column is widened to `text` before reading, so values are encrypted as their text
162
+ * form; the inverse `decryptColumn` can restore a non-text type via its `columnType`
163
+ * option.
164
+ *
165
+ * ```ts
166
+ * // plaintext `phone` column -> encrypted `encrypted_phone` text column
167
+ * await DreamMigrationHelpers.encryptColumn(db, { table: 'users', column: 'phone' })
168
+ * ```
169
+ *
170
+ * **Rewrites rows one at a time** (each value needs a fresh random IV computed in Node,
171
+ * so the rewrite cannot be a single SQL `UPDATE`), reading them in keyset batches of
172
+ * `batchSize` to bound memory. It still holds the table for the migration's duration; on
173
+ * very large tables do not use this helper — write your own batched / online migration.
174
+ *
175
+ * **Drop any index on the column first.** Per-row updates pay index-maintenance cost on
176
+ * every write, and an index over ciphertext is useless anyway (encrypted values are not
177
+ * queryable). Remove the index before this migration and do not re-add it.
178
+ *
179
+ * @param db - The Kysely database object passed into the migration up/down function
180
+ * @param options - Configuration options
181
+ * @param options.table - The name of the table
182
+ * @param options.column - The current (plaintext) column name
183
+ * @param options.encryptedColumnName - The target encrypted column name. Defaults to `encrypted_<column>`, matching the `@Encrypted` decorator's default; pass this when the decorator was given a custom encrypted column name.
184
+ * @param options.primaryKey - The primary key column used to keyset-paginate and target each row's update. Defaults to `id`.
185
+ * @param options.batchSize - How many rows to read per batch. Defaults to `1000`.
186
+ */
187
+ static encryptColumn(db: Kysely<any>, { table, column, encryptedColumnName, primaryKey, batchSize, }: EncryptColumnOpts): Promise<void>;
188
+ /**
189
+ * Inverse of {@link DreamMigrationHelpers.encryptColumn}: decrypt an
190
+ * `encrypted_<column>` column back to plaintext and rename it to `column`.
191
+ *
192
+ * Every non-null value is decrypted with the same path the decorator's getter uses
193
+ * (`InternalEncrypt.decryptColumn`, which honors both the `current` and `legacy`
194
+ * encryption keys), then the column is renamed back. Called with the same `table` and
195
+ * `column`, this exactly reverses `encryptColumn`.
196
+ *
197
+ * By default the column is left as `text`, because the original column type cannot be
198
+ * recovered from the encrypted state. Pass `columnType` to restore a specific type
199
+ * (e.g. `'integer'`); the conversion runs `ALTER COLUMN ... TYPE <columnType> USING
200
+ * <column>::<columnType>`.
201
+ *
202
+ * ```ts
203
+ * await DreamMigrationHelpers.decryptColumn(db, { table: 'users', column: 'phone' })
204
+ *
205
+ * // restore the original column type as part of the inverse
206
+ * await DreamMigrationHelpers.decryptColumn(db, { table: 'users', column: 'age', columnType: 'integer' })
207
+ * ```
208
+ *
209
+ * The same per-row, batched, table-locking caveat as `encryptColumn` applies.
210
+ *
211
+ * @param db - The Kysely database object passed into the migration up/down function
212
+ * @param options - Configuration options
213
+ * @param options.table - The name of the table
214
+ * @param options.column - The target (plaintext) column name to rename back to
215
+ * @param options.encryptedColumnName - The current encrypted column name. Defaults to `encrypted_<column>`.
216
+ * @param options.primaryKey - The primary key column used to keyset-paginate and target each row's update. Defaults to `id`.
217
+ * @param options.batchSize - How many rows to read per batch. Defaults to `1000`.
218
+ * @param options.columnType - When provided, the restored column is converted to this type. When omitted, the column is left as `text`.
219
+ */
220
+ static decryptColumn(db: Kysely<any>, { table, column, encryptedColumnName, primaryKey, batchSize, columnType, }: DecryptColumnOpts): Promise<void>;
221
+ /**
222
+ * Walk every non-null value of `column` in keyset batches of `batchSize`, applying
223
+ * `transform` in Node and writing the result back one row at a time. Keyset pagination
224
+ * on `primaryKey` (`WHERE pk > last ORDER BY pk`) bounds memory to a single batch and
225
+ * guarantees forward progress, so a row is never read — or transformed — twice (which
226
+ * matters because the transform is not idempotent: re-encrypting ciphertext would
227
+ * double-encrypt, and re-decrypting plaintext would throw).
228
+ *
229
+ * Reads alias the selected columns to fixed keys via `sql.ref(...).as(...)` so a
230
+ * `CamelCasePlugin` on the connection cannot rename the result keys.
231
+ */
232
+ private static transformColumnInBatches;
144
233
  }
145
234
  interface DropValueFromEnumOpts {
146
235
  enumName: string;
@@ -169,4 +258,19 @@ interface AddValueToEnumOpts {
169
258
  enumName: string;
170
259
  value: string;
171
260
  }
261
+ interface EncryptColumnOpts {
262
+ table: string;
263
+ column: string;
264
+ encryptedColumnName?: string;
265
+ primaryKey?: string;
266
+ batchSize?: number;
267
+ }
268
+ interface DecryptColumnOpts {
269
+ table: string;
270
+ column: string;
271
+ encryptedColumnName?: string;
272
+ primaryKey?: string;
273
+ batchSize?: number;
274
+ columnType?: ColumnDataType;
275
+ }
172
276
  export {};
@@ -0,0 +1,42 @@
1
+ import type DreamApp from '../dream-app/index.js';
2
+ /**
3
+ * The number of databases in the pre-created pool (including the unsuffixed
4
+ * base at index 1). Shared by the CLI pool-creation / drop loops and the claim
5
+ * probe so they always agree on the index range.
6
+ */
7
+ export declare function testDatabasePoolSize(parallelTests: number | undefined): number;
8
+ /**
9
+ * The database name for a pool index given the un-suffixed base name. Index 1
10
+ * is the base itself (no suffix); higher indexes are `<base>_<index>`. This is
11
+ * the single naming rule, used by both the claim path and the CLI loops.
12
+ */
13
+ export declare function testDatabaseNameForIndex(baseName: string, index: number): string;
14
+ /**
15
+ * Sync read of the already-claimed pool index, or null if no claim has
16
+ * completed yet in this process. `DreamApp.dbName()` (synchronous, called when
17
+ * Kysely builds a pool) relies on the claim having been awaited earlier in the
18
+ * worker — `DreamApp.init()` awaits {@link claimTestDatabase} before the first
19
+ * connection is built, and `truncate` awaits it too — so by the time any
20
+ * connection resolves its database name the index is populated.
21
+ */
22
+ export declare function claimedTestDatabaseIndexOrNull(): number | null;
23
+ /**
24
+ * Claim a free pool database for this worker (idempotent). On first call it
25
+ * opens the default driver's dedicated lock session, probes
26
+ * {@link TestDatabaseLockSession.tryAcquire} across the pool indexes until one
27
+ * succeeds, and caches the claimed index for the rest of the process.
28
+ * Subsequent calls (and concurrent callers) return the same index.
29
+ *
30
+ * Throws a loud, actionable error if the pool is genuinely exhausted after the
31
+ * wait-for-free window — telling the reader to recreate the pool with
32
+ * `<packageManager> psy db:reset`.
33
+ */
34
+ export declare function claimTestDatabase(dreamApp: DreamApp): Promise<number>;
35
+ /**
36
+ * Test-only escape hatch for dream's own spec suite: release the claim and
37
+ * close the lock session so a test can exercise the claim machinery in
38
+ * isolation. Not part of the public API.
39
+ *
40
+ * @internal
41
+ */
42
+ export declare function __resetTestDatabaseClaimForSpec(): Promise<void>;
@@ -121,6 +121,30 @@ export default class QueryDriverBase<DreamInstance extends Dream> {
121
121
  */
122
122
  static setDatabaseTypeParsers(connectionName: string): Promise<void>;
123
123
  static duplicateDatabase(connectionName: string): Promise<void>;
124
+ /**
125
+ * @internal
126
+ *
127
+ * Whether this driver implements the per-worker test-database claim seam
128
+ * ({@link openTestDatabaseLockSession}). Adapters that can hold a
129
+ * process-lifetime, auto-releasing lock to reserve one database from the
130
+ * test pool override this to `true`. It defaults to `false` so that a new
131
+ * adapter fails loud under vitest rather than silently sharing a single
132
+ * database across overlapping workers — see `src/db/testDatabasePool.ts`.
133
+ */
134
+ static supportsParallelTestDatabases: boolean;
135
+ /**
136
+ * @internal
137
+ *
138
+ * Open a dedicated, process-lifetime lock session used to claim one database
139
+ * from the per-worker test pool. Only ever called when
140
+ * {@link supportsParallelTestDatabases} is `true`; the base default throws.
141
+ *
142
+ * The returned session wraps a single dedicated connection: the orchestrator
143
+ * (`src/db/testDatabasePool.ts`) probes many pool indexes on the one session
144
+ * via `tryAcquire` and keeps whichever it acquires, holding it for the
145
+ * worker's lifetime.
146
+ */
147
+ static openTestDatabaseLockSession(connectionName: string): Promise<TestDatabaseLockSession>;
124
148
  static getColumnData(connectionName: string, tableName: string, allTableAssociationData: {
125
149
  [key: string]: SchemaBuilderAssociationData;
126
150
  }): Promise<{
@@ -327,3 +351,26 @@ export default class QueryDriverBase<DreamInstance extends Dream> {
327
351
  hydratePreload(this: QueryDriverBase<DreamInstance>, dream: Dream): Promise<void>;
328
352
  }
329
353
  export type GenericDbType = 'datetime' | 'date';
354
+ /**
355
+ * One dedicated lock session backing the per-worker test-database claim. The
356
+ * orchestrator (`src/db/testDatabasePool.ts`) opens one session per worker,
357
+ * probes pool indexes with {@link tryAcquire}, and keeps the first index it
358
+ * wins for the worker's lifetime.
359
+ *
360
+ * The session intentionally carries no adapter-specific lock-key type:
361
+ * `tryAcquire` takes a semantic `(namespace, index)` pair and each driver maps
362
+ * it to its own primitive (Postgres `pg_try_advisory_lock(int4, int4)`, MySQL
363
+ * `GET_LOCK`), so no Postgres int-pair leaks into the shared contract.
364
+ */
365
+ export interface TestDatabaseLockSession {
366
+ /**
367
+ * Try to acquire the lock for `(namespace, index)` without blocking. Returns
368
+ * `true` if this session now holds it, `false` if another live session does.
369
+ * Must not wait — the orchestrator drives its own probe / wait-for-free loop.
370
+ */
371
+ tryAcquire(namespace: string, index: number): Promise<boolean>;
372
+ /**
373
+ * Close the underlying connection, releasing any lock it holds.
374
+ */
375
+ release(): Promise<void>;
376
+ }
@@ -1,6 +1,7 @@
1
1
  import Dream from '../../Dream.js';
2
2
  import { SchemaBuilderAssociationData, SchemaBuilderColumnData } from '../../helpers/cli/ASTBuilder.js';
3
3
  import KyselyQueryDriver from './Kysely.js';
4
+ import type { TestDatabaseLockSession } from './Base.js';
4
5
  export default class PostgresQueryDriver<DreamInstance extends Dream> extends KyselyQueryDriver<DreamInstance> {
5
6
  /**
6
7
  * create the database. Must respond to the NODE_ENV value.
@@ -33,4 +34,22 @@ export default class PostgresQueryDriver<DreamInstance extends Dream> extends Ky
33
34
  [key: string]: SchemaBuilderColumnData;
34
35
  }>;
35
36
  static duplicateDatabase(connectionName: string): Promise<void>;
37
+ static supportsParallelTestDatabases: boolean;
38
+ /**
39
+ * @internal
40
+ *
41
+ * Postgres implementation of the per-worker test-database claim seam (see
42
+ * {@link TestDatabaseLockSession} and `src/db/testDatabasePool.ts`).
43
+ *
44
+ * Postgres advisory locks are global to the cluster (not scoped to a single
45
+ * database), so one dedicated connection — to the `postgres` maintenance
46
+ * database — can reserve a pool index for the whole cluster. We use the
47
+ * two-`int4` form `pg_try_advisory_lock(key1, key2)`, whose lock space is
48
+ * distinct from the single-`bigint` form `pg_advisory_lock(bigint)` that
49
+ * application code typically uses, so a claim can never clash with an
50
+ * app-level advisory lock. `key1` folds a hash of the lock namespace (the
51
+ * base database name) into a fixed dream-pool constant; `key2` is the pool
52
+ * index. The lock auto-releases when the connection drops on process exit.
53
+ */
54
+ static openTestDatabaseLockSession(connectionName: string): Promise<TestDatabaseLockSession>;
36
55
  }
@@ -148,12 +148,75 @@ export default class DreamApp {
148
148
  constructor(opts?: Partial<DreamAppOpts>);
149
149
  get models(): Record<string, typeof Dream>;
150
150
  get serializers(): Record<string, DreamModelSerializerType | SimpleObjectSerializerType>;
151
+ /**
152
+ * The database name to connect to for a given connection. In dev/prod this is
153
+ * simply the configured name. Under vitest (test env) it is the per-worker
154
+ * database claimed from the pool — see {@link testDatabaseClaimEnabled} and
155
+ * `src/db/testDatabasePool.ts`.
156
+ *
157
+ * This is synchronous because Kysely builds its `pg.Pool` synchronously. It
158
+ * reads the already-claimed pool index; the claim is awaited earlier in the
159
+ * worker — `DreamApp.init()` calls {@link claimTestDatabase} before the first
160
+ * connection is built, and `dream-spec-helpers`' `truncate` awaits
161
+ * {@link testDatabaseName} — so the index is always populated by the time any
162
+ * connection resolves its name.
163
+ */
151
164
  dbName(connectionName: string, connection: DbConnectionType): string;
165
+ /**
166
+ * Single source of truth for the claimed test-database name, used both by
167
+ * dream's connection resolution and by `dream-spec-helpers`' `truncate`
168
+ * (which opens its own `pg.Client` and so must read — not recompute — the
169
+ * claim). Awaits the pool claim if it has not happened yet in this worker.
170
+ */
171
+ testDatabaseName(connectionName: string, connection: DbConnectionType): Promise<string>;
172
+ /**
173
+ * Claim this worker's pool database if it hasn't been claimed yet. Idempotent
174
+ * and a no-op outside the test-under-vitest path. Called by `DreamApp.init()`
175
+ * before any connection is built so the synchronous {@link dbName} always
176
+ * sees a populated index.
177
+ */
178
+ ensureTestDatabaseClaimed(): Promise<void>;
152
179
  dbConnectionConfig(connectionName: string, connection: DbConnectionType): DreamDbConfig;
153
180
  dbConnectionQueryDriverClass<T extends Dream = Dream>(connectionName: string): typeof QueryDriverBase<T>;
154
181
  dbConnectionKeys(): string[];
155
182
  hasReplicaConfig(connectionName: string): boolean;
156
- get parallelDatabasesEnabled(): boolean;
183
+ /**
184
+ * Whether the runtime *requires* a per-worker test-database claim. Gated on
185
+ * test env *under vitest* (not on `parallelTests > 1`): the slot-reuse
186
+ * collision reproduces even serially, because vitest respawns a process per
187
+ * file and the lingering process overlaps the new one on the same database.
188
+ * CLI db tasks (`db:migrate`, `db:reset`, …) do not run under vitest, so they
189
+ * keep the unsuffixed name and operate on the whole pool explicitly.
190
+ *
191
+ * This is independent of whether the default driver can actually claim — when
192
+ * the need is present but the capability is absent, the claim path fails loud
193
+ * (see {@link requireTestDatabaseClaimSupport}) rather than silently sharing
194
+ * one database.
195
+ */
196
+ get runtimeNeedsTestDatabaseClaim(): boolean;
197
+ /**
198
+ * Whether the default connection's query driver implements the per-worker
199
+ * test-database claim seam (`openTestDatabaseLockSession`). The claim is keyed
200
+ * off the **default** connection's capability so a Postgres-default app with a
201
+ * MySQL secondary keeps working (the secondary borrows the Postgres-claimed
202
+ * index for its database name).
203
+ */
204
+ private get defaultConnectionSupportsParallelTestDatabases();
205
+ /**
206
+ * Whether the per-worker test-database pool claim is active: needed by the
207
+ * runtime *and* supported by the default connection's driver. Consumers that
208
+ * key behavior off the claimed index (connection-cache suffixing) use this;
209
+ * the claim entry points additionally fail loud when the need is present but
210
+ * support is absent.
211
+ */
212
+ get testDatabaseClaimEnabled(): boolean;
213
+ /**
214
+ * Throw an actionable error when the runtime needs a per-worker test-database
215
+ * claim but the default connection's driver cannot provide one — rather than
216
+ * silently sharing a single database across overlapping vitest workers (which
217
+ * reintroduces the flake this whole mechanism exists to fix).
218
+ */
219
+ private requireTestDatabaseClaimSupport;
157
220
  load<RT extends 'models' | 'serializers'>(resourceType: RT, resourcePath: string, importCb: (path: string) => Promise<any>): Promise<void>;
158
221
  plugin(cb: (app: DreamApp) => void | Promise<void>): void;
159
222
  set<ApplyOpt extends DreamAppSetOption>(applyOption: ApplyOpt, options: ApplyOpt extends 'bypassDeprecationChecks' ? boolean : ApplyOpt extends 'db' ? DreamDbCredentialOptions | string : ApplyOpt extends 'encryption' ? DreamAppEncryptionOptions : ApplyOpt extends 'primaryKeyType' ? LegacyCompatiblePrimaryKeyType : ApplyOpt extends 'importExtension' ? GeneratorImportStyle : ApplyOpt extends 'logger' ? DreamLogger : ApplyOpt extends 'projectRoot' ? string : ApplyOpt extends 'inflections' ? () => void | Promise<void> : ApplyOpt extends 'packageManager' ? DreamAppAllowedPackageManagersEnum : ApplyOpt extends 'paths' ? DreamDirectoryPaths : ApplyOpt extends 'parallelTests' ? number : ApplyOpt extends 'unicodeNormalization' ? UnicodeNormalizationForm : ApplyOpt extends 'paginationPageSize' ? number : never, secondaryOptions?: ApplyOpt extends 'db' ? DreamDbCredentialOptions : never): void;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Performs a deep equality check between two values, supporting primitives, arrays,
3
- * Dream instances, CalendarDate, DateTime, and objects.
3
+ * Dream instances, CalendarDate, DateTime, ClockTime, ClockTimeTz, and objects.
4
4
  *
5
5
  * Examples:
6
6
  * areEqual(undefined, undefined) // true