qubu 0.0.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/canonical-BbnqavJm.mjs +972 -0
  2. package/dist/codegen.d.mts +107 -0
  3. package/dist/codegen.mjs +1199 -0
  4. package/dist/column-CXMxx8Hq.mjs +118 -0
  5. package/dist/complete-D5Djh-zo.mjs +1788 -0
  6. package/dist/complete-types-BdFqUfbb.d.mts +371 -0
  7. package/dist/core.d.mts +33 -0
  8. package/dist/core.mjs +36 -0
  9. package/dist/ddl.d.mts +118 -0
  10. package/dist/ddl.mjs +1120 -0
  11. package/dist/dialect-b2-Z6uBF.mjs +13 -0
  12. package/dist/diff.d.mts +2 -0
  13. package/dist/diff.mjs +883 -0
  14. package/dist/drizzle-mysql.d.mts +24 -0
  15. package/dist/drizzle-mysql.mjs +72 -0
  16. package/dist/drizzle-postgres.d.mts +24 -0
  17. package/dist/drizzle-postgres.mjs +73 -0
  18. package/dist/drizzle-sqlite.d.mts +24 -0
  19. package/dist/drizzle-sqlite.mjs +82 -0
  20. package/dist/drizzle.d.mts +13 -0
  21. package/dist/drizzle.mjs +2 -0
  22. package/dist/errors-BGCoLe_r.mjs +14 -0
  23. package/dist/errors-Dxv73YJu.mjs +26 -0
  24. package/dist/explain-CkIK13L_.mjs +98 -0
  25. package/dist/index-Ds7-mhJi.d.mts +146 -0
  26. package/dist/index-Dug5HnLB.d.mts +204 -0
  27. package/dist/index.d.mts +2 -0
  28. package/dist/index.mjs +1276 -0
  29. package/dist/introspection.d.mts +117 -0
  30. package/dist/introspection.mjs +4341 -0
  31. package/dist/json-CUZlv4HT.mjs +169 -0
  32. package/dist/migration.d.mts +215 -0
  33. package/dist/migration.mjs +1161 -0
  34. package/dist/mysql-DqkqXB6A.mjs +355 -0
  35. package/dist/mysql.d.mts +5 -0
  36. package/dist/mysql.mjs +50 -0
  37. package/dist/naming-QVCOnSj2.mjs +20 -0
  38. package/dist/omit-OxV58AwX.mjs +5 -0
  39. package/dist/on-conflict-BxnxubMb.mjs +143 -0
  40. package/dist/postgres-DEBBeh52.mjs +235 -0
  41. package/dist/postgres.d.mts +123 -0
  42. package/dist/postgres.mjs +52 -0
  43. package/dist/registry-BufIskVN.mjs +569 -0
  44. package/dist/relational-DCZrrNia.mjs +77 -0
  45. package/dist/runtime-BTr-MTlo.mjs +193 -0
  46. package/dist/schema.d.mts +2 -0
  47. package/dist/schema.mjs +5 -0
  48. package/dist/serialize-PF1cfH2P.mjs +556 -0
  49. package/dist/snapshot-CWPgzxNx.mjs +392 -0
  50. package/dist/snapshot.d.mts +4 -0
  51. package/dist/snapshot.mjs +8 -0
  52. package/dist/source-DUoJVXmL.mjs +68 -0
  53. package/dist/sqlite-BU6DBxef.mjs +320 -0
  54. package/dist/sqlite.d.mts +5 -0
  55. package/dist/sqlite.mjs +48 -0
  56. package/dist/standard-BTVYKh_F.mjs +12 -0
  57. package/dist/table-llv9tsZ8.mjs +384 -0
  58. package/dist/types-4Q076HKo.d.mts +3636 -0
  59. package/dist/types-BX0mckiU.d.mts +644 -0
  60. package/dist/types-CO1KaRNc.d.mts +46 -0
  61. package/dist/types-Cec0xzo4.mjs +116 -0
  62. package/dist/value-BvilP0oz.mjs +29 -0
  63. package/dist/vite/ambient.d.ts +449 -0
  64. package/dist/vite.d.mts +35 -0
  65. package/dist/vite.mjs +498 -0
  66. package/docs/config.json +21 -0
  67. package/docs/dialects-and-execution.md +427 -0
  68. package/docs/getting-started.md +96 -0
  69. package/docs/guides/compose-queries.md +225 -0
  70. package/docs/guides/drizzle.md +129 -0
  71. package/docs/guides/extensions/dialects.md +40 -0
  72. package/docs/guides/extensions/overview.md +36 -0
  73. package/docs/guides/extensions/sources-and-clauses.md +82 -0
  74. package/docs/guides/extensions/typed-expressions.md +94 -0
  75. package/docs/guides/extensions/unsafe-syntax.md +27 -0
  76. package/docs/guides/json.md +65 -0
  77. package/docs/guides/mutations.md +138 -0
  78. package/docs/guides/select/conditions.md +110 -0
  79. package/docs/guides/select/grouping-and-windows.md +82 -0
  80. package/docs/guides/select/ordering-and-pagination.md +78 -0
  81. package/docs/guides/select/overview.md +139 -0
  82. package/docs/guides/sql-templates.md +157 -0
  83. package/docs/guides/vite-plugin.md +92 -0
  84. package/docs/index.md +111 -0
  85. package/docs/query-model/fragments.md +109 -0
  86. package/docs/query-model/result-shapes.md +125 -0
  87. package/docs/query-model/source-scope.md +182 -0
  88. package/docs/reference/introspection-support.md +196 -0
  89. package/docs/reference/mysql-snapshot.md +55 -0
  90. package/docs/reference/postgres-snapshot.md +61 -0
  91. package/docs/reference/sqlite-snapshot.md +53 -0
  92. package/docs/reference/supported-surface.md +104 -0
  93. package/docs/schema/catalog-model.md +58 -0
  94. package/docs/schema/code-generation.md +139 -0
  95. package/docs/schema/columns-and-writes.md +130 -0
  96. package/docs/schema/constraints-and-indexes.md +180 -0
  97. package/docs/schema/ddl-emission.md +75 -0
  98. package/docs/schema/diff.md +75 -0
  99. package/docs/schema/introspection.md +214 -0
  100. package/docs/schema/migration-plans.md +76 -0
  101. package/docs/schema/snapshots.md +93 -0
  102. package/docs/schema/storage-and-schema-sql.md +104 -0
  103. package/docs/schema/tables-and-names.md +106 -0
  104. package/docs/sql-semantic-types.md +141 -0
  105. package/docs/troubleshooting.md +151 -0
  106. package/package.json +132 -10
  107. package/skills/qubu/SKILL.md +42 -0
  108. package/skills/qubu/agents/openai.yaml +6 -0
  109. package/readme.md +0 -1
