Skip to content

Commit ddb3510

Browse files
committed
Add infer-model utility for type inference of columns and tables
1 parent da6c958 commit ddb3510

File tree

7 files changed

+208
-14
lines changed

7 files changed

+208
-14
lines changed

__tests__/schema.test.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import {Table} from "../src/schema";
2+
import {integer, serial, timestamp, uuid, varchar} from "../src/types/column-type";
3+
4+
// Define test interfaces
5+
interface User {
6+
id: string;
7+
name: string;
8+
email: string;
9+
age?: number;
10+
createdAt: Date;
11+
}
12+
13+
interface Post {
14+
id: string;
15+
title: string;
16+
content?: string;
17+
authorId: string;
18+
createdAt: Date;
19+
}
20+
21+
describe("Schema and Table", () => {
22+
describe("Table Creation", () => {
23+
it("should create a table with basic columns", () => {
24+
const table = new Table<User>("users");
25+
table.column("id", uuid());
26+
table.column("name", varchar(255));
27+
table.column("email", varchar(255));
28+
table.column("age", integer());
29+
table.column("createdAt", timestamp()).default("now()");
30+
const sql = table.createTableSql();
31+
expect(sql).toContain("CREATE TABLE IF NOT EXISTS users");
32+
expect(sql).toContain('"id" UUID');
33+
expect(sql).toContain('"name" VARCHAR(255)');
34+
expect(sql).toContain('"email" VARCHAR(255)');
35+
expect(sql).toContain('"age" INTEGER');
36+
expect(sql).toContain('"createdAt" TIMESTAMP');
37+
});
38+
39+
it("should create a table with primary key", () => {
40+
const table = new Table<User>("users");
41+
table.column("id", uuid()).primaryKey();
42+
table.column("name", varchar());
43+
44+
const sql = table.createTableSql()
45+
expect(sql).toContain(`UUID PRIMARY KEY NOT NULL`);
46+
});
47+
48+
it("should create a table with not null constraints", () => {
49+
const table = new Table<User>("users");
50+
table.column("id", serial()).primaryKey();
51+
table.column("name", varchar()).notNull();
52+
table.column("email", varchar()).notNull();
53+
54+
const sql = table.createTableSql();
55+
expect(sql).toContain('"name" VARCHAR(255) NOT NULL');
56+
expect(sql).toContain('"email" VARCHAR(255) NOT NULL');
57+
});
58+
59+
it("should create a table with unique constraints", () => {
60+
const table = new Table<User>("users");
61+
table.column("id", uuid()).primaryKey();
62+
table.column("email", varchar()).unique();
63+
64+
const sql = table.createTableSql();
65+
expect(sql).toContain('"email" VARCHAR(255) UNIQUE');
66+
});
67+
68+
it("should create a table with default values", () => {
69+
const table = new Table<User>("users");
70+
table.column("id", uuid()).primaryKey().$defaultUUID();
71+
table.column("createdAt", timestamp()).default("now()");
72+
73+
const sql = table.createTableSql();
74+
expect(sql).toContain('"createdAt" TIMESTAMP DEFAULT now()');
75+
});
76+
77+
it("should create a table with foreign key references", () => {
78+
const table = new Table<Post>("posts");
79+
table.column("id", uuid()).primaryKey().$defaultUUID();
80+
table.column("authorId", uuid()).references({table: "users", column: "id", onDelete: 'CASCADE'});
81+
82+
const sql = table.createTableSql();
83+
expect(sql).toContain('"authorId" UUID REFERENCES users(id)');
84+
});
85+
//
86+
// it("should create a table with all constraints combined", () => {
87+
// const table = new Table<User>("users");
88+
// table.column("id", "UUID").primaryKey();
89+
// table.column("name", "VARCHAR(255)").notNull();
90+
// table.column("email", "VARCHAR(255)").unique().notNull();
91+
// table.column("age", "INTEGER");
92+
// table
93+
// .column("createdAt", "TIMESTAMP")
94+
// .default("CURRENT_TIMESTAMP")
95+
// .notNull();
96+
//
97+
// const sql = table.toString();
98+
// expect(sql).toContain('"id" UUID PRIMARY KEY');
99+
// expect(sql).toContain('"name" VARCHAR(255) NOT NULL');
100+
// expect(sql).toContain('"email" VARCHAR(255) UNIQUE NOT NULL');
101+
// expect(sql).toContain('"age" INTEGER');
102+
// expect(sql).toContain(
103+
// '"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL'
104+
// );
105+
// });
106+
//
107+
// it("should handle complex table relationships", () => {
108+
// const usersTable = new Table<User>("users");
109+
// usersTable.column("id", "UUID").primaryKey();
110+
// usersTable.column("name", "VARCHAR(255)").notNull();
111+
//
112+
// const postsTable = new Table<Post>("posts");
113+
// postsTable.column("id", "UUID").primaryKey();
114+
// postsTable.column("title", "VARCHAR(255)").notNull();
115+
// postsTable.column("content", "TEXT");
116+
// postsTable.column("authorId", "UUID").notNull().references("users", "id");
117+
// postsTable
118+
// .column("createdAt", "TIMESTAMP")
119+
// .default("CURRENT_TIMESTAMP")
120+
// .notNull();
121+
//
122+
// const postsSql = postsTable.toString();
123+
// expect(postsSql).toContain(
124+
// '"authorId" UUID NOT NULL REFERENCES users(id)'
125+
// );
126+
// expect(postsSql).toContain(
127+
// '"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL'
128+
// );
129+
// });
130+
//
131+
// it("should handle different default value types", () => {
132+
// const table = new Table<any>("test");
133+
// table.column("id", "UUID").primaryKey();
134+
// table.column("stringDefault", "VARCHAR(255)").default("default value");
135+
// table.column("numberDefault", "INTEGER").default(42);
136+
// table.column("nullDefault", "VARCHAR(255)").default(null);
137+
//
138+
// const sql = table.toString();
139+
// expect(sql).toContain(
140+
// "\"stringDefault\" VARCHAR(255) DEFAULT 'default value'"
141+
// );
142+
// expect(sql).toContain('"numberDefault" INTEGER DEFAULT 42');
143+
// expect(sql).toContain('"nullDefault" VARCHAR(255) DEFAULT NULL');
144+
// });
145+
});
146+
});

