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.
- package/README.md +112 -0
- package/dist/codegen.d.mts +1 -1
- package/dist/codegen.mjs +1 -1
- package/dist/{column-r1Y4ivwt.mjs → column-BzN8KFJa.mjs} +39 -2
- package/dist/{column-Da37jYSD.mjs → column-CFvSbil0.mjs} +1 -1
- package/dist/{complete-types-CY0KbzNw.d.mts → complete-types-CNMWBWap.d.mts} +1 -1
- package/dist/{constraints-YGyNPQ_z.mjs → constraints-DM_tarXc.mjs} +2 -2
- package/dist/core.d.mts +1 -1
- package/dist/core.mjs +2 -3
- package/dist/diagnostics-I9vVtXkc.mjs +40 -0
- package/dist/diff.d.mts +2 -2
- package/dist/expressions-BCjc08zw.mjs +129 -0
- package/dist/{index-B2rZf3-2.d.mts → index-CGui70hi.d.mts} +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +58 -15
- package/dist/introspection/mysql.d.mts +26 -0
- package/dist/introspection/mysql.mjs +1145 -0
- package/dist/introspection/postgres.d.mts +40 -0
- package/dist/introspection/postgres.mjs +1554 -0
- package/dist/introspection/sqlite.d.mts +15 -0
- package/dist/introspection/sqlite.mjs +986 -0
- package/dist/introspection.d.mts +3 -78
- package/dist/introspection.mjs +5 -3683
- package/dist/mysql.d.mts +1 -1
- package/dist/mysql.mjs +2 -2
- package/dist/{on-conflict-DZQ85f1t.mjs → on-conflict-CnaY5qso.mjs} +76 -4
- package/dist/postgres-Dey7QXPL.mjs +69 -0
- package/dist/postgres.d.mts +3 -3
- package/dist/postgres.mjs +3 -52
- package/dist/registry-oWDiqD7i.mjs +127 -0
- package/dist/{relational-CxnLCqZQ.mjs → relational-DSAJ-l58.mjs} +1 -2
- package/dist/schema.d.mts +1 -1
- package/dist/schema.mjs +7 -6
- package/dist/{serialize-BN07IK0v.mjs → serialize-CE-gw5_s.mjs} +3 -3
- package/dist/{serialize-CEIIlWhC.d.mts → serialize-OvXCLzjm.d.mts} +1 -1
- package/dist/snapshot/mysql.d.mts +2 -2
- package/dist/snapshot/mysql.mjs +28 -28
- package/dist/snapshot/postgres.d.mts +4 -4
- package/dist/snapshot/postgres.mjs +21 -21
- package/dist/snapshot/sqlite.d.mts +4 -4
- package/dist/snapshot/sqlite.mjs +27 -27
- package/dist/{snapshot-Xam8-q0j.mjs → snapshot-DgsOhf_8.mjs} +4 -42
- package/dist/snapshot.d.mts +4 -4
- package/dist/snapshot.mjs +1 -1
- package/dist/{source-SqrKWjFJ.mjs → source-BDuUXmAk.mjs} +2 -2
- package/dist/sqlite.d.mts +1 -1
- package/dist/sqlite.mjs +6 -6
- package/dist/{table-BwflqeAj.mjs → table-C1QGNe4P.mjs} +3 -3
- package/dist/{types-CTCqtFlS.d.mts → types-BEn0N_al.d.mts} +1 -1
- package/dist/{types-BIJsj2fJ.mjs → types-BLNRatG_.mjs} +2 -4
- package/dist/{types-C0VkiwpR.d.mts → types-DUe6eeI0.d.mts} +57 -28
- package/docs/guides/compose-queries.md +22 -0
- package/docs/guides/drizzle.md +11 -11
- package/docs/guides/mutations.md +89 -0
- package/docs/guides/valtio-sync.md +113 -0
- package/docs/migrations/index.md +50 -12
- package/docs/migrations/operations.md +20 -6
- package/docs/reference/supported-surface.md +17 -6
- package/docs/schema/code-generation.md +3 -2
- package/docs/schema/introspection.md +3 -2
- package/docs/sql-semantic-types.md +8 -0
- package/package.json +13 -1
- package/dist/registry-BRcUuazJ.mjs +0 -256
- package/dist/standard-DfcZEVOj.mjs +0 -12
- 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.
|
package/docs/migrations/index.md
CHANGED
|
@@ -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
|
|
29
|
-
|
|
|
30
|
-
| `@qubu/migrate/plan`
|
|
31
|
-
| `@qubu/migrate/ddl`
|
|
32
|
-
| `@qubu/migrate/
|
|
33
|
-
| `@qubu/migrate/
|
|
34
|
-
| `@qubu/migrate/
|
|
35
|
-
| `@qubu/migrate/
|
|
36
|
-
| `@qubu/migrate/
|
|
37
|
-
| `@qubu/migrate/
|
|
38
|
-
| `@qubu/migrate/
|
|
39
|
-
| `@qubu/migrate/
|
|
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
|
|
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
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 |
|
|
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
|
|
27
|
-
| `@qubu/migrate/
|
|
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 |
|
|
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
|
|
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
|
|
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
|
|
17
|
+
import { mapCatalogToSnapshot } from "qubu/introspection"
|
|
18
|
+
import { readCatalog } from "qubu/introspection/sqlite"
|
|
18
19
|
|
|
19
|
-
const catalog = await
|
|
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
|
|
62
|
+
import { mapCatalogToSnapshot } from "qubu/introspection"
|
|
63
|
+
import { readCatalog } from "qubu/introspection/sqlite"
|
|
63
64
|
|
|
64
|
-
const catalog = await
|
|
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.
|
|
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 };
|