@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.
Files changed (82) hide show
  1. package/README.md +2 -2
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/postgres-resource/SKILL.md +2 -2
  4. package/build/declarations.d.ts +1 -8
  5. package/build/declarations.d.ts.map +1 -1
  6. package/build/declarations.js.map +1 -1
  7. package/build/errors.d.ts.map +1 -1
  8. package/build/errors.js.map +1 -1
  9. package/build/resource.d.ts +1 -1
  10. package/build/resource.d.ts.map +1 -1
  11. package/build/resource.js.map +1 -1
  12. package/build/types.d.ts +6 -1
  13. package/build/types.d.ts.map +1 -1
  14. package/build/types.local.d.ts +14 -0
  15. package/build/types.local.d.ts.map +1 -0
  16. package/build/types.local.js +2 -0
  17. package/build/types.local.js.map +1 -0
  18. package/build/utils/consts.local.d.ts +33 -0
  19. package/build/utils/consts.local.d.ts.map +1 -0
  20. package/build/utils/consts.local.js +61 -0
  21. package/build/utils/consts.local.js.map +1 -0
  22. package/build/utils/criteria.d.ts +2 -2
  23. package/build/utils/criteria.d.ts.map +1 -1
  24. package/build/utils/criteria.js +2 -3
  25. package/build/utils/criteria.js.map +1 -1
  26. package/build/utils/diff.d.ts.map +1 -1
  27. package/build/utils/diff.js +5 -3
  28. package/build/utils/diff.js.map +1 -1
  29. package/build/utils/index.d.ts +1 -0
  30. package/build/utils/index.d.ts.map +1 -1
  31. package/build/utils/index.js.map +1 -1
  32. package/build/utils/introspect.d.ts.map +1 -1
  33. package/build/utils/introspect.js +1 -29
  34. package/build/utils/introspect.js.map +1 -1
  35. package/build/utils/life-cycle.d.ts +3 -8
  36. package/build/utils/life-cycle.d.ts.map +1 -1
  37. package/build/utils/life-cycle.js +5 -2
  38. package/build/utils/life-cycle.js.map +1 -1
  39. package/build/utils/marshal.d.ts.map +1 -1
  40. package/build/utils/marshal.js +11 -5
  41. package/build/utils/marshal.js.map +1 -1
  42. package/build/utils/name.d.ts.map +1 -1
  43. package/build/utils/name.js +2 -2
  44. package/build/utils/name.js.map +1 -1
  45. package/build/utils/schema.d.ts.map +1 -1
  46. package/build/utils/schema.js +6 -4
  47. package/build/utils/schema.js.map +1 -1
  48. package/build/utils/sql.d.ts +1 -9
  49. package/build/utils/sql.d.ts.map +1 -1
  50. package/build/utils/sql.js +0 -0
  51. package/build/utils/sql.js.map +1 -1
  52. package/build/utils/sync.d.ts.map +1 -1
  53. package/build/utils/sync.js +6 -16
  54. package/build/utils/sync.js.map +1 -1
  55. package/build/utils/types.d.ts +18 -0
  56. package/build/utils/types.d.ts.map +1 -0
  57. package/build/utils/types.js +2 -0
  58. package/build/utils/types.js.map +1 -0
  59. package/build/utils/types.local.d.ts +33 -0
  60. package/build/utils/types.local.d.ts.map +1 -0
  61. package/build/utils/types.local.js +2 -0
  62. package/build/utils/types.local.js.map +1 -0
  63. package/package.json +14 -13
  64. package/src/declarations.ts +1 -9
  65. package/src/errors.ts +1 -11
  66. package/src/resource.ts +3 -11
  67. package/src/types.local.ts +16 -0
  68. package/src/types.ts +7 -4
  69. package/src/utils/consts.local.ts +68 -0
  70. package/src/utils/criteria.ts +4 -19
  71. package/src/utils/diff.ts +5 -4
  72. package/src/utils/index.ts +1 -0
  73. package/src/utils/introspect.ts +1 -32
  74. package/src/utils/life-cycle.ts +8 -15
  75. package/src/utils/marshal.ts +12 -7
  76. package/src/utils/name.ts +2 -3
  77. package/src/utils/schema.ts +8 -32
  78. package/src/utils/sql.ts +0 -0
  79. package/src/utils/sync.ts +6 -18
  80. package/src/utils/types.local.ts +35 -0
  81. package/src/utils/types.ts +19 -0
  82. 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,2 @@
1
+ import { PG_KEYWORD } from '../consts.js';
2
+ //# sourceMappingURL=types.local.js.map
@@ -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.41",
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": "^1.5.0",
30
- "@owlmeans/basic-ids": "^0.1.18-rc.34",
31
- "@owlmeans/context": "^0.1.18-rc.32",
32
- "@owlmeans/resource": "^0.1.18-rc.37",
33
- "@owlmeans/server-context": "^0.1.18-rc.40",
34
- "@scure/base": "^2.3.0",
35
- "drizzle-orm": "~0.45.2"
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.30",
40
- "@types/bun": "^1.4.0",
41
- "@types/node": "^26.1.0",
42
- "@types/pg": "^8.20.4",
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.22.0",
45
+ "pg": "^8.23.1",
45
46
  "typescript": "^7.0.2"
46
47
  },
47
48
  "publishConfig": {
@@ -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 { PgIndexSpec, PostgresTx } from './types.js'
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
+ ]
@@ -1,13 +1,9 @@
1
- import { UnsupportedArgumentError } from '@owlmeans/resource'
2
- import type { Criteria, FieldOperators, Sort } from '@owlmeans/resource'
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
- const COMPARISON: Record<string, string> = {
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
- if (constraint.type === 'p' || constraint.type === 'f') {
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
@@ -2,6 +2,7 @@ export * from './criteria.js'
2
2
  export * from './diff.js'
3
3
  export * from './introspect.js'
4
4
  export * from './life-cycle.js'
5
+ export type * from './types.js'
5
6
  export * from './marshal.js'
6
7
  export * from './migrations.js'
7
8
  export * from './name.js'
@@ -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 (
@@ -1,14 +1,12 @@
1
1
  import type { BasicContext } from '@owlmeans/context'
2
- import { MigrationStage, runMigrations } from '@owlmeans/resource'
3
- import type { DbConfig, MigrationReport, ResourceRecord } from '@owlmeans/resource'
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
- export interface TableInit {
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
- console.log(
94
- `@owlmeans/postgres-resource: ${spec.qualified} applied ${report.stage} migrations —`
95
- + ` ${report.applied.join(', ')}`
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 {
@@ -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
- console.warn(
48
- `@owlmeans/postgres-resource: "${column.column}" holds ${value}, which exceeds the safe`
49
- + ' integer range — the value read back is imprecise.'
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.
@@ -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
- interface RawProperty {
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
- console.warn(
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
- console.warn(
309
- `@owlmeans/postgres-resource: index on secure column "${alias}.${name}" — encryption`
310
- + ' is non-deterministic, so equality lookups through it will never match.'
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
- 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
- )
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
+ }