src/query-builder/base.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { SqlExecutor } from "../types/common";
22

33
export abstract class BaseQueryBuilder<T> {
4-
constructor(
4+
protected constructor(
55
protected readonly tableName: string,
66
protected readonly executor: SqlExecutor
77
) {}
88

99
protected abstract build(): { sql: string; values: any[] };
1010

11-
async execute(): Promise<any> {
11+
async commit(): Promise<any> {
1212
const { sql, values } = this.build();
1313
return this.executor.executeSQL(sql, values);
1414
}

src/schema/column.ts

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
export class Column<T> {
1+
2+
export class Column {
23
private _nullable: boolean = true;
34
private _primaryKey: boolean = false;
5+
private _generateUUID: boolean = false;
46
private _unique: boolean = false;
57
private _default?: any;
6-
private _references?: { table: string; column: string };
8+
private _references?: { table: string; column: string; onDelete?: 'CASCADE' | 'SET NULL' | 'NO ACTION' | 'RESTRICT' | 'SET DEFAULT' };
79

810
constructor(
9-
public readonly name: string,
10-
public readonly type: string
11+
public readonly name: string,
12+
public readonly type: string
1113
) {}
1214

1315
notNull(): this {
@@ -31,8 +33,23 @@ export class Column<T> {
3133
return this;
3234
}
3335

34-
references(table: string, column: string = "id"): this {
35-
this._references = { table, column };
36+
$defaultUUID(): this {
37+
// @ts-ignore
38+
this._generateUUID = true;
39+
return this;
40+
}
41+
42+
$defaultNOW(): this {
43+
this._default = `now()`;
44+
return this;
45+
}
46+
47+
references({table, column = "id", onDelete}: {
48+
table: string,
49+
column?: string,
50+
onDelete?: "CASCADE" | "SET NULL" | "NO ACTION" | "RESTRICT" | "SET DEFAULT"
51+
}): this {
52+
this._references = { table, column, onDelete };
3653
return this;
3754
}
3855

@@ -61,8 +78,12 @@ export class Column<T> {
6178
}
6279
}
6380

81+
if (this._generateUUID) {
82+
definition += " DEFAULT gen_random_uuid()";
83+
}
84+
6485
if (this._references) {
65-
definition += ` REFERENCES ${this._references.table}(${this._references.column})`;
86+
definition += ` REFERENCES ${this._references.table}(${this._references.column}) ON DELETE ${this._references.onDelete ?? 'SET NULL'}`;
6687
}
6788

6889
return definition;

src/schema/table.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
import { Column } from "./column";
22

33
export class Table<T> {
4-
readonly columns: Column<T>[] = [];
4+
readonly columns: Column[] = [];
55

66
constructor(public readonly name: string) {}
77

8-
column<K extends keyof T>(name: K, type: string): Column<T> {
9-
const column = new Column<T>(name.toString(), type);
8+
column<K extends keyof T>(name: K, type: string): Column{
9+
const column = new Column(name.toString(), type);
1010
this.columns.push(column);
1111
return column;
1212
}
1313

14-
toString(): string {
14+
createTableSql(): string {
1515
const columnDefinitions = this.columns
1616
.map((column) => column.toString())
1717
.join(",\n ");
1818

19-
return `CREATE TABLE ${this.name} (\n ${columnDefinitions}\n);`;
19+
return `CREATE TABLE IF NOT EXISTS ${this.name} (\n ${columnDefinitions}\n);`;
2020
}
2121
}

src/types/column-type.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const varchar = (length: number = 255) => `VARCHAR(${length})`;
2+
export const char = (length: number = 50) => `CHAR(${length})`;
3+
export const decimal = (precision: number, scale: number) =>
4+
`DECIMAL(${precision}, ${scale})`;
5+
6+
export const uuid = () => "UUID";
7+
export const text = () => "TEXT";
8+
export const integer = () => "INTEGER";
9+
export const serial = () => "SERIAL";
10+
export const boolean = () => "BOOLEAN";
11+
export const timestamp = () => "TIMESTAMP";
12+
export const date = () => "DATE";
13+
export const json = () => "JSON";
14+
export const jsonb = () => "JSONB";

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./clauses";
22
export * from "./formatting";
3+
export * from "./infer-model";

src/utils/infer-model.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// import {Column, Table} from "../schema";
2+
//
3+
// type InferColumnType<C extends Column<any, any>> =
4+
// C extends Column<any, infer T>
5+
// ? C["isNullable"] extends true
6+
// ? T | undefined
7+
// : T
8+
// : never;
9+
//
10+
// export type InferTable<T extends Table<any>> = {
11+
// [K in keyof T["columns"]]: InferColumnType<T["columns"][K]>;
12+
// };

0 commit comments

Comments
 (0)