qubu 0.6.0 → 0.6.1

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 (65) hide show
  1. package/README.md +112 -0
  2. package/dist/codegen.d.mts +1 -1
  3. package/dist/codegen.mjs +1 -1
  4. package/dist/{column-r1Y4ivwt.mjs → column-BzN8KFJa.mjs} +39 -2
  5. package/dist/{column-Da37jYSD.mjs → column-CFvSbil0.mjs} +1 -1
  6. package/dist/{complete-types-CY0KbzNw.d.mts → complete-types-CNMWBWap.d.mts} +1 -1
  7. package/dist/{constraints-YGyNPQ_z.mjs → constraints-DM_tarXc.mjs} +2 -2
  8. package/dist/core.d.mts +1 -1
  9. package/dist/core.mjs +2 -3
  10. package/dist/diagnostics-I9vVtXkc.mjs +40 -0
  11. package/dist/diff.d.mts +2 -2
  12. package/dist/expressions-BCjc08zw.mjs +129 -0
  13. package/dist/{index-B2rZf3-2.d.mts → index-CGui70hi.d.mts} +2 -2
  14. package/dist/index.d.mts +2 -2
  15. package/dist/index.mjs +58 -15
  16. package/dist/introspection/mysql.d.mts +26 -0
  17. package/dist/introspection/mysql.mjs +1145 -0
  18. package/dist/introspection/postgres.d.mts +40 -0
  19. package/dist/introspection/postgres.mjs +1554 -0
  20. package/dist/introspection/sqlite.d.mts +15 -0
  21. package/dist/introspection/sqlite.mjs +986 -0
  22. package/dist/introspection.d.mts +3 -78
  23. package/dist/introspection.mjs +5 -3683
  24. package/dist/mysql.d.mts +1 -1
  25. package/dist/mysql.mjs +2 -2
  26. package/dist/{on-conflict-DZQ85f1t.mjs → on-conflict-CnaY5qso.mjs} +76 -4
  27. package/dist/postgres-Dey7QXPL.mjs +69 -0
  28. package/dist/postgres.d.mts +3 -3
  29. package/dist/postgres.mjs +3 -52
  30. package/dist/registry-oWDiqD7i.mjs +127 -0
  31. package/dist/{relational-CxnLCqZQ.mjs → relational-DSAJ-l58.mjs} +1 -2
  32. package/dist/schema.d.mts +1 -1
  33. package/dist/schema.mjs +7 -6
  34. package/dist/{serialize-BN07IK0v.mjs → serialize-CE-gw5_s.mjs} +3 -3
  35. package/dist/{serialize-CEIIlWhC.d.mts → serialize-OvXCLzjm.d.mts} +1 -1
  36. package/dist/snapshot/mysql.d.mts +2 -2
  37. package/dist/snapshot/mysql.mjs +28 -28
  38. package/dist/snapshot/postgres.d.mts +4 -4
  39. package/dist/snapshot/postgres.mjs +21 -21
  40. package/dist/snapshot/sqlite.d.mts +4 -4
  41. package/dist/snapshot/sqlite.mjs +27 -27
  42. package/dist/{snapshot-Xam8-q0j.mjs → snapshot-DgsOhf_8.mjs} +4 -42
  43. package/dist/snapshot.d.mts +4 -4
  44. package/dist/snapshot.mjs +1 -1
  45. package/dist/{source-SqrKWjFJ.mjs → source-BDuUXmAk.mjs} +2 -2
  46. package/dist/sqlite.d.mts +1 -1
  47. package/dist/sqlite.mjs +6 -6
  48. package/dist/{table-BwflqeAj.mjs → table-C1QGNe4P.mjs} +3 -3
  49. package/dist/{types-CTCqtFlS.d.mts → types-BEn0N_al.d.mts} +1 -1
  50. package/dist/{types-BIJsj2fJ.mjs → types-BLNRatG_.mjs} +2 -4
  51. package/dist/{types-C0VkiwpR.d.mts → types-DUe6eeI0.d.mts} +57 -28
  52. package/docs/guides/compose-queries.md +22 -0
  53. package/docs/guides/drizzle.md +11 -11
  54. package/docs/guides/mutations.md +89 -0
  55. package/docs/guides/valtio-sync.md +113 -0
  56. package/docs/migrations/index.md +50 -12
  57. package/docs/migrations/operations.md +20 -6
  58. package/docs/reference/supported-surface.md +17 -6
  59. package/docs/schema/code-generation.md +3 -2
  60. package/docs/schema/introspection.md +3 -2
  61. package/docs/sql-semantic-types.md +8 -0
  62. package/package.json +13 -1
  63. package/dist/registry-BRcUuazJ.mjs +0 -256
  64. package/dist/standard-DfcZEVOj.mjs +0 -12
  65. package/dist/value-D14I_XgL.mjs +0 -29
