@simtlix/simfinity-postgres 3.6.0 → 3.7.0
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 +4 -4
- package/package.json +3 -3
- package/src/plugin.js +6 -1
- package/src/query/compiler.js +56 -2
- package/types/index.d.ts +13 -0
package/README.md
CHANGED
|
@@ -31,9 +31,9 @@ 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.
|
|
34
|
+
Install `@simtlix/simfinity-sql@3.7.0` 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, and
|
|
36
|
+
Both factories permanently bind one pool and schema, and five optional settings: `multiRowInserts: false` keeps one `INSERT` per owned row instead of multi-row statements; `explicitNullsOrder: true` keeps the `NULLS` clause on sorts of `id` and required fields, as 3.5.9 did, instead of letting indexes serve them; `batchedCollections: true` reads the generated collection fields of the parents that share their arguments with one statement per 100 parents, in a transaction that turns JIT off for that transaction only, instead of one statement per field as 3.6.0 did and as the default still does, and can return unsorted and tied rows in another order; the opt-in `refreshTriggers: 'perRoot'` refreshes each changed root's owned shape and unique keys once per transaction, while its guard trigger stays deferred, instead of once per changed row; and the opt-in `ownerIndexes: 'minimal'` leaves out the `(__owner_id)` indexes that the primary key or a unique index covers on new owned and guard tables. An unknown `refreshTriggers` or `ownerIndexes` value throws `INVALID_DATABASE_SCHEMA` (400). Read the [upgrade notes](https://simtlix.github.io/simfinity.js/resources/compatibility.html#upgrade-to-3-6-0) before enabling the opt-ins: versions up to 3.5.9 cannot validate databases created with them. 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
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. `create` mode reads the catalog without table locks and locks nothing when nothing is missing. When it creates objects, writers of the tables that it changes, of existing tables that a new foreign key references, or of roots whose keys it rebuilds wait until it commits, so use `mode: 'validate'` to check a live instance. `simfinity.compileDatabaseSchema()` returns the instance's DDL for review or export.
|
|
39
39
|
|
|
@@ -55,14 +55,14 @@ await initializeDatabase({ mode: 'validate' });
|
|
|
55
55
|
|
|
56
56
|
Called without arguments after `createSchema()`, the module's `describeDatabase()` and `compileDatabaseSchema()` describe and compile this default instance in its configured schema.
|
|
57
57
|
|
|
58
|
-
Transactions use one borrowed `pg` client and repeatable-read isolation. Reads without a session of types without owned embedded tables run as a single statement on a pooled connection instead. 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.
|
|
58
|
+
Transactions use one borrowed `pg` client and repeatable-read isolation. Reads without a session of types without owned embedded tables run as a single statement on a pooled connection instead. With `batchedCollections: true`, the runtime reads the generated collection fields of up to 100 parents that share their arguments in one batch, with one statement, a `UNION ALL` of each parent's own statement with its parent ID as a parameter of its own, so PostgreSQL plans each parent's rows as it plans that parent's own statement; more parents run as several batches, each in its own transaction. Each batch runs in one repeatable-read transaction: `BEGIN`, `SET LOCAL jit = off`, the statement, the owned-row reads, then `COMMIT`. Only a direct `findChildrenBatch` call on the SQL adapter with more than 100 IDs repeats `SET LOCAL` and the statement for each further 100 within its transaction. The batch's plan can cost more than `jit_above_cost` when no parent's own plan does, and JIT compilation of its branches can take longer than the statement itself; `SET LOCAL` ends with the transaction, so pooled connections keep the server's setting. A batch takes these 4 round trips, or more with owned rows, one after another on one connection and runs its parents one after another, where per-parent statements take one round trip each, concurrently on the pool's free connections. It pays off when the pool is saturated or much smaller than the number of parents per level, as with many parents with small child lists; with free connections it can be slower, even on a remote database, so measure before enabling it. Transaction-mode PgBouncer keeps each batch on one server connection, while Amazon RDS Proxy pins the pooled connections that run batches, for their `SET LOCAL` and their statements over 16 KB. See [batched collection reads](https://simtlix.github.io/simfinity.js/guide/relationships.html#batched-collection-reads). 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.
|
|
59
59
|
|
|
60
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.
|
|
61
61
|
|
|
62
62
|
MCP integration is an independent opt-in and does not add MCP dependencies to PostgreSQL applications:
|
|
63
63
|
|
|
64
64
|
```sh
|
|
65
|
-
npm install @simtlix/simfinity-mcp@3.
|
|
65
|
+
npm install @simtlix/simfinity-mcp@3.7.0 @modelcontextprotocol/sdk@^1.31.0
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simtlix/simfinity-postgres",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.7.0",
|
|
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.
|
|
31
|
-
"@simtlix/simfinity-sql": "3.
|
|
30
|
+
"@simtlix/simfinity-core": "3.7.0",
|
|
31
|
+
"@simtlix/simfinity-sql": "3.7.0",
|
|
32
32
|
"pg": "^8.16.3"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
package/src/plugin.js
CHANGED
|
@@ -4,7 +4,7 @@ import { identifier, generatedName } from './schema/sql.js';
|
|
|
4
4
|
import { describeRelationalSchema } from './schema/describe.js';
|
|
5
5
|
import { initializeDatabase } from './schema/initialize.js';
|
|
6
6
|
import { compileDatabaseSchema } from './schema/ddl.js';
|
|
7
|
-
import { compileQuery } from './query/compiler.js';
|
|
7
|
+
import { compileChildrenQuery, compileQuery } from './query/compiler.js';
|
|
8
8
|
import { compileRecord, compileRecordBatch, MAX_RECORD_BATCH_VALUES } from './record-statements.js';
|
|
9
9
|
import { castId, encodeScalar, decodeScalar, encodeParameter, isRetryableDatabaseError, normalizeDatabaseError } from './codecs.js';
|
|
10
10
|
|
|
@@ -23,6 +23,11 @@ export const postgresPlugin = (options) => ({
|
|
|
23
23
|
initialize: (configuration, description, initialization) => initializeDatabase(configuration.pool, description, initialization),
|
|
24
24
|
compileSchema: compileDatabaseSchema,
|
|
25
25
|
compileQuery,
|
|
26
|
+
// Optional member: the find statements of many parents in one statement, for findChildrenBatch, which the SQL
|
|
27
|
+
// adapter defines only with `batchedCollections: true`. Each statement carries setup that turns JIT off for its
|
|
28
|
+
// transaction, and the compileQuery it reproduces, so a copy of this plugin with another compileQuery reads each
|
|
29
|
+
// parent alone.
|
|
30
|
+
compileChildrenQuery,
|
|
26
31
|
compileRecord,
|
|
27
32
|
compileRecordBatch,
|
|
28
33
|
maxRecordBatchValues: MAX_RECORD_BATCH_VALUES,
|
package/src/query/compiler.js
CHANGED
|
@@ -4,11 +4,65 @@ import { encodeScalar, storageTypes, invalidValue } from '../codecs.js';
|
|
|
4
4
|
|
|
5
5
|
const unsupported = (message) => { throw new SimfinityError(message, 'UNSUPPORTED_QUERY', 400); };
|
|
6
6
|
|
|
7
|
+
// Alias of each per-parent statement in the batched children statement. The per-parent statement only uses t<n>,
|
|
8
|
+
// f<n> and short subquery aliases, so it never shadows this one.
|
|
9
|
+
const CHILDREN = 'simfinity_children';
|
|
10
|
+
// Stands for the parent's parameter in the per-parent statement that compileChildrenQuery repeats once per parent.
|
|
11
|
+
// Compiled statements carry no NUL character, which PostgreSQL rejects in statement text: caller values are
|
|
12
|
+
// parameters and identifiers and literals come from GraphQL names.
|
|
13
|
+
const PARENT = '\u0000';
|
|
14
|
+
// Runs before each batched children statement, in its transaction; SET LOCAL ends with that transaction, so the
|
|
15
|
+
// connection goes back to the pool with the server's JIT setting.
|
|
16
|
+
const CHILDREN_SETUP = 'SET LOCAL jit = off';
|
|
17
|
+
|
|
7
18
|
/**
|
|
8
19
|
* Compiles validated paths; all caller values are parameters, identifiers come from metadata.
|
|
9
20
|
* `configuration.explicitNullsOrder: true` keeps the NULLS clause on every sort term, as 3.5.9 did.
|
|
10
21
|
*/
|
|
11
|
-
export const compileQuery = (models, database, plan, extra = null, configuration = null) =>
|
|
22
|
+
export const compileQuery = (models, database, plan, extra = null, configuration = null) => compileSelect(models, database, plan, extra, configuration, false);
|
|
23
|
+
|
|
24
|
+
// A result column for the batched statement that no column of the child table has. Planning rejects storage names
|
|
25
|
+
// that start with `__`, and generated root columns end in `_state` or `_present`, so the loop only guards a
|
|
26
|
+
// description built by hand.
|
|
27
|
+
const freeColumn = (table, name, taken = []) => {
|
|
28
|
+
let column = name;
|
|
29
|
+
while (taken.includes(column) || table.columns.some((item) => item.name === column)) column = `_${column}`;
|
|
30
|
+
return column;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The find statement of each parent in `extra.ids`, in one statement: a UNION ALL with one branch per ID, in
|
|
35
|
+
* order. Each branch is the statement compileQuery returns for `{ column, id }`, with the same filters, joins,
|
|
36
|
+
* ORDER BY, LIMIT and OFFSET, and with that ID as a parameter of its own, so PostgreSQL plans every branch for its
|
|
37
|
+
* own parent, with the statistics of that value, as it plans the per-parent statement. The other parameters, such
|
|
38
|
+
* as filter values and the page, are shared by every branch. Each branch tags its rows with the 1-based position of
|
|
39
|
+
* its parent ID, in `parentColumn`, and with their row number in the order its statement returns them: the
|
|
40
|
+
* statement always has a LIMIT, so PostgreSQL keeps it a subquery and never merges it into the branch. The outer
|
|
41
|
+
* ORDER BY on both returns each parent's rows together, in position order, and in its statement's order, which a
|
|
42
|
+
* Parallel Append would not keep.
|
|
43
|
+
* Values are the per-parent statement's values without the parent ID, then one uuid per parent ID.
|
|
44
|
+
* Its `setup` turns JIT compilation off for the transaction the SQL adapter runs it in: the plan of up to 100
|
|
45
|
+
* branches costs about as much as the per-parent plans together, so it can cross jit_above_cost when none of them
|
|
46
|
+
* does, and JIT then compiles every branch's expressions, which can take longer than running the statement.
|
|
47
|
+
*/
|
|
48
|
+
export const compileChildrenQuery = (models, database, plan, extra, configuration = null) => {
|
|
49
|
+
if (plan?.mode !== 'find' || !plan.pagination) unsupported('Only paginated find queries read children in batches');
|
|
50
|
+
if (!Array.isArray(extra?.ids) || !extra.ids.length) unsupported('Batched children reads require parent IDs');
|
|
51
|
+
const { text, values } = compileSelect(models, database, plan, { column: extra.column, id: null }, configuration, true);
|
|
52
|
+
const [before, after, ...rest] = text.split(PARENT);
|
|
53
|
+
if (after === undefined || rest.length) unsupported('Invalid batched children statement');
|
|
54
|
+
const root = database.tables.find((item) => item.name === plan.entity);
|
|
55
|
+
const parentColumn = freeColumn(root, '__simfinity_parent');
|
|
56
|
+
const ordinalColumn = freeColumn(root, '__simfinity_ordinal', [parentColumn]);
|
|
57
|
+
const branches = extra.ids.map((id, index) => `SELECT ${index + 1}::integer AS ${q(parentColumn)}, row_number() OVER () AS ${q(ordinalColumn)}, ${CHILDREN}.* FROM (${before}$${values.length + index + 1}::uuid${after}) ${CHILDREN}`);
|
|
58
|
+
return { text: `${branches.join(' UNION ALL ')} ORDER BY ${q(parentColumn)}, ${q(ordinalColumn)}`, values: [...values, ...extra.ids], parentColumn, setup: [{ text: CHILDREN_SETUP, values: [] }] };
|
|
59
|
+
};
|
|
60
|
+
// The compileQuery whose per-parent statements compileChildrenQuery reproduces. bindPlugin binds the children
|
|
61
|
+
// compiler only for a plugin whose compileQuery is this one, so a plugin copy that overrides compileQuery reads
|
|
62
|
+
// each parent with its own compileQuery.
|
|
63
|
+
Object.defineProperty(compileChildrenQuery, 'compileQuery', { value: compileQuery });
|
|
64
|
+
|
|
65
|
+
const compileSelect = (models, database, plan, extra, configuration, batched) => {
|
|
12
66
|
const explicitNullsOrder = configuration?.explicitNullsOrder === true;
|
|
13
67
|
const values = [];
|
|
14
68
|
const joins = new Map();
|
|
@@ -232,7 +286,7 @@ export const compileQuery = (models, database, plan, extra = null, configuration
|
|
|
232
286
|
if (extra) {
|
|
233
287
|
const column = root.columns.find((item) => item.name === extra.column);
|
|
234
288
|
if (!column || column.type !== 'uuid') unsupported('Invalid inverse connection field');
|
|
235
|
-
condition += ` AND ${col('t0', column.name)} = ${bind(extra.id, 'uuid')}`;
|
|
289
|
+
condition += ` AND ${col('t0', column.name)} = ${batched ? PARENT : bind(extra.id, 'uuid')}`;
|
|
236
290
|
}
|
|
237
291
|
let select = 't0.*';
|
|
238
292
|
// List values that MIN and MAX facts read, each evaluated once per row in a lateral subquery.
|
package/types/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type {
|
|
|
16
16
|
AuthPluginOptions,
|
|
17
17
|
AuthRule,
|
|
18
18
|
AuthRuleFunction,
|
|
19
|
+
AuthSchemaPlugin,
|
|
19
20
|
EntityController,
|
|
20
21
|
EnvelopSchemaPlugin,
|
|
21
22
|
FieldValidations,
|
|
@@ -178,6 +179,18 @@ export interface PostgresConfiguration {
|
|
|
178
179
|
* indexes can return them in order. `true` keeps the explicit NULLS clause on every sort term, as 3.5.9 did.
|
|
179
180
|
*/
|
|
180
181
|
explicitNullsOrder?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Defaults to false: each generated collection field is read with its own statement, as 3.6.0 did. `true` reads the
|
|
184
|
+
* collection fields of many parents that share their arguments with one statement per 100 parents, a UNION ALL of
|
|
185
|
+
* each parent's own statement with its parent ID as a parameter of its own, so PostgreSQL plans each parent's rows
|
|
186
|
+
* for that parent's ID. A batch runs in one repeatable-read transaction, each of its statements right after
|
|
187
|
+
* `SET LOCAL jit = off`, because the batch's plan can cross jit_above_cost when no per-parent plan does.
|
|
188
|
+
* Each parent gets the same rows as before when its order is total, such as a sort that ends in `id`; PostgreSQL
|
|
189
|
+
* does not promise an order for unsorted rows and rows with equal sort keys, which can then come in another order,
|
|
190
|
+
* and on a page that does not hold all of them, other such rows can fill it. Any value other than `true` keeps the
|
|
191
|
+
* default.
|
|
192
|
+
*/
|
|
193
|
+
batchedCollections?: boolean | undefined;
|
|
181
194
|
/**
|
|
182
195
|
* Defaults to 'perRow', as 3.5.9: the deferred triggers of a root with owned tables or unique lists refresh its owned
|
|
183
196
|
* shape and unique keys once per changed row. 'perRoot' refreshes each changed root once per transaction while its
|