qubu 0.3.5 → 0.4.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/dist/{canonical-BbnqavJm.mjs → canonical-BXUguqfo.mjs} +23 -23
- package/dist/codegen.d.mts +25 -33
- package/dist/codegen.mjs +38 -45
- package/dist/column-hqKr7-1I.mjs +620 -0
- package/dist/{complete-D5Djh-zo.mjs → complete-WYyVozgK.mjs} +50 -50
- package/dist/{complete-types-B2PO6wQD.d.mts → complete-types-IjEn5VPN.d.mts} +56 -56
- package/dist/core.d.mts +2 -2
- package/dist/core.mjs +4 -4
- package/dist/ddl.d.mts +12 -12
- package/dist/ddl.mjs +13 -14
- package/dist/{dialect-b2-Z6uBF.mjs → dialect-wUKrnPMB.mjs} +2 -2
- package/dist/diff.d.mts +1 -1
- package/dist/diff.mjs +6 -7
- package/dist/{index-7qc6OcIC.d.mts → index-1DpA3mUh.d.mts} +20 -20
- package/dist/{index-CqWnouTK.d.mts → index-CPvfEheG.d.mts} +13 -15
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +148 -120
- package/dist/introspection.d.mts +8 -8
- package/dist/introspection.mjs +29 -30
- package/dist/{json-CUZlv4HT.mjs → json-Db7XRD91.mjs} +2 -2
- package/dist/migration.d.mts +20 -21
- package/dist/migration.mjs +5 -6
- package/dist/{mysql-DqkqXB6A.mjs → mysql-B_cYzzX2.mjs} +237 -7
- package/dist/mysql.d.mts +1 -1
- package/dist/mysql.mjs +3 -3
- package/dist/{on-conflict-BxnxubMb.mjs → on-conflict-hfPW0KmQ.mjs} +4 -4
- package/dist/postgres.d.mts +5 -5
- package/dist/postgres.mjs +6 -6
- package/dist/{registry-BufIskVN.mjs → registry-BRMLYwDp.mjs} +27 -135
- package/dist/{relational-DCZrrNia.mjs → relational-BZ3WDPzC.mjs} +4 -4
- package/dist/schema.d.mts +2 -2
- package/dist/schema.mjs +6 -6
- package/dist/{snapshot-CWPgzxNx.mjs → snapshot-C-W65HEd.mjs} +5 -5
- package/dist/snapshot.d.mts +3 -3
- package/dist/snapshot.mjs +5 -7
- package/dist/{source-DUoJVXmL.mjs → source-BcS2AsIg.mjs} +7 -9
- package/dist/{serialize-PF1cfH2P.mjs → sqlite-Cg0nwYEH.mjs} +326 -12
- package/dist/sqlite.d.mts +2 -2
- package/dist/sqlite.mjs +2 -2
- package/dist/{standard-BTVYKh_F.mjs → standard-DfcZEVOj.mjs} +1 -1
- package/dist/{table-CCUJ60rB.mjs → table-Bp5irMSj.mjs} +5 -7
- package/dist/{types-D8M1yZF4.d.mts → types-BK1COGZe.d.mts} +2661 -2150
- package/dist/{types-Cec0xzo4.mjs → types-JM3FcAnX.mjs} +8 -8
- package/dist/{types-LBt5rclR.d.mts → types-JSZHpUEj.d.mts} +83 -84
- package/dist/{value-BvilP0oz.mjs → value-Bi71Agyf.mjs} +1 -1
- package/dist/vite/ambient.d.ts +311 -390
- package/dist/vite.d.mts +5 -6
- package/dist/vite.mjs +3 -4
- package/docs/dialects-and-execution.md +121 -81
- package/docs/getting-started.md +4 -4
- package/docs/guides/better-auth.md +57 -0
- package/docs/guides/compose-queries.md +24 -67
- package/docs/guides/drizzle.md +23 -23
- package/docs/guides/extensions/dialects.md +4 -4
- package/docs/guides/extensions/sources-and-clauses.md +15 -15
- package/docs/guides/extensions/typed-expressions.md +13 -17
- package/docs/guides/extensions/unsafe-syntax.md +3 -3
- package/docs/guides/json.md +7 -7
- package/docs/guides/mutations.md +14 -21
- package/docs/guides/select/conditions.md +6 -8
- package/docs/guides/select/grouping-and-windows.md +4 -15
- package/docs/guides/select/ordering-and-pagination.md +6 -19
- package/docs/guides/select/overview.md +12 -26
- package/docs/guides/sql-templates.md +20 -24
- package/docs/guides/vite-plugin.md +9 -13
- package/docs/index.md +5 -7
- package/docs/query-model/fragments.md +4 -15
- package/docs/query-model/result-shapes.md +11 -14
- package/docs/query-model/source-scope.md +31 -49
- package/docs/reference/mysql-snapshot.md +2 -5
- package/docs/reference/postgres-snapshot.md +3 -3
- package/docs/reference/sqlite-snapshot.md +2 -5
- package/docs/reference/supported-surface.md +33 -33
- package/docs/schema/catalog-model.md +2 -5
- package/docs/schema/code-generation.md +18 -18
- package/docs/schema/columns-and-writes.md +17 -25
- package/docs/schema/constraints-and-indexes.md +36 -52
- package/docs/schema/ddl-emission.md +6 -6
- package/docs/schema/diff.md +5 -5
- package/docs/schema/introspection.md +7 -7
- package/docs/schema/migration-plans.md +9 -9
- package/docs/schema/snapshots.md +3 -3
- package/docs/schema/storage-and-schema-sql.md +11 -11
- package/docs/schema/tables-and-names.md +11 -14
- package/docs/sql-semantic-types.md +10 -10
- package/docs/troubleshooting.md +10 -10
- package/package.json +21 -33
- package/skills/qubu/agents/openai.yaml +3 -3
- package/dist/column-CXMxx8Hq.mjs +0 -118
- package/dist/column-CYMbKbOy.mjs +0 -290
- package/dist/drizzle-mysql.d.mts +0 -24
- package/dist/drizzle-mysql.mjs +0 -72
- package/dist/drizzle-postgres.d.mts +0 -24
- package/dist/drizzle-postgres.mjs +0 -73
- package/dist/drizzle-sqlite.d.mts +0 -50
- package/dist/drizzle-sqlite.mjs +0 -108
- package/dist/drizzle.d.mts +0 -13
- package/dist/drizzle.mjs +0 -2
- package/dist/errors-BGCoLe_r.mjs +0 -14
- package/dist/naming-QVCOnSj2.mjs +0 -20
- package/dist/postgres-DEBBeh52.mjs +0 -235
- package/dist/runtime-Cn_Xgzta.mjs +0 -193
- package/dist/sqlite-BU6DBxef.mjs +0 -320
- package/dist/types-Ctlxz1ip.d.mts +0 -45
|
@@ -12,19 +12,19 @@ Read and map one namespace in strict mode, then pass that exact result to the
|
|
|
12
12
|
generator:
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
import { writeFile } from
|
|
16
|
-
import { generateSchemaSource } from
|
|
17
|
-
import { mapCatalogToSnapshot, readSqliteCatalog } from
|
|
15
|
+
import { writeFile } from "node:fs/promises"
|
|
16
|
+
import { generateSchemaSource } from "qubu/codegen"
|
|
17
|
+
import { mapCatalogToSnapshot, readSqliteCatalog } from "qubu/introspection"
|
|
18
18
|
|
|
19
|
-
const catalog = await readSqliteCatalog(connection, { namespace:
|
|
20
|
-
const introspection = mapCatalogToSnapshot(catalog, { namespace:
|
|
19
|
+
const catalog = await readSqliteCatalog(connection, { namespace: "main" })
|
|
20
|
+
const introspection = mapCatalogToSnapshot(catalog, { namespace: "main" })
|
|
21
21
|
const generated = generateSchemaSource(introspection)
|
|
22
22
|
|
|
23
23
|
if (!generated.ok) {
|
|
24
|
-
throw new Error(generated.diagnostics.map(issue => issue.message).join(
|
|
24
|
+
throw new Error(generated.diagnostics.map((issue) => issue.message).join("\n"))
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
await writeFile(
|
|
27
|
+
await writeFile("src/schema.generated.ts", generated.source, "utf8")
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
`writeFile()` belongs to the application; `generateSchemaSource()` only
|
|
@@ -48,13 +48,13 @@ the generated module is accepted, its serialized snapshot becomes the identity
|
|
|
48
48
|
baseline for the next catalog read:
|
|
49
49
|
|
|
50
50
|
```ts
|
|
51
|
-
import { mapCatalogToSnapshot } from
|
|
52
|
-
import { createSqliteSchemaSnapshot } from
|
|
53
|
-
import { mainSchema } from
|
|
51
|
+
import { mapCatalogToSnapshot } from "qubu/introspection"
|
|
52
|
+
import { createSqliteSchemaSnapshot } from "qubu/snapshot"
|
|
53
|
+
import { mainSchema } from "./schema.generated.ts"
|
|
54
54
|
|
|
55
55
|
const previousSnapshot = createSqliteSchemaSnapshot(mainSchema)
|
|
56
56
|
const next = mapCatalogToSnapshot(nextCatalog, {
|
|
57
|
-
namespace:
|
|
57
|
+
namespace: "main",
|
|
58
58
|
previousSnapshot,
|
|
59
59
|
})
|
|
60
60
|
```
|
|
@@ -75,17 +75,17 @@ Use the controlled callbacks to adopt trusted names or application mappings:
|
|
|
75
75
|
```ts
|
|
76
76
|
const generated = generateSchemaSource(introspection, {
|
|
77
77
|
naming(context) {
|
|
78
|
-
if (context.kind ===
|
|
79
|
-
return
|
|
78
|
+
if (context.kind === "table" && context.physicalName === "user_records") {
|
|
79
|
+
return "users"
|
|
80
80
|
}
|
|
81
81
|
},
|
|
82
82
|
mapColumn(context) {
|
|
83
|
-
if (context.columnPhysicalName ===
|
|
83
|
+
if (context.columnPhysicalName === "account_id") {
|
|
84
84
|
return {
|
|
85
|
-
output:
|
|
86
|
-
insert:
|
|
87
|
-
update:
|
|
88
|
-
sqlDomain:
|
|
85
|
+
output: "string",
|
|
86
|
+
insert: "string",
|
|
87
|
+
update: "string",
|
|
88
|
+
sqlDomain: "uuid",
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
},
|
|
@@ -17,9 +17,9 @@ Use `column<Output, Insert, Update>()` when the driver returns a different type
|
|
|
17
17
|
from the type the application writes:
|
|
18
18
|
|
|
19
19
|
```ts
|
|
20
|
-
import { column, integer, table, text } from
|
|
20
|
+
import { column, integer, table, text } from "qubu"
|
|
21
21
|
|
|
22
|
-
const accounts = table(
|
|
22
|
+
const accounts = table("accounts", {
|
|
23
23
|
id: integer({ generated: true }),
|
|
24
24
|
email: text(),
|
|
25
25
|
nickname: text({ nullable: true, hasDefault: true }),
|
|
@@ -36,27 +36,19 @@ The legacy hasDefault and generated flags describe the write contract. Use
|
|
|
36
36
|
complete metadata when schema tooling also needs the database fact:
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
table,
|
|
45
|
-
text,
|
|
46
|
-
value,
|
|
47
|
-
} from 'qubu'
|
|
48
|
-
import { defineSchemaExpression } from 'qubu/schema'
|
|
49
|
-
|
|
50
|
-
const currentTimestamp = defineSchemaExpression('function', context => {
|
|
51
|
-
context.append('CURRENT_TIMESTAMP')
|
|
39
|
+
import { boolean, generatedColumn, identityColumn, integer, table, text, value } from "qubu"
|
|
40
|
+
import { defineSchemaExpression } from "qubu/schema"
|
|
41
|
+
|
|
42
|
+
const currentTimestamp = defineSchemaExpression("function", (context) => {
|
|
43
|
+
context.append("CURRENT_TIMESTAMP")
|
|
52
44
|
})
|
|
53
45
|
|
|
54
|
-
const accounts = table(
|
|
55
|
-
id: integer({ identity: identityColumn(
|
|
56
|
-
status: text({ default:
|
|
46
|
+
const accounts = table("accounts", {
|
|
47
|
+
id: integer({ identity: identityColumn("always") }),
|
|
48
|
+
status: text({ default: "pending" }),
|
|
57
49
|
active: boolean({ default: true }),
|
|
58
50
|
score: integer({
|
|
59
|
-
generatedColumn: generatedColumn(value(1),
|
|
51
|
+
generatedColumn: generatedColumn(value(1), "stored"),
|
|
60
52
|
}),
|
|
61
53
|
createdAt: text({
|
|
62
54
|
default: currentTimestamp,
|
|
@@ -90,10 +82,10 @@ Use `$type<T>()` to narrow a helper's TypeScript type without changing its
|
|
|
90
82
|
runtime column definition:
|
|
91
83
|
|
|
92
84
|
```ts
|
|
93
|
-
import { table, text } from
|
|
85
|
+
import { table, text } from "qubu"
|
|
94
86
|
|
|
95
|
-
const users = table(
|
|
96
|
-
status: text().$type<
|
|
87
|
+
const users = table("users", {
|
|
88
|
+
status: text().$type<"active" | "disabled">(),
|
|
97
89
|
})
|
|
98
90
|
```
|
|
99
91
|
|
|
@@ -106,14 +98,14 @@ does not validate values at runtime or add a database constraint.
|
|
|
106
98
|
code. The following example uses the accounts table from the earlier example:
|
|
107
99
|
|
|
108
100
|
```ts
|
|
109
|
-
import type { TableInsertInput, TableUpdateInput } from
|
|
101
|
+
import type { TableInsertInput, TableUpdateInput } from "qubu"
|
|
110
102
|
|
|
111
103
|
type AccountInsert = TableInsertInput<typeof accounts.definitions>
|
|
112
104
|
type AccountUpdate = TableUpdateInput<typeof accounts.definitions>
|
|
113
105
|
|
|
114
106
|
const insert: AccountInsert = {
|
|
115
|
-
email:
|
|
116
|
-
externalScore:
|
|
107
|
+
email: "ada@example.com",
|
|
108
|
+
externalScore: "10",
|
|
117
109
|
}
|
|
118
110
|
|
|
119
111
|
const update: AccountUpdate = {
|
|
@@ -20,45 +20,38 @@ import {
|
|
|
20
20
|
text,
|
|
21
21
|
unique,
|
|
22
22
|
value,
|
|
23
|
-
} from
|
|
23
|
+
} from "qubu"
|
|
24
24
|
|
|
25
|
-
const accounts = table(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
accountsSlug: index([accounts.slug], { unique: true }),
|
|
34
|
-
},
|
|
35
|
-
})
|
|
36
|
-
)
|
|
25
|
+
const accounts = table("accounts", { id: integer(), slug: text() }, (accounts) => ({
|
|
26
|
+
constraints: {
|
|
27
|
+
accountsPrimary: primaryKey(accounts.id),
|
|
28
|
+
},
|
|
29
|
+
indexes: {
|
|
30
|
+
accountsSlug: index([accounts.slug], { unique: true }),
|
|
31
|
+
},
|
|
32
|
+
}))
|
|
37
33
|
|
|
38
34
|
const memberships = table(
|
|
39
|
-
|
|
35
|
+
"memberships",
|
|
40
36
|
{
|
|
41
37
|
id: integer(),
|
|
42
38
|
accountId: integer(),
|
|
43
39
|
slug: text(),
|
|
44
40
|
displayName: text(),
|
|
45
41
|
},
|
|
46
|
-
memberships => ({
|
|
42
|
+
(memberships) => ({
|
|
47
43
|
constraints: {
|
|
48
44
|
membershipsPrimary: primaryKey(memberships.id),
|
|
49
45
|
membershipsUnique: unique(memberships.accountId, memberships.slug),
|
|
50
|
-
membershipsAccount: foreignKey(
|
|
51
|
-
|
|
52
|
-
references(accounts, accounts.id)
|
|
53
|
-
),
|
|
54
|
-
membershipsCheck: check(eq(memberships.slug, value('public'))),
|
|
46
|
+
membershipsAccount: foreignKey([memberships.accountId], references(accounts, accounts.id)),
|
|
47
|
+
membershipsCheck: check(eq(memberships.slug, value("public"))),
|
|
55
48
|
},
|
|
56
49
|
indexes: {
|
|
57
50
|
membershipsAccountSlug: index([memberships.accountId, memberships.slug], {
|
|
58
51
|
unique: true,
|
|
59
52
|
}),
|
|
60
53
|
},
|
|
61
|
-
})
|
|
54
|
+
}),
|
|
62
55
|
)
|
|
63
56
|
```
|
|
64
57
|
|
|
@@ -81,21 +74,17 @@ Use `uniqueConstraint()` when the database enforces uniqueness but the rule shou
|
|
|
81
74
|
not prove a functional dependency:
|
|
82
75
|
|
|
83
76
|
```ts
|
|
84
|
-
import { table, text, uniqueConstraint } from
|
|
85
|
-
|
|
86
|
-
const accounts = table(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
indexes: {},
|
|
97
|
-
})
|
|
98
|
-
)
|
|
77
|
+
import { table, text, uniqueConstraint } from "qubu"
|
|
78
|
+
|
|
79
|
+
const accounts = table("accounts", { email: text({ nullable: true }) }, (accounts) => ({
|
|
80
|
+
constraints: {
|
|
81
|
+
emailUnique: uniqueConstraint(accounts.email, {
|
|
82
|
+
nulls: "distinct",
|
|
83
|
+
physicalName: "accounts_email_key",
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
indexes: {},
|
|
87
|
+
}))
|
|
99
88
|
```
|
|
100
89
|
|
|
101
90
|
`nulls: 'distinct'` describes the common rule where multiple NULLs do not
|
|
@@ -115,20 +104,15 @@ eligible unique index. Options such as onUpdate, onDelete, match, deferrable,
|
|
|
115
104
|
and initially remain metadata:
|
|
116
105
|
|
|
117
106
|
```ts
|
|
118
|
-
const memberships = table(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
),
|
|
128
|
-
},
|
|
129
|
-
indexes: {},
|
|
130
|
-
})
|
|
131
|
-
)
|
|
107
|
+
const memberships = table("memberships", { accountId: integer() }, (memberships) => ({
|
|
108
|
+
constraints: {
|
|
109
|
+
accountForeign: foreignKey([memberships.accountId], references(accounts, accounts.id), {
|
|
110
|
+
onDelete: "cascade",
|
|
111
|
+
onUpdate: "cascade",
|
|
112
|
+
}),
|
|
113
|
+
},
|
|
114
|
+
indexes: {},
|
|
115
|
+
}))
|
|
132
116
|
```
|
|
133
117
|
|
|
134
118
|
Use the preliminary callback table for direct self-references. Wrap the target
|
|
@@ -145,12 +129,12 @@ Grouping every column in a declared candidate key lets Qubu select other
|
|
|
145
129
|
columns from that source:
|
|
146
130
|
|
|
147
131
|
```ts
|
|
148
|
-
import { count, from, groupBy, select } from
|
|
132
|
+
import { count, from, groupBy, select } from "qubu"
|
|
149
133
|
|
|
150
134
|
const summary = select(
|
|
151
135
|
{ displayName: memberships.displayName, total: count() },
|
|
152
136
|
from(memberships),
|
|
153
|
-
groupBy(memberships.accountId, memberships.slug)
|
|
137
|
+
groupBy(memberships.accountId, memberships.slug),
|
|
154
138
|
)
|
|
155
139
|
```
|
|
156
140
|
|
|
@@ -8,13 +8,13 @@ migration journal. Preflight runs before rendering, so a blocked or incompatible
|
|
|
8
8
|
plan returns diagnostics and no SQL.
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
|
-
import { emitMigrationPlan } from
|
|
12
|
-
import { postgresSchemaDialect } from
|
|
11
|
+
import { emitMigrationPlan } from "qubu/ddl"
|
|
12
|
+
import { postgresSchemaDialect } from "qubu/snapshot"
|
|
13
13
|
|
|
14
14
|
const result = emitMigrationPlan(plan, postgresSchemaDialect)
|
|
15
15
|
if (!result.ok) {
|
|
16
16
|
// Review result.diagnostics. result.sql is an empty string.
|
|
17
|
-
throw new Error(result.diagnostics.map(item => item.message).join(
|
|
17
|
+
throw new Error(result.diagnostics.map((item) => item.message).join("\n"))
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
for (const statement of result.statements) {
|
|
@@ -44,9 +44,9 @@ produces `transaction-conflict`; no transaction is opened by the emitter.
|
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
46
|
const result = emitMigrationPlan(plan, postgresSchemaDialect, {
|
|
47
|
-
transaction:
|
|
48
|
-
lock:
|
|
49
|
-
serverVersion:
|
|
47
|
+
transaction: "managed",
|
|
48
|
+
lock: "exclusive",
|
|
49
|
+
serverVersion: "16",
|
|
50
50
|
})
|
|
51
51
|
```
|
|
52
52
|
|
package/docs/schema/diff.md
CHANGED
|
@@ -8,7 +8,7 @@ It keeps the object kind, namespace, logical ID, physical name, dialect, path,
|
|
|
8
8
|
and catalog evidence on every result object.
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
|
-
import { diffSnapshots } from
|
|
11
|
+
import { diffSnapshots } from "qubu/diff"
|
|
12
12
|
|
|
13
13
|
const result = diffSnapshots(previousSnapshot, currentSnapshot)
|
|
14
14
|
|
|
@@ -32,10 +32,10 @@ use an ID, a physical name, or an exact path when a nested scope repeats an ID.
|
|
|
32
32
|
const result = diffSnapshots(previousSnapshot, currentSnapshot, {
|
|
33
33
|
renameHints: [
|
|
34
34
|
{
|
|
35
|
-
kind:
|
|
36
|
-
namespace:
|
|
37
|
-
from:
|
|
38
|
-
to:
|
|
35
|
+
kind: "table",
|
|
36
|
+
namespace: "public",
|
|
37
|
+
from: "legacy_accounts",
|
|
38
|
+
to: "accounts",
|
|
39
39
|
},
|
|
40
40
|
],
|
|
41
41
|
})
|
|
@@ -37,10 +37,10 @@ queries, authenticate, or start a transaction. Adapt the driver you already
|
|
|
37
37
|
use to `CatalogConnection`:
|
|
38
38
|
|
|
39
39
|
```ts
|
|
40
|
-
import type { CatalogConnection } from
|
|
40
|
+
import type { CatalogConnection } from "qubu/introspection"
|
|
41
41
|
|
|
42
42
|
const connection: CatalogConnection = {
|
|
43
|
-
dialect:
|
|
43
|
+
dialect: "sqlite",
|
|
44
44
|
query(statement, options) {
|
|
45
45
|
// Adapt this call to the driver used by the application.
|
|
46
46
|
return db.query(statement, options)
|
|
@@ -59,16 +59,16 @@ catalog can later support inspection, source generation, or another snapshot
|
|
|
59
59
|
format:
|
|
60
60
|
|
|
61
61
|
```ts
|
|
62
|
-
import { mapCatalogToSnapshot, readSqliteCatalog } from
|
|
62
|
+
import { mapCatalogToSnapshot, readSqliteCatalog } from "qubu/introspection"
|
|
63
63
|
|
|
64
|
-
const catalog = await readSqliteCatalog(connection, { namespace:
|
|
64
|
+
const catalog = await readSqliteCatalog(connection, { namespace: "main" })
|
|
65
65
|
const result = mapCatalogToSnapshot(catalog, {
|
|
66
|
-
namespace:
|
|
67
|
-
mode:
|
|
66
|
+
namespace: "main",
|
|
67
|
+
mode: "strict",
|
|
68
68
|
})
|
|
69
69
|
|
|
70
70
|
if (!result.ok) {
|
|
71
|
-
throw new Error(result.diagnostics.map(issue => issue.message).join(
|
|
71
|
+
throw new Error(result.diagnostics.map((issue) => issue.message).join("\n"))
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
result.snapshot.tables // canonical Snapshot v1 data
|
|
@@ -8,7 +8,7 @@ logical and physical identity evidence, dependency edges, preconditions, safety,
|
|
|
8
8
|
lock and transaction requirements, and reversibility markers.
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
|
-
import { createMigrationPlan } from
|
|
11
|
+
import { createMigrationPlan } from "qubu/migration"
|
|
12
12
|
|
|
13
13
|
const result = createMigrationPlan(diff)
|
|
14
14
|
if (!result.ok) {
|
|
@@ -31,11 +31,11 @@ kind, namespace, and path, and each decision carries a review reason.
|
|
|
31
31
|
```ts
|
|
32
32
|
const reviewed = createMigrationPlan(diff, {
|
|
33
33
|
decisions: result.plan.operations
|
|
34
|
-
.filter(operation => operation.status ===
|
|
35
|
-
.map(operation => ({
|
|
34
|
+
.filter((operation) => operation.status === "decision-required")
|
|
35
|
+
.map((operation) => ({
|
|
36
36
|
operationId: operation.id,
|
|
37
|
-
action:
|
|
38
|
-
reason:
|
|
37
|
+
action: "allow",
|
|
38
|
+
reason: "Reviewed against the deployment change request",
|
|
39
39
|
})),
|
|
40
40
|
})
|
|
41
41
|
```
|
|
@@ -48,10 +48,10 @@ dialect, safety declaration, reason, reversibility, and dependency position:
|
|
|
48
48
|
createMigrationPlan(diff, {
|
|
49
49
|
customSql: [
|
|
50
50
|
{
|
|
51
|
-
sql:
|
|
52
|
-
dialect: { name:
|
|
53
|
-
safety:
|
|
54
|
-
reason:
|
|
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
55
|
reversible: false,
|
|
56
56
|
position: 3,
|
|
57
57
|
},
|
package/docs/schema/snapshots.md
CHANGED
|
@@ -8,7 +8,7 @@ hashed, checked into source control, and handed to a dialect adapter. Importing
|
|
|
8
8
|
the snapshot entrypoint is optional; ordinary query imports do not load it.
|
|
9
9
|
|
|
10
10
|
```ts
|
|
11
|
-
import { createSchemaSnapshot, encodeSchemaSnapshot } from
|
|
11
|
+
import { createSchemaSnapshot, encodeSchemaSnapshot } from "qubu/snapshot"
|
|
12
12
|
|
|
13
13
|
const snapshot = createSchemaSnapshot(appSchema)
|
|
14
14
|
const json = encodeSchemaSnapshot(snapshot)
|
|
@@ -29,12 +29,12 @@ expression retains its dialect tag and is rejected when it does not match the
|
|
|
29
29
|
selected snapshot dialect.
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
|
-
import { decodeSchemaSnapshot } from
|
|
32
|
+
import { decodeSchemaSnapshot } from "qubu/snapshot"
|
|
33
33
|
|
|
34
34
|
const decoded = decodeSchemaSnapshot(json)
|
|
35
35
|
if (!decoded.ok) {
|
|
36
36
|
for (const issue of decoded.diagnostics) {
|
|
37
|
-
console.error(issue.path.join(
|
|
37
|
+
console.error(issue.path.join("."), issue.code, issue.message)
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
```
|
|
@@ -33,10 +33,10 @@ Use a dialect-native descriptor when a column needs an exact vendor
|
|
|
33
33
|
declaration:
|
|
34
34
|
|
|
35
35
|
```ts
|
|
36
|
-
import { nativeColumn, nativeStorage, table } from
|
|
36
|
+
import { nativeColumn, nativeStorage, table } from "qubu"
|
|
37
37
|
|
|
38
|
-
const accounts = table(
|
|
39
|
-
handle: nativeColumn(nativeStorage(
|
|
38
|
+
const accounts = table("accounts", {
|
|
39
|
+
handle: nativeColumn(nativeStorage("postgresql", 'citext COLLATE "C"')),
|
|
40
40
|
})
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -52,12 +52,12 @@ can be rendered for checks, generated columns, indexes, and other declaration
|
|
|
52
52
|
metadata:
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
-
import { eq, table, text } from
|
|
56
|
-
import { renderSchemaSql } from
|
|
55
|
+
import { eq, table, text } from "qubu"
|
|
56
|
+
import { renderSchemaSql } from "qubu/schema"
|
|
57
57
|
|
|
58
|
-
const accounts = table(
|
|
58
|
+
const accounts = table("accounts", { status: text() })
|
|
59
59
|
|
|
60
|
-
renderSchemaSql(eq(accounts.status,
|
|
60
|
+
renderSchemaSql(eq(accounts.status, "active"), { mode: "check" })
|
|
61
61
|
// ("status" = 'active')
|
|
62
62
|
```
|
|
63
63
|
|
|
@@ -74,13 +74,13 @@ subqueries are rejected in every schema mode.
|
|
|
74
74
|
An extension must opt into the schema contract with `defineSchemaExpression()`:
|
|
75
75
|
|
|
76
76
|
```ts
|
|
77
|
-
import { defineSchemaExpression, renderSchemaSql } from
|
|
77
|
+
import { defineSchemaExpression, renderSchemaSql } from "qubu/schema"
|
|
78
78
|
|
|
79
|
-
const currentDate = defineSchemaExpression(
|
|
80
|
-
context.append(
|
|
79
|
+
const currentDate = defineSchemaExpression("function", (context) => {
|
|
80
|
+
context.append("CURRENT_DATE")
|
|
81
81
|
})
|
|
82
82
|
|
|
83
|
-
renderSchemaSql(currentDate, { mode:
|
|
83
|
+
renderSchemaSql(currentDate, { mode: "default" })
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
## Use raw schema SQL only when necessary
|
|
@@ -12,17 +12,17 @@ record keys become logical table IDs and stay stable when a physical SQL name
|
|
|
12
12
|
changes:
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
|
-
import { integer, schema, table, text } from
|
|
15
|
+
import { integer, schema, table, text } from "qubu"
|
|
16
16
|
|
|
17
|
-
const accounts = table(
|
|
17
|
+
const accounts = table("account_records", {
|
|
18
18
|
id: integer(),
|
|
19
19
|
email: text(),
|
|
20
20
|
})
|
|
21
|
-
const memberships = table(
|
|
21
|
+
const memberships = table("membership_records", {
|
|
22
22
|
accountId: integer(),
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
-
const appSchema = schema({ accounts, memberships }, { namespace:
|
|
25
|
+
const appSchema = schema({ accounts, memberships }, { namespace: "public" })
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
appSchema.registry.accounts.id is "accounts", while the physical table name is
|
|
@@ -47,17 +47,14 @@ Write schema keys in camelCase. Qubu converts them to snake_case in SQL, then
|
|
|
47
47
|
uses the camelCase keys in the returned row:
|
|
48
48
|
|
|
49
49
|
```ts
|
|
50
|
-
import { from, select, table, timestamp, uuid } from
|
|
50
|
+
import { from, select, table, timestamp, uuid } from "qubu"
|
|
51
51
|
|
|
52
|
-
const events = table(
|
|
52
|
+
const events = table("events", {
|
|
53
53
|
userId: uuid(),
|
|
54
54
|
createdAt: timestamp(),
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
const query = select(
|
|
58
|
-
{ userId: events.userId, createdAt: events.createdAt },
|
|
59
|
-
from(events)
|
|
60
|
-
)
|
|
57
|
+
const query = select({ userId: events.userId, createdAt: events.createdAt }, from(events))
|
|
61
58
|
```
|
|
62
59
|
|
|
63
60
|
The query selects "events"."user_id" and "events"."created_at", then aliases
|
|
@@ -76,8 +73,8 @@ Prefer `userId` and `apiKey` when you control the TypeScript name. Use `sqlName`
|
|
|
76
73
|
the database name does not follow the convention:
|
|
77
74
|
|
|
78
75
|
```ts
|
|
79
|
-
const events = table(
|
|
80
|
-
createdAt: timestamp({ sqlName:
|
|
76
|
+
const events = table("events", {
|
|
77
|
+
createdAt: timestamp({ sqlName: "creation_timestamp" }),
|
|
81
78
|
})
|
|
82
79
|
```
|
|
83
80
|
|
|
@@ -89,9 +86,9 @@ their projected names stay snake_case. Only the outer result projection uses
|
|
|
89
86
|
camelCase aliases. Pass the database relation name explicitly:
|
|
90
87
|
|
|
91
88
|
```ts
|
|
92
|
-
import { table, uuid } from
|
|
89
|
+
import { table, uuid } from "qubu"
|
|
93
90
|
|
|
94
|
-
const accounts = table(
|
|
91
|
+
const accounts = table("user_accounts", {
|
|
95
92
|
id: uuid(),
|
|
96
93
|
})
|
|
97
94
|
```
|
|
@@ -43,10 +43,10 @@ Specific result domains are still retained. `lower(textColumn)` produces
|
|
|
43
43
|
`SqlText`, while `avg(integerColumn)` produces `SqlDecimal`:
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
|
-
import { avg, integer, lower, table, text } from
|
|
47
|
-
import type { SqlTypeOf } from
|
|
46
|
+
import { avg, integer, lower, table, text } from "qubu"
|
|
47
|
+
import type { SqlTypeOf } from "qubu"
|
|
48
48
|
|
|
49
|
-
const metrics = table(
|
|
49
|
+
const metrics = table("metrics", {
|
|
50
50
|
label: text(),
|
|
51
51
|
sampleCount: integer(),
|
|
52
52
|
})
|
|
@@ -72,15 +72,15 @@ semantics. Contextual JavaScript literals remain ergonomic for compatible
|
|
|
72
72
|
operators:
|
|
73
73
|
|
|
74
74
|
```ts
|
|
75
|
-
import { asc, eq, inList, like, lower, orderBy, table, uuid } from
|
|
75
|
+
import { asc, eq, inList, like, lower, orderBy, table, uuid } from "qubu"
|
|
76
76
|
|
|
77
|
-
const records = table(
|
|
77
|
+
const records = table("records", { id: uuid() })
|
|
78
78
|
|
|
79
|
-
eq(records.id,
|
|
80
|
-
inList(records.id, [
|
|
79
|
+
eq(records.id, "108cb836-20d2-41b2-8c23-f0c94700aa7e") // valid
|
|
80
|
+
inList(records.id, ["first-id", "second-id"]) // valid
|
|
81
81
|
|
|
82
82
|
lower(records.id) // TypeScript error: SqlUuid is not SqlTextLike
|
|
83
|
-
like(records.id,
|
|
83
|
+
like(records.id, "%uuid%") // TypeScript error: SqlUuid is not SqlTextLike
|
|
84
84
|
orderBy(asc(records.id)) // TypeScript error: SqlUuid is not portably orderable
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -90,10 +90,10 @@ operation. This contextual typing does not relabel an expression: comparing a
|
|
|
90
90
|
the database operation intentionally changes domains:
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
import { cast, like, text } from
|
|
93
|
+
import { cast, like, text } from "qubu"
|
|
94
94
|
|
|
95
95
|
const idAsText = cast(records.id, text())
|
|
96
|
-
like(idAsText,
|
|
96
|
+
like(idAsText, "108c%")
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Built-in definitions carry logical cast targets, so the active dialect can
|