@@ -0,0 +1,113 @@
1
+ # Valtio Sync
2
+
3
+ > Check Valtio Sync fields against Qubu tables and apply each client mutation
4
+ > with its sync event in one Qubu-owned transaction.
5
+
6
+ Install the optional integration beside Qubu, Valtio Sync, and Zod:
7
+
8
+ ```sh
9
+ pnpm add qubu @qubu/valtio-sync valtio-sync zod
10
+ ```
11
+
12
+ ## Check synced fields against Qubu tables
13
+
14
+ Use the integration's schema wrappers instead of `valtio-sync/schema` when a
15
+ definition corresponds to a Qubu table. Every selected table field must appear
16
+ in `fields`. Mark persistence-only or server-controlled fields with
17
+ `serverOnly()` so they are excluded from Valtio Sync validation and records.
18
+
19
+ ```ts
20
+ import { $type, defineCollection, serverOnly } from "@qubu/valtio-sync"
21
+ import { boolean, integer, table, text } from "qubu"
22
+ import { z } from "zod"
23
+
24
+ const todosTable = table("todos", {
25
+ ownerId: integer(),
26
+ id: text(),
27
+ title: text(),
28
+ done: boolean(),
29
+ })
30
+
31
+ export const todos = defineCollection({
32
+ dbType: $type<typeof todosTable>(),
33
+ fields: {
34
+ ownerId: serverOnly(),
35
+ id: z.string(),
36
+ title: z.string().default(""),
37
+ done: z.boolean().default(false),
38
+ },
39
+ })
40
+ ```
41
+
42
+ The Zod output for each synced field must be assignable to its Qubu selected
43
+ value. Narrow schemas are allowed, such as a Zod enum for a Qubu text field.
44
+ Missing fields, extra fields, and wider outputs fail type checking. The same
45
+ rules apply to `defineAccount()`.
46
+
47
+ ## Apply mutations in transactions
48
+
49
+ `applyOpsWithQubu()` converts Qubu-aware mutation handlers into the public
50
+ `ServerHandlers` contract accepted by `valtio-sync/server`. Supply a bound Qubu
51
+ client whose adapter supports transactions:
52
+
53
+ ```ts
54
+ import { applyOpsWithQubu } from "@qubu/valtio-sync"
55
+ import { and, eq, insertInto, returning, update, values, where } from "qubu"
56
+ import { valtioSync } from "valtio-sync/server"
57
+
58
+ type SyncContext = { user: { id: number } }
59
+
60
+ const handlers = applyOpsWithQubu<SyncContext>({
61
+ db,
62
+ syncEvents: {
63
+ write: async ({ tx, ctx, collection, recordId, op }) => {
64
+ const [event] = await tx.rows(
65
+ insertInto(
66
+ syncEvents,
67
+ values({ userId: ctx.user.id, collection, recordId, op }),
68
+ returning({ seq: syncEvents.seq }),
69
+ ),
70
+ )
71
+ return event.seq
72
+ },
73
+ },
74
+ authorize: ({ ctx, collection, op }) => assertCanSync(ctx.user, collection, op),
75
+ checkConflict: ({ tx, ctx, collection, op }) =>
76
+ assertFreshBaseVersion(tx, ctx.user.id, collection, op),
77
+ handlers: {
78
+ todos: {
79
+ readChanges: ({ ctx, since }) => readTodoChanges(ctx.user.id, since),
80
+ create: async ({ tx, ctx, record }) => {
81
+ const value = todos.recordSchema.parse(record)
82
+
83
+ await tx.execute(insertInto(todosTable, values({ ...value, ownerId: ctx.user.id })))
84
+ return {}
85
+ },
86
+ update: async ({ tx, ctx, op, patch }) => {
87
+ const value = todos.recordSchema.partial().parse(patch)
88
+
89
+ await tx.execute(
90
+ update(
91
+ todosTable,
92
+ value,
93
+ where(and(eq(todosTable.id, op.id), eq(todosTable.ownerId, ctx.user.id))),
94
+ ),
95
+ )
96
+ return {}
97
+ },
98
+ },
99
+ },
100
+ })
101
+
102
+ export const sync = valtioSync({ schema: { todos }, handlers })
103
+ ```
104
+
105
+ Authorization, conflict checks, the application mutation, and
106
+ `syncEvents.write()` run in that order inside one Qubu transaction. If any step
107
+ fails, the adapter rolls the transaction back. The event sequence becomes
108
+ `serverVersion` unless the mutation handler returns an explicit version. Read
109
+ handlers pass through unchanged.
110
+
111
+ The integration does not define persistence tables, import Drizzle, or execute
112
+ driver APIs. The application owns table design, authorization, conflict policy,
113
+ event retention, and every query issued through the supplied Qubu transaction.
@@ -25,18 +25,26 @@ The `@qubu/migrate` root intentionally exports only format/version constants
25
25
  and the central plan and artifact types. Import behavior from its focused
26
26
  entrypoint:
27
27
 
