@owlmeans/postgres-resource 0.1.18-rc.42 → 0.1.18-rc.43

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 (154) hide show
  1. package/README.md +13 -13
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
  4. package/build/consts.d.ts +9 -0
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +6 -0
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +5 -1
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +25 -15
  15. package/build/declarations.js.map +1 -1
  16. package/build/errors.d.ts +0 -20
  17. package/build/errors.d.ts.map +1 -1
  18. package/build/errors.js +1 -111
  19. package/build/errors.js.map +1 -1
  20. package/build/index.d.ts +3 -0
  21. package/build/index.d.ts.map +1 -1
  22. package/build/index.js +1 -0
  23. package/build/index.js.map +1 -1
  24. package/build/pg-error/types.d.ts +24 -0
  25. package/build/pg-error/types.d.ts.map +1 -0
  26. package/build/pg-error/types.js +2 -0
  27. package/build/pg-error/types.js.map +1 -0
  28. package/build/pg-error.d.ts +4 -0
  29. package/build/pg-error.d.ts.map +1 -0
  30. package/build/pg-error.js +100 -0
  31. package/build/pg-error.js.map +1 -0
  32. package/build/resource.d.ts.map +1 -1
  33. package/build/resource.js +15 -11
  34. package/build/resource.js.map +1 -1
  35. package/build/types.d.ts.map +1 -1
  36. package/build/utils/consts.local.d.ts +2 -0
  37. package/build/utils/consts.local.d.ts.map +1 -1
  38. package/build/utils/consts.local.js +2 -0
  39. package/build/utils/consts.local.js.map +1 -1
  40. package/build/utils/criteria/types.d.ts +31 -0
  41. package/build/utils/criteria/types.d.ts.map +1 -0
  42. package/build/utils/criteria/types.js +2 -0
  43. package/build/utils/criteria/types.js.map +1 -0
  44. package/build/utils/criteria.d.ts +3 -27
  45. package/build/utils/criteria.d.ts.map +1 -1
  46. package/build/utils/criteria.js +86 -104
  47. package/build/utils/criteria.js.map +1 -1
  48. package/build/utils/diff/types.d.ts +40 -0
  49. package/build/utils/diff/types.d.ts.map +1 -0
  50. package/build/utils/diff/types.js +2 -0
  51. package/build/utils/diff/types.js.map +1 -0
  52. package/build/utils/diff.d.ts +5 -35
  53. package/build/utils/diff.d.ts.map +1 -1
  54. package/build/utils/diff.js +320 -344
  55. package/build/utils/diff.js.map +1 -1
  56. package/build/utils/index.d.ts +9 -0
  57. package/build/utils/index.d.ts.map +1 -1
  58. package/build/utils/index.js.map +1 -1
  59. package/build/utils/introspect/types.d.ts +9 -0
  60. package/build/utils/introspect/types.d.ts.map +1 -0
  61. package/build/utils/introspect/types.js +2 -0
  62. package/build/utils/introspect/types.js.map +1 -0
  63. package/build/utils/introspect.d.ts +3 -3
  64. package/build/utils/introspect.d.ts.map +1 -1
  65. package/build/utils/introspect.js +41 -38
  66. package/build/utils/introspect.js.map +1 -1
  67. package/build/utils/life-cycle/types.d.ts +37 -0
  68. package/build/utils/life-cycle/types.d.ts.map +1 -0
  69. package/build/utils/life-cycle/types.js +2 -0
  70. package/build/utils/life-cycle/types.js.map +1 -0
  71. package/build/utils/life-cycle.d.ts +3 -33
  72. package/build/utils/life-cycle.d.ts.map +1 -1
  73. package/build/utils/life-cycle.js +88 -108
  74. package/build/utils/life-cycle.js.map +1 -1
  75. package/build/utils/marshal/types.d.ts +39 -0
  76. package/build/utils/marshal/types.d.ts.map +1 -0
  77. package/build/utils/marshal/types.js +2 -0
  78. package/build/utils/marshal/types.js.map +1 -0
  79. package/build/utils/marshal.d.ts +4 -32
  80. package/build/utils/marshal.d.ts.map +1 -1
  81. package/build/utils/marshal.js +121 -144
  82. package/build/utils/marshal.js.map +1 -1
  83. package/build/utils/migrations.d.ts.map +1 -1
  84. package/build/utils/migrations.js +41 -36
  85. package/build/utils/migrations.js.map +1 -1
  86. package/build/utils/name/types.d.ts +43 -0
  87. package/build/utils/name/types.d.ts.map +1 -0
  88. package/build/utils/name/types.js +2 -0
  89. package/build/utils/name/types.js.map +1 -0
  90. package/build/utils/name.d.ts +6 -35
  91. package/build/utils/name.d.ts.map +1 -1
  92. package/build/utils/name.js +38 -57
  93. package/build/utils/name.js.map +1 -1
  94. package/build/utils/schema/types.d.ts +22 -0
  95. package/build/utils/schema/types.d.ts.map +1 -0
  96. package/build/utils/schema/types.js +2 -0
  97. package/build/utils/schema/types.js.map +1 -0
  98. package/build/utils/schema.d.ts +4 -24
  99. package/build/utils/schema.d.ts.map +1 -1
  100. package/build/utils/schema.js +339 -352
  101. package/build/utils/schema.js.map +1 -1
  102. package/build/utils/sql/types.d.ts +25 -0
  103. package/build/utils/sql/types.d.ts.map +1 -0
  104. package/build/utils/sql/types.js +2 -0
  105. package/build/utils/sql/types.js.map +1 -0
  106. package/build/utils/sql.d.ts +4 -18
  107. package/build/utils/sql.d.ts.map +1 -1
  108. package/build/utils/sql.js +0 -0
  109. package/build/utils/sql.js.map +1 -1
  110. package/build/utils/sync/types.d.ts +25 -0
  111. package/build/utils/sync/types.d.ts.map +1 -0
  112. package/build/utils/sync/types.js +2 -0
  113. package/build/utils/sync/types.js.map +1 -0
  114. package/build/utils/sync.d.ts +2 -19
  115. package/build/utils/sync.d.ts.map +1 -1
  116. package/build/utils/sync.js +64 -71
  117. package/build/utils/sync.js.map +1 -1
  118. package/package.json +7 -7
  119. package/src/consts.ts +7 -0
  120. package/src/declarations/types.ts +12 -0
  121. package/src/declarations.ts +29 -14
  122. package/src/errors.ts +1 -121
  123. package/src/index.ts +3 -0
  124. package/src/pg-error/types.ts +23 -0
  125. package/src/pg-error.ts +114 -0
  126. package/src/resource.ts +17 -13
  127. package/src/types.ts +1 -0
  128. package/src/utils/consts.local.ts +3 -0
  129. package/src/utils/criteria/types.ts +32 -0
  130. package/src/utils/criteria.ts +95 -110
  131. package/src/utils/diff/types.ts +41 -0
  132. package/src/utils/diff.ts +330 -348
  133. package/src/utils/index.ts +9 -0
  134. package/src/utils/introspect/types.ts +9 -0
  135. package/src/utils/introspect.ts +51 -45
  136. package/src/utils/life-cycle/types.ts +41 -0
  137. package/src/utils/life-cycle.ts +102 -121
  138. package/src/utils/marshal/types.ts +40 -0
  139. package/src/utils/marshal.ts +129 -147
  140. package/src/utils/migrations.ts +39 -33
  141. package/src/utils/name/types.ts +44 -0
  142. package/src/utils/name.ts +47 -60
  143. package/src/utils/schema/types.ts +26 -0
  144. package/src/utils/schema.ts +359 -366
  145. package/src/utils/sql/types.ts +25 -0
  146. package/src/utils/sql.ts +0 -0
  147. package/src/utils/sync/types.ts +25 -0
  148. package/src/utils/sync.ts +62 -68
  149. package/tests/criteria.spec.ts +4 -1
  150. package/tests/diff.spec.ts +4 -1
  151. package/tests/errors.spec.ts +4 -3
  152. package/tests/name.spec.ts +5 -3
  153. package/tests/schema.spec.ts +3 -1
  154. package/tests/sql.spec.ts +20 -22
