Schema
The schema source is a set of dbspec documents (.dbs). dbspec.md defines the language, its validation rules, the manifest text and the two hashes; contracts/fixtures/bench/bench.dbs is the schema of the tests and benchmarks. No other schema file exists: every generator, runtime and schema operation reads the document set.
1. Model generation
The ORM client packages generate models from the document set with their own tools (usage). The six configured implementations are Go, PHP, PHP extension, TypeScript, Python and Rust. The PHP extension exposes Dbspec operations but does not generate ORM models:
| Language | Tool |
|---|---|
| Go | orm-gen gen --set <dbspec-set.json> --lang go --out <directory> --scan <package pattern>... |
| PHP | vendor/bin/orm-gen gen --set <dbspec-set.json> --out <directory> --namespace <namespace> |
| TypeScript | orm-gen gen --set <dbspec-set.json> --out <directory> --scan <path> of @polyspec/orm |
| Python | orm-gen gen --set <dbspec-set.json> --out <directory> --scan <path>, the console script the package installs |
| Rust | polyspec_orm_build::Builder::set("<dbspec-set.json>").scan("src").generate() in build.rs |
SCH-1 Each tool takes the document set only as its set file and reads it with its client's set reader; a set file with an error makes the tool print every error and write nothing. Each tool then reads every listed document file with the file reader of its client, which rejects a file that does not start with the dbspec signature as a
signatureerror before parsing (files); the tool then fails withSCHEMA_INVALIDand writes no model.SCH-2 The
github.com/polyspec/orm/packages/gomodule exposes Go generation to other programs in its packagegenerator.generator.Generatetakes the runtime model of the document set, the output directory,PackageName(the directory name by default) andScan, the package patterns whose calls are generated. Naming, field mapping and output stay in the ORM generator.
2. Schema operations
The schema operations are functions of each client library. They take parsed documents and a dialect (mysql, postgres or sqlite) and return statements or located diagnostics.
| Operation | Go packages/go/engine/dbspec | PHP Polyspec\Orm\Dbspec\Dbspec | PHP extension | TypeScript | Rust | Python | |---|---|---|---|---|---| | Render the statements of a document set (dialects) | Render | render | Dbspec::render | renderDbspec | polyspec_orm_schema::dbspec::render | polyspec.orm.dbspec.render.render_dbspec | | Introspect a database into a document (dialects) | Introspect | introspect | Dbspec::introspect | introspectDbspec | polyspec_orm::dbspec::introspect | polyspec.orm.dbspec.introspect.introspect_dbspec | | Diff a plan against its source (plans) | Diff | diff | Dbspec::diff | diffPlan | polyspec_orm_schema::dbspec::diff | polyspec.orm.dbspec.plan_diff.diff_plan | | Compare a database with a document set (installation) | InstalledDifferences | installedDifferences | Dbspec::installedDifferences | installedDifferences | polyspec_orm_schema::dbspec::installed_differences | polyspec.orm.dbspec.installed.installed_differences | | Write the steps of a plan with their rollback statements (plans) | PlanSteps | planSteps | Dbspec::planSteps | planSteps | polyspec_orm_schema::dbspec::plan_steps | polyspec.orm.dbspec.plan_steps.plan_steps | | Apply a plan chain (plans) | Apply | apply | Dbspec::apply | applyPlans | polyspec_orm::dbspec::apply | polyspec.orm.dbspec.apply.apply_plans | | Continue an interrupted plan (plans) | Recover | recover | Dbspec::recover | recoverPlans | polyspec_orm::dbspec::recover | polyspec.orm.dbspec.apply.recover_plans | | Roll back the last plan (plans) | Rollback | rollback | Dbspec::rollback | rollbackPlans | polyspec_orm::dbspec::rollback | polyspec.orm.dbspec.apply.rollback_plans | | Finalize the applied plans (plans) | Finalize | finalize | Dbspec::finalize | finalizePlans | polyspec_orm::dbspec::finalize | polyspec.orm.dbspec.apply.finalize_plans |
The PHP extension declarations expose the PHP-facing Dbspec parse/render/introspection surface. ORM schema management remains a separate feature; catalog connection behavior is specified by SCH-6a and implemented by the extension's native catalog surface.
mermaid.md specifies the export of a document to a Mermaid erDiagram and the import of a diagram into a document.
3. Rust build-time catalog helpers
SCH-3 The
polyspec-orm-buildcrate's build-timetool_dbdecoder preserves actual SQL NULL and supported integer/text/boolean values, but rejects unsupported types, invalid UTF-8 and unsigned integers beyond signed 64-bit range. It must not substitute SQL NULL, replacement text or wrapped integers. These build-time helper values are separate from the typed grid cells of the shared catalog connection in interfaces.SCH-4 The
polyspec-orm-buildcrate's build-timeVal::int(),opt_int()andbool()return checked results. Required integer/boolean conversions reject SQL NULL. Optional integers preserve NULL asNone. Booleans accept only native booleans, integer 0/1 and textt,f,true,false,1,0; malformed values never become defaults. Errors omit the input value and propagate through build-time catalog operations, including transaction cleanup.
Catalog connections
SCH-5 Each client exposes its native catalog connection from one DSN, with no separate driver argument. The DSN selects the database. A catalog connection reads table metadata and pages and changes rows; runtime model execution uses its own connection surface, and build-time schema introspection reads the schema of a database.
SCH-6 SQLite catalog connections require an existing regular database file and disable automatic file creation. Closing a catalog releases its owned database resources; an operation after close fails with
CATALOG_CLOSEDwithout sending a statement.SCH-6a The catalog connection is one shared requirement of Go, PHP, the PHP extension, TypeScript, Python and Rust. Each native package exposes
connect(dsn),current_namespace,describe_table,table_page,read_only_query,read_only_grid_query,insert_row,update_rowanddelete_row, with the semantics and error codes in interfaces. A missing operation or owner case leaves the feature incomplete; implementation-specific wrappers and value representations do not change the shared result, validation, error, snapshot or cleanup behavior. A catalog connection refuses a markdown DSN. The catalog feature's database cases cover MySQL, PostgreSQL and SQLite in every client.
4. Schema installation
- SCH-7
connection.utils().schema().install(schema)installs the document set of a generated schema value, verifies that the database matches it and registers the set on the connection (protocol). A manifest text that does not hash to its declaredmanifestHashfails withCONFIGbefore any statement, and a set with a diagnostic withSCHEMA_INVALID. It introspects the database of the connection (dialects). A set that uses external documents requires each used table in the database as its external definition declares it and fails withCONFIGotherwise; it never creates or changes those tables. Of the tables that the set owns, it creates every table with the statements rendered for the dialect of the connection, triggers included, when none exists, creates nothing when every table exists, and fails withCONFIGwhen only some exist. It then compares the owned tables in the database with the set, as a comparison of their schema texts likeaddTablesAndColumns, and fails withCONFIGnaming every difference as<kind> <table>[.<name>]when they differ: a column, primary key, unique key, index, foreign key, check or setting that differs from the set, and an object of an owned table that introspection cannot read (unsupported_<kind> <table>[.<name>]: <reason>). Tables outside the set are neither compared nor changed. PostgreSQL and SQLite apply the statements and the comparison in the active transaction of the connection or in a new one, so a failed install leaves nothing. MySQL commits each schema statement implicitly, so it applies them outside a transaction, and a call inside a transaction returnsCONFIG.
| Language | Call | Input |
|---|---|---|
| Go | Utils().Schema().Install(model.Schema) | the schema value of the generated package (*orm.Schema) |
| PHP | utils()->schema()->install(\Polyspec\Orm\Tests\Model\schema()) | the schema value of the generated models (Polyspec\Orm\Schema) |
| TypeScript | utils().schema().install(SCHEMA) | the schema value of the generated module ({ manifestText, manifestHash }) |
| Rust | utils().schema().install(&model::SCHEMA).await | the schema value of the generated models (polyspec_orm::Schema) |
5. Adding tables and columns
SCH-8
connection.utils().schema().addTablesAndColumns(schema)upgrades an installed document set to a new version of it that only adds: it creates every table of the set that the database lacks, adds the missing columns of the existing tables, when each of them isnullor has a default, and creates the missing indexes of the existing tables. An index (index, notunique) refuses no row, so adding it cannot fail on the rows a table already holds. It returns a created table astable, an added column astable.columnand a created index astable.index, in table name order, then column order and then index order. It introspects the database of the connection (dialects) and compares the tables of the set that exist with the set, as a comparison of their schema texts; every table outside the set is neither compared nor changed, so one database can hold the tables of several sets, and the tables that the set uses from external documents must exist as their external definitions declare them, or the call fails withCONFIGbefore any statement. When every difference is acreate_table, anadd_columnof such a column or anadd_index, it runs the steps of one plan from those tables to the set: a created table comes with its indexes, foreign keys, checks andimmutableandaudittriggers; an added column is anADD COLUMNwith the renderer CHECK of the column on MySQL and PostgreSQL and a rebuild of the table on SQLite, an added index is aCREATE INDEX, and for an audited table the replacement of its triggers, which then copy the new columns into the history table. After the steps it compares the database with the set asinstalldoes and fails withCONFIGnaming each difference. A repeated call adds nothing and returns an empty list. It records no plan history and registers no set;registeror the connect helper of generated code registers the set (registration).SCH-9 Every other difference returns
SCHEMA_DIFFERS, which names each difference as<kind> <table>[.<name>], before any statement, also when the version adds tables: a missing column that is non-null without a default, a column that the set does not declare, a changed type,null, default or identity, a missing column before an existing one (reorder_columns, because PostgreSQL appends a column), a changed primary key, index, foreign key, check or setting of an existing table, a missing unique key of an existing table, and an object of a table of the set that introspection cannot read. A missing unique key is named asadd_unique <table>.<name>: a missing unique key can fail on the existing rows; add it with a plan: the rows a table already holds can repeat its columns, so it belongs to a plan, applied after those rows are checked or corrected. A manifest text that does not hash to its declaredmanifestHashfails withCONFIGfirst, and a set with a diagnostic withSCHEMA_INVALID. PostgreSQL applies the steps in the active transaction of the connection or in a new one. MySQL commits each schema statement implicitly and SQLite rebuilds a table with foreign keys off, which a transaction cannot change, so both apply the steps outside a transaction and returnCONFIGinside one; SQLite runs them in oneBEGIN IMMEDIATEtransaction on one connection, requiresPRAGMA foreign_key_checkto return no row before it commits, and turns foreign keys on again.
| Language | Call | Steps |
|---|---|---|
| Go | Utils().Schema().AddTablesAndColumns(model.Schema) | dbspec.AddTablesAndColumnsSteps |
| PHP | utils()->schema()->addTablesAndColumns(\Polyspec\Orm\Tests\Model\schema()) | Dbspec::addTablesAndColumnsSteps |
| TypeScript | utils().schema().addTablesAndColumns(SCHEMA) | addTablesAndColumnsSteps |
| Rust | utils().schema().add_tables_and_columns(&model::SCHEMA).await | polyspec_orm_schema::dbspec::add_tables_and_columns_steps |
- SCH-10
installcreates a set only whole and returnsCONFIGfor a set of which only some tables exist;addTablesAndColumnsbrings such a set to its new version, after whichinstallchanges nothing.
6. Schema registration
SCH-11
connection.utils().schema().register(schema)registers the set of a generated schema value on the connection, so that the connection plans its requests (protocol). It reads and writes nothing in the database, also for a set that uses external documents: it checks that the manifest text hashes to its declaredmanifestHash, and fails withCONFIGotherwise, and registers the set. Registering a registered set again changes nothing.connectSchema, which the connect helper of generated code calls, opens a connection and registers the set in the same way.SCH-12 The parsed documents of a generated schema value come from its manifest text and its external text, with no connection:
RuntimeModel::loadSet(PHP),runtimemodel.LoadSet(Go),Manifest::load_set(Rust),loadSet(TypeScript) andRuntimeModel.load_set(Python) return the documents of the manifest text, then those of the external text marked external, and fail withSCHEMA_INVALIDon a text without a dbspec header or a document with a diagnostic. A tool that reads a module's tables, columns and keys reads them there instead of splitting the texts itself.SCH-13 The database is verified when the set is installed or upgraded, not on each connection. A deployment calls
installfor a new database andaddTablesAndColumnsfor a new version that only adds; both compare the database with the set and fail when they differ.registerand the connect helper register the sets on each connection without a statement, also for a connection opened per request. A change made to the database outside the ORM after the deployment is found by the nextinstalloraddTablesAndColumns.
| When | Call | Reads or writes the database |
|---|---|---|
| deploying a new database | install(schema) | creates the tables, compares the database with the set |
| deploying a version that only adds | addTablesAndColumns(schema) | adds tables and columns, compares the database with the set |
| each connection, each request | register(schema) or the connect helper | nothing |
| Language | Call |
|---|---|
| Go | Utils().Schema().Register(model.Schema) |
| PHP | utils()->schema()->register(\Polyspec\Orm\Tests\Model\schema()) |
| TypeScript | utils().schema().register(SCHEMA) |
| Rust | utils().schema().register(&model::SCHEMA) |