28
- | Entrypoint | Use it for |
29
- | -------------------------- | -------------------------------------------------------------------------- |
30
- | `@qubu/migrate/plan` | Create, encode, decode, fingerprint, and validate migration plans |
31
- | `@qubu/migrate/ddl` | Preview deterministic dialect SQL without opening a database |
32
- | `@qubu/migrate/artifact` | Compile programs; canonicalize, digest, seal, encode, and decode artifacts |
33
- | `@qubu/migrate/repository` | Verify a complete artifact chain and its journal prefix |
34
- | `@qubu/migrate/journal` | Implement or inspect the storage-neutral journal contract |
35
- | `@qubu/migrate/executor` | Apply artifacts and reconcile uncertain attempts |
36
- | `@qubu/migrate/baseline` | Verify and record the initial non-executable baseline |
37
- | `@qubu/migrate/status` | Inspect pending work, drift, requirements, and interrupted attempts |
38
- | `@qubu/migrate/bootstrap` | Plan a fresh SQLite database through the normal compiler |
39
- | `@qubu/migrate/testing` | Test adapter capabilities and deterministic failure boundaries |
28
+ | Entrypoint | Use it for |
29
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
30
+ | `@qubu/migrate/plan` | Create, encode, decode, fingerprint, and validate migration plans |
31
+ | `@qubu/migrate/ddl` | Preview deterministic dialect SQL without opening a database |
32
+ | `@qubu/migrate/ddl/postgres` | Preview PostgreSQL SQL from an approved migration plan |
33
+ | `@qubu/migrate/ddl/sqlite` | Preview SQLite SQL from an approved migration plan |
34
+ | `@qubu/migrate/ddl/mysql` | Preview MySQL SQL from an approved migration plan |
35
+ | `@qubu/migrate/artifact` | Compile programs with a caller-supplied `SchemaDialect`; canonicalize, digest, seal, encode, and decode artifacts |
36
+ | `@qubu/migrate/artifact/postgres` | Compile reviewed plans with PostgreSQL's schema dialect |
37
+ | `@qubu/migrate/artifact/sqlite` | Compile reviewed plans with SQLite's schema dialect, including table rebuilds |
38
+ | `@qubu/migrate/artifact/mysql` | Compile reviewed plans with MySQL's schema dialect |
39
+ | `@qubu/migrate/repository` | Verify a complete artifact chain and its journal prefix |
40
+ | `@qubu/migrate/journal` | Implement or inspect the storage-neutral journal contract |
41
+ | `@qubu/migrate/executor` | Apply artifacts and reconcile uncertain attempts |
42
+ | `@qubu/migrate/baseline` | Verify and record the initial non-executable baseline |
43
+ | `@qubu/migrate/status` | Inspect pending work, drift, requirements, and interrupted attempts |
44
+ | `@qubu/migrate/bootstrap` | Prepare a fresh schema diff and expose shared bootstrap types; accepts a caller-supplied `SchemaDialect` for generic planning |
45
+ | `@qubu/migrate/bootstrap/postgres` | Plan a fresh PostgreSQL schema through the normal compiler |
46
+ | `@qubu/migrate/bootstrap/sqlite` | Plan a fresh SQLite schema through the normal compiler |
47
+ | `@qubu/migrate/testing` | Test adapter capabilities and deterministic failure boundaries |
40
48
 
41
49
  Start with [Artifacts and approval policy](artifacts-and-policy.md) when
42
50
  reviewing a migration format. Check [Adapter capability
@@ -44,3 +52,33 @@ profiles](adapters.md), use [Command line operations](operations.md) to
44
52
  configure an application, then keep [Recovery and reconciliation](recovery.md)
45
53
  with the deployment runbook. [Lotta Games adoption](lotta-adoption.md) records
46
54
  the downstream cutover boundary and current combo-matrix blocker.
55
+
56
+ Choose the dialect-specific bootstrap entrypoint when using a built-in dialect:
57
+
58
+ ```ts
59
+ import { planSchemaBootstrap } from "@qubu/migrate/bootstrap/postgres"
60
+
61
+ const result = planSchemaBootstrap(targetSnapshot)
62
+ ```
63
+
64
+ The neutral `@qubu/migrate/bootstrap` entrypoint contains the shared preparation
65
+ logic and generic planner. The PostgreSQL and SQLite entrypoints each import
66
+ only their matching schema dialect.
67
+
68
+ Use the same entrypoint pattern for convenience artifact compilers:
69
+
70
+ ```ts
71
+ import { compileMigrationProgram } from "@qubu/migrate/artifact/postgres"
72
+
73
+ const compiled = compileMigrationProgram(plan)
74
+ ```
75
+
76
+ The DDL entrypoints follow the same pattern. Use the neutral entrypoint when
77
+ the application supplies a `SchemaDialect`; use a dialect subpath when the
78
+ built-in dialect should be selected by the module:
79
+
80
+ ```ts
81
+ import { emitMigrationPlan } from "@qubu/migrate/ddl/postgres"
82
+
83
+ const preview = emitMigrationPlan(plan)
84
+ ```
@@ -70,7 +70,7 @@ working directory.
70
70
  | `qubu migrate apply [--dry-run]` | Applies the complete verified pending chain; dry-run performs status/preflight only | It never limits discovery to Git-added or branch-diff files |
71
71
  | `qubu migrate baseline <id> --confirm <fact>... [--dry-run]` | Without dry-run, strictly compares the live managed schema, initializes an empty journal, records baseline, then writes the artifact | Requires an empty artifact repository and all seven exact confirmations; dry-run does not inspect the database |
72
72
  | `qubu migrate reconcile <attempt-id> --outcome applied\|rolled_back --reason <text>` | Runs application-owned verification, then records the explicit outcome | Requires `verifyReconciliation` in config; no automatic inference |
73
- | `qubu schema bootstrap [--approve <operation-id=reason>...] [--dry-run]` | Plans an empty SQLite snapshot through diff/plan/program; executes through the normal executor unless dry-run | Currently rejects non-SQLite targets |
73
+ | `qubu schema bootstrap [--approve <operation-id=reason>...] [--dry-run]` | Plans an empty SQLite or PostgreSQL snapshot through diff/plan/program; executes through the normal executor unless dry-run | Rejects other dialects; unsafe or incomplete facts still require exact approvals or custom programs |
74
74
 
