Skip to content

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:

LanguageTool
Goorm-gen gen --set <dbspec-set.json> --lang go --out <directory> --scan <package pattern>...
PHPvendor/bin/orm-gen gen --set <dbspec-set.json> --out <directory> --namespace <namespace>
TypeScriptorm-gen gen --set <dbspec-set.json> --out <directory> --scan <path> of @polyspec/orm
Pythonorm-gen gen --set <dbspec-set.json> --out <directory> --scan <path>, the console script the package installs
Rustpolyspec_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 signature error before parsing (files); the tool then fails with SCHEMA_INVALID and writes no model.

  • SCH-2 The github.com/polyspec/orm/packages/go module exposes Go generation to other programs in its package generator. generator.Generate takes the runtime model of the document set, the output directory, PackageName (the directory name by default) and Scan, 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-build crate's build-time tool_db decoder 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-build crate's build-time Val::int(), opt_int() and bool() return checked results. Required integer/boolean conversions reject SQL NULL. Optional integers preserve NULL as None. Booleans accept only native booleans, integer 0/1 and text t, 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_CLOSED without 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_row and delete_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 declared manifestHash fails with CONFIG before any statement, and a set with a diagnostic with SCHEMA_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 with CONFIG otherwise; 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 with CONFIG when only some exist. It then compares the owned tables in the database with the set, as a comparison of their schema texts like addTablesAndColumns, and fails with CONFIG naming 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 returns CONFIG.
LanguageCallInput
GoUtils().Schema().Install(model.Schema)the schema value of the generated package (*orm.Schema)
PHPutils()->schema()->install(\Polyspec\Orm\Tests\Model\schema())the schema value of the generated models (Polyspec\Orm\Schema)
TypeScriptutils().schema().install(SCHEMA)the schema value of the generated module ({ manifestText, manifestHash })
Rustutils().schema().install(&model::SCHEMA).awaitthe 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 is null or has a default, and creates the missing indexes of the existing tables. An index (index, not unique) refuses no row, so adding it cannot fail on the rows a table already holds. It returns a created table as table, an added column as table.column and a created index as table.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 with CONFIG before any statement. When every difference is a create_table, an add_column of such a column or an add_index, it runs the steps of one plan from those tables to the set: a created table comes with its indexes, foreign keys, checks and immutable and audit triggers; an added column is an ADD COLUMN with the renderer CHECK of the column on MySQL and PostgreSQL and a rebuild of the table on SQLite, an added index is a CREATE 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 as install does and fails with CONFIG naming each difference. A repeated call adds nothing and returns an empty list. It records no plan history and registers no set; register or 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 as add_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 declared manifestHash fails with CONFIG first, and a set with a diagnostic with SCHEMA_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 return CONFIG inside one; SQLite runs them in one BEGIN IMMEDIATE transaction on one connection, requires PRAGMA foreign_key_check to return no row before it commits, and turns foreign keys on again.

LanguageCallSteps
GoUtils().Schema().AddTablesAndColumns(model.Schema)dbspec.AddTablesAndColumnsSteps
PHPutils()->schema()->addTablesAndColumns(\Polyspec\Orm\Tests\Model\schema())Dbspec::addTablesAndColumnsSteps
TypeScriptutils().schema().addTablesAndColumns(SCHEMA)addTablesAndColumnsSteps
Rustutils().schema().add_tables_and_columns(&model::SCHEMA).awaitpolyspec_orm_schema::dbspec::add_tables_and_columns_steps
  • SCH-10 install creates a set only whole and returns CONFIG for a set of which only some tables exist; addTablesAndColumns brings such a set to its new version, after which install changes 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 declared manifestHash, and fails with CONFIG otherwise, 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) and RuntimeModel.load_set (Python) return the documents of the manifest text, then those of the external text marked external, and fail with SCHEMA_INVALID on 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 install for a new database and addTablesAndColumns for a new version that only adds; both compare the database with the set and fail when they differ. register and 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 next install or addTablesAndColumns.

WhenCallReads or writes the database
deploying a new databaseinstall(schema)creates the tables, compares the database with the set
deploying a version that only addsaddTablesAndColumns(schema)adds tables and columns, compares the database with the set
each connection, each requestregister(schema) or the connect helpernothing
LanguageCall
GoUtils().Schema().Register(model.Schema)
PHPutils()->schema()->register(\Polyspec\Orm\Tests\Model\schema())
TypeScriptutils().schema().register(SCHEMA)
Rustutils().schema().register(&model::SCHEMA)

MIT License · Go / PHP / Rust / TypeScript