@@ -0,0 +1,75 @@
1
+ # DDL emission
2
+
3
+ > Turn an approved migration plan into deterministic SQL while keeping database execution outside Qubu.
4
+
5
+ The `qubu/ddl` entrypoint accepts only a `MigrationPlan` and a `SchemaDialect`.
6
+ It does not read a catalog, open a connection, start a transaction, or write a
7
+ migration journal. Preflight runs before rendering, so a blocked or incompatible
8
+ plan returns diagnostics and no SQL.
9
+
10
+ ```ts
11
+ import { emitMigrationPlan } from 'qubu/ddl'
12
+ import { postgresSchemaDialect } from 'qubu/snapshot'
13
+
14
+ const result = emitMigrationPlan(plan, postgresSchemaDialect)
15
+ if (!result.ok) {
16
+ // Review result.diagnostics. result.sql is an empty string.
17
+ throw new Error(result.diagnostics.map(item => item.message).join('\n'))
18
+ }
19
+
20
+ for (const statement of result.statements) {
21
+ console.log(statement.operationId, statement.sql, statement.parameters)
22
+ }
23
+ ```
24
+
25
+ `statements` is the stable handoff for an application-owned executor. Each
26
+ statement carries its operation ID, topological position, SQL text, and an
27
+ ordered parameter list.
28
+ Schema literals and expressions are parameter-free by contract. `sql` joins
29
+ the statements with a newline and adds a semicolon for migration-file writers.
30
+
31
+ ## Review gates
32
+
33
+ The emitter rejects a plan with `ready: false`, `decision-required` operations,
34
+ unknown or lossy facts, unsupported safety, or destructive changes unless the
35
+ caller supplies the matching explicit option. `allowUnsafe` is available for a
36
+ reviewed integration that owns all of those decisions, but it does not make an
37
+ opaque object renderable. Opaque and deferred catalog records always need an
38
+ explicit tagged `custom-sql` operation.
39
+
40
+ Lock and transaction requirements describe what a later executor must provide.
41
+ Pass `lock` or `transaction` to preflight those requirements against the
42
+ executor's context. A required transaction with `transaction: 'autocommit'`
43
+ produces `transaction-conflict`; no transaction is opened by the emitter.
44
+
45
+ ```ts
46
+ const result = emitMigrationPlan(plan, postgresSchemaDialect, {
47
+ transaction: 'managed',
48
+ lock: 'exclusive',
49
+ serverVersion: '16',
50
+ })
51
+ ```
52
+
53
+ ## First-party support
54
+
55
+ | Object or operation | PostgreSQL | SQLite | MySQL |
56
+ | ----------------------------------------------- | ------------------------ | ------------------------------------------------------------------------ | ------------------------ |
57
+ | Tables, columns, constraints, indexes | yes | tables, columns, and indexes; constraints require an inline/rebuild path | yes |
58
+ | Views | yes | yes | yes |
59
+ | Materialized views | yes | no | no |
60
+ | Sequences, enums, domains, policies, extensions | yes | no | no |
61
+ | Routines | functions and procedures | no | functions and procedures |
62
+ | Partitions | yes | no | add/drop partition forms |
63
+ | Triggers | yes | yes | yes |
64
+ | Comments and ownership | yes | no | table comments only |
65
+ | Explicit custom SQL | dialect tag must match | dialect tag must match | dialect tag must match |
66
+
67
+ The support table describes syntax Qubu can render from modeled snapshot facts.
68
+ Server-version checks still apply, such as SQLite column rename and drop-column
69
+ limits and MySQL check-constraint support. An unsupported operation is reported
70
+ as a diagnostic rather than silently omitted. SQLite table constraints are not
71
+ emitted as `ALTER TABLE ... ADD CONSTRAINT`; use a schema snapshot that carries
72
+ an inline constraint declaration or an explicit rebuild/custom-SQL operation.
73
+
74
+ Custom SQL stays opaque and appears at its plan position. The emitter does not
75
+ inspect it for object names or infer SQL from an opaque catalog record.
@@ -0,0 +1,75 @@
1
+ # Snapshot diffing
2
+
3
+ > Compare canonical Snapshot v1 or v2 values and review identity changes before a later planning step.
4
+
5
+ The optional `qubu/diff` entrypoint compares immutable snapshot data. It does
6
+ not open a connection, render SQL, execute a change, or infer migration history.
7
+ It keeps the object kind, namespace, logical ID, physical name, dialect, path,
8
+ and catalog evidence on every result object.
9
+
10
+ ```ts
11
+ import { diffSnapshots } from 'qubu/diff'
12
+
13
+ const result = diffSnapshots(previousSnapshot, currentSnapshot)
14
+
15
+ for (const change of result.changes) {
16
+ console.log(change.type, change.kind, change.logicalId)
17
+ }
18
+ ```
19
+
20
+ The matcher first uses an explicit rename hint, then a stable logical ID. A
21
+ physical-name change for a stable match is a `physical-rename` operation. Other
22
+ matched fields become `property-change` operations. Unmatched records remain
23
+ separate `remove` and `add` operations.
24
+
25
+ ## Explicit rename hints
26
+
27
+ Use a hint when an introspector or schema edit changed the logical ID as well as
28
+ the physical name. Hints are scoped by object kind and namespace. A target may
29
+ use an ID, a physical name, or an exact path when a nested scope repeats an ID.
30
+
31
+ ```ts
32
+ const result = diffSnapshots(previousSnapshot, currentSnapshot, {
33
+ renameHints: [
34
+ {
35
+ kind: 'table',
36
+ namespace: 'public',
37
+ from: 'legacy_accounts',
38
+ to: 'accounts',
39
+ },
40
+ ],
41
+ })
42
+
43
+ result.renames[0]?.source // 'explicit-hint'
44
+ ```
45
+
46
+ Hints must resolve to one object on each side. A wrong namespace, unsupported
47
+ kind, empty target, duplicate mapping, or target that resolves more than once
48
+ produces a diagnostic. `encodeSnapshotRenameHints()` writes deterministic JSON;
49
+ `decodeSnapshotRenameHints()` validates it without throwing.
50
+
51
+ ## Suggestions and safety diagnostics
52
+
53
+ Structural matching can report a `rename-suggestion` with a confidence score and
54
+ evidence. Suggestions never enter `result.renames` and never replace the
55
+ corresponding add and remove operations. An ambiguous structural match reports
56
+ an `ambiguous` diagnostic and leaves both operations visible for review.
57
+
58
+ Removing an object is marked `destructive`. Narrowing nullability, changing
59
+ storage, removing a value, or changing a constraint can also receive that
60
+ classification. Opaque and deferred Snapshot v2 records remain visible as
61
+ `add` or `remove` data and produce `lossy` or `unsupported` diagnostics. They
62
+ cannot be silently promoted to a rename.
63
+
64
+ `result.equal` means that no diff operation was emitted. A result can therefore
65
+ be equal while still carrying a warning about an unchanged opaque record. The
66
+ diagnostics remain part of the review boundary.
67
+
68
+ Snapshot arrays are normalized by stable IDs or positions before comparison, so
69
+ reordering tables, object groups, columns, or other canonical collections does
70
+ not create changes. Ordered index terms, foreign-key columns, enum positions,
71
+ and routine parameter positions retain their meaning.
72
+
73
+ Diff output is data only. Feed a resolved result into the
74
+ [migration-plan boundary](migration-plans.md) when a reviewed change list is
75
+ needed; DDL rendering and database execution remain later boundaries.
@@ -0,0 +1,214 @@
1
+ # Database introspection
2
+
3
+ > Read one existing database namespace into explainable catalog data and an optional canonical Snapshot v1 or complete Snapshot v2 without giving Qubu ownership of the connection.
4
+
5
+ Database introspection is an optional capability exported from
6
+ `qubu/introspection`. It discovers database facts; it does not recreate the
7
+ original TypeScript declarations. Planning and DDL emission use the separate
8
+ `qubu/migration` and `qubu/ddl` entrypoints, while migration execution remains
9
+ application-owned. The separate
10
+ `qubu/codegen` entrypoint can create a new machine-owned schema module from a
11
+ complete Snapshot v1 result.
12
+
13
+ ## The pipeline
14
+
15
+ The caller owns the connection and supplies a small catalog query adapter:
16
+
17
+ ```mermaid
18
+ flowchart LR
19
+ A[User-owned connection] --> B[Dialect catalog reader]
20
+ B --> C[Normalized catalog]
21
+ C --> D[Pure snapshot mapper]
22
+ D --> E[Canonical snapshot]
23
+ E -. later .-> F[Snapshot diffing or migration planning]
24
+ E -. optional .-> G[Generated TypeScript schema]
25
+ ```
26
+
27
+ The reader owns catalog SQL and dialect-specific row normalization. The
28
+ normalized catalog retains physical names, native types, opaque SQL text,
29
+ provenance, current-run catalog references, capabilities, deferred objects,
30
+ and diagnostics. The mapper owns Snapshot v1 shape, stable ordering, identity
31
+ continuity, and strict versus lossy output.
32
+
33
+ ## Supply a connection
34
+
35
+ Qubu does not open or close a connection, select a driver, manage a pool, retry
36
+ queries, authenticate, or start a transaction. Adapt the driver you already
37
+ use to `CatalogConnection`:
38
+
39
+ ```ts
40
+ import type { CatalogConnection } from 'qubu/introspection'
41
+
42
+ const connection: CatalogConnection = {
43
+ dialect: 'sqlite',
44
+ query(statement, options) {
45
+ // Adapt this call to the driver used by the application.
46
+ return db.query(statement, options)
47
+ },
48
+ }
49
+ ```
50
+
51
+ The adapter receives fixed catalog statements and bound parameters. Catalog
52
+ text returned by the database is data. Qubu never evaluates it or turns it
53
+ into an executable schema expression.
54
+
55
+ ## Read and map a catalog
56
+
57
+ Readers return normalized facts. Mapping is a separate operation so the same
58
+ catalog can later support inspection, source generation, or another snapshot
59
+ format:
60
+
61
+ ```ts
62
+ import { mapCatalogToSnapshot, readSqliteCatalog } from 'qubu/introspection'
63
+
64
+ const catalog = await readSqliteCatalog(connection, { namespace: 'main' })
65
+ const result = mapCatalogToSnapshot(catalog, {
66
+ namespace: 'main',
67
+ mode: 'strict',
68
+ })
69
+
70
+ if (!result.ok) {
71
+ throw new Error(result.diagnostics.map(issue => issue.message).join('\n'))
72
+ }
73
+
74
+ result.snapshot.tables // canonical Snapshot v1 data
75
+ ```
76
+
77
+ Pass the successful result itself—not a detached or edited snapshot—to
78
+ `generateSchemaSource()` when a replaceable TypeScript schema is needed. See
79
+ [Generate a schema from introspection](code-generation.md) for that workflow
80
+ and its identity handoff.
81
+
82
+ Readers may expose additional typed object families through the normalized
83
+ catalog. Use `createCompleteIntrospectionCatalog()` to materialize and freeze
84
+ all optional collections, then `mapCatalogToCompleteSnapshot()` when an
85
+ adapter-supported family such as views, routines, triggers, partitions,
86
+ collations, comments, or retained opaque and deferred objects must cross the
87
+ strict Snapshot v2 boundary. Snapshot v1 is still selected explicitly by
88
+ `mapCatalogToSnapshot()` and remains table-shaped.
89
+
90
+ The result is successful only when Snapshot v1 validation succeeds. A failed
91
+ result may retain the partial catalog and structured diagnostics, but it has no
92
+ snapshot.
93
+
94
+ ## Identity and physical names
95
+
96
+ Introspection uses this identity precedence:
97
+
98
+ 1. an explicit identity hint;
99
+ 2. a matching entity in a previous snapshot;
100
+ 3. the physical database name;
101
+ 4. a deterministic fallback for unnamed or invalid names.
102
+
103
+ Physical names remain unchanged in the snapshot. OIDs, SQLite rowids, and
104
+ internal `sqlite_autoindex_*` names are current-run or implementation details,
105
+ not persisted Qubu identities. A changed physical name is not automatically a
106
+ rename. Pass the previous snapshot or an identity hint when a later diff must
107
+ preserve identity across a rename. See [snapshot diffing](diff.md) for the
108
+ comparison and hint boundary.
109
+
110
+ The first version selects one namespace: a PostgreSQL schema, MySQL database,
111
+ or SQLite database such as `main`. It does not combine attached databases or
112
+ multiple PostgreSQL schemas into one Snapshot v1 value.
113
+
114
+ ## Strict and lossy output
115
+
116
+ Strict mode is the default. If an included table, column, default, generated
117
+ expression, identity, constraint, foreign key, or index cannot be represented
118
+ soundly, mapping returns diagnostics and no snapshot.
119
+
120
+ Lossy mode is explicit. It may return a snapshot with warnings, but the result
121
+ is marked lossy. Migration planning blocks lossy facts unless the caller
122
+ explicitly handles the omitted behavior.
123
+
124
+ Only unambiguous literal defaults become snapshot literals. Other defaults,
125
+ generated expressions, checks, predicates, and expression index terms remain
126
+ dialect-tagged SQL. Falsy values such as `0`, `false`, `NULL`, and empty
127
+ strings are preserved.
128
+
129
+ PostgreSQL readers expose views, materialized views, sequences, enums, domains,
130
+ collations, routines, triggers, policies, partitions, extensions, comments,
131
+ and ownership as typed complete catalog records. `mapCatalogToCompleteSnapshot`
132
+ retains those records in Snapshot v2. The existing `mapCatalogToSnapshot`
133
+ mapper still emits the table-only Snapshot v1 and does not fabricate these
134
+ objects into tables. If a PostgreSQL catalog row lacks the evidence needed for
135
+ safe normalization, the reader retains a deferred or opaque record and emits a
136
+ diagnostic.
137
+
138
+ SQLite readers expose recoverable views and triggers as typed complete records.
139
+ They retain virtual and shadow tables as deferred objects, and keep attached
140
+ databases outside the selected namespace as opaque boundary records. SQLite
141
+ declared types, derived affinity, generated expressions, rowid identity, and
142
+ `AUTOINCREMENT` stay tagged with SQLite dialect metadata. When an attached
143
+ database is selected, table PRAGMAs may be visible but CREATE SQL remains
144
+ limited to the fixed `main` and `temp` catalog statements, so the reader marks
145
+ the catalog visibility as limited instead of combining namespaces.
146
+
147
+ ## MySQL 8 complete catalog surface
148
+
149
+ The MySQL reader supports MySQL 8.0.16 and later within the MySQL 8 series and
150
+ rejects MariaDB. It reads one selected database from `INFORMATION_SCHEMA` and
151
+ keeps catalog SQL as tagged, unevaluated MySQL data. The complete catalog has
152
+ typed records for views, routines and parameters, triggers, partitions,
153
+ collations used by the selected tables or columns, and comments.
154
+
155
+ View definitions come from `INFORMATION_SCHEMA.VIEWS`. The reader cross-
156
+ references each view with its `INFORMATION_SCHEMA.COLUMNS` rows by physical
157
+ table name, so view columns remain attached to the view and Snapshot v2 can
158
+ validate their own column IDs. A missing definition or an unresolved
159
+ cross-object reference becomes a deferred record with a diagnostic.
160
+
161
+ MySQL scheduled events are kept as `CatalogOpaqueObject` records with their
162
+ metadata and definition tagged as opaque SQL. The reader emits an
163
+ `unmodeled-object` warning, and Snapshot v2 retains the record in
164
+ `opaqueObjects` without treating it as a typed routine, trigger, or migration
165
+ operation.
166
+
167
+ Sequences and materialized views are not typed MySQL families. A sequence-like
168
+ or other non-base table row reported by the catalog is retained as a deferred
169
+ object. MySQL row-level security (RLS) policies, extension objects, and
170
+ ownership are unsupported. Definers are retained as dialect metadata on
171
+ objects that expose them; they do not become ownership records. MySQL capability flags mark
172
+ these families as unsupported.
173
+
174
+ The query and normalization layout follows the catalog-reading portions of the
175
+ [Drizzle MySQL introspector](https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-kit/src/introspect-mysql.ts).
176
+ The reader keeps that metadata as normalized typed data and never evaluates
177
+ database-provided SQL. Optional source generation remains a later, pure step
178
+ with a controlled literal printer.
179
+
180
+ Use `mapCatalogToCompleteSnapshot()` for the typed MySQL families and its
181
+ opaque or deferred boundaries. Use `mapCatalogToSnapshot()` when the caller
182
+ needs the table-only Snapshot v1.
183
+
184
+ ## Diagnostics and safety
185
+
186
+ Diagnostics include a severity, stable code, catalog path, physical reference
187
+ when available, and a remediation hint. They distinguish connection/query
188
+ failures, permission limits, unsupported products or versions, unresolved
189
+ references, expression recovery failures, unmodeled objects, and lossy
190
+ mappings.
191
+
192
+ Introspection is read-only from Qubu's perspective. Do not pass credentials or
193
+ DSNs through diagnostic fields. Keep driver-specific error text in the
194
+ application's logging boundary, and treat database-provided SQL as opaque
195
+ input.
196
+
197
+ ## What comes next
198
+
199
+ The canonical snapshot is the handoff to Qubu's pure schema pipeline:
200
+
201
+ - diffing compares canonical snapshots;
202
+ - rename resolution consumes previous/current snapshots and explicit hints;
203
+ - migration planning consumes semantic diff operations;
204
+ - DDL emitters consume approved plans and dialect capabilities;
205
+ - source generation creates a new machine-owned Snapshot v1 schema baseline.
206
+
207
+ None of those layers opens a database connection or changes how introspection
208
+ represents catalog facts. DDL emission produces statements; it does not apply
209
+ them. The [ownership map](../reference/supported-surface.md#ownership-boundary)
210
+ keeps this handoff separate from application-owned migration execution.
211
+
212
+ See the [introspection support matrix](../reference/introspection-support.md)
213
+ for version baselines and dialect-specific limits. Snapshot serialization
214
+ itself remains documented in [Canonical schema snapshots](snapshots.md).
@@ -0,0 +1,76 @@
1
+ # Migration plans
2
+
3
+ > Describe reviewed snapshot changes as deterministic data before selecting a DDL emitter.
4
+
5
+ The optional `qubu/migration` entrypoint consumes a resolved `SnapshotDiff` and
6
+ returns an immutable migration-plan IR. It contains operation IDs, paths,
7
+ logical and physical identity evidence, dependency edges, preconditions, safety,
8
+ lock and transaction requirements, and reversibility markers.
9
+
10
+ ```ts
11
+ import { createMigrationPlan } from 'qubu/migration'
12
+
13
+ const result = createMigrationPlan(diff)
14
+ if (!result.ok) {
15
+ // Review result.plan.diagnostics and provide explicit decisions.
16
+ }
17
+ ```
18
+
19
+ Creation is pure. The planner does not open a connection, execute a transaction,
20
+ render SQL, or create migration history. A physical rename remains a
21
+ `physical-rename` operation; it is never represented as custom SQL or silently
22
+ changed into a drop and add.
23
+
24
+ ## Safety decisions
25
+
26
+ Safe operations can be inspected immediately. Destructive, review-required,
27
+ unsupported, unknown, and lossy facts remain blocked until an explicit decision
28
+ or matching option is supplied. Decisions are tied to an operation ID or to a
29
+ kind, namespace, and path, and each decision carries a review reason.
30
+
31
+ ```ts
32
+ const reviewed = createMigrationPlan(diff, {
33
+ decisions: result.plan.operations
34
+ .filter(operation => operation.status === 'decision-required')
35
+ .map(operation => ({
36
+ operationId: operation.id,
37
+ action: 'allow',
38
+ reason: 'Reviewed against the deployment change request',
39
+ })),
40
+ })
41
+ ```
42
+
43
+ Unknown or lossy records are not converted into SQL. If a dialect-specific step
44
+ is genuinely needed, attach it explicitly with `customSql`, including its
45
+ dialect, safety declaration, reason, reversibility, and dependency position:
46
+
47
+ ```ts
48
+ createMigrationPlan(diff, {
49
+ customSql: [
50
+ {
51
+ sql: 'ALTER TABLE accounts VALIDATE CONSTRAINT accounts_check',
52
+ dialect: { name: 'postgresql', version: 1 },
53
+ safety: 'review-required',
54
+ reason: 'The dialect emitter does not model this catalog fact yet',
55
+ reversible: false,
56
+ position: 3,
57
+ },
58
+ ],
59
+ })
60
+ ```
61
+
62
+ The string is retained as an explicit custom operation only. The planner never
63
+ extracts SQL from opaque catalog payloads.
64
+
65
+ ## Ordering and validation
66
+
67
+ Parent creation precedes child creation, while child removal precedes parent
68
+ removal. Reference edges and explicit custom-SQL dependencies are included in
69
+ the stable topological ordering. `encodeMigrationPlan()` emits canonical JSON;
70
+ `decodeMigrationPlan()` and `validateMigrationPlan()` reject unknown fields,
71
+ future versions, malformed operations, missing edges, and dependency cycles.
72
+
73
+ After review and approval, pass the plan to
74
+ [`qubu/ddl`](./ddl-emission.md). DDL emission remains separate from
75
+ application-owned migration execution in the package-wide [ownership
76
+ map](../reference/supported-surface.md#ownership-boundary).
@@ -0,0 +1,93 @@
1
+ # Canonical schema snapshots
2
+
3
+ > Serialize schema metadata into strict, deterministic data and keep serialization separate from diffing, planning, and DDL emission.
4
+
5
+ Qubu's schema tooling lives behind the `qubu/snapshot` entrypoint. It converts
6
+ an immutable `schema()` registry into versioned data that can be inspected,
7
+ hashed, checked into source control, and handed to a dialect adapter. Importing
8
+ the snapshot entrypoint is optional; ordinary query imports do not load it.
9
+
10
+ ```ts
11
+ import { createSchemaSnapshot, encodeSchemaSnapshot } from 'qubu/snapshot'
12
+
13
+ const snapshot = createSchemaSnapshot(appSchema)
14
+ const json = encodeSchemaSnapshot(snapshot)
15
+ ```
16
+
17
+ The v1 envelope contains a format version, an independently versioned dialect
18
+ extension, a versioned naming-policy description, an optional namespace, and
19
+ arrays of tables. Tables, columns, constraints, and indexes are sorted by
20
+ stable logical ID. Physical names are values in the snapshot, not identities:
21
+ changing a physical name does not change the TypeScript field or metadata key.
22
+
23
+ Snapshot data is deliberately not executable Qubu state. Expressions are
24
+ parameter-free data records, and decoding never creates tables, column
25
+ references, or render closures. The neutral fallback renders branded built-in
26
+ expressions through the standard schema context; a dialect adapter may replace
27
+ that hook with its own literal and expression policy. An explicitly unsafe
28
+ expression retains its dialect tag and is rejected when it does not match the
29
+ selected snapshot dialect.
30
+
31
+ ```ts
32
+ import { decodeSchemaSnapshot } from 'qubu/snapshot'
33
+
34
+ const decoded = decodeSchemaSnapshot(json)
35
+ if (!decoded.ok) {
36
+ for (const issue of decoded.diagnostics) {
37
+ console.error(issue.path.join('.'), issue.code, issue.message)
38
+ }
39
+ }
40
+ ```
41
+
42
+ The decoder is strict. It reports unknown fields, malformed nodes, unsupported
43
+ future format or extension versions, non-canonical entity ordering, wrong
44
+ dialect metadata, and broken foreign-key or column references as structured
45
+ diagnostics. It does not call `process.exit()` and has no runtime validation
46
+ library dependency.
47
+
48
+ `schemaSnapshotDigest()` computes a deterministic content digest from canonical
49
+ JSON. The digest is useful for cache keys and fixture assertions only. It is not
50
+ an entity identity, a rename marker, or migration lineage.
51
+
52
+ ## Adapter boundary
53
+
54
+ `SchemaDialect` is a capability superset of `Dialect`. Create one with
55
+ `createSchemaDialect(queryDialect, hooks)`; the resulting object retains the
56
+ query dialect's name, identifier quoting, placeholders, literals, JSON, casts,
57
+ and advertised capabilities while adding schema encoders and validation under
58
+ `.schema`. Snapshot adapters reference that object instead of constructing a
59
+ second query dialect. The schema snapshot format version remains independent
60
+ from the dialect identity.
61
+
62
+ The common traversal owns logical IDs, fixed property order, canonical sorting,
63
+ portable constraints, cross-reference checks, and the immutable snapshot
64
+ envelope. A dialect adapter owns physical storage mapping, SQL literal and
65
+ expression encoding, dialect extensions, capability checks, and any dialect
66
+ naming policy. PostgreSQL, SQLite, and MySQL adapters can implement
67
+ `SchemaSnapshotAdapter` without duplicating traversal or decoder rules.
68
+ The PostgreSQL adapter is documented in the [PostgreSQL snapshot support
69
+ matrix](../reference/postgres-snapshot.md). Its schema dialect extends the
70
+ `postgresql` query dialect, and snapshot metadata uses that same identity.
71
+ The SQLite adapter is documented in the [SQLite snapshot support
72
+ matrix](../reference/sqlite-snapshot.md).
73
+ The MySQL adapter is documented in the [MySQL snapshot support
74
+ matrix](../reference/mysql-snapshot.md). Its query and snapshot dialects both
75
+ use `mysql`, while MySQL-only `ON UPDATE` and `AUTO_INCREMENT` details remain
76
+ inside the column and identity metadata they describe.
77
+
78
+ Snapshot serialization remains separate from database introspection,
79
+ comparison, rename resolution, migration planning, and DDL emission. The
80
+ optional `qubu/introspection` entrypoint can produce the same canonical
81
+ Snapshot v1 data from a user-owned catalog connection. The complete normalized
82
+ catalog can also be encoded as strict Snapshot v2 with the dedicated
83
+ complete-snapshot APIs described in [the catalog model](catalog-model.md).
84
+ Readers and connection lifecycle do not belong to this pure serialization
85
+ layer. Diffing can compare either snapshot version. Resolved diffs feed
86
+ migration plans, and approved plans feed DDL emission. The package-wide
87
+ [ownership map](../reference/supported-surface.md#ownership-boundary) keeps
88
+ those pure steps separate from application-owned database execution.
89
+
90
+ The optional [schema source generator](code-generation.md) consumes a complete,
91
+ non-lossy introspection result and makes its generated schema the next identity
92
+ baseline. It does not replace snapshot serialization or generate Snapshot v2
93
+ object families.
@@ -0,0 +1,104 @@
1
+ # Storage and schema SQL
2
+
3
+ > Keep application types, SQL domains, physical storage, and schema expressions separate so each adapter can make its own rendering decision.
4
+
5
+ ## Keep application and SQL types separate
6
+
7
+ A column can have an application value type, a SQL semantic domain, a physical
8
+ storage descriptor, and a cast target. These facts answer different questions.
9
+
10
+ For example, `numeric()` decodes to a TypeScript number, carries `SqlDecimal`,
11
+ uses portable numeric storage, and has a logical decimal cast target. Read
12
+ [SQL semantic types](../sql-semantic-types.md) for operator compatibility and
13
+ contextual literals.
14
+
15
+ ## Record physical storage
16
+
17
+ Built-in helpers use portable storage descriptors:
18
+
19
+ | Helper | Portable storage |
20
+ | ----------- | ---------------- |
21
+ | integer() | integer |
22
+ | numeric() | numeric |
23
+ | text() | text |
24
+ | boolean() | boolean |
25
+ | date() | date |
26
+ | timestamp() | timestamp |
27
+ | uuid() | uuid |
28
+ | json<T>() | json |
29
+ | bigint() | bigint |
30
+ | binary() | binary |
31
+
32
+ Use a dialect-native descriptor when a column needs an exact vendor
33
+ declaration:
34
+
35
+ ```ts
36
+ import { nativeColumn, nativeStorage, table } from 'qubu'
37
+
38
+ const accounts = table('accounts', {
39
+ handle: nativeColumn(nativeStorage('postgresql', 'citext COLLATE "C"')),
40
+ })
41
+ ```
42
+
43
+ `nativeStorage()` preserves the declaration text and freezes the descriptor. The
44
+ `ColumnStorageOf`, `ColumnStorageTypeOf`, `ColumnStorageDialectOf`, and
45
+ `ColumnStorageDeclarationOf` helpers read its metadata. Native storage is
46
+ descriptive. It does not change selection, mutation, or query rendering.
47
+
48
+ ## Render deterministic schema expressions
49
+
50
+ Schema SQL uses a different context from query SQL. Built-in scalar expressions
51
+ can be rendered for checks, generated columns, indexes, and other declaration
52
+ metadata:
53
+
54
+ ```ts
55
+ import { eq, table, text } from 'qubu'
56
+ import { renderSchemaSql } from 'qubu/schema'
57
+
58
+ const accounts = table('accounts', { status: text() })
59
+
60
+ renderSchemaSql(eq(accounts.status, 'active'), { mode: 'check' })
61
+ // ("status" = 'active')
62
+ ```
63
+
64
+ The schema context emits SQL literals instead of placeholders. It supports
65
+ strings, finite numbers, booleans, bigint, and NULL through the portable
66
+ fallback. A dialect can provide renderSchemaLiteral for another spelling.
67
+ Unsupported values and direct calls to parameter() fail.
68
+
69
+ Column references render as bare physical identifiers for generated, check, and
70
+ index expressions. Default expressions reject column references because a
71
+ column default cannot depend on another row value. Aggregates, windows, and
72
+ subqueries are rejected in every schema mode.
73
+
74
+ An extension must opt into the schema contract with `defineSchemaExpression()`:
75
+
76
+ ```ts
77
+ import { defineSchemaExpression, renderSchemaSql } from 'qubu/schema'
78
+
79
+ const currentDate = defineSchemaExpression('function', context => {
80
+ context.append('CURRENT_DATE')
81
+ })
82
+
83
+ renderSchemaSql(currentDate, { mode: 'default' })
84
+ ```
85
+
86
+ ## Use raw schema SQL only when necessary
87
+
88
+ Use `unsafeSchemaSql(dialect, sql)` only for trusted, parameter-free syntax that
89
+ Qubu does not model. Its dialect tag is checked when rendering, and its text is
90
+ preserved apart from normalizing line endings. A normal `makeExpression()`
91
+ extension must pass through the explicit `schemaExpression()` audit boundary
92
+ before schema rendering accepts it.
93
+
94
+ > [!WARNING]
95
+ > Raw schema SQL does not quote identifiers or bind values. Keep it in trusted
96
+ > extension code.
97
+
98
+ ## Read next
99
+
100
+ - [Tables and names](tables-and-names.md) covers logical IDs and SQL naming.
101
+ - [Constraints, keys, and indexes](constraints-and-indexes.md) covers metadata
102
+ consumed by grouped queries and schema adapters.
103
+ - [Canonical schema snapshots](snapshots.md) explains how storage and
104
+ constraints become serialized data.