@nublox/metaobject-storage-mysql
v1.0.0
Published
Release-qualified MySQL StorageAdapter, MetadataStore, safe query pushdown and versioned schema migrations for @nublox/metaobject.
Readme
@nublox/metaobject-storage-mysql
MySQL persistence for @nublox/metaobject, implemented against the public StorageAdapter and MetadataStore contracts using the NuBlox @nublox/mysql client.
Status
M79 exact MySQL identity semantics. Version 0.9.0 targets stable @nublox/[email protected] and @nublox/[email protected], advances the object/metadata physical schemas to version 3, and is published under npm next through the trusted GitHub Actions workflow with provenance.
The package remains inside the NuBlox/metaobject monorepo while the MetaObject core remains database-neutral and MySQL-free.
Requirements
- Node.js 22 or newer
- MySQL 8.x
@nublox/[email protected]@nublox/[email protected]
Usage
import { createPool } from "@nublox/mysql/promise";
import {
MySqlMetadataStore,
MySqlStorageAdapter,
compileMySqlObjectQueryPlan,
} from "@nublox/metaobject-storage-mysql";
const pool = createPool({
host: "127.0.0.1",
user: "root",
password: "secret",
database: "app",
timezone: "Z",
supportBigNumbers: true,
bigNumberStrings: true,
});
const storage = new MySqlStorageAdapter(pool);
const metadata = new MySqlMetadataStore(pool);
await storage.initialize();
await metadata.initialize();
const plan = compileMySqlObjectQueryPlan(storage.tableName, {
objectType: "example.item",
where: [{ attribute: "status", operator: "eq", value: "open" }],
limit: 25,
});
console.log(plan.pushedFilters.length, plan.residualFilters.length);initialize() runs the versioned physical-schema migration engine before returning. Custom tables, migration-ledger/lock settings and transaction retry policies can be configured independently. Transient MySQL deadlock/lock-timeout failures retry twice by default through @nublox/mysql. Optimistic-concurrency failures are not retried as transient lock failures.
Runtime object persistence
MySqlStorageAdapter stores runtime object snapshots in InnoDB keyed by (object_type, object_id) with optimistic object versions, atomic batches, detached reads and the public query contract.
The bounded lossless codec preserves Date, BigInt, undefined, NaN, infinities and negative zero while rejecting cycles, accessors, sparse arrays, malformed canonical values and hostile/tampered payloads.
Since M79, object_type and object_id explicitly use utf8mb4_0900_bin. Case variants and trailing-space variants are therefore distinct identities, matching the JavaScript string equality used by the stable-v1 reference adapter.
Metadata persistence
MySqlMetadataStore is backed by a separate InnoDB table keyed by (object_type_id, object_type_version) and implements optimistic metadata revisions, atomic ordered batches, deterministic filtering and fail-closed decoding.
The metadata envelope is stored as validated LONGTEXT rather than native MySQL JSON, preserving exact record-member order required by the stable-v1 conformance suite while retaining JSON_VALID(...) enforcement.
Since M79, object_type_id explicitly uses utf8mb4_0900_bin so metadata identities have the same case/trailing-space semantics as MemoryMetadataStore.
M72 query pushdown
MySqlStorageAdapter.query() compiles the SQL-safe subset of ObjectQuery and executes it with NuBloxSQL server-side prepared statements through PromisePool.execute().
The following predicates are pushed into MySQL while preserving the stable-v1 StorageAdapter semantics:
eqandneqfor persisted primitive values, includingundefined,null, booleans, strings, finite numbers,NaN, infinities, negative zero andBigInt;inandnotInusing the sameObject.issemantics as the reference adapter;isNullandisNotNull, including the distinction between absent attributes, encodedundefinedand encodednull.
When every filter is SQL-safe and no attribute sort is requested, non-negative safe-integer limit/offset pagination is also pushed into MySQL.
Every attribute JSON path is supplied as a bound prepared-statement parameter. Attribute names are never interpolated into SQL text. The adapter decodes returned rows and reapplies the original predicates as a semantic defence-in-depth check.
Deliberate fallback boundary
The following remain in JavaScript for stable-v1 compatibility:
gt,gte,lt,lte;contains,startsWith,endsWith;- attribute
orderBy.
The core reference adapter currently uses JavaScript localeCompare() for string and mixed-type comparison. MySQL collation ordering is not guaranteed to be equivalent. M72 therefore prefers a correct fallback over a faster but observably different SQL result. A future core comparison contract can make those semantics deterministic enough for broader pushdown.
compileMySqlObjectQueryPlan() is exported for diagnostics and tests. It reports the generated prepared SQL, parameters, pushed filters, residual filters and whether pagination was pushed.
M73/M79 schema evolution
The object and metadata physical schemas are both at version 3 and are managed by an append-only migration ledger:
- version
1— immutable pre-M73 baseline; - version
2— composite lookup/query indexes; - version
3— exact identity-column collations.
Version 2 adds:
object storage: (object_type, schema_version, object_id)
metadata storage: (object_type_id, status, object_type_version)Version 3 changes only identity-bearing columns to utf8mb4_0900_bin:
object storage: object_type, object_id
metadata storage: object_type_idThe migration layer provides:
- SHA-256 migration-definition checksums;
- contiguous-version and newer-than-supported guards;
- automatic adoption of structurally valid legacy tables;
- idempotent crash recovery when DDL committed before its ledger row was written;
- a dedicated MySQL named advisory lock for each database/component/target-table migration stream;
- structural verification through
information_schemafor engine, table collation, required columns/types/nullability, explicit identity-column collations and required indexes; - fail-closed rejection of migration-ledger tampering and required-schema drift.
MySQL DDL is not treated as one rollbackable transaction. The migrator relies on serialized initialization, MySQL's atomic DDL guarantees for individual operations, post-DDL verification and recoverable ledger reconciliation.
Migration configuration is available through adapter/store options:
const storage = new MySqlStorageAdapter(pool, {
migrations: {
migrationTableName: "metaobject_schema_migrations",
lockTimeoutSeconds: 30,
},
});The infrastructure functions are also exported directly:
await migrateMySqlStorageSchema(pool);
await migrateMySqlMetadataSchema(pool);See ../../docs/mysql-schema-migrations.md for the complete lifecycle and failure model.
M74 certification and stress hardening
M74 adds an executable certification gate above the focused conformance suite. It certifies the adapter with:
- a deterministic 1,200-object corpus compared query-for-query with the core
MemoryStorageAdapterreference implementation; - 16 query shapes spanning pushed equality/membership/null predicates, pagination and residual range/string/order semantics;
- 400 concurrent reads through a four-connection pool, followed by health/queue/connection-return assertions;
- 32 concurrent object writers and 32 concurrent metadata writers against one version/revision, requiring one winner and
ConcurrencyErrorfor every loser; - object and metadata batch rollback probes where multiple valid writes execute before an intentional stale write;
- 16 concurrent
initialize()calls against one migration stream using a six-connection pool, requiring one ledger row per schema version; - broad elapsed-time regression guardrails plus machine-readable
M74_CERTIFICATIONevidence lines.
Run the live certification locally with:
npm run test:liveSee ../../docs/mysql-m74-certification.md for the acceptance model, thresholds and evidence boundary.
M75 external-consumer release qualification
M75 verifies the packed adapter from a completely separate temporary project instead of relying only on repository-local imports.
The clean-consumer gate packs the package, installs that tarball with lifecycle scripts disabled, resolves the declared published NuBlox dependencies from npm, runs an ESM runtime import probe, and compiles a strict NodeNext TypeScript consumer against the public package declarations.
CI runs this clean-consumer gate on Node.js 22 and 24. The live persistence/certification job also runs independently against MySQL 8.0 and MySQL 8.4.
Run repository-local release qualification with:
npm run release:checkWith MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD and MYSQL_DATABASE configured, run the complete package + live database qualification with:
npm run release:check:liveSee ../../docs/mysql-m75-release-qualification.md for the exact release boundary and acceptance criteria.
M78 stable-v1 alignment
M78 moved the adapter from the immutable RC-core dependency used by 0.7.0 to stable core 1.0.0 without changing adapter runtime behaviour. The packed clean-consumer gate reads the actually installed core package and requires exactly 1.0.0 before runtime and strict TypeScript checks execute.
0.8.0 was qualified on Node.js 22/24 and MySQL 8.0/8.4, then published under npm next with trusted GitHub Actions provenance. See ../../docs/mysql-m78-stable-v1-alignment.md.
M79 exact identity equality
M79 corrects the remaining MySQL key-comparison mismatch with the stable-v1 reference stores.
The prior utf8mb4_unicode_ci key behaviour could collapse identifiers that JavaScript treats as different. Schema v3 moves only the identity columns to utf8mb4_0900_bin, a binary NO PAD MySQL 8.x collation. The live regression suite explicitly persists and retrieves object and metadata identities that differ only by case or trailing spaces.
M79 also:
- upgrades v1 and v2 databases through the immutable migration chain to v3;
- checks required identity
COLLATION_NAMEvalues throughinformation_schema; - fails closed if a later DDL change reintroduces identity-collation drift;
- extends migration-stampede certification to require one ledger row each for versions 1, 2 and 3;
- advances the adapter package to
0.9.0while leaving stable core@nublox/[email protected]unchanged.
0.9.0 was qualified on Node.js 22/24 and MySQL 8.0/8.4, then published from storage-mysql-v0.9.0 under npm next using trusted GitHub Actions publication with signed provenance. See ../../docs/mysql-m79-exact-identity.md.
Conformance
CI executes both runStorageAdapterConformance and runMetadataStoreConformance against MySQL 8.0 and 8.4, followed by M70/M71 concurrency/tamper tests, M72 live query-equivalence cases, M73/M79 migration fresh-install/upgrade/race/drift/ledger-integrity handling, M79 exact identity regression coverage and the M74 certification stress gate. Separate Node.js 22/24 jobs install the packed adapter into a clean external consumer and verify runtime plus TypeScript package-root consumption against stable @nublox/[email protected].
Publication
Version 0.9.0 is published from the immutable annotated tag:
storage-mysql-v0.9.0The tag resolves to commit 2416eabfa29d7d51db41ab431c472bf2d363c94f. GitHub Actions workflow run 36367576342 verified tag/version/main ancestry, reran release:check, authenticated through npm trusted publishing and published explicitly with --access public --tag next. npm accepted @nublox/[email protected] and the workflow emitted signed provenance to the Sigstore transparency log.
Published versions are immutable; fixes after publication require a new version. Existing 0.8.0 and 0.9.0 remain immutable.
License
Apache-2.0. Copyright 2026 Stephen J T Spittal.
