Skip to content

v3.7.0 · MongoDB & PostgreSQL. Install from npm or download the starters.

Database compatibility contract ​

This ledger records the v3.6.0 behavior shared by the MongoDB and PostgreSQL implementations and the remaining differences. The MongoDB contract is in tests/integration/mongodb.test.js, PostgreSQL execution in the runtime/lifecycle/options suites, and direct result/schema comparison in the query-parity suites, using the graph in tests/contracts/model-fixtures.js.

The test paths below are available in the v3.6.0 source. To try the API, use the released starters.

When running the source tests, set SIMFINITY_MONGODB_URI and SIMFINITY_POSTGRES_URI to disposable databases for the cross-backend suites. Set SIMFINITY_TEST_MONGODB_URI to a separate disposable MongoDB database for upstream opt-in regressions. Mongo setup drops its configured databases, so these URIs must never identify application data. When a variable is absent, its integration suites are reported as skipped.

MongoDB contract ​

BehaviorDecisionExecutable evidence
A non-embedded relation with no connectionField stores its ObjectId under the GraphQL field name.Fixed and sharedUnit regression plus real ContractSerie.label create/read/clear flow.
Clearing a nullable singular reference unsets its storage field (connectionField when declared, otherwise the GraphQL field). Clearing an inverse collection does not unset the child connectionField on the parent.Fixed and sharedUnit regression and real update followed by a raw MongoDB record check.
An explicit null for a nullable member of a singular embedded patch clears it: a list member becomes [], other members are stored as null (a reference under its storage field), and a non-null member keeps its stored value. Every GraphQL read path returns the cleared value; a top-level null still removes the field.Fixed and shared (3.5.5)Memory regressions in tests/embedded-update-required.test.js; tests/integration/embedded-update-required-parity.test.js compares update responses, by-ID and list reads, plus raw MongoDB documents, in default and transactional MongoDB modes and on PostgreSQL.
On MongoDB, an optional singular embedded object that holds no data (members absent or null, lists empty, nested embedded objects likewise) reads as null on every read path, whatever the selection, when a required member would read as null: an object absent from the stored document, because a create omitted it, the document was written without it or a top-level null removed it, and one that a create omitted and Mongoose stored with list defaults. id, readOnly, interface and union, application-resolved and referenced-collection members do not count. The rule reads stored data (the adapter's rawEmbeddedValue), never a schema getter or virtual, counts a stored _id as id, reads nothing of a type without a member that counts, and returns the object unchanged when reading fails. MongoDB reads Mongoose maps by their entries, and its hydrated reads (by ID, references, update responses) keep, as 3.5.8 rendered them, objects whose schema declares a getter, a virtual other than the automatic id, an alias or a subdocument method anywhere inside them; generated models declare none. A create that writes an empty object for a required embedded member stores the member as absent (Mongoose minimizes it), so an object that holds nothing else also reads as null, except in by-ID and reference reads and update responses where the member is a nested path, as generated models declare every embedded member except one named type. Runtimes that share the type read it the same way. PostgreSQL rejects such creates with REQUIRED_VALUE.Fixed (3.5.9); creates differ by backendobjects that hold no data in tests/runtime.test.js and tests/mongodb-ids.test.js; tests/integration/embedded-required-reads.test.js checks update responses, by-ID and list reads and raw MongoDB documents, and PostgreSQL's results, and keeps an object whose only data is a reference stored under its connectionField; it also pins the minimized empty object with a generated model and with a supplied single nested schema, and, with supplied models, that no getter of an unselected member runs, that an embedded entity whose only stored data is its _id reads its id on list and by-ID reads, as on 3.5.8, that by-ID, reference and update-response reads of objects whose required member a getter default, a virtual or an alias supplies render as on 3.5.8, that an empty Mongoose map reads null on by-ID, reference, update and list reads, and that a generated model with an embedded list still reads data-less objects as null. reading stored members and the stored _id test in tests/runtime.test.js and the getter, subdocument _id, getter/virtual/alias at any depth, map and generated-model tests in tests/mongodb-ids.test.js pin the same without a database.
A scalar-only no-endpoint type receives a model when another registered type references it. It still receives no root endpoint.Fixed and sharedUnit regression and the real ContractLabel reference fixture.
Every entity type, registered with connect() or targeted by a non-embedded relation, resolves id to its stored identity whatever the query and mutation allowlists include. Embedded-only types return their declared id. Embedded copies of entity types return their declared id (null for copies created through add inputs, the supplied id for copies written through update inputs, saveObject() or native writes), except that on MongoDB copies in an embedded list report the automatic subdocument _id.Fixed (3.5.5); embedded list copies of entity types differ by backendtests/integration/id-resolver-parity.test.js on both backends and entity id resolution in tests/runtime.test.js.
onUpdated receives the resolved updated record after the database operation completes.Fixed and sharedUnit regression and live hook assertions.
Multiple flat terms on one relation are all present in the generated match.Fixed and sharedUnit regressions for author.name plus author.id, and two predicates on the same author.name path.
Filters, sorts, groupId and fact paths on <embedded>.id use the declared id member, the value that reads return. On MongoDB, entity types embedded as lists use the automatic subdocument _id, which their reads also return. Supplied subdocument schemas without an id path keep using the subdocument _id, which only hydrated reads expose, through Mongoose's id virtual; list queries return null for that id.Fixed and shared (3.5.5); MongoDB exceptions as notedtests/integration/embedded-query-parity.test.js filters, sorts and groups by every displayed embedded ID on both backends; generated, supplied and schema-less paths in tests/mongodb-query-paths.test.js.
Root reads and generated non-embedded relationships run target middleware and scopes. Relationship IDs and parent predicates cannot be redirected by mutable arguments.v3.1.0 sharedUpstream authorization integration tests and cross-backend runtime-v31.test.js.
A one-to-many filter uses $lookup plus $unwind; a root with two matching children appears twice. Count uses the same multiplicity.Preserve current cardinalityLive collection-filter result contains the same root twice and sets context.count to 2.
Generated mutations own a transaction and pass its native session plus the GraphQL context to validation and controller hooks. A nested failure rolls back the parent write.PreserveLive nested validation failure leaves neither parent nor child records; hook assertions observe an active session and the original context.
In default mode, a custom mutation's owned session uses the default Mongoose connection, except while that connection has no client, no model is compiled on it or on any of its useDb() descendants and every registered model shares one MongoDB client: then it uses that client.Fixed (3.5.9); MongoDB onlytests/mongodb-custom-mutation-connection.test.js with stub connections and real useDb() children and grandchildren of the unopened default connection; tests/integration/mongodb-custom-mutation-connection.test.js commits and rolls back writes through one mongoose.createConnection() connection and its useDb() children, and keeps the default connection for models on two clients or a generated model; tests/integration/mongodb-custom-mutation-usedb.test.js and tests/integration/mongodb-custom-mutation-usedb-nested.test.js keep it for an application model on a useDb() connection of the default one, or on a useDb() connection of that one, when the app connects after the request.
saveObject(typeName, args, session, context) owns the entire workflow transaction without a supplied session. Active supplied sessions remain caller-owned.v3.1.0 sharedStandalone nested middleware/hook/FK failure rolls back parent, child and embedded writes.
Accepted empty strings, false, 0 and empty arrays survive materialization. Nullable embedded list items retain null; null referenced-operation items are skipped.v3.1.0 sharedUpstream materialization regressions and live standalone save/update checks on both backends.
A malformed record, reference, scalar ID or embedded id value fails with NOT_VALID_ID (400) before anything is written; supplied models' _id setters still apply. A well-formed ID of a missing record keeps null from by-ID reads and NOT_VALID_ID (404) from writes. ID filters and scoped by-ID reads keep INVALID_FILTER_VALUE. A stored reference value that the adapter rejects is a data error: its field fails with INTERNAL_SERVER_ERROR.Fixed and shared (3.5.5); MongoDB filters keep their own codemalformed identifiers and supplied keys with setters in tests/integration/mongodb.test.js, tests/mongodb-ids.test.js, and supplied-session aborts in tests/integration/mongodb-integrity.test.js. Stored references in tests/runtime.test.js and tests/integration/id-resolver-parity.test.js. PostgreSQL already returned NOT_VALID_ID.
Fields named aggregation or conditions are ordinary top-level filters on list, count and collection queries and in scopes; aggregation is the aggregation expression only on aggregate queries.Fixed and shared (3.5.5)tests/integration/query-parity.test.js and tests/integration/query-path-authorization.test.js on both backends.
Embedded types that contain each other, directly or through other embedded types, as objects or lists, are rejected at createSchema() with INVALID_MODEL (400, Embedded cycle at …), also with readOnly links: always in transactional MongoDB mode and on PostgreSQL, and in default MongoDB mode when Simfinity generates a model whose embedded fields reach the cycle. In default mode a cycle that only supplied models reach is not checked, as before: it fails with INPUT_TYPE_UNRESOLVED, or builds with a readOnly link. In default mode a type that embeds itself keeps its message, now with INVALID_MODEL (400); transactional mode and PostgreSQL report it as Embedded cycle at Type.field.Fixed (3.5.9); in default mode the path can start at an embedded type, and supplied models are not checkedembedded cycles in tests/mongodb-models.test.js.
GraphQLNonNull controls GraphQL inputs but does not automatically add a Mongoose required validator.Preserve for MongoDBPostgreSQL schema metadata may use the declaration for NOT NULL; this is a documented storage-level difference.
Lists preserve outer and item nullability ([T], [T!], [T]!, [T!]!). Update fields allow omission while retaining item wrappers.Fixed and sharedRuntime input/schema regressions; real scalar/date/embedded list operations.
Inverse relations may use a scalar ID or an alias for the child's reference storage field.Fixed and sharedExplicit connection-field normalization and real nested create/read/update regressions.
A referenced collection whose private connectionField equals the connectionField of a collection on the child type, including a self-referencing collection, gives the generated child model its own private ObjectId field, so nested writes store the link.Fixed and shared (3.5.5)Model-path regressions in both modes (tests/mongodb-regressions.test.js), nested create/read with raw link checks on both backends (tests/integration/relationship-aliases.test.js) and transactional initialization (tests/integration/mongodb-integrity.test.js).
A referenced collection requires a non-empty connectionField. A writable one whose resolver Simfinity generates is rejected at startup with INVALID_MODEL (400) in every mode, as PostgreSQL does. In default mode, one with its own resolver or marked readOnly builds with a warning, and its nested writes fail with INVALID_MODEL (500).Fixed (3.5.5); the exemption is default-MongoDB-onlyreferenced collection connectionField in tests/runtime.test.js and the startup cases in tests/mongodb-regressions.test.js.
Writable referenced collections may form a cycle across types, such as Department.employees and Employee.managedDepartments, and nested inputs follow it at any depth. Several types may have a self-referencing collection with the same name: the first registered type that the generated mutations of the first createSchema() reach keeps OneToManyA<field>/OneToManyU<field>, and the other types that a schema reaches together with it are named after their type. A type registered again, or after a createSchema() call, keeps the unqualified names unless its schema also reaches another type, still registered, that has them. Collections that share a child's connectionField share one added item input. deleted is [ID] whatever the item nullability, and null IDs are ignored.Fixed and shared (3.5.8)tests/integration/collection-inputs.test.js on both backends; tests/collection-inputs.test.js on core, the MongoDB facade and PostgreSQL with a stub pool.
A generated input that would have no fields is left out with a Configuration issue warning: the added or updated operation of a collection whose child has no writable fields for it, an embedded field whose type has none, and the add, update and state-action mutations of a type whose create or update input would be empty. A non-null embedded field without a create input is rejected with INVALID_MODEL (400) when the generated mutations reach its owner.Fixed and shared (3.5.9)tests/empty-generated-inputs.test.js on core, the MongoDB facade and PostgreSQL with a stub pool, plus a transactional MongoDB case; tests/integration/empty-generated-inputs.test.js creates children and updates and deletes them through such a collection on both backends.
Nested updated and deleted children are owned through the parent's stored identifier, and added and updated children are linked with it, so any spelling of the parent ID that the backend accepts, such as an uppercase ObjectId or UUID, owns its children. A foreign child raises ForbiddenError (FORBIDDEN, 403).Fixed and shared (3.5.8)Uppercase parent IDs in tests/integration/runtime-v31.test.js on both backends, including a MongoDB child model that stores the link as a String; nested collection ownership in tests/runtime.test.js.
Filter list literals accept QLValue variables as items, an unset item reads as null and is rejected like an explicit null element, and enum literals read as their names.Fixed and shared (3.5.8)tests/integration/query-parity.test.js compares the results with the literal forms on both backends; tests/query-correctness.test.js.
validators.arrayLength() runs helper results and single validators on list items; dateFormat() and futureDate() reject impossible ISO and MM/DD/YYYY dates; null bounds mean no bound in validators and bounded scalars.Fixed and shared (3.5.8)tests/integration/validation-helpers-parity.test.js on both backends, including a Date field with a DD/MM/YYYY format.
Aggregation through inverse collections uses the child FK and counts contributing rows.Fixed and sharedLive cross-database SUM/COUNT and scalar Boolean MIN/MAX comparison.
Embedded lists can be restored after being cleared. Object patches stay shallow, list patches replace the list.Fixed and sharedRuntime regression and real owned-reference-list clear/restore flow.
Retried generated/custom mutations start with fresh typed input structures; root middleware executes once, while the middleware of nested collection children runs again with the retried transaction.Fixed and sharedSynthetic retry tests retain enum identity and reset nested lists/objects/Dates; real PostgreSQL aborted-attempt rollback; a retried transaction runs the middleware of nested collection children again, not the root middleware in tests/runtime.test.js (3.6.0).
State storage/guards use a single adapter-specific representation. SQL backends require the state field to be a GraphQL enum and reject any other with INVALID_MODEL (400) at createSchema().Shared orchestration; enum requirement fixed for SQL (3.5.6)Mongo preserves state names; PostgreSQL stores/decodes enum internal values. Real tests cover overlapping names/values and concurrent transitions. SQL state machine fields in tests/sql-plugin.test.js covers the startup error and unchanged enum stateNames.
Fields named like Object.prototype members (constructor, toString, valueOf, …) hold only their own values: an omitted scalar or reference is not stored and an omitted list stores [], an update that does not mention a field keeps the stored value, an absent one reads null, and a reference with such a name, under any connectionField, reads that field.Fixed and shared (3.5.6); MongoDB rejects at createSchema() with INVALID_MODEL the fields Mongoose cannot store: embedded objects and lists with these names, and any field stored under constructor. On MongoDB, a collection whose connectionField is constructor and whose child type has no such field is rejected at startup the same way, naming Child.constructor. Open on MongoDB: a document stored without such a list reads null for it in by-ID reads, where a missing list with another name reads []tests/integration/prototype-field-names-parity.test.js on both backends, fields named like Object.prototype members in tests/runtime.test.js and tests/mongodb-models.test.js, and SQL records with fields named like Object.prototype members in tests/sql-plugin.test.js.
Errors that application code throws in an owned transaction (validators, controllers, nested middleware, custom mutations, withTransaction callbacks) propagate unchanged, with or without a code.Fixed and shared (3.5.6); PostgreSQL maps the errors it reports and masks failed driver calls as DATABASE_ERRORSQL error classification and SQL runtime error propagation in tests/sql-transactions.test.js, PostgreSQL error normalization in tests/postgres-configuration.test.js, and tests/integration/postgres-lifecycle.test.js.
The facade leaves global Mongoose options, including strictQuery, as the application sets them; generated queries do not depend on them.Fixed (3.5.5)tests/mongodb-global-options.test.js checks fresh processes; the MongoDB contract suite runs under strictQuery: 'throw'.

PostgreSQL execution contract and limits ​

The query parity corpus compares actual schemas and responses for operators, nested logical filters, dates, enums, scalar arrays, nullable values, array sorts, embedded paths, scopes, joined row multiplicity, pagination/counts and aggregate facts. Typed invalid values and unsupported paths produce explicit errors. PostgreSQL text comparisons use binary C collation, including on databases configured with an ICU language locale. Since 3.6.0, sorts on id and required root fields omit the NULLS clause, so the primary key and declared indexes serve them; nullable, joined, embedded and list paths keep MongoDB's null placement, and explicitNullsOrder: true restores the 3.5.9 SQL.

PostgreSQL owner/embedded reads use one repeatable-read snapshot; since 3.6.0, other sessionless reads are a single statement, without BEGIN/COMMIT. Generated and custom mutations share one transaction for hooks, parent rows, owned embeddeds and independent nested writes. Supplied session handles are joined without commit/release; expired/foreign handles are rejected. Confirmed serialization/deadlock aborts retry up to five times, each after a random backoff that starts once the failed attempt has rolled back and released its client. Standalone saveObject also wraps its full lifecycle, including hooks and nested writes, in that transaction.

PostgreSQL identities are UUIDs and native models/sessions are not Mongoose objects. Optional root scalar storage uses SQL NULL for absence. Embedded records preserve absent versus explicit-null fields through private presence markers, materialize descendant list defaults and minimize empty inline objects; native hooks/code still use plain records rather than Mongoose documents. Required typed database columns and real FKs are stronger storage constraints than the Mongo generator. Native methods and pipeline helpers are outside GraphQL API parity.

Unsupported schema mappings are rejected before DDL: implicit many-to-many reciprocal lists, non-embedded collections inside embeddeds, embedded cycles, nested list wrappers, whole embedded-object uniqueness, unrecognized custom scalar storage and conflicting metadata. Whole embedded-object sorting/grouping remains unsupported. Scalar-list leaves inside nested embedded lists support filters/sorts and ragged group projections. Array facts use SUM=0/AVG=null, joined-row COUNT and whole-array lexicographic MIN/MAX. Aggregate sorting selects immediate array extrema, including default groupId order; ties need an additional sort term. Date and string-enum outputs and state-name aggregates retain Mongo's logical JSON values. For numeric and boolean enums, PostgreSQL compares, sorts and groups the text form (aggregate groupId "10", sorted before "2"), while MongoDB uses numbers and booleans. Generated immutable comparison helpers and private presence columns are validated during initialization; existing schemas need an explicit migration. See PostgreSQL documentation for setup and details.

Reference and nested writes retain the generated GraphQL input shapes. The live fixture creates a parent with an ObjectId reference and nested child, patches an embedded object, updates and deletes the child through the parent mutation, clears the reference, reads generated relation fields, and deletes the parent. Scalar list order and duplicates remain storage data. Aggregate queries retain { groupId, facts }, default ascending ordering by groupId (ties are unspecified), and MongoDB numeric results.

Verification ledger ​

The regression suite was first run against the unchanged implementation. All five new tests failed for the expected reasons: the omitted relation fallback discarded the ObjectId, null clearing targeted the API field, the inbound no-endpoint target had no model, onUpdated received a pending query, and only the last flat relation term survived.

Final foundation verification (2026-09-10):

  • Full npm test with both disposable database URIs: 19 files, 361 tests passed, no skips.
  • PostgreSQL integration: 11 tests passed on each of PostgreSQL 15, 16 and 18. Includes live constraints, nullable embedded/enum items, disabled-trigger detection, schema drift, concurrent initialization and orphan-data rollback.
  • npm run lint: passed. Diff whitespace check passed with the original package JSON CRLF convention preserved.
  • npm run test:packages: core, PostgreSQL and MongoDB packed installations passed; core/PostgreSQL installed no MongoDB/Mongoose/MCP packages.
  • The review added regressions for collection-null storage isolation and repeated same-path terms; both fixes pass with the existing filter/update suite.

The foundation verification above is retained as the first increment's baseline. Runtime verification is recorded below after the complete shared-engine and PostgreSQL execution changes.

Independent runtime review reproduced and resolved: inconsistent owner/embedded read snapshots, enum-name/value state collisions, DDL prevention, locale-dependent text comparisons, unsupported array group projections, Boolean/UUID MIN/MAX, and partial standalone owner writes after an embedded FK failure. Review repeated these cases against PostgreSQL 16, including an ICU-configured database, with no open findings.

Runtime verification (2026-09-11):

  • Full npm test with disposable MongoDB 7 and PostgreSQL 16: 28 files, 442 tests passed, no skips.
  • PostgreSQL 15 and 18: 68 schema/runtime/parity integration cases passed on each version, including connection-field aliases and private inverse FKs against MongoDB.
  • npm run lint and the diff whitespace check passed.
  • npm run test:packages installed all three actual archives outside the workspace, exercised their runtimes/exports/introspection, and compiled strict TypeScript consumers with GraphQL 16. Core/PostgreSQL installed no MongoDB, Mongoose or MCP packages.
  • At this runtime checkpoint, CI provisioned MongoDB 7 and PostgreSQL 15/16/18; normal unit runs skipped database suites explicitly when their URI was absent.

v3.1.0 authorization and query limits ​

Nested child saves/updates/deletes run target middleware with root argument shapes. Update/delete ownership is checked from persisted records after middleware selects the effective ID, against the parent's stored identifier (3.5.8), so any spelling of the parent ID that the backend accepts owns its children. Missing records raise NOT_VALID_ID; foreign-parent records raise ForbiddenError (FORBIDDEN). Hooks cannot clear or replace the required parent connection. Query scopes authorize reads only; custom resolvers and native database access retain application-defined checks.

configureQueryLimits({ maxPageSize }) is available on MongoDB and PostgreSQL facades and runtime instances. Its shared process-wide default is 1000; unpaged lists use the smaller of 100 and the configured maximum. Explicit page/size and calculated skip must be positive/safe integers within the maximum; unpaged aggregates remain unbounded. Invalid sort/filter paths and malformed filter values fail with domain errors. v3.1.0 rejects whole-array EQ values and null elements in filter lists; these older permissive parity cases now assert rejection on both backends. The later process-wide configureMutationLimits({ maxNestedOperations }) setting, also on both facades and runtime instances, caps nested collection operations per generated mutation and is unlimited by default.

MongoDB transactions use the registered model connection, retry transient bodies and uncertain commits separately, and await owned cleanup. Each complete MongoDB retry first waits a random backoff that starts after the abort, and a WriteConflict that outlasts the retries fails with TRANSACTION_RETRY_EXCEEDED (409), the code PostgreSQL uses for its exhausted serialization and deadlock retries. PostgreSQL waits the same backoff before each retry, once the failed attempt has rolled back and released its client. Borrowed active sessions remain caller-owned in default Mongo mode. With opt-in transactional reference integrity, violations and guarded-write errors abort supplied sessions; those sessions require snapshot reads and majority commits. See MongoDB reference integrity. PostgreSQL validates instance-owned active handles, retains repeatable-read atomicity, and retries confirmed serialization/deadlock aborts only.

Embedded parity also covers native/GraphQL default and minimization differences, direct SQL presence, uniqueness after omitted-parent defaults, and UTC historical Date parameters under America/New_York. PostgreSQL keeps required constraints: an omitted optional inline parent materialized by descendant array defaults must contain any required scalar (MongoDB instead stores the defaulted parent without it and, since 3.5.9, reads it as null unless that scalar is id, readOnly or resolved by the application); GraphQL create omits explicit null before applying defaults. See the PostgreSQL native/default contract for the explicit-null native alternative.

Embedded query completion verification (2026-09-12): full suites passed 47 files /954 tests on each of PostgreSQL15/Mongo7 and PostgreSQL18/Mongo8; PostgreSQL16/Mongo8 integration plus schema checks passed 12 files /266 tests. All ran under America/New_York. Full lint and all five packed runtime/TypeScript consumers passed. The new embedded differential suite contributes 122 cases.

v3.2.0 package and CI delivery ​

The MongoDB facade, core, PostgreSQL, and MCP packages use version 3.2.0 in lockstep with exact internal dependency versions. All four packages are released on npm. Release archives and registry publication are ordered core, MCP, PostgreSQL, then the MongoDB facade. Core/PostgreSQL install no MongoDB, Mongoose, or MCP dependency chain; MCP and its SDK remain opt-in.

Database CI runs three bounded full-suite jobs: PostgreSQL 15/MongoDB 7, PostgreSQL 16/MongoDB 8, and PostgreSQL 18/MongoDB 8. Each job sets both MongoDB environment variables to distinct databases so the differential and upstream regression suites run without colliding.

PostgreSQL initialization exposes generated tables, primary keys, real FKs, indexes, presence columns, private ownership/key tables, functions, triggers, and maintenance metadata through a runtime's describeDatabase() and the low-level describeDatabase(registrations, { schema }), whose schema defaults to public. Since 3.5.6, the module function called without arguments describes the configured default instance. These physical constraints are PostgreSQL behavior rather than a claim that native Mongoose APIs or storage coercions are identical. See the canonical quick start and detailed storage reference.

v3.3.0 SQL core and PostgreSQL plugin ​

The new @simtlix/simfinity-sql package owns driver-free relational planning, records and session orchestration. PostgreSQL supplies physical schema/query/record compilation, native codecs, initialization and driver operations through postgresPlugin. Existing PostgreSQL factories and namespace entry points use the same SQL runtime and remain compatible. The explicit composition API is createSQL({ plugin: postgresPlugin({ pool, schema }) }); see the SQL plugin contract.

All five packages use exact internal 3.3.0 versions. Publication order is core, SQL, MCP, PostgreSQL, then MongoDB. Installing SQL alone brings no PostgreSQL, MongoDB, Mongoose or MCP driver chain. PostgreSQL is the only supported SQL plugin initially.

Logical capability checks reject missing guarantees before physical compilation and initialization. Foreign keys, embedded ownership, presence/default behavior, list ordering, NULL-equal uniqueness, scopes, hooks and state semantics are preserved. Three golden fixtures compare complete PostgreSQL descriptions and DDL with 3.2.0, including nested/null/unique/enums and special identifiers. An unchanged model requires no generated-schema migration for this extraction. The default options still produce that output; tests/fixtures/postgres-opt-in-schema.json pins the output of the opt-in refreshTriggers: 'perRoot' and ownerIndexes: 'minimal', including for a maintained root with long generated names (3.6.0). Each opt-in snapshot must differ from the default output, and a fixture has one exactly for each opt-in that changes its description.

Verification adds a non-PostgreSQL recording plugin, early contract/capability failures, fixed binding, transaction/session delegation, and real PostgreSQL interoperability between the old and new factories. Recording tests establish the plugin boundary; they do not establish support for a second SQL engine.

SQL extraction verification (2026-09-16): the full suite passed 51 files / 1,029 tests with disposable PostgreSQL 18 and MongoDB 8, with no skips. All six isolated packed runtime/strict TypeScript consumers passed. A separate registry-installed 3.2.0 runtime created real schema and nested data; 3.3.0 createSQL validated that schema without DDL, produced identical metadata/SQL, preserved the records and successfully updated them. Barber passed both backend unit suites (69 PostgreSQL and 59 MongoDB tests), its PostgreSQL 67-operation/native-constraint corpus, MongoDB transaction checks, shared 19-operation HTTP/MCP contract on each backend, both dataset load/delete suites and frontend query validation against the generated schema.

v3.4.0 optional MongoDB reference integrity ​

Mongo adapters accept immutable referentialIntegrity: 'off' | 'transactional' configuration. The default remains off. Transactional mode requires awaited startup auditing, coordinates target-document writes against concurrent deletion, and rejects missing direct/embedded/inferred/private/link references with REFERENCE_CONSTRAINT_VIOLATION. Supplied transactions require snapshot/majority options and abort on guarded-write errors. Native writers remain outside the guarantee. See the full contract. PostgreSQL physical schemas and FK behavior are unchanged.

Verification: 53 files / 1,071 tests passed against disposable MongoDB 8 replica-set and standalone deployments plus PostgreSQL 18, with no skipped tests. The 42 new cases include real GraphQL HTTP rejection, owned/borrowed rollback, post-write hook errors, three ordered concurrency schedules, independent runtime/client locks, actual transient retries, legacy off-mode Query compatibility and strict startup rejection. All six packed runtime/strict TypeScript consumers, lint and the website build passed.

v3.5.0 query authorization and reference batching ​

Client filter, sort and aggregation paths enforce read rules for every segment and restrict referenced records through the target's scope. Fields with custom resolvers require an explicit queryable: true to allow stored-value queries; queryable: false blocks them. Path rules receive the checked field's identity, with no parent and empty arguments. See the upgrade notes for compatibility and scope restrictions.

Generated single references can batch by request, type and nesting level. Both adapters preserve per-field middleware, while scoped targets and MongoDB models with any find or findOne pre/post hook retain individual reads. Shared hook functions do not establish equivalent behavior: hooks may depend on the operation or filter shape.

Regression coverage includes tests/query-path-authorization.test.js, tests/relation-batching.test.js, and their cross-backend counterparts in tests/integration/. Embedded replacement validation is covered by embedded-update-required-parity.test.js; SQL transaction tests cover expired sessions and failed rollback cleanup. The release matrix runs these cases with PostgreSQL 15/16/18 and MongoDB 7/8.

v3.5.5 data-correctness fixes ​

Version 3.5.5 fixes data-correctness defects in the shared runtime, the driver-free planner, MCP classification and the MongoDB adapter, and adds a retry backoff to SQL transactions. The table rows marked "3.5.5" record the resulting contract; see the upgrade notes for the changes applications can observe.

The new regression tests were checked against the 3.5.3 code, where they fail; tests that pin unchanged behavior pass on both. Beyond the suites named in the table, the coverage includes tests/query-plan.test.js, tests/scope-args.test.js, tests/query-path-authorization.test.js and tests/mcp.test.js for fields named like query arguments; tests/mutation-transactions.test.js for the MongoDB retry backoff and the 409 mapping, with real write conflicts in tests/mutation-transactions.integration.test.js and tests/integration/mongodb-integrity.test.js; tests/sql-transactions.test.js for the SQL retry backoff and its order after rollback and release; and tests/sql-plugin.test.js and tests/runtime.test.js for references to a numeric 0 identifier, read one at a time and in batches.

v3.5.6 SQL and PostgreSQL fixes ​

Version 3.5.6 fixes data-loss, schema-validation, query and error-classification defects in the SQL runtime, the PostgreSQL plugin and the shared runtime, and makes the MongoDB adapter reject at startup the fields named like Object.prototype members that Mongoose cannot store. The table rows marked "3.5.6" record the shared contract; see the upgrade notes for the changes applications can observe. PostgreSQL-specific results:

  • Generated queries select owned-table columns by name, so filters, sorts, counts and aggregates through reference-bearing or unique embedded lists keep their results after a migration appended columns or the embedded fields were reordered, for top-level lists, nested lists and singular embedded objects inside lists. tests/postgres-query.test.js checks the generated SQL; tests/integration/postgres-column-order.test.js compares the results before and after such a migration, with nullable and required items.
  • Initialization compares check constants by value, so enum internal values with backslashes and quotes create and validate, including from DDL exported with standard_conforming_strings off: tests/postgres-schema.test.js and enum internal values containing backslashes and quotes in tests/integration/postgres-constraints.test.js.
  • [ID] list columns get no generated index, so lists of 1000 identifiers are stored at the root and in owned tables; databases with the earlier index still validate, keep it and log a warning that names it: tests/sql-schema.test.js and PostgreSQL [ID] list columns in tests/integration/postgres-schema.test.js.
  • A transaction in which a statement failed and was caught, and that reaches COMMIT without another statement, fails with DATABASE_ERROR instead of reporting success with nothing stored. The plugin rejects only when the COMMIT result reports ROLLBACK, so a pool wrapper whose results carry no command resolves as before: PostgreSQL commit in tests/postgres-configuration.test.js and tests/integration/postgres-lifecycle.test.js.
  • Text with NUL (U+0000), native DateTime values before 4714-11-24 BC and the other input-caused data exceptions are rejected with INVALID_VALUE (400) on PostgreSQL, in writes and filters. MongoDB stores such values and matches them in equality filters, while its LIKE filter rejects NUL with a driver error. An index row over the btree size limit stays DATABASE_ERROR (500) on PostgreSQL. tests/postgres-configuration.test.js and tests/integration/postgres-constraints.test.js.
  • Rejected initialization requests keep readiness, a ready runtime keeps serving while it is initialized again, and the initialization started last decides: SQL storage readiness in tests/sql-initialization.test.js.
  • SQL and PostgreSQL runtimes export DDL for their own schema, and the module's describeDatabase() and compileDatabaseSchema() work without arguments: SQL runtime DDL export in tests/sql-plugin.test.js and PostgreSQL DDL export for configured runtimes in tests/postgres-schema.test.js. Both tests/sql-plugin.test.js and PostgreSQL DDL exported by the configured default instance in tests/integration/postgres-schema.test.js run the exported DDL in an empty non-public schema, which validation then accepts.
  • The PostgreSQL package no longer ships the wrapper modules its exports never exposed, and none of its modules imports the deprecated SQL ./internal/* subpaths, which remain available: tests/postgres-packaging.test.js.

The new regression tests fail on the 3.5.5 code and pass with the fixes. Tests that pin unchanged behavior pass on both, such as savepoint recovery after a statement error, retries of an application error with code 40001, and unchanged stateNames for enum states.

v3.5.7 MCP tool contract ​

Version 3.5.7 fixes MCP tool results, tool definitions, argument handling, configuration checks, remote execution and the Streamable HTTP handler. The MCP package behaves the same with either backend. Core gains the field markers it reads, extensions.simfinityQuery.countSink and extensions.simfinityMutation; Yoga, Apollo and Envelop integrations are unchanged. See the upgrade notes for the changes applications can observe and the MCP reference for the contract.

BehaviorDecisionExecutable evidence
A mutation whose result exceeds maxResultBytes and succeeded returns isError: false, _meta.truncated and only its root id(s), when they fit within the cap, next to a truncated notice whose wording follows the operation and idempotentHint and which reports the cap rounded down to whole bytes; with GraphQL errors it stays MCP_RESULT_TOO_LARGE with extensions.applied (true or 'unknown', never false) and firstError. Queries keep the error, whose message also reports the cap rounded down.Fixed (3.5.7)oversized mutation results and the fractional maxResultBytes query rows in tests/mcp-execution.test.js; an oversized generated add with the real runtime in tests/mcp.test.js; SDK Client output validation, including fractional caps, and Streamable HTTP in MCP totalCount and truncation notice contracts in tests/mcp-contracts.test.js.
Counted list calls return totalCount next to the results (__totalCount on collision), keep _meta.count, declare it on list tools with a pagination argument, count it toward maxResultBytes, and put it in extensions.totalCount of an oversized list's error.Fixed (3.5.7)Count tests in tests/mcp-execution.test.js, tests/mcp.test.js and MCP totalCount and truncation notice contracts in tests/mcp-contracts.test.js, including a remote count that is not a non-negative integer.
Generated list tools receive the caller's context object on counted calls; the count reaches MCP through a per-call root-value sink that the generated find resolver reads from parent, then info.rootValue, and otherwise writes context.count. List fields without the countSink marker keep the per-call context layer; the marker, not the resolver that runs, decides, so an application that replaces or wraps a generated list resolver forwards parent or info, reports through the sink or clears the marker before creating the tools.Fixed (3.5.7)MCP counted list calls keep the caller context in tests/mcp.test.js (class, Map, own-property, spread, WeakMap, frozen and null contexts, the stock auth plugin, three-argument wrappers, concurrent calls) and pagination count reporting in tests/runtime.test.js.
Output schemas publish a JSON type only for spec scalars and validated chains ending at one; Date, DateTime, Time, storage-hinted and opaque scalars are untyped, with a curated date/time description. Input schemas keep their hints.Fixed (3.5.7)nullable output schemas in tests/mcp-options.test.js; serializer-defined scalar outputs over Streamable HTTP in tests/mcp-transports.test.js with the SDK 1.32.1 Client.
Input schemas are closed at the top level, and undeclared keys the caller sent return MCP_UNKNOWN_ARGUMENT without executing; keys middleware adds pass, and caller keys middleware deletes pass, including keys named like Object.prototype members.Fixed (3.5.7)MCP unknown arguments in tests/mcp-contracts.test.js (including no remote request) and MCP unknown arguments on generated tools in tests/mcp.test.js.
limits is validated at setup: falsy values are unset, caps are numbers of at least 1, defaultPagination is coerced against each published tool's pagination type; limits is still read on every call.Fixed (3.5.7)setup validation in the limits tests of tests/mcp-execution.test.js and the createHTTPMCPHandler setup table in tests/mcp-contracts.test.js.
includeId is accepted and ignored; selections and output schemas are unchanged.Deprecated (3.5.7)selectionDepth and includeId guards in tests/mcp-options.test.js.
Remote headers accept plain objects, Headers, Maps and pairs, send one Content-Type, are read on every call and never quote values in errors; remote timeoutMs is normalized; invalid containers and timeouts throw MCP_INVALID_EXECUTION_CONFIG at setup, where a getter defined on the execution object for either is not run and a read that throws (an inherited getter or a Proxy) is left to each call. A remote endpoint with credentials throws at setup; other endpoints the default fetch rejects (relative, unparsable, non-http(s)) are accepted at setup, as a Request is, and fail each call; no message quotes the URL.Fixed (3.5.7)endpoint, headers, timeoutMs and remote headers and timeoutMs in tests/mcp-execution.test.js, including the endpoint rows, a relative and a Request endpoint with a stubbed fetch, the headers getters (own, inherited, a Proxy execution object and a Proxy headers object), the timeoutMs getters (own and inherited), and an application fetch error kept for a relative endpoint; real HTTP headers, a Request endpoint and endpoints changed after setup in MCP remote response contracts, and the createHTTPMCPHandler setup table and per-call endpoint rows, in tests/mcp-contracts.test.js.
Client cancellation during a remote retry backoff rejects at once without another request, for callTool, stdio and createMCPServer over a persistent transport. Through the stateless createHTTPMCPHandler, an MCP cancellation notification cannot reach the call in flight; only a closed HTTP connection cancels it, which is documented, not changed.Fixed (3.5.7)cancellation during a retry backoff in tests/mcp-execution.test.js and the SDK Client cancellation over an in-memory transport in tests/mcp-contracts.test.js.
Generated updates whose input can carry added collection items are not advertised as idempotent; deletes stay destructive and idempotent.Fixed (3.5.7)update idempotency in tests/mcp-options.test.js, which repeats an update with added items against the real runtime.
Mutations are classified by extensions.simfinityMutation, or by placeholder description plus name prefix, never by name alone; state-machine actions are transitions; queries without pagination or id are read-only custom queries.Fixed (3.5.7)mutation classification, custom root queries and MCP operation classification of core-generated schemas in tests/mcp-options.test.js, including a schema with the marker removed.
A synchronous tool-middleware throw rejects the previous next() promise, and no dispatched promise becomes an unhandled rejection.Fixed (3.5.7)toolMiddleware tests in tests/mcp-execution.test.js and the createMCPServer boundary test in tests/mcp-transports.test.js.
The server factories load the SDK and their transport before validation, createHTTPMCPHandler's transportOptions check included, and before schemaPlugins; all validation precedes the plugins, so setup errors leave resolvers untouched.Fixed (3.5.7)MCP SDK loading before schemaPlugins in tests/mcp-sdk-setup.test.js; schemaPlugins validation guards in tests/mcp-execution.test.js and tests/mcp-contracts.test.js.
The stateless HTTP handler answers methods other than POST with 405 and Allow: POST, opening no stream.Fixed (3.5.7)createHTTPMCPHandler over Streamable HTTP in tests/mcp-transports.test.js, including an SDK 1.32.1 client that leaves no idle stream.
The factories' TypeScript parameters are the input-only GenerateMCPToolsInit, MCPServerInit and HTTPMCPHandlerInit, which accept the MCPRemoteExecutionInit and MCPLimitsInit forms (numeric strings, bigints, null, false, Headers, Maps and pairs); the option interfaces keep their 3.5.6 shapes for code that reads options back.Fixed (3.5.7)The mcpTypes consumer in scripts/test-packages.js (npm run test:packages), with read-back lines and @ts-expect-error cases.

Each new regression test was checked to fail without its fix and to pass with it; tests labeled as guards pass on both. The unused scripts/repro-mcp.mjs reproduction is replaced by the transition tests above.

Verification (2026-10-06): the full suite passed 82 files / 2,279 tests against disposable MongoDB and PostgreSQL databases, with no skips. The MCP suites ran against @modelcontextprotocol/sdk 1.32.1, within the ^1.31.0 peer range.

3.5.9: MongoDB fixes and follow-ups ​

Version 3.5.9 fixes MongoDB reads of embedded objects that hold no data, embedded cycles in default MongoDB mode and the session of custom mutations whose models use another connection. It also follows up 3.5.8: generated inputs that would have no fields, application types named like the introspection metadata types, and the error classes of the core root. The table rows marked "3.5.9" record the resulting contract; see the upgrade notes for the changes applications can observe. Other coverage:

  • Application types named RelationType or FieldExtensionsType: tests/field-metadata-names.test.js runs tests/fixtures/field-metadata-names.js in fresh processes on core, the MongoDB facade and PostgreSQL with a stub pool, for the fallback names, RESERVED_TYPE_NAME before adapter.prepare() and, for a type inside a registered input's fields, when the schema is constructed, an input-object clash, a failed construction and a second evaluation of the runtime. Registered mutation inputs whose fields function calls getInputType() build and run in a second runtime of the process and after a first createSchema() on the same runtime.
  • The error classes of the core root and the declarations of MongoDB's legacy deep imports: tests/core-subpath-types.test.js, and npm run test:packages, whose MongoDB consumer compiles every declared deep import with exact type equality under NodeNext, Node16, Bundler and Node10. Both consumers also compile export * of the auth and plugins declarations together, core subpaths and legacy deep imports.
  • The MongoDB filter builder lost dead code without a behavior change; MongoDB filters on NonNull object fields in tests/mongodb-query-paths.test.js pins its results.

Each new regression test was checked to fail without its fix and to pass with it; tests labeled as guards pass on both.

3.6.0: fewer reads, statements and round trips ​

Version 3.6.0 reduces the reads, statements, lock writes and round trips of generated mutations, MongoDB reference integrity, PostgreSQL reads, writes and initialization, and MCP tools. It also adds opt-in PostgreSQL schema options and MCP options. The rows marked "(3.6.0)" record the resulting contract; see the upgrade notes for the changes applications can observe. The executable evidence is included in the v3.6.0 source.

BehaviorDecisionExecutable evidence
Generated mutations skip reads by ID whose result they do not use. State actions and nested updated children read their record once, or twice when their input merges a singular embedded object or writes a non-empty embedded list; a nested deleted child is read once, by the ownership check, and onDelete receives that record. A root delete without onDelete, and a root update that merges no embedded value and has neither onUpdating nor nested items, skip their read only when adapter.readByIdIsInert(model) returns true: always on PostgreSQL, and on MongoDB for models without find, findOne, findOneAndUpdate or findOneAndDelete middleware and without application _id setters or overrides of the by-ID statics. Responses, stored data, hook arguments and errors are unchanged.Default (3.6.0)tests/mutation-reads.test.js; tests/integration/mutation-reads.test.js asserts each scenario's response, stored record, hook arguments, reads and in-transaction commands or statements on default and transactional MongoDB and on PostgreSQL, the parent SELECT … FOR UPDATE that PostgreSQL still runs first, and the error scenarios. Guards: a supplied Mongoose model with a findOne guard › still rejects a delete without onDelete and an update that merges no embedded value; a supplied Mongoose model with write middleware › rejects an invalid ID before the write middleware runs; a supplied Mongoose model that overrides the by-ID statics; a supplied Mongoose model with an operation-specific ID setter.
Each transaction attempt receives its own typed input copy: the first is taken before the transaction starts, and a retry copies the request input again.Default (3.6.0)input copies per transaction attempt in tests/runtime.test.js. Guards: a retried attempt starts from the input as middleware left it, a later root field that reuses the input variable gets the request input and the existing retry-isolation tests.
MongoDB transactional integrity reads only a guarded record's _id and reference paths, and locks its targets with one write per target collection, in batches of 1,000, after one existence read when a batch holds several targets, so a missing target fails with REFERENCE_CONSTRAINT_VIOLATION without a retry, unless a target of an earlier collection or an earlier batch of 1,000 is held by another transaction. A guarded delete queries each referencing collection once, with the $or of its paths.Default (3.6.0); MongoDB onlyguarded write round trips in tests/integration/mongodb-integrity.test.js: the save, update, 1,000-target and missing-target tests, a missing target that sorts after a target held by another transaction fails at once without retries, and the delete-probe, self-reference and probe-plan tests. Guards: the single-target test, the sorts before variant, the three conflict tests, a guarded update locks exactly the targets its stored document references, whatever their shape and a single reference through … blocks the delete of its target.
onModelCreated: (model) => model.schema.removeIndex({ id: 1 }) removes the top-level { id: 1 } declaration of a connect() type; generated models still declare it.Documented (3.6.0); MongoDB onlyTop-level id index opt-out in tests/objectid-indexes.test.js.
SQL plugins may advertise singleStatementReads; sessionless reads of types without owned tables then run on one connection without begin and commit, and are retried without a rollback step; after a failure that isRetryable() rejects, rollback probes the connection and a probe failure reaches release, so a dead connection is discarded. Plugins without it keep a transaction around every read. PostgreSQL advertises it.Default for PostgreSQL (3.6.0)SQL single-statement connections and SQL single-statement reads in tests/sql-transactions.test.js; SQL sessionless reads in tests/sql-plugin.test.js; runs sessionless reads of types without owned tables as single statements in tests/integration/postgres-statements.test.js. Guards: the plugin without the capability, keeps one repeatable-read transaction for a read that loads owned rows, and discards a connection terminated during a single-statement read, so the next read succeeds in tests/integration/postgres-connection-loss.test.js.
Owned rows are inserted with multi-row statements when the plugin has compileRecordBatch and maxRecordBatchValues (32,767 values on PostgreSQL) and multiRowInserts is not false. A later item's database error can be reported instead of an earlier item's REFERENCE_CONSTRAINT_VIOLATION; nothing is stored.Default with opt-out (3.6.0)SQL owned-row batches in tests/sql-plugin.test.js; PostgreSQL owned-row batches in tests/postgres-query.test.js; tests/integration/postgres-statements.test.js, including error precedence of a write with several rejected items. Guards: the per-row cases of SQL owned-row batches and inserts one owned row per statement with multiRowInserts: false, from the factory options or configure().
PostgreSQL sorts on id and required root columns omit the NULLS clause, so default-order indexes serve them; other paths keep MongoDB's null placement. explicitNullsOrder: true restores the 3.5.9 SQL.Default with opt-out (3.6.0)PostgreSQL sort null placement in tests/postgres-query.test.js; serves sorts on id and required indexed columns from an index in tests/postgres-sargable.test.js. Guards: keeps the explicit NULLS clause on every sort term when explicitNullsOrder is set, as 3.5.9 did, returns the same pages for required and nullable sorts with a unique tiebreaker, pages sorts on required fields with ties identically in tests/integration/query-parity.test.js, and the explicitNullsOrder test in tests/integration/postgres-statements.test.js.
PostgreSQL MIN and MAX facts over list paths evaluate each list once per row.Default (3.6.0)PostgreSQL list aggregates in tests/postgres-query.test.js and scans an owned list once per root row for MIN and MAX in tests/postgres-sargable.test.js. Guard: aggregates MIN, MAX and COUNT of several list paths in one aggregation in tests/integration/embedded-query-parity.test.js.
PostgreSQL create initialization reads the catalog in a constant number of statements without table locks, locks nothing when nothing is missing, otherwise explicitly locks only the tables of roots whose maintenance objects it creates, in one statement, while other DDL takes its own locks, rebuilds only their keys, and no longer re-scans checks.Default (3.6.0)tests/integration/postgres-initialization.test.js: takes no table locks and runs no backfill when nothing is missing, reads the catalog in a constant number of statements, does not wait for writers on managed tables when nothing is missing, adding an index does not wait for writers on other managed tables, rebuilds no keys and scans no checks, locks only the tables of the root whose keys it rebuilds, in one statement and the four stale-root tests. Guards: reports drift after a missing function exactly as before and reports a foreign key that existing rows violate before later trigger drift, exactly as before.
refreshTriggers: 'perRoot' refreshes each changed root once per transaction through a deferred trigger on its guard table, while that trigger stays deferred (the default). Initialization keeps each existing root on its layout and rejects a root with objects of both. The default description and DDL are unchanged.Opt-in (3.6.0)tests/integration/postgres-refresh-triggers.test.js, including the equivalence cases under both layouts; tests/integration/postgres-constraints.test.js runs its suite under both layouts; tests/sql-schema.test.js keeps the 3.2 golden for the default options and pins the opt-in output in tests/fixtures/postgres-opt-in-schema.json, including a long-named maintained root; tests/integration/postgres-long-names.test.js creates and validates such a root; tests/postgres-schema.test.js rejects unknown values.
ownerIndexes: 'minimal' omits the (__owner_id) index that the primary key or a unique index covers on owned and guard tables; existing ones are accepted and kept.Opt-in (3.6.0)tests/integration/postgres-owner-indexes.test.js, tests/integration/postgres-long-names.test.js and tests/postgres-schema.test.js. Guard: accepts and keeps the owner indexes of a database created with them.
MCP nestedCollections: 'none' leaves nested collections out of the generated selection and outputSchema, globally or per tool. Invalid values throw MCP_INVALID_OPTIONS at setup, for every toolOverrides entry.Opt-in (3.6.0)nestedCollections in tests/mcp-options.test.js; tests/integration/mcp-nested-collections.test.js on both backends; the createHTTPMCPHandler rows in tests/mcp-contracts.test.js. Guard: keeps the 3.5.9 selection and outputSchema when unset or 'all' (guard).
MCP textFormat: 'compact' sends compact JSON text, which maxResultBytes measures.Opt-in (3.6.0)MCP textFormat compact, accepts a payload exactly at its compact size with textFormat: 'compact' and the MCP_INVALID_OPTIONS rows in tests/mcp-contracts.test.js. Guard: keeps the pretty-printed text with textFormat %j (guard).
MCP remote retry.retryAfter and retry.jitter are opt-in; without them, retry k waits backoffMs × k.Opt-in (3.6.0)Retry-After and jitter in tests/mcp-execution.test.js, including reads %s Retry-After as GMT, waiting as long as the IMF-fixdate in any process time zone for the asctime and RFC 850 forms. Guards: the tests that ignore Retry-After without the opt-in, keep the linear schedule, fall back to the backoff for a missing or invalid header (text that is not an HTTP-date, such as May 2030, included), never retry a mutation, and apply timeoutMs to each attempt on real timers.
In-process MCP calls parse and validate each tool's operation once, validate the schema on every call, and return what graphql() returns.Default (3.6.0)tests/mcp-operation-cache.test.js. Guard: MCP in-process results equal graphql() results in tests/mcp-execution.test.js.
Generated MCP tools share one $defs entry per input or enum type; serialized input schemas are unchanged.Default (3.6.0)shared $defs in tests/mcp-options.test.js. Guards: publishes the inputSchema graphqlArgsToJSONSchema builds for each tool, key order included (guard), which also pins the 3.5.9 $defs key order and one tool's full serialized inputSchema, and keeps graphqlArgsToJSONSchema returning independent objects (guard).
MCP option interfaces gain only optional members; existing members keep their 3.5.6 shapes for code that reads options back.Default (3.6.0)The mcpTypes consumer in scripts/test-packages.js (npm run test:packages), with read-back lines and @ts-expect-error cases.
Core, SQL and PostgreSQL declarations gain only optional members: DatabaseAdapter.readByIdIsInert, SQLPlugin.compileRecordBatch and maxRecordBatchValues, the trailing configuration argument of describeSchema and compileQuery, the singleStatementReads capability, multiRowInserts, explicitNullsOrder, refreshTriggers, ownerIndexes, and DatabaseTrigger.events and when, plus the new SQLInsertManyOperation type. SQLRecordOperation is not widened.Default (3.6.0)The coreTypes, sqlTypes and postgresTypes consumers in scripts/test-packages.js (npm run test:packages): the 3.5.9-shaped adapter, plugin and configuration objects still compile, the new members compile, and @ts-expect-error cases reject wrong value types.

Each new regression test was checked to fail on 3.5.9 and to pass with the change; tests labeled as guards pass on both.

3.7.0: batched collection reads and one EnvelopSchemaPlugin ​

Version 3.7.0 can read generated collection fields in batches, opt-in on PostgreSQL and MongoDB, and makes @simtlix/simfinity-mcp re-export core's EnvelopSchemaPlugin. The rows below record the resulting contract; see the upgrade notes for the changes applications can observe. The evidence is in the repository's tests/ and scripts/test-packages.js.

BehaviorDecisionExecutable evidence
Generated collection fields of one request that share a related type, a connection storage field and effective arguments of plain data, requested in the same tick, are read with one findChildrenBatch call of at most 100 distinct parents, when the adapter defines it and batchesChildren does not return false; a group with one parent calls findChildren. Middleware, scopes, joined scopes and auth rules run per field; fields of one parent share its records with their own lists; a failed or misshapen batch reads each field alone and warns once per type unless the error is a SimfinityError. Without the method, or when the predicate returns false, each field calls findChildren at once with its own arguments object, as in 3.6.0.Default when the adapter defines findChildrenBatch and batchesChildren does not return falsetests/collection-batching.test.js on a memory adapter, including the grouping key block, read at once, with the arguments object the field built, and call nothing else, which pins the 3.6.0 path, a findChildren override disables batching through the pairing check, so the override applies, concurrent requests with the same arguments keep separate batches, each in its own async context, the the parent a batched record shows block and the failed batches that change their arguments; tests/integration/collection-batching-core.test.js passes both bundled adapters' records and raw parent IDs through a batch. Guards: a lone parent reads with findChildren, a middleware change to a shared variable object reaches each parent as it would per parent, middleware that waits for I/O still returns the per-parent results, cyclic, accessor, sparse, inherited, sealed, hidden and exotic arguments read per parent, a request without a context object reads per parent without asking the adapter and invalid pagination fails each field without a batch.
With batchedCollections: true, PostgreSQL reads a group with one statement per 100 parents, a UNION ALL of each parent's statement with its own parent parameter, ordered by parent position and row number, so each parent is planned for its own ID, in one repeatable-read transaction on one connection: BEGIN, SET LOCAL jit = off, the statement, one owned-row statement per owned table, COMMIT. Total orders return the same rows; unsorted and tied rows normally keep their order, which PostgreSQL does not promise, and pages that hold all of a parent's rows hold the same rows. Only true enables it: otherwise the adapter has no findChildrenBatch and sends 3.6.0's statements on 3.6.0's connections. A plugin copy whose compileQuery is not the one compileChildrenQuery names reads per parent.Opt-in (PostgreSQL batchedCollections: true)tests/postgres-collection-batching.test.js; tests/integration/postgres-collection-batching.test.js, which compares every request with a batchedCollections: false runtime, exactly, unsorted and tied rows included, with the skewed parents after HOT updates block and its plan test, the skewed connection statistics block, whose EXPLAIN shows every small parent read through the connection index, with no sequential or primary-key scan filtering on the connection column, and each parent of a mixed batch planned as its own statement, and the JIT block, whose batch costs more than jit_above_cost with a JIT block under jit = on, and none after the SET LOCAL jit = off the adapter sends, while the connection is back at the server's setting after release; tests/integration/postgres-statements.test.js (books{chapters} in 6 statements on 3 connections instead of 22 on 22); tests/sql-plugin.test.js. Guards, which assert 3.6.0's statements: defines neither member by default, nor for any batchedCollections value other than true, sends 3.6.0's statements by default and with batchedCollections: false: one per field, each on its own connection, sends exactly the statements of batchedCollections: false, 3.6.0's, by default, reads each collection field with its own statement by default and with batchedCollections: false, from the factory options or configure(), leaves compileQuery's per-parent statement as it was and turns batching off for a plugin copy whose compileQuery hides rows, so its filter applies to every parent.
A children statement may carry setup statements, which the SQL adapter runs, in order, on the same connection right before it; when any statement of a batch has setup, the whole batch, owned-row hydration included, runs in one owned transaction, even for types without owned tables. A setup that is not a list of { text } statements fails the batch with a TypeError before a connection is taken, which the runtime reports once per type before reading each parent alone. PostgreSQL's setup is SET LOCAL jit = off, a fresh list per statement.Plugin contract, used with batchedCollections: trueruns a plugin's setup statements in order before its statement, and a batch without setup as findChildren runs, splits more than 100 distinct parents into statements of 100 in one transaction, each after its setup, fails with a TypeError before taking a connection when setup is not a list of statements, reads each parent alone, and reports it once, when a plugin returns setup that is not a list of statements, fails with DATABASE_ERROR, and rolls the transaction back, when a row names no parent of its statement and asks for JIT to be off before every batched statement, with a fresh setup list each time in tests/postgres-collection-batching.test.js.
With createMongoAdapter({ batchedCollections: true }), MongoDB reads a group with concurrent aggregate commands of up to collectionBatchSize parents (1 to 100, default 5), each parent's pipeline in its own $unionWith sub-pipeline. Models with aggregate middleware, an own aggregate static, a discriminator child model or an own collection.aggregate, sessions, transactionAsyncLocalStorage transactions and findChildren overrides keep per-parent reads. Without the option the adapter has neither member. Invalid values throw INVALID_MONGO_ADAPTER_OPTIONS (400).Opt-in (MongoDB only)tests/mongodb-collection-batching.test.js and tests/integration/mongodb-collection-batching.test.js: 20 authors read their books with 4 commands instead of 20, total orders compared exactly and other orders as sets, rotated parent order, a supplied model without an index on its connection field, and the gates. Guards: is off by default, and then the adapter has neither batching member, keeps rejecting unknown and misspelled options as before, keeps per-parent reads for aggregate middleware and discriminators, which a batch would bypass and keeps per-parent reads for an override of findChildren, so the override applies.
Declarations gain only optional members: DatabaseAdapter.findChildrenBatch and batchesChildren, SQLPlugin.compileChildrenQuery, SQLConfiguration and PostgresConfiguration.batchedCollections, and MongoAdapterOptions.batchedCollections and collectionBatchSize, plus the new SQLChildrenStatement, with its optional setup, and SQLChildrenQueryCompiler types.DefaultThe coreTypes, sqlTypes, postgresTypes and mongoTypes consumers in scripts/test-packages.js (npm run test:packages): 3.6.0-shaped objects still compile, the new members compile, and @ts-expect-error cases reject wrong shapes. The exact-optional.ts fixtures compile the documented undefined opt-outs with exactOptionalPropertyTypes.
@simtlix/simfinity-mcp re-exports core's EnvelopSchemaPlugin, whose hook returns void or Promise<void>, and createAuthPlugin returns the new AuthSchemaPlugin, whose hook returns void. So export * of MCP or src/mcp.js with the core root, /auth, /plugins, the database packages or MongoDB's legacy deep imports exports one declaration.Fixed: one declaration, plus AuthSchemaPluginschema-plugins.ts in the core consumer, whose shared shape also pins the void-sink and implementing-class breaks with @ts-expect-error; barrel.ts in the mcp (NodeNext, Node16, Bundler and Node10), mcp-sdk, sql and postgres consumers; legacy-mcp-auth.ts, legacy-mcp-plugins.ts and legacy-mcp-root.ts in the mongo consumer of scripts/test-packages.js; EnvelopSchemaPlugin declarations in tests/core-subpath-types.test.js. Guard: legacy-mcp-workaround.ts, the explicit 3.5.9 re-export, which compiles on both versions.

Each new regression test was checked to fail on 3.6.0 and to pass with the change; tests labeled as guards pass on both. 3.6.0 rejects the MongoDB options batchedCollections and collectionBatchSize, so the MongoDB guards ran there through a test shim that drops those options before creating the adapter. The PostgreSQL tests of the opt-in, of setup and of JIT were also checked to fail on an earlier draft that batched by default without a transaction, and the default's statements were compared with the 3.6.0 adapter's on five query shapes.

Open source. Released under the Apache 2.0 License.