@@ -1,12 +1,21 @@
1
1
  export * from './criteria.js'
2
+ export type * from './criteria/types.js'
2
3
  export * from './diff.js'
4
+ export type * from './diff/types.js'
3
5
  export * from './introspect.js'
6
+ export type * from './introspect/types.js'
4
7
  export * from './life-cycle.js'
8
+ export type * from './life-cycle/types.js'
5
9
  export type * from './types.js'
6
10
  export * from './marshal.js'
11
+ export type * from './marshal/types.js'
7
12
  export * from './migrations.js'
8
13
  export * from './name.js'
14
+ export type * from './name/types.js'
9
15
  export * from './schema.js'
16
+ export type * from './schema/types.js'
10
17
  export * from './sql.js'
18
+ export type * from './sql/types.js'
11
19
  export * from './sync.js'
20
+ export type * from './sync/types.js'
12
21
  export * from './table.js'
@@ -0,0 +1,9 @@
1
+ import type { LiveTable } from '../../types.js'
2
+
3
+ /** Catalog reads over one checked out connection. */
4
+ export interface PgIntrospectHelper {
5
+ /** Read the table exactly as Postgres currently holds it. */
6
+ introspectTable: (schema: string, table: string, qualified: string) => Promise<LiveTable>
7
+ /** Count the rows a destructive step would lose, so the boot log can say how much. */
8
+ countNonNull: (qualified: string, column: string) => Promise<number>
9
+ }
@@ -2,55 +2,61 @@ import type { PoolClient } from 'pg'
2
2
 