75
75
  JSON output is stable, newline-terminated, recursively key-sorted, and redacts
76
76
  credential-like keys and credentials or secrets embedded in URLs. Human output
@@ -94,11 +94,25 @@ snapshot. Logical IDs help reporting but do not prove equality. Objects not
94
94
  owned by the managed snapshot are returned separately as `unmanagedObjects`;
95
95
  Qubu journal objects are excluded by migration snapshot readers.
96
96
 
97
- `schema bootstrap` is for a fresh SQLite database. It produces the same
98
- versioned program and validation path as a migration. SQLite inline constraints
99
- are compiled into table creation, while table rebuilds are explicit phases with
100
- copy/postcondition checks. Session settings such as SQLite PRAGMAs remain in
101
- the application or adapter setup.
97
+ `schema bootstrap` is for a fresh SQLite database or a fresh PostgreSQL schema.
98
+ It produces the same reviewed plan, versioned program, sealed artifact, and
99
+ executor path as a migration. A complete PostgreSQL snapshot retains standalone
100
+ enums as authoritative objects; bootstrap creates each enum before a table that
101
+ uses it as a native column type. SQLite inline constraints are compiled into
102
+ table creation, while table rebuilds are explicit phases with copy/postcondition
103
+ checks. Session settings such as SQLite PRAGMAs remain in the application or
104
+ adapter setup.
105
+
106
+ Use the reviewed complete snapshot directly as the PostgreSQL target:
107
+
108
+ ```bash
109
+ qubu schema bootstrap --dry-run --format json --non-interactive
110
+ ```
111
+
112
+ The dry run prints the ordered phases without opening the adapter. Remove
113
+ `--dry-run` only after reviewing any operation IDs that require `--approve` or
114
+ an application-owned custom program. Bootstrap does not import or replay
115
+ Drizzle migration history.
102
116
 
103
117
  ## Baseline and cutover checklist
104
118
 
@@ -10,7 +10,10 @@
10
10
  | `qubu/core` | Runtime | Fragment and rendering primitives, dialect construction, SQL types, and extension constructors |
11
11
  | `qubu/codegen` | Runtime | Deterministic machine-owned TypeScript schemas from complete, non-lossy introspection |
12
12
  | `qubu/diff` | Runtime | Canonical Snapshot v1 or v2 comparison, rename hints, suggestions, and safety diagnostics |
13
- | `qubu/introspection` | Runtime | Catalog readers, normalized catalogs, and mapping to Snapshot v1 or v2 |
13
+ | `qubu/introspection` | Runtime | Shared catalog contracts, normalized catalog models, diagnostics, and mapping to Snapshot v1 or v2 |
14
+ | `qubu/introspection/postgres` | Runtime | PostgreSQL catalog reader and catalog queries for one selected namespace |
15
+ | `qubu/introspection/sqlite` | Runtime | SQLite catalog reader and catalog queries for one selected namespace |
16
+ | `qubu/introspection/mysql` | Runtime | MySQL catalog reader and catalog queries for one selected namespace |
14
17
  | `qubu/mysql` | Runtime | The MySQL query dialect policy |
15
18
  | `qubu/postgres` | Runtime | PostgreSQL query dialect helpers such as `postgresDialect()` and `ilike()` |
16
19
  | `qubu/schema` | Runtime | Advanced schema metadata, storage and constraint models, source models, and schema-expression extensions |
@@ -23,14 +26,22 @@
23
26
  | `qubu/package.json` | JSON | The published package manifest |
24
27
  | `@qubu/migrate` | Runtime | Migration compiler format identity and shared plan types |
25
28
  | `@qubu/migrate/plan` | Runtime | Pure migration planning with dependencies, decisions, preconditions, and explicit custom SQL |
26
- | `@qubu/migrate/ddl` | Runtime | DDL preflight and deterministic PostgreSQL, SQLite, or MySQL emission from a migration plan |
27
- | `@qubu/migrate/artifact` | Runtime | Versioned programs, strict artifacts and baselines, canonical encoding, and SHA-256 integrity |
29
+ | `@qubu/migrate/ddl` | Runtime | DDL preflight and generic emission from a migration plan and supplied schema dialect |
30
+ | `@qubu/migrate/ddl/postgres` | Runtime | PostgreSQL DDL emission from an approved migration plan |
31
+ | `@qubu/migrate/ddl/sqlite` | Runtime | SQLite DDL emission from an approved migration plan |
32
+ | `@qubu/migrate/ddl/mysql` | Runtime | MySQL DDL emission from an approved migration plan |
33
+ | `@qubu/migrate/artifact` | Runtime | Generic versioned program compilation with a caller-supplied schema dialect, plus strict artifacts and baselines |
34
+ | `@qubu/migrate/artifact/postgres` | Runtime | PostgreSQL versioned program compilation |
35
+ | `@qubu/migrate/artifact/sqlite` | Runtime | SQLite versioned program compilation, including table rebuilds |
36
+ | `@qubu/migrate/artifact/mysql` | Runtime | MySQL versioned program compilation |
28
37
  | `@qubu/migrate/repository` | Runtime | Strict full-chain and journal-prefix verification |
29
38
  | `@qubu/migrate/journal` | Runtime | Storage-neutral journal records, transitions, validation, and reference storage |
