@sigitex/outlaw 1.0.0
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/LICENSE +7 -0
- package/README.md +9 -0
- package/package.json +52 -0
- package/src/api/DatabaseTable.ts +51 -0
- package/src/api/DatabaseView.ts +22 -0
- package/src/api/api.types.ts +200 -0
- package/src/api/createDatabase.ts +24 -0
- package/src/api/index.ts +4 -0
- package/src/bun/bun.ts +27 -0
- package/src/bun/index.ts +1 -0
- package/src/cloudflare/cloudflare.ts +18 -0
- package/src/cloudflare/index.ts +1 -0
- package/src/console.ts +14 -0
- package/src/cowboyMigration/Compare.ts +97 -0
- package/src/cowboyMigration/CowboyConnection.ts +57 -0
- package/src/cowboyMigration/CowboyMigrator.ts +437 -0
- package/src/cowboyMigration/CowboySeeder.ts +245 -0
- package/src/cowboyMigration/cowboyMigration.types.ts +29 -0
- package/src/cowboyMigration/createSchemaHacker.ts +28 -0
- package/src/cowboyMigration/index.ts +3 -0
- package/src/crypto.d.ts +9 -0
- package/src/framework/Format.ts +31 -0
- package/src/framework/definitions.d.ts +4 -0
- package/src/framework/index.ts +1 -0
- package/src/index.ts +10 -0
- package/src/queryBuilder/DeleteBuilder.ts +40 -0
- package/src/queryBuilder/InsertBuilder.ts +31 -0
- package/src/queryBuilder/Mappings.ts +55 -0
- package/src/queryBuilder/SelectBuilder.ts +39 -0
- package/src/queryBuilder/SelectQueryBuilder.ts +93 -0
- package/src/queryBuilder/UpdateBuilder.ts +44 -0
- package/src/queryBuilder/addWhereClause.ts +35 -0
- package/src/queryBuilder/index.ts +7 -0
- package/src/queryBuilder/operators.ts +35 -0
- package/src/queryBuilder/queryBuilders.types.ts +105 -0
- package/src/queryGenerator/Clause.ts +79 -0
- package/src/queryGenerator/generateDelete.ts +18 -0
- package/src/queryGenerator/generateInsert.ts +29 -0
- package/src/queryGenerator/generateSelect.ts +89 -0
- package/src/queryGenerator/generateUpdate.ts +29 -0
- package/src/queryGenerator/index.ts +4 -0
- package/src/reflection/Reflector.ts +47 -0
- package/src/reflection/index.ts +2 -0
- package/src/reflection/reflection.types.ts +12 -0
- package/src/schemaBuilder/Mapping.ts +44 -0
- package/src/schemaBuilder/columnBuilders.ts +67 -0
- package/src/schemaBuilder/createFixture.ts +112 -0
- package/src/schemaBuilder/createIndex.ts +33 -0
- package/src/schemaBuilder/createSchema.ts +23 -0
- package/src/schemaBuilder/createTable.ts +80 -0
- package/src/schemaBuilder/createView.ts +21 -0
- package/src/schemaBuilder/index.ts +9 -0
- package/src/schemaBuilder/metadata.ts +73 -0
- package/src/schemaBuilder/schemaBuilder.types.ts +139 -0
- package/src/schemaGenerator/generateCreateIndex.ts +18 -0
- package/src/schemaGenerator/generateCreateTable.ts +68 -0
- package/src/schemaGenerator/generateCreateView.ts +13 -0
- package/src/schemaGenerator/index.ts +3 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { join, newline, print } from "@sigitex/print"
|
|
2
|
+
import { Format } from "../framework"
|
|
3
|
+
import type { InsertCommand } from "../queryBuilder"
|
|
4
|
+
import { Clause } from "./Clause"
|
|
5
|
+
|
|
6
|
+
export function generateInsert({
|
|
7
|
+
table,
|
|
8
|
+
columns,
|
|
9
|
+
rows,
|
|
10
|
+
returning,
|
|
11
|
+
}: InsertCommand) {
|
|
12
|
+
return print([
|
|
13
|
+
"insert into ",
|
|
14
|
+
Format.name(table),
|
|
15
|
+
" (",
|
|
16
|
+
join(", ", columns, Format.name),
|
|
17
|
+
")",
|
|
18
|
+
newline,
|
|
19
|
+
"values ",
|
|
20
|
+
rows.map((row, i) => [
|
|
21
|
+
i > 0 && ", ",
|
|
22
|
+
"(",
|
|
23
|
+
columns.map((col, c) => [c > 0 && ", ", Format.value(row[col])]),
|
|
24
|
+
")",
|
|
25
|
+
]),
|
|
26
|
+
newline,
|
|
27
|
+
returning && Clause.returning(returning),
|
|
28
|
+
])
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { indent, join, newline, type Node, print } from "@sigitex/print"
|
|
2
|
+
import { Format } from "../framework"
|
|
3
|
+
import type { JoinClause, OrderBySort, SelectQuery } from "../queryBuilder"
|
|
4
|
+
import { Clause } from "./Clause"
|
|
5
|
+
|
|
6
|
+
export function generateSelect(query: SelectQuery) {
|
|
7
|
+
return print([generateSelectNode(query)])
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function generateSelectNode({
|
|
11
|
+
selected,
|
|
12
|
+
table,
|
|
13
|
+
conditions,
|
|
14
|
+
limit,
|
|
15
|
+
offset,
|
|
16
|
+
orderBy,
|
|
17
|
+
joins,
|
|
18
|
+
}: SelectQuery): Node {
|
|
19
|
+
const hasJoins = !!joins?.length
|
|
20
|
+
const aliasMap = hasJoins ? buildAliasMap(table, joins!) : undefined
|
|
21
|
+
return [
|
|
22
|
+
"select ",
|
|
23
|
+
selected &&
|
|
24
|
+
join(", ", selected, (col: string) =>
|
|
25
|
+
hasJoins ? qualifyColumn(table, col) : Format.name(col),
|
|
26
|
+
),
|
|
27
|
+
!selected &&
|
|
28
|
+
(hasJoins ? allColumnsQualified(table, joins!, aliasMap!) : "*"),
|
|
29
|
+
newline,
|
|
30
|
+
"from ",
|
|
31
|
+
Format.name(table),
|
|
32
|
+
newline,
|
|
33
|
+
hasJoins && Clause.joins(joins!, aliasMap!),
|
|
34
|
+
conditions?.length &&
|
|
35
|
+
Clause.where(conditions, hasJoins ? table : undefined),
|
|
36
|
+
limit && ["limit ", Format.number(limit), newline],
|
|
37
|
+
offset && ["offset ", Format.number(offset), newline],
|
|
38
|
+
orderBy && orderByClause(orderBy),
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildAliasMap(
|
|
43
|
+
baseTable: string,
|
|
44
|
+
joins: JoinClause[],
|
|
45
|
+
): Map<JoinClause, string> {
|
|
46
|
+
const map = new Map<JoinClause, string>()
|
|
47
|
+
const used = new Set([baseTable])
|
|
48
|
+
|
|
49
|
+
for (const clause of joins) {
|
|
50
|
+
const sourceName =
|
|
51
|
+
clause.target.kind === "table" ? clause.target.name : clause.target.table
|
|
52
|
+
let alias = sourceName
|
|
53
|
+
let i = 1
|
|
54
|
+
while (used.has(alias)) {
|
|
55
|
+
alias = `${sourceName}_${i++}`
|
|
56
|
+
}
|
|
57
|
+
used.add(alias)
|
|
58
|
+
map.set(clause, alias)
|
|
59
|
+
}
|
|
60
|
+
return map
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function qualifyColumn(baseTable: string, column: string): string {
|
|
64
|
+
return `${Format.name(baseTable)}.${Format.name(column)}`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function allColumnsQualified(
|
|
68
|
+
table: string,
|
|
69
|
+
joins: JoinClause[],
|
|
70
|
+
aliasMap: Map<JoinClause, string>,
|
|
71
|
+
): Node {
|
|
72
|
+
const names = [table, ...joins.map((j) => aliasMap.get(j)!)]
|
|
73
|
+
return names.map((t, i) => [i > 0 && ", ", Format.name(t), ".*"])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function orderByClause(orderBy: OrderBySort[]): Node {
|
|
77
|
+
return [
|
|
78
|
+
"order by ",
|
|
79
|
+
indent(
|
|
80
|
+
orderBy.map(({ column, direction }, index) => [
|
|
81
|
+
index > 0 && ", ",
|
|
82
|
+
Format.name(column),
|
|
83
|
+
" ",
|
|
84
|
+
direction,
|
|
85
|
+
newline,
|
|
86
|
+
]),
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { indent, newline, print } from "@sigitex/print"
|
|
2
|
+
import { Format } from "../framework"
|
|
3
|
+
import type { UpdateCommand } from "../queryBuilder"
|
|
4
|
+
import { Clause } from "./Clause"
|
|
5
|
+
|
|
6
|
+
export function generateUpdate({
|
|
7
|
+
table,
|
|
8
|
+
assignments,
|
|
9
|
+
conditions,
|
|
10
|
+
returning,
|
|
11
|
+
}: UpdateCommand) {
|
|
12
|
+
const columns = Object.keys(assignments)
|
|
13
|
+
return print([
|
|
14
|
+
"update ",
|
|
15
|
+
Format.name(table),
|
|
16
|
+
newline,
|
|
17
|
+
"set ",
|
|
18
|
+
indent(
|
|
19
|
+
columns.map((col, i) => [
|
|
20
|
+
i > 0 && [",", newline],
|
|
21
|
+
Format.name(col),
|
|
22
|
+
" = ",
|
|
23
|
+
Format.value(assignments[col]),
|
|
24
|
+
]),
|
|
25
|
+
),
|
|
26
|
+
conditions?.length && Clause.where(conditions),
|
|
27
|
+
returning && Clause.returning(returning),
|
|
28
|
+
])
|
|
29
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Connection } from "../api"
|
|
2
|
+
import type { TableListItem } from "./reflection.types"
|
|
3
|
+
|
|
4
|
+
export class Reflector {
|
|
5
|
+
private readonly connection: Connection
|
|
6
|
+
private tableListItems: TableListItem[] | undefined = undefined
|
|
7
|
+
|
|
8
|
+
constructor(connection: Connection) {
|
|
9
|
+
this.connection = connection
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async hasTable(tableName: string) {
|
|
13
|
+
return (await this.getTableList()).some(
|
|
14
|
+
({ name, type }) => type === "table" && name === tableName,
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async isMissingTable(tableName: string) {
|
|
19
|
+
return !(await this.hasTable(tableName))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async hasView(viewName: string) {
|
|
23
|
+
return (await this.getTableList()).some(
|
|
24
|
+
({ name, type }) => type === "view" && name === viewName,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async hasIndex(indexName: string) {
|
|
29
|
+
const results = await this.connection.query<{ name: string }>(
|
|
30
|
+
`select name from sqlite_master where type = 'index' and name = '${indexName}'`,
|
|
31
|
+
)
|
|
32
|
+
return results.length > 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
invalidate() {
|
|
36
|
+
this.tableListItems = undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private async getTableList() {
|
|
40
|
+
if (this.tableListItems === undefined) {
|
|
41
|
+
const results =
|
|
42
|
+
await this.connection.query<TableListItem>("pragma table_list")
|
|
43
|
+
this.tableListItems = results.filter(({ schema }) => schema === "main")
|
|
44
|
+
}
|
|
45
|
+
return this.tableListItems
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type TableListType = "table" | "view" | "shadow" | "virtual"
|
|
2
|
+
|
|
3
|
+
export type TableListItem = {
|
|
4
|
+
readonly schema: string
|
|
5
|
+
readonly name: string
|
|
6
|
+
readonly type: TableListType
|
|
7
|
+
/** Number of columns **/
|
|
8
|
+
readonly ncol: number
|
|
9
|
+
/** WITHOUT ROWID */
|
|
10
|
+
readonly wr: number
|
|
11
|
+
strict: number
|
|
12
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type Mapping<From, To> = {
|
|
2
|
+
from(value: From): To
|
|
3
|
+
to(value: To): From
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export namespace Mapping {
|
|
7
|
+
export const boolean: Mapping<number, boolean> = {
|
|
8
|
+
from(n) {
|
|
9
|
+
return n !== 0
|
|
10
|
+
},
|
|
11
|
+
to(b) {
|
|
12
|
+
return b ? 1 : 0
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const timestamp: Mapping<number, Date> = {
|
|
17
|
+
from(n) {
|
|
18
|
+
return new Date(n)
|
|
19
|
+
},
|
|
20
|
+
to(d) {
|
|
21
|
+
return d.getTime()
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const date: Mapping<string, Date> = {
|
|
26
|
+
from(s) {
|
|
27
|
+
return new Date(s)
|
|
28
|
+
},
|
|
29
|
+
to(d) {
|
|
30
|
+
return d.toISOString()
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function json<T>(): Mapping<string, T> {
|
|
35
|
+
return {
|
|
36
|
+
from(s) {
|
|
37
|
+
return JSON.parse(s) as T
|
|
38
|
+
},
|
|
39
|
+
to(o) {
|
|
40
|
+
return JSON.stringify(o)
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
import type { CheckExpression, ColumnData, ColumnRef, MappingData } from "./metadata"
|
|
3
|
+
import { Mapping } from "./Mapping"
|
|
4
|
+
import type {
|
|
5
|
+
BuildColumn,
|
|
6
|
+
BuildColumnInner,
|
|
7
|
+
BuildColumnMap,
|
|
8
|
+
BuildPrimaryKey,
|
|
9
|
+
} from "./schemaBuilder.types"
|
|
10
|
+
|
|
11
|
+
export const text: BuildColumn<string, never> = composeColumn({
|
|
12
|
+
datatype: "text",
|
|
13
|
+
})
|
|
14
|
+
export const integer: BuildColumn<number, never> = composeColumn({
|
|
15
|
+
datatype: "integer",
|
|
16
|
+
})
|
|
17
|
+
export const real: BuildColumn<number, never> = composeColumn({
|
|
18
|
+
datatype: "real",
|
|
19
|
+
})
|
|
20
|
+
export const blob: BuildColumn<ArrayBuffer, never> = composeColumn({
|
|
21
|
+
datatype: "blob",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
function composeColumn(
|
|
25
|
+
$meta: Partial<ColumnData>,
|
|
26
|
+
): BuildColumnInner & BuildPrimaryKey<any, never> {
|
|
27
|
+
return {
|
|
28
|
+
$meta,
|
|
29
|
+
get notNull() {
|
|
30
|
+
return composeColumn({ ...$meta, notNull: true })
|
|
31
|
+
},
|
|
32
|
+
get primaryKey() {
|
|
33
|
+
return composeColumn({ ...$meta, primaryKey: { autoincrement: false } })
|
|
34
|
+
},
|
|
35
|
+
get autoincrement() {
|
|
36
|
+
return composeColumn({ ...$meta, primaryKey: { autoincrement: true } })
|
|
37
|
+
},
|
|
38
|
+
default(sql) {
|
|
39
|
+
return composeColumn({ ...$meta, default: sql })
|
|
40
|
+
},
|
|
41
|
+
check(expression: CheckExpression) {
|
|
42
|
+
return composeColumn({ ...$meta, check: expression })
|
|
43
|
+
},
|
|
44
|
+
get unique() {
|
|
45
|
+
return composeColumn({ ...$meta, unique: true })
|
|
46
|
+
},
|
|
47
|
+
get foreignKey() {
|
|
48
|
+
return {
|
|
49
|
+
references(ref: ColumnRef) {
|
|
50
|
+
return composeColumn({
|
|
51
|
+
...$meta,
|
|
52
|
+
foreignKey: ref,
|
|
53
|
+
})
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
get map(): BuildColumnMap<any, never> {
|
|
58
|
+
const custom = (mapping?: MappingData) =>
|
|
59
|
+
composeColumn(mapping ? { ...$meta, mapping } : $meta)
|
|
60
|
+
custom.boolean = composeColumn({ ...$meta, mapping: Mapping.boolean })
|
|
61
|
+
custom.timestamp = composeColumn({ ...$meta, mapping: Mapping.timestamp })
|
|
62
|
+
custom.date = composeColumn({ ...$meta, mapping: Mapping.date })
|
|
63
|
+
custom.json = () => composeColumn({ ...$meta, mapping: Mapping.json() })
|
|
64
|
+
return custom as BuildColumnMap<any, never>
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
import type { AnyBuildTable, InferTable, RefBy } from "./schemaBuilder.types"
|
|
3
|
+
|
|
4
|
+
export type Fixtures = {
|
|
5
|
+
[key: string]: Fixture<any>
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type Seeds = Fixtures
|
|
9
|
+
|
|
10
|
+
export type FixtureRow<Table> = {
|
|
11
|
+
[K in keyof InferTable<Table>]: InferTable<Table>[K] | RefBy
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type Fixture<Table> = {
|
|
15
|
+
table: Table
|
|
16
|
+
rows: FixtureRow<Table>[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export namespace Fixture {
|
|
20
|
+
export function isRefBy(value: unknown): value is RefBy {
|
|
21
|
+
return (
|
|
22
|
+
value != null &&
|
|
23
|
+
typeof value === "object" &&
|
|
24
|
+
(value as any)._tag === "RefBy"
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// TODO: just call it Row<Table>, avoid this alias
|
|
30
|
+
type Row<Table> = InferTable<Table>
|
|
31
|
+
|
|
32
|
+
type AllowRefBy<T> = T | RefBy
|
|
33
|
+
|
|
34
|
+
export type FixtureTemplate<Table> = {
|
|
35
|
+
[K in keyof Row<Table>]?: AllowRefBy<Row<Table>[K]> | (() => Row<Table>[K])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type TemplateKeys<Template> = keyof {
|
|
39
|
+
[K in keyof Template as Template[K] extends undefined ? never : K]: true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type NullableKeys<T> = {
|
|
43
|
+
[K in keyof T]: null extends T[K] ? K : never
|
|
44
|
+
}[keyof T]
|
|
45
|
+
|
|
46
|
+
type RequiredRows<Table, Template> = {
|
|
47
|
+
[K in keyof Omit<
|
|
48
|
+
Row<Table>,
|
|
49
|
+
TemplateKeys<Template> | NullableKeys<Row<Table>>
|
|
50
|
+
>]: AllowRefBy<Row<Table>[K]>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type OptionalRows<Table, Template> = Partial<{
|
|
54
|
+
[K in (TemplateKeys<Template> | NullableKeys<Row<Table>>) &
|
|
55
|
+
keyof Row<Table>]: AllowRefBy<Row<Table>[K]>
|
|
56
|
+
}>
|
|
57
|
+
|
|
58
|
+
export type FixtureRows<Table, Template> = RequiredRows<Table, Template> &
|
|
59
|
+
OptionalRows<Table, Template>
|
|
60
|
+
|
|
61
|
+
export function createFixture<
|
|
62
|
+
Table extends AnyBuildTable,
|
|
63
|
+
const Template extends FixtureTemplate<Table>,
|
|
64
|
+
>(
|
|
65
|
+
table: Table,
|
|
66
|
+
template: Template,
|
|
67
|
+
rows: FixtureRows<Table, Template>[],
|
|
68
|
+
): Fixture<Table>
|
|
69
|
+
|
|
70
|
+
export function createFixture<Table extends AnyBuildTable>(
|
|
71
|
+
table: Table,
|
|
72
|
+
rows: InferTable<Table>[],
|
|
73
|
+
): Fixture<Table>
|
|
74
|
+
|
|
75
|
+
export function createFixture(
|
|
76
|
+
table: AnyBuildTable,
|
|
77
|
+
templateOrRows: Record<string, unknown> | Record<string, unknown>[],
|
|
78
|
+
maybeRows?: Record<string, unknown>[],
|
|
79
|
+
): Fixture<AnyBuildTable> {
|
|
80
|
+
if (Array.isArray(templateOrRows)) {
|
|
81
|
+
return { table, rows: templateOrRows } as any
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const template = templateOrRows
|
|
85
|
+
const rows = maybeRows!
|
|
86
|
+
|
|
87
|
+
const nullableKeys = table.$meta.columns
|
|
88
|
+
.filter((col) => !col.notNull && !col.primaryKey)
|
|
89
|
+
.map((col) => col.name)
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
table,
|
|
93
|
+
rows: rows.map((row) => {
|
|
94
|
+
const result: Record<string, unknown> = { ...row }
|
|
95
|
+
for (const key of nullableKeys) {
|
|
96
|
+
if (!(key in result) && !(key in template)) {
|
|
97
|
+
result[key] = null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const key in template) {
|
|
101
|
+
if (key in row) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const value = template[key]
|
|
105
|
+
result[key] = typeof value === "function" ? value() : value
|
|
106
|
+
}
|
|
107
|
+
return result
|
|
108
|
+
}),
|
|
109
|
+
} as any
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export const createSeed = createFixture
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ColumnRef, IndexData } from "./metadata"
|
|
2
|
+
import type { BuildIndex } from "./schemaBuilder.types"
|
|
3
|
+
|
|
4
|
+
export function createIndex(name: string): IndexOn {
|
|
5
|
+
return {
|
|
6
|
+
on(...columns: ColumnRef[]) {
|
|
7
|
+
return finalize(name, columns, false)
|
|
8
|
+
},
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createUniqueIndex(name: string): IndexOn {
|
|
13
|
+
return {
|
|
14
|
+
on(...columns: ColumnRef[]) {
|
|
15
|
+
return finalize(name, columns, true)
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type IndexOn = {
|
|
21
|
+
on(...columns: ColumnRef[]): BuildIndex
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function finalize(name: string, columns: ColumnRef[], unique: boolean): BuildIndex {
|
|
25
|
+
const $meta: IndexData = {
|
|
26
|
+
kind: "index",
|
|
27
|
+
name,
|
|
28
|
+
table: columns[0].table,
|
|
29
|
+
columns: columns.map(c => c.column),
|
|
30
|
+
unique,
|
|
31
|
+
}
|
|
32
|
+
return { $kind: "index", $meta }
|
|
33
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Schema, SchemaMembers } from "./schemaBuilder.types"
|
|
2
|
+
|
|
3
|
+
export function createSchema<M extends SchemaMembers>(
|
|
4
|
+
members: M,
|
|
5
|
+
): Schema<M> {
|
|
6
|
+
const tables: Record<string, unknown> = {}
|
|
7
|
+
const views: Record<string, unknown> = {}
|
|
8
|
+
const indexes: Record<string, unknown> = {}
|
|
9
|
+
for (const [key, member] of Object.entries(members)) {
|
|
10
|
+
switch (member.$kind) {
|
|
11
|
+
case "table":
|
|
12
|
+
tables[key] = member
|
|
13
|
+
break
|
|
14
|
+
case "view":
|
|
15
|
+
views[key] = member
|
|
16
|
+
break
|
|
17
|
+
case "index":
|
|
18
|
+
indexes[key] = member
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { tables, views, indexes } as Schema<M>
|
|
23
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ColumnData, ColumnRef, TableData } from "./metadata"
|
|
2
|
+
import type {
|
|
3
|
+
BuildColumns,
|
|
4
|
+
BuildColumnInner,
|
|
5
|
+
BuildTable,
|
|
6
|
+
} from "./schemaBuilder.types"
|
|
7
|
+
import { SelectQueryBuilder } from "../queryBuilder/SelectQueryBuilder"
|
|
8
|
+
|
|
9
|
+
export function createTable<Columns extends BuildColumns>(
|
|
10
|
+
name: string,
|
|
11
|
+
defineColumns: Columns,
|
|
12
|
+
): BuildTable<Columns> {
|
|
13
|
+
const columns = Object.entries(defineColumns).map<ColumnData>(
|
|
14
|
+
([name, define]) => {
|
|
15
|
+
const meta = (define as BuildColumnInner).$meta
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
datatype: meta.datatype!,
|
|
19
|
+
default: meta.default,
|
|
20
|
+
foreignKey: meta.foreignKey,
|
|
21
|
+
notNull: !!meta.notNull,
|
|
22
|
+
primaryKey: meta.primaryKey,
|
|
23
|
+
unique: !!meta.unique,
|
|
24
|
+
mapping: meta.mapping,
|
|
25
|
+
check: meta.check,
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
const $meta: TableData = {
|
|
30
|
+
name,
|
|
31
|
+
columns,
|
|
32
|
+
constraints: [],
|
|
33
|
+
}
|
|
34
|
+
const by = new Proxy({}, {
|
|
35
|
+
get(_, col: string) {
|
|
36
|
+
return (value: unknown) => ({
|
|
37
|
+
_tag: "RefBy" as const,
|
|
38
|
+
table: defineTable,
|
|
39
|
+
column: col,
|
|
40
|
+
value,
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
function select(...columns: ("*" | string)[]) {
|
|
45
|
+
if (columns[0] === "*" || columns.length === 0) {
|
|
46
|
+
return new SelectQueryBuilder($meta, "*")
|
|
47
|
+
}
|
|
48
|
+
return new SelectQueryBuilder($meta, columns)
|
|
49
|
+
}
|
|
50
|
+
const defineTable = { $kind: "table" as const, $meta, by, primaryKey, unique, check, select } as unknown as BuildTable<Columns>
|
|
51
|
+
for (const col of Object.keys(defineColumns)) {
|
|
52
|
+
// oxlint-disable-next-line typescript/no-explicit-any
|
|
53
|
+
;(defineTable as any)[col] = { table: name, column: col } satisfies ColumnRef
|
|
54
|
+
}
|
|
55
|
+
return defineTable
|
|
56
|
+
|
|
57
|
+
function primaryKey(...columns: (keyof Columns)[]) {
|
|
58
|
+
$meta.constraints.push({
|
|
59
|
+
type: "primaryKey",
|
|
60
|
+
columns: columns as string[],
|
|
61
|
+
})
|
|
62
|
+
return defineTable
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function unique(...columns: (keyof Columns)[]) {
|
|
66
|
+
$meta.constraints.push({
|
|
67
|
+
type: "unique",
|
|
68
|
+
columns: columns as string[],
|
|
69
|
+
})
|
|
70
|
+
return defineTable
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function check(expression: string | ((columns: Record<string, string>, table: TableData) => string)) {
|
|
74
|
+
$meta.constraints.push({
|
|
75
|
+
type: "check",
|
|
76
|
+
expression,
|
|
77
|
+
})
|
|
78
|
+
return defineTable
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
import { print } from "@sigitex/print"
|
|
3
|
+
import { generateSelectNode } from "../queryGenerator/generateSelect"
|
|
4
|
+
import type { SelectQueryBuilder } from "../queryBuilder/SelectQueryBuilder"
|
|
5
|
+
import type { TableData, ViewData } from "./metadata"
|
|
6
|
+
import type { BuildView, SchemaSelect } from "./schemaBuilder.types"
|
|
7
|
+
|
|
8
|
+
export function createView<SelectColumns>(
|
|
9
|
+
name: string,
|
|
10
|
+
queryBuilder: SchemaSelect<SelectColumns, any>,
|
|
11
|
+
): BuildView<SelectColumns> {
|
|
12
|
+
const builder = queryBuilder as unknown as SelectQueryBuilder
|
|
13
|
+
const sql = print([generateSelectNode(builder.query)])
|
|
14
|
+
const $meta: ViewData = { kind: "view", name, sql }
|
|
15
|
+
const allColumns = [
|
|
16
|
+
...builder.table.columns,
|
|
17
|
+
...(builder.query.joins?.flatMap(j => j.target.tableData.columns) ?? []),
|
|
18
|
+
]
|
|
19
|
+
const $tableData: TableData = { name, columns: allColumns, constraints: [] }
|
|
20
|
+
return { $kind: "view", $meta, $tableData }
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./Mapping"
|
|
2
|
+
export * from "./metadata"
|
|
3
|
+
export * from "./schemaBuilder.types"
|
|
4
|
+
export * from "./createSchema"
|
|
5
|
+
export * from "./createTable"
|
|
6
|
+
export * from "./createIndex"
|
|
7
|
+
export * from "./createView"
|
|
8
|
+
export * from "./columnBuilders"
|
|
9
|
+
export * from "./createFixture"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
export type Text = "text"
|
|
3
|
+
export type Integer = "integer"
|
|
4
|
+
export type Real = "real"
|
|
5
|
+
export type Blob = "blob"
|
|
6
|
+
|
|
7
|
+
export type Datatype = Text | Integer | Real | Blob
|
|
8
|
+
|
|
9
|
+
export type JSType = string | number | ArrayBuffer
|
|
10
|
+
|
|
11
|
+
export type MappingData = { from(value: any): any; to(value: any): any }
|
|
12
|
+
|
|
13
|
+
export type TableData = {
|
|
14
|
+
readonly name: string
|
|
15
|
+
readonly columns: ColumnData[]
|
|
16
|
+
readonly constraints: TableConstraintData[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CheckExpression = string | ((name: string, column: ColumnData) => string)
|
|
20
|
+
|
|
21
|
+
export type ColumnData = {
|
|
22
|
+
readonly name: string
|
|
23
|
+
readonly datatype: Datatype
|
|
24
|
+
readonly notNull: boolean
|
|
25
|
+
readonly primaryKey: PrimaryKeyData | undefined
|
|
26
|
+
readonly default: string | undefined
|
|
27
|
+
readonly unique: boolean
|
|
28
|
+
readonly foreignKey: ForeignKeyData | undefined
|
|
29
|
+
readonly mapping: MappingData | undefined
|
|
30
|
+
readonly check: CheckExpression | undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type ColumnRef = {
|
|
34
|
+
readonly table: string
|
|
35
|
+
readonly column: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ForeignKeyData = ColumnRef
|
|
39
|
+
|
|
40
|
+
export type TableConstraintData = TablePrimaryKeyData | TableUniqueData | TableCheckData
|
|
41
|
+
|
|
42
|
+
export type TablePrimaryKeyData = {
|
|
43
|
+
readonly type: "primaryKey"
|
|
44
|
+
readonly columns: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TableUniqueData = {
|
|
48
|
+
readonly type: "unique"
|
|
49
|
+
readonly columns: string[]
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type TableCheckData = {
|
|
53
|
+
readonly type: "check"
|
|
54
|
+
readonly expression: string | ((columns: Record<string, string>, table: TableData) => string)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type PrimaryKeyData = {
|
|
58
|
+
readonly autoincrement: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type IndexData = {
|
|
62
|
+
readonly kind: "index"
|
|
63
|
+
readonly name: string
|
|
64
|
+
readonly table: string
|
|
65
|
+
readonly columns: string[]
|
|
66
|
+
readonly unique: boolean
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type ViewData = {
|
|
70
|
+
readonly kind: "view"
|
|
71
|
+
readonly name: string
|
|
72
|
+
readonly sql: string
|
|
73
|
+
}
|