@owlmeans/postgres-resource 0.1.18-rc.41 → 0.1.18-rc.42
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 +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/postgres-resource/SKILL.md +2 -2
- package/build/declarations.d.ts +1 -8
- package/build/declarations.d.ts.map +1 -1
- package/build/declarations.js.map +1 -1
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js.map +1 -1
- package/build/resource.d.ts +1 -1
- package/build/resource.d.ts.map +1 -1
- 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 +33 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +61 -0
- package/build/utils/consts.local.js.map +1 -0
- package/build/utils/criteria.d.ts +2 -2
- package/build/utils/criteria.d.ts.map +1 -1
- package/build/utils/criteria.js +2 -3
- package/build/utils/criteria.js.map +1 -1
- package/build/utils/diff.d.ts.map +1 -1
- package/build/utils/diff.js +5 -3
- package/build/utils/diff.js.map +1 -1
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/introspect.d.ts.map +1 -1
- package/build/utils/introspect.js +1 -29
- package/build/utils/introspect.js.map +1 -1
- package/build/utils/life-cycle.d.ts +3 -8
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +5 -2
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/marshal.d.ts.map +1 -1
- package/build/utils/marshal.js +11 -5
- package/build/utils/marshal.js.map +1 -1
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +2 -2
- package/build/utils/name.js.map +1 -1
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +6 -4
- package/build/utils/schema.js.map +1 -1
- package/build/utils/sql.d.ts +1 -9
- 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.d.ts.map +1 -1
- package/build/utils/sync.js +6 -16
- 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/declarations.ts +1 -9
- package/src/errors.ts +1 -11
- package/src/resource.ts +3 -11
- package/src/types.local.ts +16 -0
- package/src/types.ts +7 -4
- package/src/utils/consts.local.ts +68 -0
- package/src/utils/criteria.ts +4 -19
- package/src/utils/diff.ts +5 -4
- package/src/utils/index.ts +1 -0
- package/src/utils/introspect.ts +1 -32
- package/src/utils/life-cycle.ts +8 -15
- package/src/utils/marshal.ts +12 -7
- package/src/utils/name.ts +2 -3
- package/src/utils/schema.ts +8 -32
- package/src/utils/sql.ts +0 -0
- package/src/utils/sync.ts +6 -18
- package/src/utils/types.local.ts +35 -0
- package/src/utils/types.ts +19 -0
- package/tests/diff.spec.ts +24 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SQL } from 'drizzle-orm';
|
|
2
|
+
import { PG_KEYWORD } from '../consts.js';
|
|
3
|
+
import type { PgPropertyOverride, PgRootOverride } from '../types.js';
|
|
4
|
+
/**
|
|
5
|
+
* A value at a path, as the jsonb it is (`#>`) and as text (`#>>`). The path is ONE bound `text[]`
|
|
6
|
+
* parameter, so a segment holding a comma or a brace stays one segment.
|
|
7
|
+
*/
|
|
8
|
+
export interface PathTarget {
|
|
9
|
+
value: SQL;
|
|
10
|
+
text: SQL;
|
|
11
|
+
/** Absent, or a JSON `null` — what every other store reads as "no value". */
|
|
12
|
+
missing: SQL;
|
|
13
|
+
}
|
|
14
|
+
export interface RawProperty {
|
|
15
|
+
type?: string | string[];
|
|
16
|
+
format?: string;
|
|
17
|
+
enum?: unknown[];
|
|
18
|
+
items?: RawProperty;
|
|
19
|
+
maxLength?: number;
|
|
20
|
+
maximum?: number;
|
|
21
|
+
minimum?: number;
|
|
22
|
+
default?: unknown;
|
|
23
|
+
nullable?: boolean;
|
|
24
|
+
secure?: boolean;
|
|
25
|
+
[PG_KEYWORD]?: PgPropertyOverride;
|
|
26
|
+
}
|
|
27
|
+
export interface RawSchema {
|
|
28
|
+
properties?: Record<string, RawProperty>;
|
|
29
|
+
required?: string[];
|
|
30
|
+
allOf?: RawSchema[];
|
|
31
|
+
[PG_KEYWORD]?: PgRootOverride;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=types.local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.local.d.ts","sourceRoot":"","sources":["../../src/utils/types.local.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AACzC,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAErE;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,GAAG,CAAA;IACV,IAAI,EAAE,GAAG,CAAA;IACT,6EAA6E;IAC7E,OAAO,EAAE,GAAG,CAAA;CACb;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACxB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,OAAO,EAAE,CAAA;IAChB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,CAAC,UAAU,CAAC,CAAC,EAAE,kBAAkB,CAAA;CAClC;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,KAAK,CAAC,EAAE,SAAS,EAAE,CAAA;IACnB,CAAC,UAAU,CAAC,CAAC,EAAE,cAAc,CAAA;CAC9B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.local.js","sourceRoot":"","sources":["../../src/utils/types.local.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/postgres-resource",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.42",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -26,22 +26,23 @@
|
|
|
26
26
|
"pg": "*"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@noble/hashes": "^
|
|
30
|
-
"@owlmeans/basic-ids": "^0.1.18-rc.
|
|
31
|
-
"@owlmeans/context": "^0.1.18-rc.
|
|
32
|
-
"@owlmeans/
|
|
33
|
-
"@owlmeans/
|
|
34
|
-
"@
|
|
35
|
-
"
|
|
29
|
+
"@noble/hashes": "^2.4.0",
|
|
30
|
+
"@owlmeans/basic-ids": "^0.1.18-rc.36",
|
|
31
|
+
"@owlmeans/context": "^0.1.18-rc.33",
|
|
32
|
+
"@owlmeans/log": "^0.1.18-rc.1",
|
|
33
|
+
"@owlmeans/resource": "^0.1.18-rc.39",
|
|
34
|
+
"@owlmeans/server-context": "^0.1.18-rc.42",
|
|
35
|
+
"@scure/base": "^2.4.0",
|
|
36
|
+
"drizzle-orm": "~0.45.3"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"@owlmeans/dep-config": "workspace:*",
|
|
39
|
-
"@owlmeans/test-integration": "^0.1.18-rc.
|
|
40
|
-
"@types/bun": "^1.4.
|
|
41
|
-
"@types/node": "^26.
|
|
42
|
-
"@types/pg": "^8.
|
|
40
|
+
"@owlmeans/test-integration": "^0.1.18-rc.32",
|
|
41
|
+
"@types/bun": "^1.4.2",
|
|
42
|
+
"@types/node": "^26.6.4",
|
|
43
|
+
"@types/pg": "^8.23.1",
|
|
43
44
|
"nodemon": "^3.1.14",
|
|
44
|
-
"pg": "^8.
|
|
45
|
+
"pg": "^8.23.1",
|
|
45
46
|
"typescript": "^7.0.2"
|
|
46
47
|
},
|
|
47
48
|
"publishConfig": {
|
package/src/declarations.ts
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import { createMigrationRegistry } from '@owlmeans/resource'
|
|
2
|
-
import type { MigrationRegistry } from '@owlmeans/resource'
|
|
3
|
-
import type { AnySchema } from 'ajv'
|
|
4
2
|
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
export interface PostgresDeclaration {
|
|
8
|
-
schema?: AnySchema
|
|
9
|
-
indexes: PgIndexSpec[]
|
|
10
|
-
migrations: MigrationRegistry<PostgresTx>
|
|
11
|
-
}
|
|
3
|
+
import type { PostgresTx, PostgresDeclaration } from './types.js'
|
|
12
4
|
|
|
13
5
|
/**
|
|
14
6
|
* Per-alias declaration store, held at module scope rather than on the resource object.
|
package/src/errors.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from '@owlmeans/resource'
|
|
5
5
|
|
|
6
6
|
import { PgErrorCode } from './consts.js'
|
|
7
|
+
import type { PgDriverError } from './types.local.js'
|
|
7
8
|
|
|
8
9
|
export class PostgresError extends ResourceError {
|
|
9
10
|
public static override typeName = `${ResourceError.typeName}PostgresError`
|
|
@@ -106,17 +107,6 @@ export class PostgresBootstrapError extends PostgresError {
|
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
interface PgDriverError {
|
|
110
|
-
code?: string
|
|
111
|
-
detail?: string
|
|
112
|
-
hint?: string
|
|
113
|
-
severity?: string
|
|
114
|
-
constraint?: string
|
|
115
|
-
table?: string
|
|
116
|
-
column?: string
|
|
117
|
-
message?: string
|
|
118
|
-
}
|
|
119
|
-
|
|
120
110
|
const isDriverError = (error: unknown): error is PgDriverError =>
|
|
121
111
|
error != null && typeof error === 'object' && 'code' in error
|
|
122
112
|
|
package/src/resource.ts
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { appendContextual, assertContext } from '@owlmeans/context'
|
|
2
|
-
import {
|
|
3
|
-
MisshapedRecord, RecordExists, RecordUpdateFailed, UnknownRecordError, UnsupportedArgumentError
|
|
4
|
-
} from '@owlmeans/resource'
|
|
5
|
-
import type {
|
|
6
|
-
Criteria, FirstOptions, ListOptions, ListResult, MigrationStage, ResourceRecord, WriteOptions
|
|
7
|
-
} from '@owlmeans/resource'
|
|
8
|
-
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
|
+
import { MisshapedRecord, RecordExists, RecordUpdateFailed, UnknownRecordError, UnsupportedArgumentError, type Criteria, type FirstOptions, type ListOptions, type ListResult, type MigrationStage, type ResourceRecord, type WriteOptions } from '@owlmeans/resource'
|
|
9
3
|
import type { AnySchema, JSONSchemaType } from 'ajv'
|
|
10
|
-
import { eq, sql } from 'drizzle-orm'
|
|
11
|
-
import type { SQL } from 'drizzle-orm'
|
|
4
|
+
import { eq, sql, type SQL } from 'drizzle-orm'
|
|
12
5
|
import type { PoolClient, QueryResultRow } from 'pg'
|
|
13
6
|
|
|
14
7
|
import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE, ID_FIELD } from './consts.js'
|
|
@@ -24,9 +17,8 @@ import { initializeTable } from './utils/life-cycle.js'
|
|
|
24
17
|
import { recordToFullValues, recordToValues, resultToRecord, rowToRecord } from './utils/marshal.js'
|
|
25
18
|
import { makeTx } from './utils/migrations.js'
|
|
26
19
|
import { refOf, resolvePlaceholders } from './utils/sql.js'
|
|
20
|
+
import type { Config, Context } from './types.local.js'
|
|
27
21
|
|
|
28
|
-
type Config = ServerConfig
|
|
29
|
-
type Context<C extends Config = Config> = ServerContext<C>
|
|
30
22
|
|
|
31
23
|
/** Postgres has no row expiry — silently ignoring a TTL would lose data. */
|
|
32
24
|
const refuseTtl = (opts?: WriteOptions): void => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
|
|
2
|
+
|
|
3
|
+
export interface PgDriverError {
|
|
4
|
+
code?: string
|
|
5
|
+
detail?: string
|
|
6
|
+
hint?: string
|
|
7
|
+
severity?: string
|
|
8
|
+
constraint?: string
|
|
9
|
+
table?: string
|
|
10
|
+
column?: string
|
|
11
|
+
message?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Config = ServerConfig
|
|
15
|
+
|
|
16
|
+
export type Context<C extends Config = Config> = ServerContext<C>
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Criteria, DbLocker, LockableResource, MigratableResource, Resource,
|
|
3
|
-
ResourceDbService, ResourceRecord, WriteOptions
|
|
4
|
-
} from '@owlmeans/resource'
|
|
1
|
+
import type { Criteria, DbLocker, LockableResource, MigratableResource, Resource, ResourceDbService, ResourceRecord, WriteOptions, MigrationRegistry } from '@owlmeans/resource'
|
|
5
2
|
import type { AnySchema } from 'ajv'
|
|
6
3
|
import type { Pool, PoolClient, QueryResultRow } from 'pg'
|
|
7
4
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
|
|
@@ -321,3 +318,9 @@ export interface DdlPlan {
|
|
|
321
318
|
fresh: boolean
|
|
322
319
|
statements: DdlStatement[]
|
|
323
320
|
}
|
|
321
|
+
|
|
322
|
+
export interface PostgresDeclaration {
|
|
323
|
+
schema?: AnySchema
|
|
324
|
+
indexes: PgIndexSpec[]
|
|
325
|
+
migrations: MigrationRegistry<PostgresTx>
|
|
326
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PgErrorCode } from '../consts.js'
|
|
2
|
+
|
|
3
|
+
export const COMPARISON: Record<string, string> = {
|
|
4
|
+
$eq: '=', $ne: '<>', $gt: '>', $gte: '>=', $lt: '<', $lte: '<='
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** A type cast as `pg_get_expr` / `pg_get_indexdef` print one: `::text`, `::character varying(64)[]`. */
|
|
8
|
+
export const CAST = /::(?:character varying|double precision|bit varying|timestamp with(?:out)? time zone|time with(?:out)? time zone|[a-z_][a-z0-9_]*)(?:\(\d+(?:\s*,\s*\d+)?\))?(?:\[\])?/g
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `pg_attribute` + `format_type` rather than `information_schema.columns`, because it
|
|
12
|
+
* returns one canonical type string with the typmod already baked in
|
|
13
|
+
* (`character varying(320)`). `information_schema` splits the same information across
|
|
14
|
+
* four columns and reports every array as `ARRAY`, which no comparison can use.
|
|
15
|
+
*/
|
|
16
|
+
export const COLUMNS = `
|
|
17
|
+
SELECT a.attname AS name,
|
|
18
|
+
format_type(a.atttypid, a.atttypmod) AS type,
|
|
19
|
+
a.attnotnull AS not_null,
|
|
20
|
+
pg_get_expr(d.adbin, d.adrelid) AS default_expr,
|
|
21
|
+
a.attidentity AS identity,
|
|
22
|
+
a.attgenerated AS generated,
|
|
23
|
+
a.attnum AS ordinal
|
|
24
|
+
FROM pg_attribute a
|
|
25
|
+
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
26
|
+
WHERE a.attrelid = to_regclass($1) AND a.attnum > 0 AND NOT a.attisdropped
|
|
27
|
+
ORDER BY a.attnum
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
export const INDEXES = `
|
|
31
|
+
SELECT indexname AS name, indexdef AS definition
|
|
32
|
+
FROM pg_indexes
|
|
33
|
+
WHERE schemaname = $1 AND tablename = $2
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
export const CONSTRAINTS = `
|
|
37
|
+
SELECT c.conname AS name, c.contype AS type, pg_get_constraintdef(c.oid) AS definition
|
|
38
|
+
FROM pg_constraint c
|
|
39
|
+
WHERE c.conrelid = to_regclass($1)
|
|
40
|
+
AND c.contype <> 'n'
|
|
41
|
+
`
|
|
42
|
+
|
|
43
|
+
export const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* `{{ }}` / `{{self}}` — the owning resource's qualified table
|
|
47
|
+
* `{{alias}}` — another registered postgres resource's qualified table
|
|
48
|
+
* `{{alias.property}}` — a qualified column of that resource
|
|
49
|
+
* `{{#alias}}` — the bare quoted table name, for `ON CONFLICT ON CONSTRAINT`
|
|
50
|
+
* `{{$}}` — the owning resource's quoted schema
|
|
51
|
+
*
|
|
52
|
+
* Resource aliases in this codebase use `-` and `:` but never `.`, so `.` is unambiguous
|
|
53
|
+
* as the column separator.
|
|
54
|
+
*/
|
|
55
|
+
export const PLACEHOLDER = /\{\{\s*([#$]?)([A-Za-z0-9_:-]*)(?:\.([A-Za-z0-9_]+))?\s*\}\}/g
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Codes that mean "the cast is legal, the data isn't": a `text` column holding `'a'` retyped to
|
|
59
|
+
* `integer`, a value too wide for the new length, a number past the new range. Postgres refuses
|
|
60
|
+
* each of these rather than truncating, and the remedy is the same one a refused cast needs — so
|
|
61
|
+
* they are only read as a cast problem here, inside DDL. On the CRUD path the identical code means
|
|
62
|
+
* a caller passed a bad value, which is a different bug with a different fix.
|
|
63
|
+
*/
|
|
64
|
+
export const CAST_DATA_CODES: string[] = [
|
|
65
|
+
PgErrorCode.InvalidTextRepresentation,
|
|
66
|
+
PgErrorCode.StringDataRightTruncation,
|
|
67
|
+
PgErrorCode.NumericValueOutOfRange
|
|
68
|
+
]
|
package/src/utils/criteria.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { UnsupportedArgumentError } from '@owlmeans/resource'
|
|
2
|
-
import
|
|
3
|
-
import { and, asc, desc, or, param, sql } from 'drizzle-orm'
|
|
4
|
-
import type { SQL } from 'drizzle-orm'
|
|
1
|
+
import { UnsupportedArgumentError, type Criteria, type FieldOperators, type Sort } from '@owlmeans/resource'
|
|
2
|
+
import { and, asc, desc, or, param, sql, type SQL } from 'drizzle-orm'
|
|
5
3
|
|
|
6
4
|
import type { ColumnSpec, PgRuntimeTable, TableSpec } from '../types.js'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
$eq: '=', $ne: '<>', $gt: '>', $gte: '>=', $lt: '<', $lte: '<='
|
|
10
|
-
}
|
|
5
|
+
import { COMPARISON } from './consts.local.js'
|
|
6
|
+
import type { PathTarget } from './types.local.js'
|
|
11
7
|
|
|
12
8
|
const columnRef = (table: PgRuntimeTable, column: ColumnSpec): SQL => sql`${table[column.property]}`
|
|
13
9
|
|
|
@@ -166,17 +162,6 @@ const escapeLike = (value: string): string => value.replace(/[\\%_]/g, match =>
|
|
|
166
162
|
|
|
167
163
|
// ─── Dotted paths into a jsonb column ────────────────────────────────────────────────────────────
|
|
168
164
|
|
|
169
|
-
/**
|
|
170
|
-
* A value at a path, as the jsonb it is (`#>`) and as text (`#>>`). The path is ONE bound `text[]`
|
|
171
|
-
* parameter, so a segment holding a comma or a brace stays one segment.
|
|
172
|
-
*/
|
|
173
|
-
interface PathTarget {
|
|
174
|
-
value: SQL
|
|
175
|
-
text: SQL
|
|
176
|
-
/** Absent, or a JSON `null` — what every other store reads as "no value". */
|
|
177
|
-
missing: SQL
|
|
178
|
-
}
|
|
179
|
-
|
|
180
165
|
const pathTarget = (table: PgRuntimeTable, column: ColumnSpec, path: string[]): PathTarget => {
|
|
181
166
|
const value = sql`(${columnRef(table, column)} #> ${param(path)}::text[])`
|
|
182
167
|
|
package/src/utils/diff.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ColumnSpec, DdlPlan, DdlStatement, LiveTable, PgIndexSpec, PostgresResource, TableSpec
|
|
8
8
|
} from '../types.js'
|
|
9
9
|
import { qualify, quoteIdent } from './name.js'
|
|
10
|
+
import { CAST } from './consts.local.js'
|
|
10
11
|
|
|
11
12
|
const literal = (value: string | number | boolean | null): string => {
|
|
12
13
|
if (value === null) return 'NULL'
|
|
@@ -23,9 +24,6 @@ const defaultExpression = (column: ColumnSpec): string | undefined => {
|
|
|
23
24
|
return undefined
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/** A type cast as `pg_get_expr` / `pg_get_indexdef` print one: `::text`, `::character varying(64)[]`. */
|
|
27
|
-
const CAST = /::(?:character varying|double precision|bit varying|timestamp with(?:out)? time zone|time with(?:out)? time zone|[a-z_][a-z0-9_]*)(?:\(\d+(?:\s*,\s*\d+)?\))?(?:\[\])?/g
|
|
28
|
-
|
|
29
27
|
/**
|
|
30
28
|
* Postgres echoes defaults back with explicit casts — a declared `'active'` comes back as
|
|
31
29
|
* `'active'::text`. Compare without them, so an unchanged default doesn't look like drift on
|
|
@@ -303,7 +301,10 @@ export const planSync = (
|
|
|
303
301
|
}
|
|
304
302
|
const liveConstraints: Record<string, string> = {}
|
|
305
303
|
for (const constraint of live.constraints) {
|
|
306
|
-
|
|
304
|
+
// Primary and foreign keys have their own paths. A NOT NULL (`n`) is a constraint row since
|
|
305
|
+
// Postgres 18 (`<table>_<column>_not_null`) and is owned by the column step above: dropping it by
|
|
306
|
+
// name is refused on a primary key column, and on any other it would undo a `required` property.
|
|
307
|
+
if (constraint.type === 'p' || constraint.type === 'f' || constraint.type === 'n') {
|
|
307
308
|
continue
|
|
308
309
|
}
|
|
309
310
|
liveConstraints[constraint.name] = constraint.definition
|
package/src/utils/index.ts
CHANGED
package/src/utils/introspect.ts
CHANGED
|
@@ -1,38 +1,7 @@
|
|
|
1
1
|
import type { PoolClient } from 'pg'
|
|
2
2
|
|
|
3
3
|
import type { LiveColumn, LiveConstraint, LiveIndex, LiveTable } from '../types.js'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* `pg_attribute` + `format_type` rather than `information_schema.columns`, because it
|
|
7
|
-
* returns one canonical type string with the typmod already baked in
|
|
8
|
-
* (`character varying(320)`). `information_schema` splits the same information across
|
|
9
|
-
* four columns and reports every array as `ARRAY`, which no comparison can use.
|
|
10
|
-
*/
|
|
11
|
-
const COLUMNS = `
|
|
12
|
-
SELECT a.attname AS name,
|
|
13
|
-
format_type(a.atttypid, a.atttypmod) AS type,
|
|
14
|
-
a.attnotnull AS not_null,
|
|
15
|
-
pg_get_expr(d.adbin, d.adrelid) AS default_expr,
|
|
16
|
-
a.attidentity AS identity,
|
|
17
|
-
a.attgenerated AS generated,
|
|
18
|
-
a.attnum AS ordinal
|
|
19
|
-
FROM pg_attribute a
|
|
20
|
-
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
|
|
21
|
-
WHERE a.attrelid = to_regclass($1) AND a.attnum > 0 AND NOT a.attisdropped
|
|
22
|
-
ORDER BY a.attnum
|
|
23
|
-
`
|
|
24
|
-
|
|
25
|
-
const INDEXES = `
|
|
26
|
-
SELECT indexname AS name, indexdef AS definition
|
|
27
|
-
FROM pg_indexes
|
|
28
|
-
WHERE schemaname = $1 AND tablename = $2
|
|
29
|
-
`
|
|
30
|
-
|
|
31
|
-
const CONSTRAINTS = `
|
|
32
|
-
SELECT c.conname AS name, c.contype AS type, pg_get_constraintdef(c.oid) AS definition
|
|
33
|
-
FROM pg_constraint c
|
|
34
|
-
WHERE c.conrelid = to_regclass($1)
|
|
35
|
-
`
|
|
4
|
+
import { COLUMNS, CONSTRAINTS, INDEXES } from './consts.local.js'
|
|
36
5
|
|
|
37
6
|
/** Read the table exactly as Postgres currently holds it. */
|
|
38
7
|
export const introspectTable = async (
|
package/src/utils/life-cycle.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { BasicContext } from '@owlmeans/context'
|
|
2
|
-
import { MigrationStage, runMigrations } from '@owlmeans/resource'
|
|
3
|
-
import
|
|
2
|
+
import { MigrationStage, runMigrations, type DbConfig, type MigrationReport, type ResourceRecord } from '@owlmeans/resource'
|
|
3
|
+
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
7
|
import { getDeclaration } from '../declarations.js'
|
|
8
8
|
import { describePgError, pgErrorToResourceError } from '../errors.js'
|
|
9
|
-
import type {
|
|
10
|
-
PgRuntimeTable, PostgresDb, PostgresMeta, PostgresResource, TableSpec
|
|
11
|
-
} from '../types.js'
|
|
9
|
+
import type { PostgresDb, PostgresMeta, PostgresResource, TableSpec } from '../types.js'
|
|
12
10
|
import { planForeignKeys, planSync } from './diff.js'
|
|
13
11
|
import { introspectTable } from './introspect.js'
|
|
14
12
|
import { makeMigrationStore } from './migrations.js'
|
|
@@ -17,13 +15,9 @@ import { schemaToTableSpec } from './schema.js'
|
|
|
17
15
|
import { refOf, resolvePlaceholders } from './sql.js'
|
|
18
16
|
import { acquireLock, applyPlan, ensureSchema, releaseLock } from './sync.js'
|
|
19
17
|
import { specToTable } from './table.js'
|
|
18
|
+
import type { TableInit } from './types.js'
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
spec: TableSpec
|
|
23
|
-
/** The Drizzle table CRUD is built against. */
|
|
24
|
-
entity: PgRuntimeTable
|
|
25
|
-
reports: MigrationReport[]
|
|
26
|
-
}
|
|
20
|
+
const log = logger('postgres-resource')
|
|
27
21
|
|
|
28
22
|
/**
|
|
29
23
|
* Bring a resource's table to the shape its schema declares, then hand back everything the
|
|
@@ -90,10 +84,9 @@ export const initializeTable = async (
|
|
|
90
84
|
|
|
91
85
|
for (const report of reports) {
|
|
92
86
|
if (report.applied.length > 0) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
87
|
+
log.info('Migrations applied', {
|
|
88
|
+
resource: resource.alias, table: spec.qualified, stage: report.stage, applied: report.applied,
|
|
89
|
+
}, { event: 'migration.applied' })
|
|
97
90
|
}
|
|
98
91
|
}
|
|
99
92
|
} finally {
|
package/src/utils/marshal.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ResourceRecord } from '@owlmeans/resource'
|
|
2
|
+
import { logThrottle, logger } from '@owlmeans/log'
|
|
2
3
|
import { types as pgTypes } from 'pg'
|
|
3
4
|
|
|
4
5
|
import { ID_FIELD, PgTypeOid } from '../consts.js'
|
|
5
6
|
import type { ColumnSpec, TableSpec } from '../types.js'
|
|
6
7
|
|
|
8
|
+
const log = logger('postgres-resource')
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Parse a temporal value the way the driver would have.
|
|
9
12
|
*
|
|
@@ -23,7 +26,7 @@ const toDate = (value: string, column: ColumnSpec): unknown => {
|
|
|
23
26
|
return (pgTypes.getTypeParser(oid as never) as (raw: string) => unknown)(value)
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
const fromDriver = (value: unknown, column: ColumnSpec): unknown => {
|
|
29
|
+
const fromDriver = (value: unknown, column: ColumnSpec, table?: string): unknown => {
|
|
27
30
|
if (value == null) {
|
|
28
31
|
return value
|
|
29
32
|
}
|
|
@@ -44,10 +47,12 @@ const fromDriver = (value: unknown, column: ColumnSpec): unknown => {
|
|
|
44
47
|
}
|
|
45
48
|
const parsed = Number(value)
|
|
46
49
|
if (!Number.isSafeInteger(parsed)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
// Fires per row read: one line per window per column is enough.
|
|
51
|
+
if (logThrottle(`postgres-resource:${table ?? ''}:${column.column}:unsafe-bigint`, 300_000)) {
|
|
52
|
+
log.warn('Bigint column value exceeds the safe integer range; the value read back is imprecise', {
|
|
53
|
+
table, column: column.column,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
return parsed
|
|
@@ -100,7 +105,7 @@ export const rowToRecord = <T extends ResourceRecord>(row: Record<string, unknow
|
|
|
100
105
|
record[key] = value
|
|
101
106
|
continue
|
|
102
107
|
}
|
|
103
|
-
record[column.property] = fromDriver(value, column)
|
|
108
|
+
record[column.property] = fromDriver(value, column, spec.qualified)
|
|
104
109
|
}
|
|
105
110
|
if (record[ID_FIELD] != null) {
|
|
106
111
|
record[ID_FIELD] = `${record[ID_FIELD]}`
|
|
@@ -123,7 +128,7 @@ export const resultToRecord = <T extends ResourceRecord>(
|
|
|
123
128
|
const record: Record<string, unknown> = {}
|
|
124
129
|
for (const [key, value] of Object.entries(row)) {
|
|
125
130
|
const column = spec.byProperty[key]
|
|
126
|
-
record[key] = column == null ? value : fromDriver(value, column)
|
|
131
|
+
record[key] = column == null ? value : fromDriver(value, column, spec.qualified)
|
|
127
132
|
}
|
|
128
133
|
if (record[ID_FIELD] != null) {
|
|
129
134
|
record[ID_FIELD] = `${record[ID_FIELD]}`
|
package/src/utils/name.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { sha256 } from '@noble/hashes/sha2'
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha2.js'
|
|
2
2
|
import { hex } from '@scure/base'
|
|
3
3
|
import type { DbConfig, ResourceRecord } from '@owlmeans/resource'
|
|
4
4
|
|
|
5
5
|
import { PG_MAX_IDENTIFIER } from '../consts.js'
|
|
6
6
|
import type { PostgresResource } from '../types.js'
|
|
7
|
-
|
|
8
|
-
const SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_$]*$/
|
|
7
|
+
import { SAFE_IDENTIFIER } from './consts.local.js'
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Coerce an arbitrary name into a legal Postgres identifier of at most 63 bytes.
|
package/src/utils/schema.ts
CHANGED
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
import { UnsupportedArgumentError } from '@owlmeans/resource'
|
|
2
2
|
import type { AnySchema } from 'ajv'
|
|
3
|
+
import { logger } from '@owlmeans/log'
|
|
3
4
|
|
|
4
5
|
import {
|
|
5
6
|
DEF_ID_DEFAULT, DEF_JSON_TYPE, DEF_SQL_TYPE, ID_FIELD, PG_KEYWORD, PgIndexMethod
|
|
6
7
|
} from '../consts.js'
|
|
7
|
-
import type {
|
|
8
|
-
ColumnJsonType, ColumnSpec, PgCheckSpec, PgIndexSpec, PgPropertyOverride, PgRootOverride,
|
|
9
|
-
PgReferenceSpec, PgUniqueSpec, TableSpec
|
|
10
|
-
} from '../types.js'
|
|
8
|
+
import type { ColumnJsonType, ColumnSpec, PgCheckSpec, PgIndexSpec, PgPropertyOverride, PgReferenceSpec, PgUniqueSpec, TableSpec } from '../types.js'
|
|
11
9
|
import { pgIdentifier, qualify, quoteIdent } from './name.js'
|
|
10
|
+
import type { RawProperty, RawSchema } from './types.local.js'
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
type?: string | string[]
|
|
15
|
-
format?: string
|
|
16
|
-
enum?: unknown[]
|
|
17
|
-
items?: RawProperty
|
|
18
|
-
maxLength?: number
|
|
19
|
-
maximum?: number
|
|
20
|
-
minimum?: number
|
|
21
|
-
default?: unknown
|
|
22
|
-
nullable?: boolean
|
|
23
|
-
secure?: boolean
|
|
24
|
-
[PG_KEYWORD]?: PgPropertyOverride
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface RawSchema {
|
|
28
|
-
properties?: Record<string, RawProperty>
|
|
29
|
-
required?: string[]
|
|
30
|
-
allOf?: RawSchema[]
|
|
31
|
-
[PG_KEYWORD]?: PgRootOverride
|
|
32
|
-
}
|
|
12
|
+
const log = logger('postgres-resource')
|
|
33
13
|
|
|
34
14
|
const INT32_MAX = 2147483647
|
|
35
15
|
|
|
@@ -244,10 +224,7 @@ const byName = <T extends { name?: string }>(specs: T[], alias: string, kind: st
|
|
|
244
224
|
for (const spec of specs) {
|
|
245
225
|
const name = spec.name!
|
|
246
226
|
if (seen.has(name)) {
|
|
247
|
-
|
|
248
|
-
`@owlmeans/postgres-resource: ${alias} declares ${kind} "${name}" more than once —`
|
|
249
|
-
+ ' keeping the first declaration.'
|
|
250
|
-
)
|
|
227
|
+
log.warn('Duplicate declaration, keeping the first one', { resource: alias, kind, name })
|
|
251
228
|
continue
|
|
252
229
|
}
|
|
253
230
|
seen.set(name, spec)
|
|
@@ -305,10 +282,9 @@ export const schemaToTableSpec = (
|
|
|
305
282
|
|
|
306
283
|
if (override.index != null && override.index !== false) {
|
|
307
284
|
if (column.secure) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
285
|
+
log.warn('Index on a secure column will never match: encryption is non-deterministic', {
|
|
286
|
+
resource: alias, column: name,
|
|
287
|
+
})
|
|
312
288
|
}
|
|
313
289
|
const specs = override.index === true
|
|
314
290
|
? [{ columns: [name] }]
|
package/src/utils/sql.ts
CHANGED
|
Binary file
|
package/src/utils/sync.ts
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
|
+
import { logger } from '@owlmeans/log'
|
|
1
2
|
import type { PoolClient } from 'pg'
|
|
2
3
|
|
|
3
|
-
import { PgErrorCode } from '../consts.js'
|
|
4
4
|
import { PostgresCastRequired, PostgresSyncError, describePgError, pgErrorToResourceError } from '../errors.js'
|
|
5
5
|
import type { DdlPlan, TableSpec } from '../types.js'
|
|
6
6
|
import { countNonNull } from './introspect.js'
|
|
7
7
|
import { advisoryKey, quoteIdent } from './name.js'
|
|
8
|
+
import { CAST_DATA_CODES } from './consts.local.js'
|
|
8
9
|
|
|
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
|
-
]
|
|
10
|
+
const log = logger('postgres-resource')
|
|
21
11
|
|
|
22
12
|
/**
|
|
23
13
|
* Serialize initialization across replicas. A session level lock, not `xact`, because
|
|
@@ -75,11 +65,9 @@ export const applyPlan = async (
|
|
|
75
65
|
for (const statement of plan.statements) {
|
|
76
66
|
current = statement.sql
|
|
77
67
|
if (statement.destructive === true && (statement.affected ?? 0) > 0) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
+ ' or set `pg: { managed: false }` to keep it.'
|
|
82
|
-
)
|
|
68
|
+
log.warn('Dropping a column that holds values; declare it under pg.unmanaged or set pg.managed false to keep it', {
|
|
69
|
+
table: spec.qualified, column: statement.target, rows: statement.affected,
|
|
70
|
+
}, { event: 'migration.drop' })
|
|
83
71
|
}
|
|
84
72
|
await client.query(statement.sql)
|
|
85
73
|
}
|
|
@@ -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
|
+
}
|