@simtlix/simfinity-postgres 3.5.5 → 3.5.6
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 +10 -6
- package/package.json +3 -3
- package/src/codecs.js +33 -12
- package/src/index.js +13 -2
- package/src/plugin.js +14 -3
- package/src/query/compiler.js +4 -1
- package/src/schema/initialize.js +33 -4
- package/types/index.d.ts +34 -4
- package/src/adapter.js +0 -4
- package/src/records.js +0 -4
- package/src/transactions.js +0 -4
package/README.md
CHANGED
|
@@ -31,11 +31,11 @@ import { postgresPlugin } from '@simtlix/simfinity-postgres';
|
|
|
31
31
|
const simfinity = createSQL({ plugin: postgresPlugin({ pool, schema: 'library' }) });
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Install `@simtlix/simfinity-sql@3.5.
|
|
34
|
+
Install `@simtlix/simfinity-sql@3.5.6` as a direct dependency when importing its factory. PostgreSQL 15 or later is the only supported SQL plugin. The [SQL package guide](https://github.com/simtlix/simfinity.js/tree/master/packages/sql) documents relational planning and the versioned plugin contract.
|
|
35
35
|
|
|
36
|
-
Both factories permanently bind one pool and schema. The returned runtime supports registration, generated queries and mutations, middleware and scopes, controllers and validators, custom mutations, state machines, nested relationship writes, embedded reconstruction, and native PostgreSQL model handles. Native handles expose `findById`, `find`, `create`, `update`, and `delete`; they are not Mongoose models. `find(args, { session })` accepts the generated GraphQL list query arguments. Existing `pg.Pool` configuration and `pg.PoolClient` query interfaces remain compatible across both factories.
|
|
36
|
+
Both factories permanently bind one pool and schema. The returned runtime supports registration, generated queries and mutations, middleware and scopes, controllers and validators, custom mutations, state machines, nested relationship writes, embedded reconstruction, and native PostgreSQL model handles. Native handles expose `findById`, `find`, `create`, `update`, and `delete`; they are not Mongoose models. `find(args, { session })` accepts the generated GraphQL list query arguments. Existing `pg.Pool` configuration and `pg.PoolClient` query interfaces remain compatible across both factories. A custom pool or client wrapper should pass `pg`'s `command` through in query results: the plugin reads it from the `COMMIT` result to detect a transaction that PostgreSQL rolled back because one of its statements failed, and without it such a transaction reports success with nothing stored.
|
|
37
37
|
|
|
38
|
-
Operations attempted before successful initialization fail with `DATABASE_NOT_INITIALIZED`. Call `createSchema` before `initializeDatabase`. Configuration and registrations become immutable once schema preparation begins. `preventCreatingCollection(true)` makes instance initialization validate existing storage without issuing creation DDL.
|
|
38
|
+
Operations attempted before successful initialization fail with `DATABASE_NOT_INITIALIZED`. Call `createSchema` before `initializeDatabase`. Configuration and registrations become immutable once schema preparation begins. `preventCreatingCollection(true)` makes instance initialization validate existing storage without issuing creation DDL. A rejected initialization request leaves readiness as it was; a ready instance keeps serving while it is initialized again, and becomes unavailable if that initialization fails. Use `mode: 'validate'` to check a live instance, because `create` mode locks the managed tables against writes until it commits. `simfinity.compileDatabaseSchema()` returns the instance's DDL for review or export.
|
|
39
39
|
|
|
40
40
|
A default module instance is also available when a singleton facade is preferable:
|
|
41
41
|
|
|
@@ -53,6 +53,8 @@ const schema = createSchema();
|
|
|
53
53
|
await initializeDatabase({ mode: 'validate' });
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
Called without arguments after `createSchema()`, the module's `describeDatabase()` and `compileDatabaseSchema()` describe and compile this default instance in its configured schema.
|
|
57
|
+
|
|
56
58
|
Transactions use one borrowed `pg` client and repeatable-read isolation. A supplied Simfinity PostgreSQL session joins the active transaction. Serialization failures and deadlocks are retried up to five times, each after a short random delay that starts once the client is released; mutation input is reset for each attempt. Unknown commit outcomes are not replayed. The caller owns the pool lifecycle.
|
|
57
59
|
|
|
58
60
|
The module and every `createPostgres` instance expose the shared `auth`, `validators`, `scalars`, and `plugins` helpers. These are the same helper objects exported by the MongoDB package, so rules, scalar identities, and errors can be shared safely between backends.
|
|
@@ -60,7 +62,7 @@ The module and every `createPostgres` instance expose the shared `auth`, `valida
|
|
|
60
62
|
MCP integration is an independent opt-in and does not add MCP dependencies to PostgreSQL applications:
|
|
61
63
|
|
|
62
64
|
```sh
|
|
63
|
-
npm install @simtlix/simfinity-mcp@3.5.
|
|
65
|
+
npm install @simtlix/simfinity-mcp@3.5.6 @modelcontextprotocol/sdk@^1.31.0
|
|
64
66
|
```
|
|
65
67
|
|
|
66
68
|
```javascript
|
|
@@ -71,10 +73,12 @@ const mcpServer = await createMCPServer(schema);
|
|
|
71
73
|
|
|
72
74
|
## Storage and compatible subset
|
|
73
75
|
|
|
74
|
-
Entity identities and references use UUIDs. Scalars use native PostgreSQL types, scalar lists use arrays, reference-free embedded values use JSONB, and embedded values containing references use private owned tables. Foreign keys, inverse collections, explicit linking entities, missing/null markers, nullable embedded-list items, hooks, rollback, and state transitions are supported by the runtime.
|
|
76
|
+
Entity identities and references use UUIDs. Scalars use native PostgreSQL types, scalar lists (including `[ID]` lists) use arrays without a generated index, reference-free embedded values use JSONB, and embedded values containing references use private owned tables. Foreign keys, inverse collections, explicit linking entities, missing/null markers, nullable embedded-list items, hooks, rollback, and state transitions are supported by the runtime.
|
|
75
77
|
|
|
76
78
|
Queries support nested scalar/reference paths, logical filter groups, scopes, sort, pagination, count, and grouped `SUM`, `COUNT`, `AVG`, `MIN`, and `MAX` facts. The PostgreSQL implementation preserves root multiplicity when joining collection paths.
|
|
77
79
|
|
|
80
|
+
Errors that PostgreSQL reports become `SimfinityError`s without SQL details, with the driver error as `getCause()`; errors that application code throws propagate unchanged. Text cannot contain NUL and native date-times start at 4714-11-24 BC: such input fails with `INVALID_VALUE` (400).
|
|
81
|
+
|
|
78
82
|
PostgreSQL enforces required values, real FKs, JSONB/owned embedded shapes, root and embedded scalar/reference uniqueness, and scalar-list multikey uniqueness. Root unique indexes use `NULLS NOT DISTINCT`; private typed owner-key tables allow repeated keys within one owner but reject the same key across owners. Presence markers preserve missing versus explicit null fields. This is stronger and broader than generated Mongoose storage.
|
|
79
83
|
|
|
80
84
|
Scalar-list leaves inside nested embedded lists support filters, sorts, ragged group projections, and array-valued facts. Array `MIN`/`MAX` compares complete arrays; result sorting uses immediate extrema. Date parameters use per-query UTC encoding without changing global `pg` behavior.
|
|
@@ -97,4 +101,4 @@ const statements = compileDatabaseSchema(description);
|
|
|
97
101
|
await initializeDatabase(pool, description, { mode: 'validate' });
|
|
98
102
|
```
|
|
99
103
|
|
|
100
|
-
`compileDatabaseSchema` returns reviewable SQL. Low-level `initializeDatabase(pool, description, { mode })` creates or validates storage transactionally and never closes the pool. Create mode adds missing objects and rejects incompatible definitions; validate mode performs no DDL. Neither mode performs destructive schema synchronization or data migration.
|
|
104
|
+
Low-level `describeDatabase(registrations, { schema })` uses the `public` schema when `schema` is omitted, not the schema of a configured runtime; pass the schema given to `configure()` or `createPostgres()`, or call `describeDatabase()` and `compileDatabaseSchema()` without arguments for the default instance. `compileDatabaseSchema` returns reviewable SQL. Low-level `initializeDatabase(pool, description, { mode })` creates or validates storage transactionally and never closes the pool. Create mode adds missing objects and rejects incompatible definitions; validate mode performs no DDL. Neither mode performs destructive schema synchronization or data migration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-postgres",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.6",
|
|
4
4
|
"description": "PostgreSQL GraphQL runtime and schema generation with foreign keys for Simfinity",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"directory": "packages/postgres"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@simtlix/simfinity-core": "3.5.
|
|
31
|
-
"@simtlix/simfinity-sql": "3.5.
|
|
30
|
+
"@simtlix/simfinity-core": "3.5.6",
|
|
31
|
+
"@simtlix/simfinity-sql": "3.5.6",
|
|
32
32
|
"pg": "^8.16.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
package/src/codecs.js
CHANGED
|
@@ -44,19 +44,40 @@ export const decodeScalar = (field, value, gqlField) => {
|
|
|
44
44
|
return value;
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
const databaseErrors = {
|
|
48
|
+
'23503': ['Reference constraint violated', 'REFERENCE_CONSTRAINT_VIOLATION', 409],
|
|
49
|
+
'23505': ['Unique value already exists', 'DUPLICATE_KEY', 409],
|
|
50
|
+
'23502': ['A required value is missing', 'REQUIRED_VALUE', 400],
|
|
51
|
+
'23514': ['A value violates the generated schema', 'INVALID_VALUE', 400],
|
|
52
|
+
'22P02': ['Invalid stored value', 'INVALID_VALUE', 400],
|
|
53
|
+
'22003': ['Numeric value is out of range', 'INVALID_VALUE', 400],
|
|
54
|
+
// Values PostgreSQL cannot store: longer than a sized column, a timestamp or date outside its range
|
|
55
|
+
// (including generated make_date calls), and text or JSON with NUL (U+0000) or a character the
|
|
56
|
+
// server encoding lacks. Index row size limits (54000) stay a DATABASE_ERROR.
|
|
57
|
+
'22001': ['Value is too long', 'INVALID_VALUE', 400],
|
|
58
|
+
'22008': ['Date or time value is out of range', 'INVALID_VALUE', 400],
|
|
59
|
+
'22021': ['Text contains a character the database cannot store', 'INVALID_VALUE', 400],
|
|
60
|
+
'22P05': ['Text contains a character the database cannot store', 'INVALID_VALUE', 400],
|
|
61
|
+
'40001': ['Concurrent write could not be completed', 'TRANSACTION_RETRY_EXCEEDED', 409],
|
|
62
|
+
'40P01': ['Concurrent write could not be completed', 'TRANSACTION_RETRY_EXCEEDED', 409],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** Serialization failures and deadlocks: confirmed aborts that are safe to retry. */
|
|
66
|
+
export const isRetryableDatabaseError = (error) => ['40001', '40P01'].includes(error?.code);
|
|
67
|
+
|
|
68
|
+
// An error the server reported, as node-postgres exposes it: a five-character SQLSTATE and a severity.
|
|
69
|
+
// An application error that only has a `code`, such as ECONNREFUSED or a domain code, is not one.
|
|
70
|
+
export const isPostgresError = (error) => typeof error?.code === 'string' && /^[0-9A-Z]{5}$/.test(error.code) && typeof error.severity === 'string';
|
|
71
|
+
|
|
72
|
+
/** Maps PostgreSQL errors (and exhausted retries) without SQL details; returns every other value unchanged. */
|
|
47
73
|
export const normalizeDatabaseError = (error) => {
|
|
48
|
-
if (error instanceof SimfinityError || !error
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'22003': ['Numeric value is out of range', 'INVALID_VALUE', 400],
|
|
56
|
-
'40001': ['Concurrent write could not be completed', 'TRANSACTION_RETRY_EXCEEDED', 409],
|
|
57
|
-
'40P01': ['Concurrent write could not be completed', 'TRANSACTION_RETRY_EXCEEDED', 409],
|
|
58
|
-
};
|
|
59
|
-
return new SimfinityError(...(errors[error.code] || ['Database operation failed', 'DATABASE_ERROR', 500]));
|
|
74
|
+
if (error instanceof SimfinityError || !(isPostgresError(error) || isRetryableDatabaseError(error))) return error;
|
|
75
|
+
const normalized = new SimfinityError(...(databaseErrors[error.code] || ['Database operation failed', 'DATABASE_ERROR', 500]));
|
|
76
|
+
// Kept for server logging, but non-enumerable as on a native Error, so serializing or spreading the
|
|
77
|
+
// client-facing error never copies the driver's detail, table or constraint.
|
|
78
|
+
Object.defineProperty(normalized, 'cause', { value: error, writable: true, configurable: true, enumerable: false });
|
|
79
|
+
normalized.getCause = () => normalized.cause;
|
|
80
|
+
return normalized;
|
|
60
81
|
};
|
|
61
82
|
|
|
62
83
|
/** pg's local Date formatting truncates historical offset seconds; use UTC per parameter. */
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createSQL } from '@simtlix/simfinity-sql';
|
|
2
2
|
import { postgresPlugin } from './plugin.js';
|
|
3
|
+
import { describeDatabase as describeSchema } from './schema/describe.js';
|
|
4
|
+
import { compileDatabaseSchema as compileSchemaSQL } from './schema/ddl.js';
|
|
3
5
|
import { initializeDatabase as initializeSchema } from './schema/initialize.js';
|
|
4
6
|
|
|
5
7
|
export { postgresPlugin } from './plugin.js';
|
|
6
|
-
export { describeDatabase } from './schema/describe.js';
|
|
7
|
-
export { compileDatabaseSchema } from './schema/ddl.js';
|
|
8
8
|
export {
|
|
9
9
|
auth,
|
|
10
10
|
buildErrorFormatter,
|
|
@@ -25,3 +25,14 @@ export const { configureQueryLimits, configureMutationLimits, configure, connect
|
|
|
25
25
|
export const initializeDatabase = (poolOrOptions, description, options) => description
|
|
26
26
|
? initializeSchema(poolOrOptions, description, options)
|
|
27
27
|
: defaultInstance.initializeDatabase(poolOrOptions);
|
|
28
|
+
/**
|
|
29
|
+
* With arguments, the low-level describer, whose schema defaults to `public`; without, the configured
|
|
30
|
+
* default instance's description, in the schema given to configure().
|
|
31
|
+
*/
|
|
32
|
+
export const describeDatabase = (registrations, options) => registrations === undefined && options === undefined
|
|
33
|
+
? defaultInstance.describeDatabase()
|
|
34
|
+
: describeSchema(registrations, options);
|
|
35
|
+
/** With a description, the low-level DDL compiler; without, the configured default instance's DDL. */
|
|
36
|
+
export const compileDatabaseSchema = (description) => description === undefined
|
|
37
|
+
? defaultInstance.compileDatabaseSchema()
|
|
38
|
+
: compileSchemaSQL(description);
|
package/src/plugin.js
CHANGED
|
@@ -6,7 +6,7 @@ import { initializeDatabase } from './schema/initialize.js';
|
|
|
6
6
|
import { compileDatabaseSchema } from './schema/ddl.js';
|
|
7
7
|
import { compileQuery } from './query/compiler.js';
|
|
8
8
|
import { compileRecord } from './record-statements.js';
|
|
9
|
-
import { castId, encodeScalar, decodeScalar, encodeParameter, normalizeDatabaseError } from './codecs.js';
|
|
9
|
+
import { castId, encodeScalar, decodeScalar, encodeParameter, isRetryableDatabaseError, normalizeDatabaseError } from './codecs.js';
|
|
10
10
|
|
|
11
11
|
export const postgresPlugin = (options) => ({
|
|
12
12
|
apiVersion: 1,
|
|
@@ -29,11 +29,22 @@ export const postgresPlugin = (options) => ({
|
|
|
29
29
|
query: (configuration, statement, client) => (client || configuration.pool).query(statement.text, statement.values?.map(encodeParameter)),
|
|
30
30
|
acquire: (configuration) => configuration.pool.connect(),
|
|
31
31
|
begin: (client) => client.query('BEGIN ISOLATION LEVEL REPEATABLE READ'),
|
|
32
|
-
|
|
32
|
+
// PostgreSQL answers COMMIT with ROLLBACK, and no error, when a statement in the transaction failed
|
|
33
|
+
// and was caught; nothing was stored, so the operation must fail. A result without a command, from a
|
|
34
|
+
// pool wrapper that does not pass pg's through, resolves as before.
|
|
35
|
+
commit: async (client) => {
|
|
36
|
+
const result = await client.query('COMMIT');
|
|
37
|
+
if (result?.command !== 'ROLLBACK') return;
|
|
38
|
+
const error = new SimfinityError('Transaction was rolled back because one of its statements failed', 'DATABASE_ERROR', 500);
|
|
39
|
+
// The same shape as every other PostgreSQL DATABASE_ERROR: a non-enumerable cause, without SQL.
|
|
40
|
+
Object.defineProperty(error, 'cause', { value: new Error('COMMIT reported ROLLBACK'), writable: true, configurable: true, enumerable: false });
|
|
41
|
+
error.getCause = () => error.cause;
|
|
42
|
+
throw error;
|
|
43
|
+
},
|
|
33
44
|
rollback: (client) => client.query('ROLLBACK'),
|
|
34
45
|
// A release error makes pg-pool destroy a connection whose transaction may still be open.
|
|
35
46
|
release: (client, error) => client.release(error),
|
|
36
|
-
isRetryable:
|
|
47
|
+
isRetryable: isRetryableDatabaseError,
|
|
37
48
|
normalizeError: normalizeDatabaseError,
|
|
38
49
|
},
|
|
39
50
|
});
|
package/src/query/compiler.js
CHANGED
|
@@ -87,9 +87,12 @@ export const compileQuery = (models, database, plan, extra = null) => {
|
|
|
87
87
|
const stored = alias();
|
|
88
88
|
// Missing/null lists contribute a missing path, empty lists and null items do not.
|
|
89
89
|
const condition = `${col(stored, '__owner_id')} = ${ownerId} AND ${state}${presence(stored)}`;
|
|
90
|
+
// UNION ALL pairs columns by position, and stored.* would follow the physical column order,
|
|
91
|
+
// which a migration that appends columns changes. Both branches list the description's columns.
|
|
92
|
+
const storedRow = owned.columns.map((column) => col(stored, column.name)).join(', ');
|
|
90
93
|
const nullRow = owned.columns.map((column) => `NULL::${column.type} AS ${q(column.name)}`).join(', ');
|
|
91
94
|
const noItems = usage === 'sort' ? ` OR NOT EXISTS (SELECT 1 FROM ${qualified(database.schema, owned.name)} empty_child WHERE empty_child.__owner_id = ${ownerId})` : '';
|
|
92
|
-
const source = `LATERAL (SELECT ${
|
|
95
|
+
const source = `LATERAL (SELECT ${storedRow} FROM ${qualified(database.schema, owned.name)} ${stored} WHERE ${condition} UNION ALL SELECT ${nullRow} WHERE ${col(ownerAlias, owned.ownership.stateColumn)} IS DISTINCT FROM 'present'${noItems}) ${name}`;
|
|
93
96
|
const vector = context.vector
|
|
94
97
|
? { from: `${context.vector.from} CROSS JOIN ${source}`, where: context.vector.where }
|
|
95
98
|
: { from: source, where: 'TRUE' };
|
package/src/schema/initialize.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SimfinityError } from '@simtlix/simfinity-core';
|
|
2
|
-
import { identifier, qualified } from './sql.js';
|
|
2
|
+
import { identifier, qualified, generatedName } from './sql.js';
|
|
3
3
|
import { createTableSQL, createForeignKeySQL, createIndexSQL, createFunctionSQL, createTriggerSQL } from './ddl.js';
|
|
4
4
|
|
|
5
5
|
const mismatch = (object, reason) => {
|
|
@@ -7,12 +7,24 @@ const mismatch = (object, reason) => {
|
|
|
7
7
|
};
|
|
8
8
|
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
9
9
|
|
|
10
|
+
// literal() writes a value containing a backslash as E'' with doubled backslashes, while pg_get_expr,
|
|
11
|
+
// read with standard_conforming_strings on, never renders E'' and doubles only quotes. Compare string
|
|
12
|
+
// constants by value: decode either form and re-emit one standard form. The decoder inverts only the
|
|
13
|
+
// escapes literal() emits (\\ and ''); it is not a general E'' decoder (\n or \x41 would be wrong).
|
|
14
|
+
const canonicalString = (token) => {
|
|
15
|
+
const escape = token[0] !== '\'';
|
|
16
|
+
const body = token.slice(escape ? 2 : 1, -1);
|
|
17
|
+
const value = escape ? body.replace(/\\(.)|''/gs, (match, character) => character ?? '\'') : body.replaceAll('\'\'', '\'');
|
|
18
|
+
return `'${value.replaceAll('\'', '\'\'')}'`;
|
|
19
|
+
};
|
|
20
|
+
|
|
10
21
|
// Ignore renderer whitespace/optional quotes, keeping string contents and expression grouping.
|
|
11
|
-
const normalizeExpression = (expression) => {
|
|
22
|
+
export const normalizeExpression = (expression) => {
|
|
12
23
|
if (expression == null) return null;
|
|
13
|
-
|
|
24
|
+
// An E-string is one token unless the E ends an identifier.
|
|
25
|
+
const tokens = expression.match(/(?<![\w$"])[Ee]'(?:\\.|''|[^'\\])*'|'(?:[^']|'')*'|"(?:[^"]|"")*"|\s+|./gs) || [];
|
|
14
26
|
let value = tokens.map((token) => {
|
|
15
|
-
if (token.
|
|
27
|
+
if (token.length > 1 && /^[Ee]?'/.test(token)) return canonicalString(token);
|
|
16
28
|
if (/^"[a-z_][a-z0-9_]*"$/.test(token)) return token.slice(1, -1);
|
|
17
29
|
return /^\s+$/.test(token) ? '' : token;
|
|
18
30
|
}).join('');
|
|
@@ -122,6 +134,16 @@ const validateIndex = (schema, table, index, actual) => {
|
|
|
122
134
|
}
|
|
123
135
|
};
|
|
124
136
|
|
|
137
|
+
// Earlier versions generated a btree index on every [ID] list column. It caps each list at about 167
|
|
138
|
+
// identifiers (btree row size) and no generated query uses it. Initialization keeps it, as it keeps
|
|
139
|
+
// any extra non-unique index, but reports it when it carries the exact generated name and shape.
|
|
140
|
+
const legacyListIndexes = (schema, table, actual) => table.columns
|
|
141
|
+
.filter((column) => column.type === 'uuid[]')
|
|
142
|
+
.map((column) => ({ column: column.name, name: generatedName(table.name, column.name, 'idx') }))
|
|
143
|
+
.filter(({ column, name }) => !table.indexes.some((item) => item.name === name) && actual.indexes.some((index) => index.name === name
|
|
144
|
+
&& !index.unique && !index.primary && index.method === 'btree' && same(index.columns, [column]) && index.no_predicate && index.no_expression))
|
|
145
|
+
.map(({ name }) => `${identifier(schema)}.${identifier(name)}`);
|
|
146
|
+
|
|
125
147
|
const ensureFunctions = async (client, description, mode, created) => {
|
|
126
148
|
for (const fn of description.functions || []) {
|
|
127
149
|
const { rows } = await client.query(`SELECT p.oid, pg_get_function_identity_arguments(p.oid) AS arguments,
|
|
@@ -205,6 +227,7 @@ export const initializeDatabase = async (pool, description, { mode = 'create' }
|
|
|
205
227
|
if (schema.startsWith('pg_') || schema === 'information_schema') throw new SimfinityError('System schemas cannot be managed', 'INVALID_DATABASE_SCHEMA', 400);
|
|
206
228
|
const client = await pool.connect();
|
|
207
229
|
const created = [];
|
|
230
|
+
const legacy = [];
|
|
208
231
|
let rollbackError;
|
|
209
232
|
try {
|
|
210
233
|
await client.query(mode === 'validate' ? 'BEGIN READ ONLY' : 'BEGIN');
|
|
@@ -258,10 +281,16 @@ export const initializeDatabase = async (pool, description, { mode = 'create' }
|
|
|
258
281
|
for (const index of actual.indexes) {
|
|
259
282
|
if (index.unique && !index.primary && !table.indexes.some((item) => item.name === index.name)) mismatch(`${schema}.${table.name}.${index.name}`, 'unexpected unique index');
|
|
260
283
|
}
|
|
284
|
+
legacy.push(...legacyListIndexes(schema, table, actual));
|
|
261
285
|
}
|
|
262
286
|
await ensureTriggers(client, description, mode, created);
|
|
263
287
|
if (mode === 'create' && created.length) await backfill(client, description);
|
|
264
288
|
await client.query('COMMIT');
|
|
289
|
+
if (legacy.length) {
|
|
290
|
+
console.warn(`Simfinity: PostgreSQL schema ${identifier(schema)} has btree indexes that earlier versions generated on [ID] list columns: ${legacy.join(', ')}. `
|
|
291
|
+
+ 'Each limits its list to about 167 identifiers and no generated query uses it. Once every process that uses this schema runs this version, drop them: '
|
|
292
|
+
+ `${legacy.map((name) => `DROP INDEX CONCURRENTLY IF EXISTS ${name};`).join(' ')} See the upgrade notes at https://simtlix.github.io/simfinity.js/resources/compatibility.html`);
|
|
293
|
+
}
|
|
265
294
|
return { mode, created };
|
|
266
295
|
} catch (error) {
|
|
267
296
|
try { await client.query('ROLLBACK'); } catch (failure) { rollbackError = failure; }
|
package/types/index.d.ts
CHANGED
|
@@ -109,19 +109,37 @@ export interface DatabaseDescription {
|
|
|
109
109
|
}
|
|
110
110
|
export interface DatabasePool {
|
|
111
111
|
connect(): Promise<{
|
|
112
|
-
query(sql: string, values?: unknown[]): Promise<{
|
|
112
|
+
query(sql: string, values?: unknown[]): Promise<{
|
|
113
|
+
rows: any[];
|
|
114
|
+
rowCount: number | null;
|
|
115
|
+
/** Command tag reported by pg, such as `COMMIT`. Wrappers should pass pg's `command` through, so that a COMMIT that PostgreSQL turned into ROLLBACK is detected. */
|
|
116
|
+
command?: string;
|
|
117
|
+
}>;
|
|
113
118
|
/** An error means the connection must be destroyed instead of reused. */
|
|
114
119
|
release(error?: unknown): void;
|
|
115
120
|
}>;
|
|
116
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Low-level describer for any registrations. `options.schema` defaults to `public`, not to the schema
|
|
124
|
+
* of a configured runtime; pass the schema given to configure() or createPostgres().
|
|
125
|
+
*/
|
|
117
126
|
export function describeDatabase(registrations: ModelRegistration[], options?: { schema?: string }): DatabaseDescription;
|
|
127
|
+
/** Describes the default module instance in its configured schema, after configure/connect/createSchema. */
|
|
128
|
+
export function describeDatabase(): DatabaseDescription;
|
|
118
129
|
/** Accepts a trusted description produced by describeDatabase. */
|
|
119
130
|
export function compileDatabaseSchema(description: DatabaseDescription): string[];
|
|
131
|
+
/** DDL of the default module instance in its configured schema, after configure/connect/createSchema. */
|
|
132
|
+
export function compileDatabaseSchema(): string[];
|
|
120
133
|
/** Borrows one pool client; owns its transaction, not the pool lifecycle. PostgreSQL >=15. */
|
|
121
134
|
export function initializeDatabase(pool: DatabasePool, description: DatabaseDescription, options?: { mode?: 'create' | 'validate' }): Promise<{ mode: 'create' | 'validate'; created: string[] }>;
|
|
122
135
|
|
|
123
136
|
export interface DatabaseQueryable {
|
|
124
|
-
query(sql: string, values?: any[]): Promise<{
|
|
137
|
+
query(sql: string, values?: any[]): Promise<{
|
|
138
|
+
rows: any[];
|
|
139
|
+
rowCount: number | null;
|
|
140
|
+
/** Command tag reported by pg, such as `COMMIT`. Wrappers should pass pg's `command` through, so that a COMMIT that PostgreSQL turned into ROLLBACK is detected. */
|
|
141
|
+
command?: string;
|
|
142
|
+
}>;
|
|
125
143
|
}
|
|
126
144
|
export interface PostgresPool extends DatabasePool, DatabaseQueryable {}
|
|
127
145
|
export interface PostgresSession extends DatabaseQueryable {
|
|
@@ -156,7 +174,14 @@ export interface PostgresRuntime extends Runtime<PostgresModel, PostgresSession>
|
|
|
156
174
|
/** Bind pool/schema once, before createSchema. The caller owns the pool lifetime. */
|
|
157
175
|
configure(options: PostgresConfiguration): void;
|
|
158
176
|
describeDatabase(): DatabaseDescription;
|
|
159
|
-
/**
|
|
177
|
+
/** Physical DDL for this instance's description and schema, for review or export; initializeDatabase does not run it. */
|
|
178
|
+
compileDatabaseSchema(): string[];
|
|
179
|
+
/**
|
|
180
|
+
* Must be awaited before operations execute. Does not synchronize incompatible schemas. A request rejected before
|
|
181
|
+
* storage is touched, such as an unknown mode (INVALID_INITIALIZATION_MODE, 400) or, on a validation-only runtime,
|
|
182
|
+
* a non-empty mode other than validate (DATABASE_CREATION_DISABLED, 409), keeps the current readiness; a ready runtime keeps serving while it is initialized again, and any failure of the latest
|
|
183
|
+
* initialization makes it unavailable.
|
|
184
|
+
*/
|
|
160
185
|
initializeDatabase(options?: InitializationOptions): Promise<InitializationResult>;
|
|
161
186
|
withTransaction<T>(session: PostgresSession | null | undefined, callback: (session: PostgresSession) => Promise<T> | T): Promise<T>;
|
|
162
187
|
}
|
|
@@ -165,7 +190,12 @@ export function postgresPlugin(options?: PostgresConfiguration): SQLPlugin<
|
|
|
165
190
|
PostgresConfiguration,
|
|
166
191
|
DatabaseQueryable,
|
|
167
192
|
DatabaseDescription,
|
|
168
|
-
{
|
|
193
|
+
{
|
|
194
|
+
rows: Record<string, unknown>[];
|
|
195
|
+
rowCount: number | null;
|
|
196
|
+
/** Command tag reported by pg, such as `COMMIT`. Wrappers should pass pg's `command` through, so that a COMMIT that PostgreSQL turned into ROLLBACK is detected. */
|
|
197
|
+
command?: string;
|
|
198
|
+
}
|
|
169
199
|
>;
|
|
170
200
|
export function createPostgres(options?: PostgresConfiguration): PostgresRuntime;
|
|
171
201
|
export const configure: PostgresRuntime['configure'];
|
package/src/adapter.js
DELETED
package/src/records.js
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { createRecordStore as createSQLRecordStore } from '@simtlix/simfinity-sql/internal/records';
|
|
2
|
-
import { postgresPlugin } from './plugin.js';
|
|
3
|
-
|
|
4
|
-
export const createRecordStore = (models, database, query) => createSQLRecordStore(models, database, (statement, session) => query(statement.text, statement.values, session), postgresPlugin());
|
package/src/transactions.js
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { createTransactions as createSQLTransactions } from '@simtlix/simfinity-sql/internal/transactions';
|
|
2
|
-
import { postgresPlugin } from './plugin.js';
|
|
3
|
-
|
|
4
|
-
export const createTransactions = (getPool, assertReady) => createSQLTransactions(() => ({ pool: getPool() }), assertReady, postgresPlugin());
|