@owlmeans/postgres-resource 0.1.18-rc.41 → 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 +2 -2
- 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 +6 -9
- 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 +1 -1
- 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 +6 -1
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +14 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/consts.local.d.ts +35 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +63 -0
- package/build/utils/consts.local.js.map +1 -0
- 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 +88 -107
- 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 +321 -343
- package/build/utils/diff.js.map +1 -1
- package/build/utils/index.d.ts +10 -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 +42 -67
- 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 -38
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +90 -107
- 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 +124 -141
- 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 +40 -59
- 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 +341 -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 +5 -27
- 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 +67 -84
- package/build/utils/sync.js.map +1 -1
- package/build/utils/types.d.ts +18 -0
- package/build/utils/types.d.ts.map +1 -0
- package/build/utils/types.js +2 -0
- package/build/utils/types.js.map +1 -0
- package/build/utils/types.local.d.ts +33 -0
- package/build/utils/types.local.d.ts.map +1 -0
- package/build/utils/types.local.js +2 -0
- package/build/utils/types.local.js.map +1 -0
- package/package.json +14 -13
- package/src/consts.ts +7 -0
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +30 -23
- package/src/errors.ts +1 -131
- 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 +20 -24
- package/src/types.local.ts +16 -0
- package/src/types.ts +8 -4
- package/src/utils/consts.local.ts +71 -0
- package/src/utils/criteria/types.ts +32 -0
- package/src/utils/criteria.ts +99 -129
- package/src/utils/diff/types.ts +41 -0
- package/src/utils/diff.ts +331 -348
- package/src/utils/index.ts +10 -0
- package/src/utils/introspect/types.ts +9 -0
- package/src/utils/introspect.ts +54 -79
- package/src/utils/life-cycle/types.ts +41 -0
- package/src/utils/life-cycle.ts +108 -134
- package/src/utils/marshal/types.ts +40 -0
- package/src/utils/marshal.ts +134 -147
- package/src/utils/migrations.ts +39 -33
- package/src/utils/name/types.ts +44 -0
- package/src/utils/name.ts +49 -63
- package/src/utils/schema/types.ts +26 -0
- package/src/utils/schema.ts +366 -397
- 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 +65 -83
- package/src/utils/types.local.ts +35 -0
- package/src/utils/types.ts +19 -0
- package/tests/criteria.spec.ts +4 -1
- package/tests/diff.spec.ts +28 -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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TableSpec } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `{{alias}}` placeholders of custom SQL, resolved through ONE context's registered postgres
|
|
5
|
+
* resources. Reached as `pgPlaceholdersOf(context)`.
|
|
6
|
+
*/
|
|
7
|
+
export interface PgPlaceholderHelper {
|
|
8
|
+
/**
|
|
9
|
+
* Fully qualified table of a registered postgres resource; omit the alias for `self`.
|
|
10
|
+
* This is what `PostgresTx.ref` and `PostgresResource.ref` resolve through.
|
|
11
|
+
*
|
|
12
|
+
* @throws {PostgresPlaceholderError}
|
|
13
|
+
*/
|
|
14
|
+
refOf: (self: TableSpec | null, alias?: string) => string
|
|
15
|
+
/**
|
|
16
|
+
* Rewrite `{{...}}` placeholders into quoted identifiers.
|
|
17
|
+
*
|
|
18
|
+
* Identifiers only — `$1`-style parameters are left exactly as written and their values
|
|
19
|
+
* never pass through here. Postgres can't bind an identifier as a parameter, which is the
|
|
20
|
+
* whole reason this mechanism has to exist; values have no such excuse.
|
|
21
|
+
*
|
|
22
|
+
* @throws {PostgresPlaceholderError}
|
|
23
|
+
*/
|
|
24
|
+
resolvePlaceholders: (text: string, self: TableSpec | null) => string
|
|
25
|
+
}
|
package/src/utils/sql.ts
CHANGED
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DdlPlan, TableSpec } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
/** The reconciliation steps run over one checked out connection. */
|
|
4
|
+
export interface PgSyncHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Serialize initialization across replicas. A session level lock, not `xact`, because
|
|
7
|
+
* migrations run in their own transactions inside the critical section.
|
|
8
|
+
*/
|
|
9
|
+
acquireLock: (qualified: string) => Promise<void>
|
|
10
|
+
/** Release the {@link PgSyncHelper.acquireLock} lock; never masks the failure that preceded it. */
|
|
11
|
+
releaseLock: (qualified: string) => Promise<void>
|
|
12
|
+
/** `CREATE SCHEMA IF NOT EXISTS`. */
|
|
13
|
+
ensureSchema: (schema: string) => Promise<void>
|
|
14
|
+
/**
|
|
15
|
+
* Apply a reconciliation plan in a single transaction.
|
|
16
|
+
*
|
|
17
|
+
* Postgres DDL is transactional, so a plan that fails partway leaves the table exactly as
|
|
18
|
+
* it was. That property is what makes converging without confirmation prompts tolerable —
|
|
19
|
+
* the table is either fully converged or untouched, never half migrated.
|
|
20
|
+
*
|
|
21
|
+
* @throws {PostgresCastRequired} Postgres refused an automatic cast.
|
|
22
|
+
* @throws {PostgresSyncError} any other statement failed — the message names the statement.
|
|
23
|
+
*/
|
|
24
|
+
applyPlan: (spec: TableSpec, plan: DdlPlan) => Promise<DdlPlan>
|
|
25
|
+
}
|
package/src/utils/sync.ts
CHANGED
|
@@ -1,106 +1,88 @@
|
|
|
1
|
+
import { logger } from '@owlmeans/log'
|
|
1
2
|
import type { PoolClient } from 'pg'
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { PostgresCastRequired, PostgresSyncError } from '../errors.js'
|
|
5
|
+
import { pgErrorHelper } from '../pg-error.js'
|
|
5
6
|
import type { DdlPlan, TableSpec } from '../types.js'
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { makePgIntrospectHelper } from './introspect.js'
|
|
8
|
+
import { pgNameHelper } from './name.js'
|
|
9
|
+
import { CAST_DATA_CODES } from './consts.local.js'
|
|
10
|
+
import type { PgSyncHelper } from './sync/types.js'
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
* Codes that mean "the cast is legal, the data isn't": a `text` column holding `'a'` retyped to
|
|
11
|
-
* `integer`, a value too wide for the new length, a number past the new range. Postgres refuses
|
|
12
|
-
* each of these rather than truncating, and the remedy is the same one a refused cast needs — so
|
|
13
|
-
* they are only read as a cast problem here, inside DDL. On the CRUD path the identical code means
|
|
14
|
-
* a caller passed a bad value, which is a different bug with a different fix.
|
|
15
|
-
*/
|
|
16
|
-
const CAST_DATA_CODES: string[] = [
|
|
17
|
-
PgErrorCode.InvalidTextRepresentation,
|
|
18
|
-
PgErrorCode.StringDataRightTruncation,
|
|
19
|
-
PgErrorCode.NumericValueOutOfRange
|
|
20
|
-
]
|
|
12
|
+
const log = logger('postgres-resource')
|
|
21
13
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
export const acquireLock = async (client: PoolClient, qualified: string): Promise<void> => {
|
|
27
|
-
const [first, second] = advisoryKey(qualified)
|
|
28
|
-
await client.query('SELECT pg_advisory_lock($1, $2)', [first, second])
|
|
29
|
-
}
|
|
14
|
+
export const makePgSyncHelper = (client: PoolClient): PgSyncHelper => {
|
|
15
|
+
const { describePgError, pgErrorToResourceError } = pgErrorHelper
|
|
16
|
+
const { advisoryKey, quoteIdent } = pgNameHelper
|
|
17
|
+
const introspect = makePgIntrospectHelper(client)
|
|
30
18
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
await client.query('SELECT pg_advisory_unlock($1, $2)', [first, second])
|
|
35
|
-
} catch {
|
|
36
|
-
/** Releasing the session drops the lock anyway — never mask the original failure. */
|
|
19
|
+
const acquireLock = async (qualified: string): Promise<void> => {
|
|
20
|
+
const [first, second] = advisoryKey(qualified)
|
|
21
|
+
await client.query('SELECT pg_advisory_lock($1, $2)', [first, second])
|
|
37
22
|
}
|
|
38
|
-
}
|
|
39
23
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
24
|
+
const releaseLock = async (qualified: string): Promise<void> => {
|
|
25
|
+
const [first, second] = advisoryKey(qualified)
|
|
26
|
+
try {
|
|
27
|
+
await client.query('SELECT pg_advisory_unlock($1, $2)', [first, second])
|
|
28
|
+
} catch {
|
|
29
|
+
/** Releasing the session drops the lock anyway — never mask the original failure. */
|
|
30
|
+
}
|
|
45
31
|
}
|
|
46
|
-
}
|
|
47
32
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* @throws {PostgresCastRequired} Postgres refused an automatic cast.
|
|
56
|
-
* @throws {PostgresSyncError} any other statement failed — the message names the statement.
|
|
57
|
-
*/
|
|
58
|
-
export const applyPlan = async (
|
|
59
|
-
client: PoolClient, spec: TableSpec, plan: DdlPlan
|
|
60
|
-
): Promise<DdlPlan> => {
|
|
61
|
-
if (plan.statements.length < 1) {
|
|
62
|
-
return plan
|
|
33
|
+
const ensureSchema = async (schema: string): Promise<void> => {
|
|
34
|
+
try {
|
|
35
|
+
await client.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdent(schema)}`)
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw pgErrorToResourceError(error)
|
|
38
|
+
}
|
|
63
39
|
}
|
|
64
40
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
statement.affected = await countNonNull(client, spec.qualified, statement.target)
|
|
41
|
+
const applyPlan = async (spec: TableSpec, plan: DdlPlan): Promise<DdlPlan> => {
|
|
42
|
+
if (plan.statements.length < 1) {
|
|
43
|
+
return plan
|
|
69
44
|
}
|
|
70
|
-
}
|
|
71
45
|
|
|
72
|
-
|
|
73
|
-
let current = ''
|
|
74
|
-
try {
|
|
46
|
+
/** Read what a drop would cost before the transaction opens, so the log can say it. */
|
|
75
47
|
for (const statement of plan.statements) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
console.warn(
|
|
79
|
-
`@owlmeans/postgres-resource: dropping ${spec.qualified}.${statement.target} —`
|
|
80
|
-
+ ` ${statement.affected} row(s) hold a value. Declare it under \`pg.unmanaged\``
|
|
81
|
-
+ ' or set `pg: { managed: false }` to keep it.'
|
|
82
|
-
)
|
|
48
|
+
if (statement.destructive === true) {
|
|
49
|
+
statement.affected = await introspect.countNonNull(spec.qualified, statement.target)
|
|
83
50
|
}
|
|
84
|
-
await client.query(statement.sql)
|
|
85
51
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
52
|
+
|
|
53
|
+
await client.query('BEGIN')
|
|
54
|
+
let current = ''
|
|
55
|
+
try {
|
|
56
|
+
for (const statement of plan.statements) {
|
|
57
|
+
current = statement.sql
|
|
58
|
+
if (statement.destructive === true && (statement.affected ?? 0) > 0) {
|
|
59
|
+
log.warn('Dropping a column that holds values; declare it under pg.unmanaged or set pg.managed false to keep it', {
|
|
60
|
+
table: spec.qualified, column: statement.target, rows: statement.affected,
|
|
61
|
+
}, { event: 'migration.drop' })
|
|
62
|
+
}
|
|
63
|
+
await client.query(statement.sql)
|
|
64
|
+
}
|
|
65
|
+
await client.query('COMMIT')
|
|
66
|
+
} catch (error) {
|
|
67
|
+
await client.query('ROLLBACK').catch(() => undefined)
|
|
68
|
+
const translated = pgErrorToResourceError(error)
|
|
69
|
+
const code = (error as { code?: string } | null)?.code
|
|
70
|
+
if (translated instanceof PostgresCastRequired || (code != null && CAST_DATA_CODES.includes(code))) {
|
|
71
|
+
const failure = new PostgresCastRequired(
|
|
72
|
+
`${spec.qualified}: ${describePgError(error)} — statement: ${current}.`
|
|
73
|
+
+ ' Declare `pg: { using: \'<expr>\' }` on the property, or perform the change in a'
|
|
74
|
+
+ ' `pre` migration so reconciliation observes no drift.'
|
|
75
|
+
)
|
|
76
|
+
failure.cause = error
|
|
77
|
+
throw failure
|
|
78
|
+
}
|
|
79
|
+
const failure = new PostgresSyncError(`${spec.qualified}: ${describePgError(error)} — statement: ${current}`)
|
|
97
80
|
failure.cause = error
|
|
98
81
|
throw failure
|
|
99
82
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
throw failure
|
|
83
|
+
|
|
84
|
+
return plan
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
return
|
|
87
|
+
return { acquireLock, releaseLock, ensureSchema, applyPlan }
|
|
106
88
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SQL } from 'drizzle-orm'
|
|
2
|
+
import { PG_KEYWORD } from '../consts.js'
|
|
3
|
+
import type { PgPropertyOverride, PgRootOverride } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A value at a path, as the jsonb it is (`#>`) and as text (`#>>`). The path is ONE bound `text[]`
|
|
7
|
+
* parameter, so a segment holding a comma or a brace stays one segment.
|
|
8
|
+
*/
|
|
9
|
+
export interface PathTarget {
|
|
10
|
+
value: SQL
|
|
11
|
+
text: SQL
|
|
12
|
+
/** Absent, or a JSON `null` — what every other store reads as "no value". */
|
|
13
|
+
missing: SQL
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RawProperty {
|
|
17
|
+
type?: string | string[]
|
|
18
|
+
format?: string
|
|
19
|
+
enum?: unknown[]
|
|
20
|
+
items?: RawProperty
|
|
21
|
+
maxLength?: number
|
|
22
|
+
maximum?: number
|
|
23
|
+
minimum?: number
|
|
24
|
+
default?: unknown
|
|
25
|
+
nullable?: boolean
|
|
26
|
+
secure?: boolean
|
|
27
|
+
[PG_KEYWORD]?: PgPropertyOverride
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RawSchema {
|
|
31
|
+
properties?: Record<string, RawProperty>
|
|
32
|
+
required?: string[]
|
|
33
|
+
allOf?: RawSchema[]
|
|
34
|
+
[PG_KEYWORD]?: PgRootOverride
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PgRuntimeTable, TableSpec } from '../types.js'
|
|
2
|
+
import type { MigrationReport } from '@owlmeans/resource'
|
|
3
|
+
import type { BasicContext } from '@owlmeans/context'
|
|
4
|
+
|
|
5
|
+
export interface TableInit {
|
|
6
|
+
spec: TableSpec
|
|
7
|
+
/** The Drizzle table CRUD is built against. */
|
|
8
|
+
entity: PgRuntimeTable
|
|
9
|
+
reports: MigrationReport[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The context a placeholder resolves through.
|
|
14
|
+
*
|
|
15
|
+
* Resolution reads exactly one thing — `context.resource(alias).table` — so the config a context
|
|
16
|
+
* carries is irrelevant here, and pinning one would force every caller holding a server context
|
|
17
|
+
* to cast its way in. Deliberately open, and named so callers pass what they already have.
|
|
18
|
+
*/
|
|
19
|
+
export type PlaceholderContext = BasicContext<any>
|
package/tests/criteria.spec.ts
CHANGED
|
@@ -6,9 +6,12 @@ import { sql } from 'drizzle-orm'
|
|
|
6
6
|
import type { SQL } from 'drizzle-orm'
|
|
7
7
|
import { PgDialect } from 'drizzle-orm/pg-core'
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { pgCriteriaHelper, pgSchemaHelper, specToTable } from '@owlmeans/postgres-resource'
|
|
10
10
|
import type { PgRuntimeTable, TableSpec } from '@owlmeans/postgres-resource'
|
|
11
11
|
|
|
12
|
+
const { criteriaToSql, sortToSql } = pgCriteriaHelper
|
|
13
|
+
const { schemaToTableSpec } = pgSchemaHelper
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Criteria translation is where a typo becomes either an error or a whole-table scan, and
|
|
14
17
|
* where a value has to stay a bound parameter instead of becoming statement text. Neither
|
package/tests/diff.spec.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { pgDiffHelper, pgSchemaHelper } from '@owlmeans/postgres-resource'
|
|
4
4
|
import type { LiveColumn, LiveTable, TableSpec } from '@owlmeans/postgres-resource'
|
|
5
5
|
|
|
6
|
+
const { canonicalDefinition, planSync } = pgDiffHelper
|
|
7
|
+
const { schemaToTableSpec } = pgSchemaHelper
|
|
8
|
+
|
|
6
9
|
const specOf = (idType: 'string' | 'integer'): TableSpec =>
|
|
7
10
|
schemaToTableSpec('tasks', {
|
|
8
11
|
type: 'object',
|
|
@@ -109,3 +112,27 @@ describe('@owlmeans/postgres-resource — comparing a declared definition with t
|
|
|
109
112
|
}
|
|
110
113
|
})
|
|
111
114
|
})
|
|
115
|
+
|
|
116
|
+
describe('@owlmeans/postgres-resource — NOT NULL constraints of Postgres 18', () => {
|
|
117
|
+
test('a not-null constraint row is never planned for a drop', () => {
|
|
118
|
+
// From 18 every NOT NULL is a pg_constraint row named `<table>_<column>_not_null`. The platform
|
|
119
|
+
// names its own constraints after the table too, so it took these for its own leftovers and sent
|
|
120
|
+
// `DROP CONSTRAINT …_id_not_null` — which Postgres refuses on a primary key column.
|
|
121
|
+
const live: LiveTable = {
|
|
122
|
+
exists: true,
|
|
123
|
+
columns: [
|
|
124
|
+
columnOf({ name: 'id', type: 'text' }),
|
|
125
|
+
columnOf({ name: 'title', type: 'text', notNull: false }),
|
|
126
|
+
],
|
|
127
|
+
indexes: [],
|
|
128
|
+
constraints: [
|
|
129
|
+
{ name: 'tasks_id_not_null', type: 'n', definition: 'NOT NULL id' },
|
|
130
|
+
{ name: 'task_entry_id_not_null', type: 'n', definition: 'NOT NULL id' },
|
|
131
|
+
],
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const dropped = planSync(specOf('string'), live).statements.filter(statement => statement.kind === 'drop-constraint')
|
|
135
|
+
expect(dropped).toEqual([])
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
package/tests/errors.spec.ts
CHANGED
|
@@ -2,11 +2,12 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { MisshapedRecord, RecordExists, ResourceError } from '@owlmeans/resource'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
PostgresConstraintError, PostgresDeadlockError, PostgresError, PostgresForeignKeyError
|
|
7
|
-
pgErrorToResourceError
|
|
5
|
+
PgErrorCode, pgErrorHelper, PostgresCastRequired, PostgresCheckError, PostgresConnectionError,
|
|
6
|
+
PostgresConstraintError, PostgresDeadlockError, PostgresError, PostgresForeignKeyError
|
|
8
7
|
} from '@owlmeans/postgres-resource'
|
|
9
8
|
|
|
9
|
+
const { describePgError, pgErrorToResourceError } = pgErrorHelper
|
|
10
|
+
|
|
10
11
|
interface DriverShape {
|
|
11
12
|
code: string
|
|
12
13
|
message?: string
|
package/tests/name.spec.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
advisoryKey, assertSqlIdentifier, pgIdentifier, pgTableName, qualify, quoteIdent, quoteLiteral
|
|
6
|
-
} from '@owlmeans/postgres-resource'
|
|
4
|
+
import { pgNameHelper } from '@owlmeans/postgres-resource'
|
|
7
5
|
import type { PostgresResource } from '@owlmeans/postgres-resource'
|
|
8
6
|
|
|
7
|
+
const {
|
|
8
|
+
advisoryKey, assertSqlIdentifier, pgIdentifier, pgTableName, qualify, quoteIdent, quoteLiteral
|
|
9
|
+
} = pgNameHelper
|
|
10
|
+
|
|
9
11
|
const resource = (alias: string, name?: string): PostgresResource<ResourceRecord> =>
|
|
10
12
|
({ alias, ...(name != null ? { name } : {}) }) as PostgresResource<ResourceRecord>
|
|
11
13
|
|
package/tests/schema.spec.ts
CHANGED
|
@@ -2,9 +2,11 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { UnsupportedArgumentError } from '@owlmeans/resource'
|
|
3
3
|
import type { AnySchema } from 'ajv'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { pgSchemaHelper } from '@owlmeans/postgres-resource'
|
|
6
6
|
import type { TableSpec } from '@owlmeans/postgres-resource'
|
|
7
7
|
|
|
8
|
+
const { schemaToTableSpec, toFormatType } = pgSchemaHelper
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Compiling an AJV schema into a table specification is the one step every other part of
|
|
10
12
|
* the package trusts: DDL emission, drift detection, marshalling and placeholder
|
package/tests/sql.spec.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import type { BasicContext } from '@owlmeans/context'
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
PostgresPlaceholderError, refOf, resolvePlaceholders, schemaToTableSpec
|
|
6
|
-
} from '@owlmeans/postgres-resource'
|
|
4
|
+
import { pgPlaceholdersOf, pgSchemaHelper, PostgresPlaceholderError } from '@owlmeans/postgres-resource'
|
|
7
5
|
import type { TableSpec } from '@owlmeans/postgres-resource'
|
|
8
6
|
|
|
9
7
|
const specOf = (alias: string, schema: string, table: string): TableSpec =>
|
|
10
|
-
schemaToTableSpec(alias, {
|
|
8
|
+
pgSchemaHelper.schemaToTableSpec(alias, {
|
|
11
9
|
type: 'object',
|
|
12
10
|
properties: {
|
|
13
11
|
id: { type: 'string', format: 'uuid' },
|
|
@@ -37,47 +35,47 @@ const context = contextOf({ users, posts })
|
|
|
37
35
|
|
|
38
36
|
describe('@owlmeans/postgres-resource — placeholder resolution', () => {
|
|
39
37
|
test('resolves the owning resource', () => {
|
|
40
|
-
expect(resolvePlaceholders('SELECT * FROM {{}}',
|
|
38
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{}}', posts))
|
|
41
39
|
.toBe('SELECT * FROM "app"."posts"')
|
|
42
|
-
expect(resolvePlaceholders('SELECT * FROM {{ self }}',
|
|
40
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{ self }}', posts))
|
|
43
41
|
.toBe('SELECT * FROM "app"."posts"')
|
|
44
42
|
})
|
|
45
43
|
|
|
46
44
|
test('resolves another registered resource, its columns, its bare name and its schema', () => {
|
|
47
|
-
expect(resolvePlaceholders('SELECT * FROM {{users}}',
|
|
45
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{users}}', posts))
|
|
48
46
|
.toBe('SELECT * FROM "app"."users"')
|
|
49
47
|
/** The physical column, not the property — a `pg: { column }` rename has to survive. */
|
|
50
|
-
expect(resolvePlaceholders('SELECT {{users.email}}',
|
|
48
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT {{users.email}}', posts))
|
|
51
49
|
.toBe('SELECT "app"."users"."email_address"')
|
|
52
|
-
expect(resolvePlaceholders('ON CONSTRAINT {{#users}}',
|
|
50
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('ON CONSTRAINT {{#users}}', posts))
|
|
53
51
|
.toBe('ON CONSTRAINT "users"')
|
|
54
|
-
expect(resolvePlaceholders('SET search_path = {{$}}',
|
|
52
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('SET search_path = {{$}}', posts))
|
|
55
53
|
.toBe('SET search_path = "app"')
|
|
56
54
|
})
|
|
57
55
|
|
|
58
56
|
test('leaves bound parameters exactly as written', () => {
|
|
59
57
|
const text = `SELECT * FROM {{}} WHERE "id" = $1 AND "email" = $2 AND note = '{{not-a-placeholder'`
|
|
60
|
-
expect(resolvePlaceholders(text,
|
|
58
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders(text, posts))
|
|
61
59
|
.toBe(`SELECT * FROM "app"."posts" WHERE "id" = $1 AND "email" = $2 AND note = '{{not-a-placeholder'`)
|
|
62
60
|
})
|
|
63
61
|
|
|
64
62
|
test('refuses an alias it cannot resolve rather than substituting blindly', () => {
|
|
65
|
-
expect(() => resolvePlaceholders('SELECT * FROM {{nope}}',
|
|
63
|
+
expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{nope}}', posts))
|
|
66
64
|
.toThrow(PostgresPlaceholderError)
|
|
67
|
-
expect(() => resolvePlaceholders('SELECT {{users.nope}}',
|
|
65
|
+
expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SELECT {{users.nope}}', posts))
|
|
68
66
|
.toThrow(PostgresPlaceholderError)
|
|
69
67
|
/** Registered but not yet initialized: the table name genuinely isn't known. */
|
|
70
|
-
expect(() => resolvePlaceholders('SELECT * FROM {{late}}',
|
|
68
|
+
expect(() => pgPlaceholdersOf(contextOf({ late: null })).resolvePlaceholders('SELECT * FROM {{late}}', posts))
|
|
71
69
|
.toThrow(PostgresPlaceholderError)
|
|
72
70
|
})
|
|
73
71
|
|
|
74
72
|
test('refuses a self reference in a query that has no owning resource', () => {
|
|
75
|
-
expect(() => resolvePlaceholders('SELECT * FROM {{}}',
|
|
73
|
+
expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{}}', null))
|
|
76
74
|
.toThrow(PostgresPlaceholderError)
|
|
77
|
-
expect(() => resolvePlaceholders('SET search_path = {{$}}',
|
|
75
|
+
expect(() => pgPlaceholdersOf(context).resolvePlaceholders('SET search_path = {{$}}', null))
|
|
78
76
|
.toThrow(PostgresPlaceholderError)
|
|
79
77
|
/** Other aliases stay addressable — a service level query is scopeless, not blind. */
|
|
80
|
-
expect(resolvePlaceholders('SELECT * FROM {{users}}',
|
|
78
|
+
expect(pgPlaceholdersOf(context).resolvePlaceholders('SELECT * FROM {{users}}', null))
|
|
81
79
|
.toBe('SELECT * FROM "app"."users"')
|
|
82
80
|
})
|
|
83
81
|
|
|
@@ -91,15 +89,15 @@ describe('@owlmeans/postgres-resource — placeholder resolution', () => {
|
|
|
91
89
|
const tenantA = contextOf({ users: specOf('users', 'tenant_a', 'users') })
|
|
92
90
|
const tenantB = contextOf({ users: specOf('users', 'tenant_b', 'users') })
|
|
93
91
|
|
|
94
|
-
expect(resolvePlaceholders('SELECT * FROM {{users}}',
|
|
92
|
+
expect(pgPlaceholdersOf(tenantA).resolvePlaceholders('SELECT * FROM {{users}}', null))
|
|
95
93
|
.toBe('SELECT * FROM "tenant_a"."users"')
|
|
96
|
-
expect(resolvePlaceholders('SELECT * FROM {{users}}',
|
|
94
|
+
expect(pgPlaceholdersOf(tenantB).resolvePlaceholders('SELECT * FROM {{users}}', null))
|
|
97
95
|
.toBe('SELECT * FROM "tenant_b"."users"')
|
|
98
96
|
})
|
|
99
97
|
|
|
100
98
|
test('refOf answers the same question without any SQL around it', () => {
|
|
101
|
-
expect(refOf(
|
|
102
|
-
expect(refOf(
|
|
103
|
-
expect(() => refOf(
|
|
99
|
+
expect(pgPlaceholdersOf(context).refOf(posts)).toBe('"app"."posts"')
|
|
100
|
+
expect(pgPlaceholdersOf(context).refOf(posts, 'users')).toBe('"app"."users"')
|
|
101
|
+
expect(() => pgPlaceholdersOf(context).refOf(null)).toThrow(PostgresPlaceholderError)
|
|
104
102
|
})
|
|
105
103
|
})
|