Packaging decisions
| decision | choice | reason | what would reverse it |
|---|---|---|---|
| statement planning | in the client library, in the calling process; executors use the native driver | using the ORM needs only the library; no service or daemon to deploy and operate | none |
| model generation | one generator per language: orm-gen gen --lang go (Go), vendor/bin/orm-gen (PHP), the orm-gen npm bin (TypeScript), the polyspec-orm-build crate in build.rs (Rust), the orm-gen script of the Python package (Python); each takes the document set only as its set file dbspec-set.json (document sets) | each language builds with its own tool chain, and one set file declares the set for all of them | none |
| equality across languages | tests/conformance vectors on MySQL, PostgreSQL, and SQLite | The six configured implementations—Go, PHP, PHP extension, TypeScript, Python, and Rust—share one SQL, bind, and result behavior; completed execution coverage is tracked separately | none |
| artifacts | orm-gen-<version>-<os>-<arch>, SHA256SUMS | the Go orm-gen runs at build time; the schema operations are client library functions | — (the version comes from VERSION; file names carry it, never a "latest" symlink) |
| distribution | Go: module path; PHP: composer package polyspec/orm with bin/orm-gen; TypeScript: npm package @polyspec/orm with the orm-gen bin; Rust: crates polyspec-orm and polyspec-orm-build | — | a registry publish is a separate decision |
| configuration | DSN and secrets are arguments of the connect call; the schema path is a connection option (Go, PHP, TypeScript), the generated schema is an argument of Db.connect_schema (Python), or it is embedded by the build (Rust) | — | — |
| drivers | Go go-sql-driver/mysql, pgx, modernc.org/sqlite; Rust sqlx; PHP pdo_mysql, pdo_pgsql, pdo_sqlite; TypeScript mysql2, pg, node:sqlite | perf.md §4 | a measured 2× win from another driver on the hot path |
| YAML codec | Go github.com/goccy/go-yaml (read, which keeps U+0085 as content) and go.yaml.in/yaml/v3 (write), Python rapidyaml (read) and ruamel.yaml (write), PHP purrphp/yaml (read) and symfony/yaml (write), Rust saphyr-parser (read) and serde-saphyr (write), TypeScript yaml; lock files are committed. The PHP extension currently reaches YAML through PHP library calls; native codec replacement remains open under T182-2. | The 80 vectors and invalid-input cases define the shared expectation for Go, PHP, PHP extension, TypeScript, Python, and Rust; this states the expected behavior, not completed native codec coverage. | replace a library only when the shared vectors and error cases remain unchanged |
Installing from release assets
In 0.0.x the polyspec packages are distributed through GitHub only, as release assets and tags: the npm, Composer and Python packages are on no registry (npm, Packagist, PyPI). Each GitHub Release carries polyspec-orm-npm-<version>.tgz, polyspec-orm-php-<version>.zip, polyspec-orm-gen-php-<version>.zip, polyspec-orm-php-ext-php-<version>.zip (<package>-<language>-<version>.<ext>) and the Python wheel polyspec_orm-<version>-py3-none-any.whl (see Python client release). Each asset holds the manifest of the repository tree unchanged, and that manifest names every other polyspec package by the exact version of its release, so a project downloads the release assets it needs and installs them together. make release-assets TAG=vX.Y.Z builds them from the tagged commit.
npm: list every tarball as a
file:dependency, the orm tarball and the tarball of each polyspec package it depends on, from the GitHub Release of the version that the ormpackage.jsonnames. npm satisfies the exact version of@polyspec/ordered-jsonwith the tarball installed beside it:json{ "dependencies": { "@polyspec/orm": "file:vendor/polyspec-orm-npm-<version>.tgz", "@polyspec/ordered-json": "file:vendor/polyspec-ordered-json-npm-0.0.9.tgz" } }Composer: an
artifactrepository is a directory of the downloaded zips, and Composer reads the name, version and requirements of each zip from itscomposer.json. The artifact repository holds the orm zip and the zip of each polyspec package that it requires (polyspec-ordered-json-php-0.0.9.zip).json{ "require": { "polyspec/orm": "<version>" }, "repositories": [ { "type": "artifact", "url": "vendor/polyspec" }, { "type": "vcs", "url": "https://github.com/min-0-max/yaml", "no-api": true } ] }The client requires
purrphp/yaml1.0.0-p1, the release of the fork https://github.com/min-0-max/yaml that reads plain scalars by the core schema of YAML 1.2.2 (section 10.3.2); the upstream 1.0.0 release does not. Composer readsrepositoriesonly from the root manifest of a project, never from a dependency's, so the orm package cannot carry this source: a consumer declares thevcsrepository itself, as above.no-apimakes Composer clone the public repository over HTTPS instead of asking the GitHub API, whose anonymous rate limit (HTTP 403) would otherwise send it to an SSH clone that needs a key.polyspec/orm-php-exthas the typephp-ext: PIE builds and installs it, and Composer does not.
Python client release
The Python client, packages/python/pyproject.toml (distribution polyspec-orm), is released as a PEP 427 wheel, polyspec_orm-<version>-py3-none-any.whl, one of the assets of its GitHub Release beside the npm and Composer archives. make release-assets TAG=vX.Y.Z builds the wheel from the tagged commit with uv build, which takes the build backend from build-system.requires of the pyproject.toml (setuptools==84.0.0). The wheel keeps the PEP 427 name because it is not an archive of the <package>-<language>-<version>.<ext> form.
A Requires-Dist of the wheel is an exact name==version for a registry package, and a direct reference with the sha256 of the asset for a polyspec package (kit K49): polyspec-ordered-json @ https://github.com/polyspec/ordered-json/releases/download/v0.0.9/polyspec_ordered_json-0.0.9-py3-none-any.whl#sha256=bc4d…f01d. ordered-json is on no package index, and the reference names its release asset, so a pip install without a lock fetches it from GitHub and never asks a package index for that name. The lock of the Python client, packages/python/uv.lock, holds every dependency, transitive ones included, with the hash of each file, and ordered-json by the URL of its release asset with its sha256; make orm-python-lock writes it with uv lock from packages/python/pyproject.toml. make orm-install-python installs the client non-editable from it with uv sync --locked --no-editable --group test, and uv checks each file by the hash of the lock. The wheel supports Python 3.14 and CPython only: requires-python is >=3.14, the minor release of the Python of mise.toml that CI tests, and no older release, since the dependency polyspec-ordered-json==0.0.9 requires 3.14; packages/python/tests/test_requires_python.py compares the two. The toolchain of orm is the CPython of python-build-standalone, and the package declares the classifier Programming Language :: Python :: Implementation :: CPython. Its dependency on psycopg-binary==3.3.6 therefore names the binary build of psycopg directly, with no marker for other implementations.
The binary extra of psycopg is written as the two plain requirements that it expands to, psycopg==3.3.6 and psycopg-binary==3.3.6. The extra excludes PyPy, which a plain requirement cannot express, so psycopg-binary is also required there.
Development layout
The released manifests are packages/ts/package.json, packages/php/composer.json and packages/php-ext/composer.json. They take the polyspec packages of other repositories by exact version and declare no repository. Two private root manifests, which are never released, resolve those versions in this repository:
- package.json at the repository root lists
packages/*underworkspaces, and itsoverridestake@polyspec/ordered-jsonfrom the tarball URL of its GitHub Release, the URL naming the release tag, and takevite7 for vitepress, whose latest release depends onvite5, a release with advisories.npm ciat the root installs the workspace. package-lock.json at the root is the one npm lockfile. - composer.json at the repository root installs packages/php from a
pathrepository, polyspec/ordered-json from apackagerepository whosedistis the zip URL of its GitHub Release with its shasum (the URL names the release tag), purrphp/yaml from thevcsrepository of the fork min-0-max/yaml, and the development dependency opis/json-schema2.6.0-p1from thevcsrepository (no-api) of the fork min-0-max/json-schema, which reports every refusal in its collect-all-errors mode for the PHP vector runners (tests/dbspec/vectors.php); no released manifest requires it, so a consumer declares nothing for it. Itsvendor-diris vendor-php, because vendor at the root is the vendor directory of the Go module, and itsconfig.platform.phpis8.4.0, the lowest PHP of the clients. composer.lock at the root is the one Composer lockfile, and the PHP tests load vendor-php/autoload.php.
Tracked generated sources
The error-code sources of the clients are generated by orm-gen errors --lang <lang> from docs/spec/errors.yaml, and they are tracked, not build output: packages/go/orm/codes.go, packages/php/src/Code.php, packages/rust/orm/src/codes.rs, packages/ts/src/codes.ts and packages/python/src/polyspec/orm/codes.py. The reason is that each of them is part of the source that its consumer installs (a Go module, a Composer archive, a cargo package, a TypeScript package whose build ships dist, and the Python sdist and wheel), and none of those installs runs a generator; orm-gen errors is one Go generator for every language, so the TypeScript build does not need the Go toolchain and does not regenerate codes.ts. The five paths are declared exceptions in config/generated.json. Each client's code-catalog test (packages/go/orm/codes_test.go, packages/php/tests/CodeCatalogTest.php, packages/rust/orm/tests/code_catalog.rs, packages/ts/tests/code-catalog.mjs, packages/python/tests/test_code_catalog.py) keeps its file equal to docs/spec/errors.yaml, and the error-codes feature regenerates them to check them.