3
3
  import type { LiveColumn, LiveConstraint, LiveIndex, LiveTable } from '../types.js'
4
4
  import { COLUMNS, CONSTRAINTS, INDEXES } from './consts.local.js'
5
+ import type { PgIntrospectHelper } from './introspect/types.js'
5
6
 
6
- /** Read the table exactly as Postgres currently holds it. */
7
- export const introspectTable = async (
8
- client: PoolClient, schema: string, table: string, qualified: string
9
- ): Promise<LiveTable> => {
10
- const exists = await client.query<{ present: boolean }>(
11
- 'SELECT to_regclass($1) IS NOT NULL AS present', [qualified]
12
- )
13
- if (exists.rows[0]?.present !== true) {
14
- return { exists: false, columns: [], indexes: [], constraints: [] }
15
- }
7
+ export const makePgIntrospectHelper = (client: PoolClient): PgIntrospectHelper => {
8
+ const introspectTable = async (
9
+ schema: string, table: string, qualified: string
10
+ ): Promise<LiveTable> => {
11
+ const exists = await client.query<{ present: boolean }>(
12
+ 'SELECT to_regclass($1) IS NOT NULL AS present', [qualified]
13
+ )
14
+ if (exists.rows[0]?.present !== true) {
15
+ return { exists: false, columns: [], indexes: [], constraints: [] }
16
+ }
17
+
18
+ /**
19
+ * Sequential, not `Promise.all`: this is one checked out connection, and a connection
20
+ * runs one statement at a time. Overlapping them only queues them behind each other —
21
+ * and node-postgres deprecated tolerating it.
22
+ */
23
+ const columns = await client.query<LiveColumn>(COLUMNS, [qualified])
24
+ const indexes = await client.query<LiveIndex>(INDEXES, [schema, table])
25
+ const constraints = await client.query<LiveConstraint>(CONSTRAINTS, [qualified])
16
26
 
17
- /**
18
- * Sequential, not `Promise.all`: this is one checked out connection, and a connection
19
- * runs one statement at a time. Overlapping them only queues them behind each other —
20
- * and node-postgres deprecated tolerating it.
21
- */
22
- const columns = await client.query<LiveColumn>(COLUMNS, [qualified])
23
- const indexes = await client.query<LiveIndex>(INDEXES, [schema, table])
24
- const constraints = await client.query<LiveConstraint>(CONSTRAINTS, [qualified])
25
-
26
- return {
27
- exists: true,
28
- columns: columns.rows.map(row => ({
29
- name: row.name,
30
- type: row.type,
31
- notNull: (row as unknown as { not_null: boolean }).not_null,
32
- defaultExpr: (row as unknown as { default_expr: string | null }).default_expr,
33
- identity: row.identity ?? '',
34
- generated: row.generated ?? '',
35
- ordinal: row.ordinal
36
- })),
37
- indexes: indexes.rows,
38
- constraints: constraints.rows
27
+ return {
28
+ exists: true,
29
+ columns: columns.rows.map(row => ({
30
+ name: row.name,
31
+ type: row.type,
32
+ notNull: (row as unknown as { not_null: boolean }).not_null,
33
+ defaultExpr: (row as unknown as { default_expr: string | null }).default_expr,
34
+ identity: row.identity ?? '',
35
+ generated: row.generated ?? '',
36
+ ordinal: row.ordinal
37
+ })),
38
+ indexes: indexes.rows,
39
+ constraints: constraints.rows
40
+ }
39
41
  }
40
- }
41
42
 
42
- /** Count the rows a destructive step would lose, so the boot log can say how much. */
43
- export const countNonNull = async (
44
- client: PoolClient, qualified: string, column: string
45
- ): Promise<number> => {
46
- try {
47
- const result = await client.query<{ count: string }>(
48
- `SELECT count(*)::text AS count FROM ${qualified} WHERE "${column.replace(/"/g, '""')}" IS NOT NULL`
49
- )
43
+ const countNonNull = async (qualified: string, column: string): Promise<number> => {
44
+ try {
45
+ const result = await client.query<{ count: string }>(
46
+ `SELECT count(*)::text AS count FROM ${qualified} WHERE "${column.replace(/"/g, '""')}" IS NOT NULL`
47
+ )
50
48
 
51
- return parseInt(result.rows[0]?.count ?? '0', 10)
52
- } catch {
53
- /** Never let a diagnostic read break reconciliation. */
54
- return -1
49
+ return parseInt(result.rows[0]?.count ?? '0', 10)
50
+ } catch {
51
+ /** Never let a diagnostic read break reconciliation. */
52
+ return -1
53
+ }
55
54
  }
55
+
56
+ return { introspectTable, countNonNull }
56
57
  }
58
+
59
+ /** @deprecated compat:factory-refactor — use `makePgIntrospectHelper(client).introspectTable(…)` */
60
+ export const introspectTable = async (
61
+ client: PoolClient, schema: string, table: string, qualified: string
62
+ ): Promise<LiveTable> => makePgIntrospectHelper(client).introspectTable(schema, table, qualified)
@@ -0,0 +1,41 @@
1
+ import type { BasicContext } from '@owlmeans/context'
2
+ import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
3
+
4
+ import type { PostgresResource, TableSpec } from '../../types.js'
5
+ import type { TableInit } from '../types.js'
6
+
7
+ /** A resource table's initialization over one database handle. */
8
+ export interface PgLifeCycleHelper {
9
+ /**
10
+ * Bring a resource's table to the shape its schema declares, then hand back everything the
11
+ * resource needs to query it. The Postgres counterpart of mongo's `initializeCollection`.
12
+ *
13
+ * The whole sequence runs inside a session level advisory lock keyed on the qualified table
14
+ * name, so replicas booting simultaneously converge one at a time instead of racing each
15
+ * other's DDL — a race mongo's converge-on-boot leaves open.
16
+ *
17
+ * Order is deliberate:
18
+ *
19
+ * 1. probe for the table
20
+ * 2. absent → *baseline* every registered migration; present → run the `pre` ones
21
+ * 3. reconcile structure against the schema
22
+ * 4. present → run the `post` migrations
23
+ * 5. queue foreign keys for after every resource has initialized
24
+ *
25
+ * Step 2 is what stops the two mechanisms colliding. On a fresh deployment the table is
26
+ * created directly to its final shape, so migrations that would have produced that shape
27
+ * are recorded as satisfied rather than replayed against a table that already matches.
28
+ */
29
+ initializeTable: (
30
+ config: DbConfig, resource: PostgresResource<ResourceRecord>,
31
+ context: BasicContext<any>, defer: (task: () => Promise<void>) => void
32
+ ) => Promise<TableInit>
33
+ /**
34
+ * Add the foreign keys the schema declares, skipping any Postgres already holds.
35
+ *
36
+ * Constraints are compared by name only. A key whose *definition* drifted has to be dropped
37
+ * by hand or in a migration: `ALTER TABLE ... DROP CONSTRAINT` on a foreign key is not a
38
+ * change worth making on a boot nobody is watching.
39
+ */
40
+ applyForeignKeys: (spec: TableSpec, context: BasicContext<any>) => Promise<void>
41
+ }
@@ -4,145 +4,126 @@ import { logger } from '@owlmeans/log'
4
4
  import type { PoolClient } from 'pg'
5
5
 
6
6
  import { DEF_MIGRATIONS_TABLE, PgAutoSync } from '../consts.js'
7
- import { getDeclaration } from '../declarations.js'
8
- import { describePgError, pgErrorToResourceError } from '../errors.js'
7
+ import { pgDeclarationHelper } from '../declarations.js'
8
+ import { pgErrorHelper } from '../pg-error.js'
9
9
  import type { PostgresDb, PostgresMeta, PostgresResource, TableSpec } from '../types.js'
10
- import { planForeignKeys, planSync } from './diff.js'
11
- import { introspectTable } from './introspect.js'
10
+ import { pgDiffHelper } from './diff.js'
11
+ import { makePgIntrospectHelper } from './introspect.js'
12
+ import type { PgLifeCycleHelper } from './life-cycle/types.js'
12
13
  import { makeMigrationStore } from './migrations.js'
13
- import { pgTableName } from './name.js'
14
- import { schemaToTableSpec } from './schema.js'
15
- import { refOf, resolvePlaceholders } from './sql.js'
16
- import { acquireLock, applyPlan, ensureSchema, releaseLock } from './sync.js'
14
+ import { pgNameHelper } from './name.js'
15
+ import { pgSchemaHelper } from './schema.js'
16
+ import { pgPlaceholdersOf } from './sql.js'
17
+ import { makePgSyncHelper } from './sync.js'
17
18
  import { specToTable } from './table.js'
18
19
  import type { TableInit } from './types.js'
19
20
 
20
21
  const log = logger('postgres-resource')
21
22
 
22
- /**
23
- * Bring a resource's table to the shape its schema declares, then hand back everything the
24
- * resource needs to query it. The Postgres counterpart of mongo's `initializeCollection`.
25
- *
26
- * The whole sequence runs inside a session level advisory lock keyed on the qualified table
27
- * name, so replicas booting simultaneously converge one at a time instead of racing each
28
- * other's DDL — a race mongo's converge-on-boot leaves open.
29
- *
30
- * Order is deliberate:
31
- *
32
- * 1. probe for the table
33
- * 2. absent → *baseline* every registered migration; present → run the `pre` ones
34
- * 3. reconcile structure against the schema
35
- * 4. present → run the `post` migrations
36
- * 5. queue foreign keys for after every resource has initialized
37
- *
38
- * Step 2 is what stops the two mechanisms colliding. On a fresh deployment the table is
39
- * created directly to its final shape, so migrations that would have produced that shape
40
- * are recorded as satisfied rather than replayed against a table that already matches.
41
- */
42
- export const initializeTable = async (
43
- db: PostgresDb, config: DbConfig, resource: PostgresResource<ResourceRecord>,
44
- context: BasicContext<any>, defer: (task: () => Promise<void>) => void
45
- ): Promise<TableInit> => {
46
- const declaration = getDeclaration(resource.alias)
47
- const meta = (config.meta ?? {}) as PostgresMeta
48
- const mode = meta.autoSync ?? PgAutoSync.Full
49
-
50
- const spec = schemaToTableSpec(
51
- resource.alias, resource.schema ?? declaration.schema, db.schema,
52
- pgTableName(config, resource), mode !== PgAutoSync.Off, declaration.indexes
53
- )
54
- const entity = specToTable(spec)
55
- const reports: MigrationReport[] = []
56
-
57
- const client = await db.pool.connect()
58
- try {
59
- await acquireLock(client, spec.qualified)
60
- await ensureSchema(client, spec.schema)
61
-
62
- const resolve = (text: string): string => resolvePlaceholders(text, context, spec)
63
- const ref = (alias?: string): string => refOf(context, spec, alias)
64
- const store = makeMigrationStore(client, spec, DEF_MIGRATIONS_TABLE, resolve, ref)
65
-
66
- const fresh = !(await introspectTable(client, spec.schema, spec.table, spec.qualified)).exists
67
-
68
- reports.push(await runMigrations(resource.alias, declaration.migrations, store, fresh
69
- ? { baseline: true }
70
- : { stage: MigrationStage.Pre }))
71
-
72
- if (spec.autoSync) {
73
- /** Re-read: a `pre` migration may have reshaped exactly what the diff is about to compare. */
74
- const live = await introspectTable(client, spec.schema, spec.table, spec.qualified)
75
- const plan = planSync(spec, live, mode === PgAutoSync.Additive)
76
- await applyPlan(client, spec, plan)
77
- }
23
+ export const makePgLifeCycleHelper = (db: PostgresDb): PgLifeCycleHelper => {
24
+ const { describePgError, pgErrorToResourceError } = pgErrorHelper
25
+
26
+ const initializeTable = async (
27
+ config: DbConfig, resource: PostgresResource<ResourceRecord>,
28
+ context: BasicContext<any>, defer: (task: () => Promise<void>) => void
29
+ ): Promise<TableInit> => {
30
+ const declaration = pgDeclarationHelper.getDeclaration(resource.alias)
31
+ const meta = (config.meta ?? {}) as PostgresMeta
32
+ const mode = meta.autoSync ?? PgAutoSync.Full
33
+
34
+ const spec = pgSchemaHelper.schemaToTableSpec(
35
+ resource.alias, resource.schema ?? declaration.schema, db.schema,
36
+ pgNameHelper.pgTableName(config, resource), mode !== PgAutoSync.Off, declaration.indexes
37
+ )
38
+ const entity = specToTable(spec)
39
+ const reports: MigrationReport[] = []
40
+
41
+ const client = await db.pool.connect()
42
+ const sync = makePgSyncHelper(client)
43
+ const introspect = makePgIntrospectHelper(client)
44
+ try {
45
+ await sync.acquireLock(spec.qualified)
46
+ await sync.ensureSchema(spec.schema)
47
+
48
+ const placeholders = pgPlaceholdersOf(context)
49
+ const resolve = (text: string): string => placeholders.resolvePlaceholders(text, spec)
50
+ const ref = (alias?: string): string => placeholders.refOf(spec, alias)
51
+ const store = makeMigrationStore(client, spec, DEF_MIGRATIONS_TABLE, resolve, ref)
52
+
53
+ const fresh = !(await introspect.introspectTable(spec.schema, spec.table, spec.qualified)).exists
54
+
55
+ reports.push(await runMigrations(resource.alias, declaration.migrations, store, fresh
56
+ ? { baseline: true }
57
+ : { stage: MigrationStage.Pre }))
58
+
59
+ if (spec.autoSync) {
60
+ /** Re-read: a `pre` migration may have reshaped exactly what the diff is about to compare. */
61
+ const live = await introspect.introspectTable(spec.schema, spec.table, spec.qualified)
62
+ const plan = pgDiffHelper.planSync(spec, live, mode === PgAutoSync.Additive)
63
+ await sync.applyPlan(spec, plan)
64
+ }
78
65
 
79
- if (!fresh) {
80
- reports.push(await runMigrations(
81
- resource.alias, declaration.migrations, store, { stage: MigrationStage.Post }
82
- ))
83
- }
66
+ if (!fresh) {
67
+ reports.push(await runMigrations(
68
+ resource.alias, declaration.migrations, store, { stage: MigrationStage.Post }
69
+ ))
70
+ }
84
71
 
85
- for (const report of reports) {
86
- if (report.applied.length > 0) {
87
- log.info('Migrations applied', {
88
- resource: resource.alias, table: spec.qualified, stage: report.stage, applied: report.applied,
89
- }, { event: 'migration.applied' })
72
+ for (const report of reports) {
73
+ if (report.applied.length > 0) {
74
+ log.info('Migrations applied', {
75
+ resource: resource.alias, table: spec.qualified, stage: report.stage, applied: report.applied,
76
+ }, { event: 'migration.applied' })
77
+ }
90
78
  }
79
+ } finally {
80
+ await sync.releaseLock(spec.qualified)
81
+ client.release()
91
82
  }
92
- } finally {
93
- await releaseLock(client, spec.qualified)
94
- client.release()
95
- }
96
83
 
97
- if (spec.references.length > 0) {
98
- defer(async () => { await applyForeignKeys(db, spec, context) })
99
- }
84
+ if (spec.references.length > 0) {
85
+ defer(async () => { await applyForeignKeys(spec, context) })
86
+ }
100
87
 
101
- return { spec, entity, reports }
102
- }
88
+ return { spec, entity, reports }
89
+ }
103
90
 
104
- /**
105
- * Add the foreign keys the schema declares, skipping any Postgres already holds.
106
- *
107
- * Constraints are compared by name only. A key whose *definition* drifted has to be dropped
108
- * by hand or in a migration: `ALTER TABLE ... DROP CONSTRAINT` on a foreign key is not a
109
- * change worth making on a boot nobody is watching.
110
- */
111
- export const applyForeignKeys = async (
112
- db: PostgresDb, spec: TableSpec, context: BasicContext<any>
113
- ): Promise<void> => {
114
- const client = await db.pool.connect()
115
- try {
116
- const statements = planForeignKeys(spec, context)
117
- const live = await introspectTable(client, spec.schema, spec.table, spec.qualified)
118
- if (!live.exists) {
119
- return
120
- }
121
- const present = live.constraints.map(constraint => constraint.name)
91
+ const applyForeignKeys = async (spec: TableSpec, context: BasicContext<any>): Promise<void> => {
92
+ const client = await db.pool.connect()
93
+ try {
94
+ const statements = pgDiffHelper.planForeignKeys(spec, context)
95
+ const live = await makePgIntrospectHelper(client).introspectTable(spec.schema, spec.table, spec.qualified)
96
+ if (!live.exists) {
97
+ return
98
+ }
99
+ const present = live.constraints.map(constraint => constraint.name)
122
100
 
123
- for (const statement of statements) {
124
- if (present.includes(statement.target)) {
125
- continue
101
+ for (const statement of statements) {
102
+ if (present.includes(statement.target)) {
103
+ continue
104
+ }
105
+ await apply(client, spec, statement.sql)
126
106
  }
127
- await apply(client, spec, statement.sql)
107
+ } finally {
108
+ client.release()
128
109
  }
129
- } finally {
130
- client.release()
131
110
  }
132
- }
133
111
 
134
- const apply = async (client: PoolClient, spec: TableSpec, statement: string): Promise<void> => {
135
- try {
136
- await client.query(statement)
137
- } catch (error) {
138
- const translated = pgErrorToResourceError(error)
139
- /**
140
- * A foreign key failing means live rows point at nothing. That's a data problem the
141
- * boot can't fix, and refusing to start is better than starting with the constraint
142
- * quietly missing.
143
- */
144
- throw Object.assign(translated, {
145
- message: `${spec.qualified}: ${describePgError(error)} — statement: ${statement}`
146
- })
112
+ const apply = async (client: PoolClient, spec: TableSpec, statement: string): Promise<void> => {
113
+ try {
114
+ await client.query(statement)
115
+ } catch (error) {
116
+ const translated = pgErrorToResourceError(error)
117
+ /**
118
+ * A foreign key failing means live rows point at nothing. That's a data problem the
119
+ * boot can't fix, and refusing to start is better than starting with the constraint
120
+ * quietly missing.
121
+ */
122
+ throw Object.assign(translated, {
123
+ message: `${spec.qualified}: ${describePgError(error)} — statement: ${statement}`
124
+ })
125
+ }
147
126
  }
127
+
128
+ return { initializeTable, applyForeignKeys }
148
129
  }
@@ -0,0 +1,40 @@
1
+ import type { ResourceRecord } from '@owlmeans/resource'
2
+
3
+ import type { TableSpec } from '../../types.js'
4
+
5
+ /** Records to driver values and back, driven by a compiled table spec. */
6
+ export interface PgMarshalHelper {
7
+ /**
8
+ * Turn a driver row into a record: physical column names back to schema property names,
9
+ * plus the coercions node-postgres leaves to the caller.
10
+ *
11
+ * Columns the spec doesn't know about — computed expressions, joined columns from custom
12
+ * SQL — are kept verbatim. A join result is more useful than a lossy projection.
13
+ */
14
+ rowToRecord: <T extends ResourceRecord>(row: Record<string, unknown>, spec: TableSpec) => T
15
+ /**
16
+ * Same coercion as {@link PgMarshalHelper.rowToRecord}, for rows Drizzle produced.
17
+ *
18
+ * Drizzle keys its results by the table object's JS property names — which are the schema
19
+ * property names — where raw SQL returns physical column names. Two functions rather than
20
+ * one lookup that tries both, because a schema that renames `a` to column `b` while another
21
+ * property is itself named `b` would make the combined lookup pick the wrong spec.
22
+ */
23
+ resultToRecord: <T extends ResourceRecord>(row: Record<string, unknown>, spec: TableSpec) => T
24
+ /**
25
+ * Turn a record into the values Drizzle inserts, keyed by property name (Drizzle owns the
26
+ * property to column mapping).
27
+ *
28
+ * `undefined` properties are dropped so a partial write touches only what was supplied;
29
+ * an explicit `null` is kept, because nulling a column is a real intent.
30
+ */
31
+ recordToValues: (record: Record<string, unknown>, spec: TableSpec) => Record<string, unknown>
32
+ /**
33
+ * Every managed column, with anything the record omits explicitly nulled.
34
+ *
35
+ * This is what makes `update()` a replace rather than a merge — the semantics mongo's
36
+ * `replaceOne` gives, kept identical so a resource behaves the same on either backend.
37
+ * The primary key and unmanaged columns are never nulled.
38
+ */
39
+ recordToFullValues: (record: Record<string, unknown>, spec: TableSpec) => Record<string, unknown>
40
+ }