30
39
  | `@qubu/migrate/executor` | Runtime | Portable execution, structured errors, checkpointing, and explicit reconciliation |
31
40
  | `@qubu/migrate/baseline` | Runtime | Strict live baseline verification and physical managed-schema comparison |
32
41
  | `@qubu/migrate/status` | Runtime | Pending chain, managed drift, unmanaged objects, interrupted attempts, and incompatible requirements |
33
- | `@qubu/migrate/bootstrap` | Runtime | Fresh SQLite schema planning through the normal diff, plan, and program compiler |
42
+ | `@qubu/migrate/bootstrap` | Runtime | Shared bootstrap preparation, result types, and generic planning with a caller-supplied schema dialect |
43
+ | `@qubu/migrate/bootstrap/postgres` | Runtime | Fresh PostgreSQL schema planning through the normal diff, plan, and program compiler |
44
+ | `@qubu/migrate/bootstrap/sqlite` | Runtime | Fresh SQLite schema planning through the normal diff, plan, and program compiler |
34
45
  | `@qubu/migrate/testing` | Runtime | Deterministic fake adapters, fault boundaries, and adapter conformance checks |
35
46
  | `@qubu/cli` | Runtime and CLI | `@alloc/cmd-ts` commands, typed config, filesystem repositories, stable output, and exit codes |
36
47
  | `@qubu/drizzle` | Runtime | Shared Drizzle conversion errors and dialect types |
@@ -69,7 +80,7 @@ entrypoint table.
69
80
  | Read queries | Named projections, spreadable source columns, aliases, joins, typed custom and LATERAL `FROM` sources, correlated subqueries, `WHERE`, grouping with declared-key proofs, `HAVING`, ordering, window expressions, distinctness, pagination, row locking, ordinary and recursive CTEs, subqueries, and set operations |
70
81
  | Expressions | Comparison, boolean, arithmetic, null, range, membership, aggregate, window, string, JSON scalar reads, definition-backed and raw casts, cases, parameterized SQL templates, custom expressions, and branded deterministic schema expressions |
71
82
  | SQL type metadata | Portable domains and capabilities, physical column storage descriptors, `SqlTypeOf`, projected SQL type maps, `SourceLike` and `TableLike` field constraints, contextual literals, typed extension values, calls, and casts, plus a permissive `SqlUnknown` fallback |
72
- | Write queries | `INSERT` values, defaults, and selects; `UPDATE`; `DELETE`; typed assignments; `RETURNING`; and explicit unrestricted-write opt-in |
83
+ | Write queries | `INSERT` values, defaults, and selects; typed `UPDATE`, PostgreSQL `UPDATE ... FROM`, and `DELETE`; typed assignments; `RETURNING`; and explicit unrestricted-write opt-in |
73
84
  | Rendering | Standard, PostgreSQL, SQLite, MySQL, and user-created policies for identifiers, placeholders, pagination, row locking, JSON, logical cast targets, schema literals, and EXPLAIN options |
74
85
  | Execution boundary | `QueryAdapter`, opt-in `ExplainableQueryAdapter`, `StreamingQueryAdapter`, and `TransactionalQueryAdapter` capabilities, bound clients from `qubu()`, structured results from `execute()` or `db.execute()`, row-only results from `executeRows()` or `db.rows()`, typed read streams from `stream()` or `db.stream()`, and adapter-decoded plan rows from `explain()` or `db.explain()` |
75
86
  | Snapshots | Pure Snapshot v1 and v2 creation, canonical encoding and strict decoding, immutable data, diagnostics, and FNV change-detection fingerprints |
@@ -77,7 +88,7 @@ entrypoint table.
77
88
  | Snapshot diffing | Pure Snapshot v1 and v2 comparison, explicit rename evidence, non-authoritative suggestions, and safety diagnostics |
78
89
  | Migration planning | Pure, dialect-neutral plans with stable ordering, dependency edges, preconditions, explicit review decisions, and tagged custom SQL |
79
90
  | DDL emission | Preflight plus deterministic PostgreSQL, SQLite, and MySQL statements from an approved `MigrationPlan` and matching `SchemaDialect` |
80
- | Migration operations | Strict artifacts and baselines, authoritative programs, repository and journal validation, adapter capability preflight, execution, status/drift, reconciliation, and SQLite bootstrap |
91
+ | Migration operations | Strict artifacts and baselines, authoritative programs, repository and journal validation, adapter capability preflight, execution, status/drift, reconciliation, SQLite bootstrap, and complete PostgreSQL bootstrap with standalone enum ordering |
81
92
  | Build tooling | The optional Vite directive transform and its matching TypeScript ambient declarations |
82
93
  | Drizzle conversion | Optional, dialect-specific runtime conversion from Qubu schema registries to Drizzle tables |
83
94
  | Source generation | Pure Snapshot v1 table source printing, deterministic camelCase IDs, exact physical metadata, controlled type mappings, and structured failure diagnostics |
