@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.
- package/README.md +13 -13
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +1 -1
- package/build/consts.d.ts +9 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +6 -0
- package/build/consts.js.map +1 -1
- package/build/declarations/types.d.ts +12 -0
- package/build/declarations/types.d.ts.map +1 -0
- package/build/declarations/types.js +2 -0
- package/build/declarations/types.js.map +1 -0
- package/build/declarations.d.ts +5 -1
- package/build/declarations.d.ts.map +1 -1
- package/build/declarations.js +25 -15
- package/build/declarations.js.map +1 -1
- package/build/errors.d.ts +0 -20
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +1 -111
- package/build/errors.js.map +1 -1
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/pg-error/types.d.ts +24 -0
- package/build/pg-error/types.d.ts.map +1 -0
- package/build/pg-error/types.js +2 -0
- package/build/pg-error/types.js.map +1 -0
- package/build/pg-error.d.ts +4 -0
- package/build/pg-error.d.ts.map +1 -0
- package/build/pg-error.js +100 -0
- package/build/pg-error.js.map +1 -0
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +15 -11
- package/build/resource.js.map +1 -1
- package/build/types.d.ts.map +1 -1
- package/build/utils/consts.local.d.ts +2 -0
- package/build/utils/consts.local.d.ts.map +1 -1
- package/build/utils/consts.local.js +2 -0
- package/build/utils/consts.local.js.map +1 -1
- package/build/utils/criteria/types.d.ts +31 -0
- package/build/utils/criteria/types.d.ts.map +1 -0
- package/build/utils/criteria/types.js +2 -0
- package/build/utils/criteria/types.js.map +1 -0
- package/build/utils/criteria.d.ts +3 -27
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js +86 -104
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/diff/types.d.ts +40 -0
- package/build/utils/diff/types.d.ts.map +1 -0
- package/build/utils/diff/types.js +2 -0
- package/build/utils/diff/types.js.map +1 -0
- package/build/utils/diff.d.ts +5 -35
- package/build/utils/diff.d.ts.map +1 -1
- package/build/utils/diff.js +320 -344
- package/build/utils/diff.js.map +1 -1
- package/build/utils/index.d.ts +9 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/introspect/types.d.ts +9 -0
- package/build/utils/introspect/types.d.ts.map +1 -0
- package/build/utils/introspect/types.js +2 -0
- package/build/utils/introspect/types.js.map +1 -0
- package/build/utils/introspect.d.ts +3 -3
- package/build/utils/introspect.d.ts.map +1 -1
- package/build/utils/introspect.js +41 -38
- package/build/utils/introspect.js.map +1 -1
- package/build/utils/life-cycle/types.d.ts +37 -0
- package/build/utils/life-cycle/types.d.ts.map +1 -0
- package/build/utils/life-cycle/types.js +2 -0
- package/build/utils/life-cycle/types.js.map +1 -0
- package/build/utils/life-cycle.d.ts +3 -33
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +88 -108
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/marshal/types.d.ts +39 -0
- package/build/utils/marshal/types.d.ts.map +1 -0
- package/build/utils/marshal/types.js +2 -0
- package/build/utils/marshal/types.js.map +1 -0
- package/build/utils/marshal.d.ts +4 -32
- package/build/utils/marshal.d.ts.map +1 -1
- package/build/utils/marshal.js +121 -144
- package/build/utils/marshal.js.map +1 -1
- package/build/utils/migrations.d.ts.map +1 -1
- package/build/utils/migrations.js +41 -36
- package/build/utils/migrations.js.map +1 -1
- package/build/utils/name/types.d.ts +43 -0
- package/build/utils/name/types.d.ts.map +1 -0
- package/build/utils/name/types.js +2 -0
- package/build/utils/name/types.js.map +1 -0
- package/build/utils/name.d.ts +6 -35
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +38 -57
- package/build/utils/name.js.map +1 -1
- package/build/utils/schema/types.d.ts +22 -0
- package/build/utils/schema/types.d.ts.map +1 -0
- package/build/utils/schema/types.js +2 -0
- package/build/utils/schema/types.js.map +1 -0
- package/build/utils/schema.d.ts +4 -24
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +339 -352
- package/build/utils/schema.js.map +1 -1
- package/build/utils/sql/types.d.ts +25 -0
- package/build/utils/sql/types.d.ts.map +1 -0
- package/build/utils/sql/types.js +2 -0
- package/build/utils/sql/types.js.map +1 -0
- package/build/utils/sql.d.ts +4 -18
- package/build/utils/sql.d.ts.map +1 -1
- package/build/utils/sql.js +0 -0
- package/build/utils/sql.js.map +1 -1
- package/build/utils/sync/types.d.ts +25 -0
- package/build/utils/sync/types.d.ts.map +1 -0
- package/build/utils/sync/types.js +2 -0
- package/build/utils/sync/types.js.map +1 -0
- package/build/utils/sync.d.ts +2 -19
- package/build/utils/sync.d.ts.map +1 -1
- package/build/utils/sync.js +64 -71
- package/build/utils/sync.js.map +1 -1
- package/package.json +7 -7
- package/src/consts.ts +7 -0
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +29 -14
- package/src/errors.ts +1 -121
- package/src/index.ts +3 -0
- package/src/pg-error/types.ts +23 -0
- package/src/pg-error.ts +114 -0
- package/src/resource.ts +17 -13
- package/src/types.ts +1 -0
- package/src/utils/consts.local.ts +3 -0
- package/src/utils/criteria/types.ts +32 -0
- package/src/utils/criteria.ts +95 -110
- package/src/utils/diff/types.ts +41 -0
- package/src/utils/diff.ts +330 -348
- package/src/utils/index.ts +9 -0
- package/src/utils/introspect/types.ts +9 -0
- package/src/utils/introspect.ts +51 -45
- package/src/utils/life-cycle/types.ts +41 -0
- package/src/utils/life-cycle.ts +102 -121
- package/src/utils/marshal/types.ts +40 -0
- package/src/utils/marshal.ts +129 -147
- package/src/utils/migrations.ts +39 -33
- package/src/utils/name/types.ts +44 -0
- package/src/utils/name.ts +47 -60
- package/src/utils/schema/types.ts +26 -0
- package/src/utils/schema.ts +359 -366
- package/src/utils/sql/types.ts +25 -0
- package/src/utils/sql.ts +0 -0
- package/src/utils/sync/types.ts +25 -0
- package/src/utils/sync.ts +62 -68
- package/tests/criteria.spec.ts +4 -1
- package/tests/diff.spec.ts +4 -1
- package/tests/errors.spec.ts +4 -3
- package/tests/name.spec.ts +5 -3
- package/tests/schema.spec.ts +3 -1
- package/tests/sql.spec.ts +20 -22
package/src/utils/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/introspect.ts
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
): Promise<LiveTable> => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
+
}
|
package/src/utils/life-cycle.ts
CHANGED
|
@@ -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 {
|
|
8
|
-
import {
|
|
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 {
|
|
11
|
-
import {
|
|
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 {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
66
|
+
if (!fresh) {
|
|
67
|
+
reports.push(await runMigrations(
|
|
68
|
+
resource.alias, declaration.migrations, store, { stage: MigrationStage.Post }
|
|
69
|
+
))
|
|
70
|
+
}
|
|
84
71
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
if (spec.references.length > 0) {
|
|
85
|
+
defer(async () => { await applyForeignKeys(spec, context) })
|
|
86
|
+
}
|
|
100
87
|
|
|
101
|
-
|
|
102
|
-
}
|
|
88
|
+
return { spec, entity, reports }
|
|
89
|
+
}
|
|
103
90
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
}
|