@@ -14,9 +14,10 @@ generator:
14
14
  ```ts
15
15
  import { writeFile } from "node:fs/promises"
16
16
  import { generateSchemaSource } from "qubu/codegen"
17
- import { mapCatalogToSnapshot, readSqliteCatalog } from "qubu/introspection"
17
+ import { mapCatalogToSnapshot } from "qubu/introspection"
18
+ import { readCatalog } from "qubu/introspection/sqlite"
18
19
 
19
- const catalog = await readSqliteCatalog(connection, { namespace: "main" })
20
+ const catalog = await readCatalog(connection, { namespace: "main" })
20
21
  const introspection = mapCatalogToSnapshot(catalog, { namespace: "main" })
21
22
  const generated = generateSchemaSource(introspection)
22
23
 
@@ -59,9 +59,10 @@ catalog can later support inspection, source generation, or another snapshot
59
59
  format:
60
60
 
61
61
  ```ts
62
- import { mapCatalogToSnapshot, readSqliteCatalog } from "qubu/introspection"
62
+ import { mapCatalogToSnapshot } from "qubu/introspection"
63
+ import { readCatalog } from "qubu/introspection/sqlite"
63
64
 
64
- const catalog = await readSqliteCatalog(connection, { namespace: "main" })
65
+ const catalog = await readCatalog(connection, { namespace: "main" })
65
66
  const result = mapCatalogToSnapshot(catalog, {
66
67
  namespace: "main",
67
68
  mode: "strict",
@@ -89,6 +89,14 @@ operation. This contextual typing does not relabel an expression: comparing a
89
89
  `SqlUuid` expression with a `SqlText` expression is still rejected. Cast when
90
90
  the database operation intentionally changes domains:
91
91
 
92
+ Scalar text functions bind primitive operands automatically, and `coalesce()`
93
+ uses its first expression to type primitive fallbacks:
94
+
95
+ ```ts
96
+ upper("Ada") // UPPER(?)
97
+ coalesce(metrics.label, "Anonymous") // COALESCE("metrics"."label", ?)
98
+ ```
99
+
92
100
  ```ts
93
101
  import { cast, like, text } from "qubu"
94
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qubu",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/aleclarson/qubu"
@@ -32,6 +32,18 @@
32
32
  "types": "./dist/introspection.d.mts",
33
33
  "import": "./dist/introspection.mjs"
34
34
  },
35
+ "./introspection/mysql": {
36
+ "types": "./dist/introspection/mysql.d.mts",
37
+ "import": "./dist/introspection/mysql.mjs"
38
+ },
39
+ "./introspection/postgres": {
40
+ "types": "./dist/introspection/postgres.d.mts",
41
+ "import": "./dist/introspection/postgres.mjs"
42
+ },
43
+ "./introspection/sqlite": {
44
+ "types": "./dist/introspection/sqlite.d.mts",
45
+ "import": "./dist/introspection/sqlite.mjs"
46
+ },
35
47
  "./mysql": {
36
48
  "types": "./dist/mysql.d.mts",
37
49
  "import": "./dist/mysql.mjs"
@@ -1,256 +0,0 @@
1
- import { t as standardDialect } from "./standard-DfcZEVOj.mjs";
2
- import { _ as isSchemaExpression, h as snakeCaseIdentifier, r as isColumnReference, x as markSchemaExpression, y as makeSchemaExpression } from "./column-r1Y4ivwt.mjs";
3
- import { n as isValueExpression } from "./value-D14I_XgL.mjs";
4
- //#region src/schema/expressions.ts
5
- /** Error raised before a schema expression can become persisted SQL. */
6
- var SchemaExpressionError = class extends TypeError {
7
- code;
8
- mode;
9
- constructor(code, message, mode) {
10
- super(message);
11
- this.name = "SchemaExpressionError";
12
- this.code = code;
13
- this.mode = mode;
14
- }
15
- };
16
- /**
17
- * Normalize only line endings. Whitespace, quoting, and every other byte of a raw schema expression
18
- * remain under the extension author's control.
19
- */
20
- function normalizeSchemaSql(sql) {
21
- return sql.replace(/\r\n?/g, "\n");
22
- }
23
- function schemaExpression(expression) {
24
- return markSchemaExpression(expression);
25
- }
26
- /**
27
- * Define an extension with the restricted schema context. This is the typed alternative to
28
- * {@link unsafeSchemaSql} for deterministic custom syntax.
29
- */
30
- function defineSchemaExpression(kind, render) {
31
- return makeSchemaExpression(kind, (context) => render(context));
32
- }
33
- function unsafeSchemaSql(dialectOrOptions, sql) {
34
- const dialect = typeof dialectOrOptions === "string" ? dialectOrOptions : dialectOrOptions.dialect;
35
- const source = typeof dialectOrOptions === "string" ? sql : dialectOrOptions.sql;
36
- if (!dialect) throw new TypeError("unsafeSchemaSql() requires a dialect tag");
37
- if (source === void 0) throw new TypeError("unsafeSchemaSql() requires SQL text");
38
- const normalized = normalizeSchemaSql(source);
39
- const expression = makeSchemaExpression("unsafe", (context) => context.append(normalized));
40
- return Object.freeze({
41
- ...expression,
42
- schemaSqlDialect: dialect,
43
- schemaSql: normalized
44
- });
45
- }
46
- /** Identify a dialect-tagged raw schema expression. */
47
- function isUnsafeSchemaSql(value) {
48
- return isSchemaExpression(value) && value.expressionKind === "unsafe" && typeof value.schemaSqlDialect === "string" && typeof value.schemaSql === "string";
49
- }
50
- function renderSchemaExpression(expression, optionsOrMode, dialectOption) {
51
- const options = typeof optionsOrMode === "string" ? {
52
- mode: optionsOrMode,
53
- dialect: dialectOption
54
- } : optionsOrMode;
55
- const dialect = options.dialect ?? standardDialect();
56
- if (!isSchemaExpression(expression)) throw new SchemaExpressionError("not-deterministic", "Only branded deterministic expressions can be rendered as schema SQL", options.mode);
57
- assertSupportedExpression(expression, options.mode);
58
- let text = "";
59
- const context = {
60
- dialect,
61
- projectionMode: "result",
62
- schemaMode: options.mode,
63
- append(value) {
64
- text += value;
65
- },
66
- parameter() {
67
- throw new SchemaExpressionError("parameter", "Schema expressions cannot render query parameters", options.mode);
68
- },
69
- literal(value) {
70
- text += renderSchemaLiteral(dialect, value, options.mode);
71
- },
72
- renderColumnReference(columnName) {
73
- if (options.mode === "default") throw new SchemaExpressionError("column-not-allowed", "Default expressions cannot reference table columns", options.mode);
74
- text += dialect.quoteIdentifier(columnName);
75
- },
76
- render(part) {
77
- renderSchemaPart(context, part, options.mode);
78
- },
79
- renderRelation() {
80
- throw new SchemaExpressionError("unsupported-expression", "Schema expressions cannot contain subqueries", options.mode);
81
- }
82
- };
83
- renderSchemaPart(context, expression, options.mode);
84
- return Object.freeze({
85
- text,
86
- parameters: Object.freeze([])
87
- });
88
- }
89
- /** Convenience form for callers that only need the SQL text. */
90
- function renderSchemaSql(expression, options) {
91
- return renderSchemaExpression(expression, options).text;
92
- }
93
- function renderSchemaPart(context, part, mode) {
94
- if (isValueExpression(part)) {
95
- context.literal(part.value);
96
- return;
97
- }
98
- if (isColumnReference(part)) {
99
- context.renderColumnReference(part.columnName);
100
- return;
101
- }
102
- if (isUnsafeSchemaSql(part)) {
103
- if (part.schemaSqlDialect !== context.dialect.name) throw new SchemaExpressionError("dialect-mismatch", `Schema SQL is tagged for "${part.schemaSqlDialect}" but rendered for "${context.dialect.name}"`, mode);
104
- context.append(part.schemaSql);
105
- return;
106
- }
107
- if (!isSchemaExpression(part)) throw new SchemaExpressionError("not-deterministic", "Schema expressions may only compose branded expressions, columns, and literals", mode);
108
- assertSupportedExpression(part, mode);
109
- part.render(context);
110
- }
111
- function assertSupportedExpression(expression, mode) {
112
- if (expression.expressionKind === "subquery" || expression.expressionCategory) throw new SchemaExpressionError("unsupported-expression", "Aggregates, windows, and subqueries are not valid schema expressions", mode);
113
- }
114
- function renderSchemaLiteral(dialect, value, mode) {
115
- if (dialect.renderSchemaLiteral) {
116
- const rendered = dialect.renderSchemaLiteral(value);
117
- if (typeof rendered !== "string" || rendered.includes("?")) throw new SchemaExpressionError("invalid-literal", "A schema literal renderer must return parameter-free SQL text", mode);
118
- return rendered;
119
- }
120
- if (value === null) return "NULL";
121
- if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
122
- if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
123
- if (typeof value === "bigint") return String(value);
124
- if (typeof value === "number") {
125
- if (!Number.isFinite(value)) throw new SchemaExpressionError("unsupported-value", "Schema literals require finite numbers", mode);
126
- return Object.is(value, -0) ? "0" : String(value);
127
- }
128
- throw new SchemaExpressionError("unsupported-value", `Unsupported schema literal type: ${value === void 0 ? "undefined" : typeof value}`, mode);
129
- }
130
- //#endregion
131
- //#region src/schema/registry.ts
132
- /** The first naming-policy version used by schema metadata. */
133
- const schemaNamingPolicyVersion = 1;
134
- /**
135
- * The built-in naming policy for schema-generated physical names.
136
- *
137
- * Explicit names supplied to `table()` remain unchanged. The policy is used by tooling when it
138
- * needs a physical name for a logical table ID.
139
- */
140
- const defaultSchemaNamingPolicy = Object.freeze({
141
- version: 1,
142
- tableName: snakeCaseIdentifier
143
- });
144
- /** Error thrown when a root schema fails registry or naming validation. */
145
- var SchemaValidationError = class extends Error {
146
- name = "SchemaValidationError";
147
- diagnostics;
148
- /** Alias matching validation libraries that call findings "issues". */
149
- issues;
150
- constructor(diagnostics) {
151
- const frozenDiagnostics = Object.freeze(diagnostics.map((diagnostic) => Object.freeze({
152
- ...diagnostic,
153
- path: Object.freeze([...diagnostic.path]),
154
- relatedPaths: diagnostic.relatedPaths ? Object.freeze(diagnostic.relatedPaths.map((path) => Object.freeze([...path]))) : void 0
155
- })));
156
- super(frozenDiagnostics.map((diagnostic) => diagnostic.message).join("\n"));
157
- this.diagnostics = frozenDiagnostics;
158
- this.issues = frozenDiagnostics;
159
- }
160
- };
161
- function entriesOf(input) {
162
- if (Array.isArray(input)) return input;
163
- return Object.entries(input);
164
- }
165
- function validLogicalId(id) {
166
- return id.length > 0 && id === id.trim() && !/[.\\/\u0000-\u001f\u007f]/u.test(id);
167
- }
168
- function validNamespace(namespace) {
169
- return namespace.length > 0 && namespace === namespace.trim() && !/[.\\/\u0000-\u001f\u007f"']/u.test(namespace);
170
- }
171
- function validateEntries(entries, namespace, namingPolicy) {
172
- const diagnostics = [];
173
- const ids = /* @__PURE__ */ new Map();
174
- const physicalNames = /* @__PURE__ */ new Map();
175
- const generatedNames = /* @__PURE__ */ new Map();
176
- for (const [index, [id, table]] of entries.entries()) {
177
- const path = ["tables", id];
178
- const previousId = ids.get(id);
179
- if (previousId !== void 0) diagnostics.push({
180
- code: "duplicate-table-id",
181
- message: `Table ID "${id}" is declared more than once`,
182
- path,
183
- relatedPaths: [["tables", entries[previousId][0]]]
184
- });
185
- else ids.set(id, index);
186
- if (!validLogicalId(id)) diagnostics.push({
187
- code: "invalid-table-id",
188
- message: `Table ID "${id}" must be a non-empty logical identifier`,
189
- path
190
- });
191
- const physicalName = table.tableName || namingPolicy.tableName(id);
192
- const previousPhysicalName = physicalNames.get(physicalName);
193
- if (previousPhysicalName !== void 0) diagnostics.push({
194
- code: "duplicate-physical-name",
195
- message: `Tables "${entries[previousPhysicalName][0]}" and "${id}" both use physical name "${physicalName}"`,
196
- path: [...path, "physicalName"],
197
- relatedPaths: [[
198
- "tables",
199
- entries[previousPhysicalName][0],
200
- "physicalName"
201
- ]]
202
- });
203
- else physicalNames.set(physicalName, index);
204
- const generatedName = namingPolicy.tableName(id);
205
- const previousGeneratedName = generatedNames.get(generatedName);
206
- if (previousGeneratedName !== void 0) diagnostics.push({
207
- code: "generated-name-collision",
208
- message: `Logical table IDs "${entries[previousGeneratedName][0]}" and "${id}" generate the same physical name "${generatedName}"`,
209
- path: [...path, "generatedName"],
210
- relatedPaths: [[
211
- "tables",
212
- entries[previousGeneratedName][0],
213
- "generatedName"
214
- ]]
215
- });
216
- else generatedNames.set(generatedName, index);
217
- }
218
- if (namespace !== void 0 && !validNamespace(namespace)) diagnostics.push({
219
- code: "invalid-namespace",
220
- message: `Schema namespace "${namespace}" must be a non-empty identifier without qualification or control characters`,
221
- path: ["namespace"]
222
- });
223
- return Object.freeze(diagnostics);
224
- }
225
- function freezeTableNames(entries, namingPolicy) {
226
- return Object.freeze(Object.fromEntries(entries.map(([id, table]) => [id, table.tableName || namingPolicy.tableName(id)])));
227
- }
228
- function createSchema(entries, tables, options = {}) {
229
- const namingPolicy = options.namingPolicy ?? defaultSchemaNamingPolicy;
230
- const diagnostics = validateEntries(entries, options.namespace, namingPolicy);
231
- if (diagnostics.length > 0) throw new SchemaValidationError(diagnostics);
232
- const tableNames = freezeTableNames(entries, namingPolicy);
233
- const registry = Object.freeze(Object.fromEntries(entries.map(([id, table]) => [id, Object.freeze({
234
- id,
235
- table,
236
- physicalName: tableNames[id]
237
- })])));
238
- return Object.freeze({
239
- schemaKind: "schema",
240
- tables: Object.freeze({ ...tables }),
241
- registry,
242
- tableNames,
243
- namespace: options.namespace,
244
- namingPolicy: Object.freeze({ ...namingPolicy })
245
- });
246
- }
247
- function schema(tables, options) {
248
- const entries = entriesOf(tables);
249
- return createSchema(entries, Object.fromEntries(entries), options);
250
- }
251
- /** Generate a v1 physical name for a logical table ID. */
252
- function generatedTableName(logicalId, namingPolicy = defaultSchemaNamingPolicy) {
253
- return namingPolicy.tableName(logicalId);
254
- }
255
- //#endregion
256
- export { schemaNamingPolicyVersion as a, isUnsafeSchemaSql as c, renderSchemaSql as d, schemaExpression as f, schema as i, normalizeSchemaSql as l, defaultSchemaNamingPolicy as n, SchemaExpressionError as o, unsafeSchemaSql as p, generatedTableName as r, defineSchemaExpression as s, SchemaValidationError as t, renderSchemaExpression as u };
@@ -1,12 +0,0 @@
1
- import { i as standardJson, o as createDialect } from "./json-Db7XRD91.mjs";
2
- //#region src/dialects/standard.ts
3
- /** SQL:2008-style rendering defaults used by the core builder. */
4
- function standardDialect() {
5
- return createDialect({
6
- name: "standard-sql",
7
- placeholder: () => "?",
8
- json: standardJson
9
- });
10
- }
11
- //#endregion
12
- export { standardDialect as t };