@tsqx/kit 0.0.4 → 0.0.5

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.
@@ -0,0 +1,389 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _tsqx_core = require("@tsqx/core");
3
+ let neverthrow = require("neverthrow");
4
+ //#region src/sqlite/d1/parser.ts
5
+ function stripComments(sql) {
6
+ sql = sql.replace(/\/\*[\s\S]*?\*\//g, "");
7
+ sql = sql.replace(/--.*$/gm, "");
8
+ return sql;
9
+ }
10
+ function parseColumnDef(raw) {
11
+ const trimmed = raw.trim();
12
+ if (!trimmed) return null;
13
+ const match = trimmed.match(/^("(\w+)"|(\w+))\s+(.+)$/is);
14
+ if (!match) return null;
15
+ const name = match[2] ?? match[3].toLowerCase();
16
+ const rest = match[4].trim();
17
+ const typeMatch = rest.match(/^(\w+(?:\s+(?:PRECISION|VARYING))?(?:\(\s*\d+(?:\s*,\s*\d+)?\s*\))?)/i);
18
+ if (!typeMatch) return null;
19
+ const type = typeMatch[1].toUpperCase();
20
+ const modifiers = rest.slice(typeMatch[0].length).trim().toUpperCase();
21
+ const nullable = !modifiers.includes("NOT NULL");
22
+ const primaryKey = modifiers.includes("PRIMARY KEY");
23
+ const autoincrement = modifiers.includes("AUTOINCREMENT");
24
+ const unique = modifiers.includes("UNIQUE");
25
+ let defaultValue;
26
+ const defaultMatch = rest.slice(typeMatch[0].length).match(/DEFAULT\s+(.+?)(?:\s+(?:NOT\s+NULL|NULL|PRIMARY\s+KEY|UNIQUE|REFERENCES|CHECK|CONSTRAINT|AUTOINCREMENT)|\s*$)/i);
27
+ if (defaultMatch) defaultValue = defaultMatch[1].trim();
28
+ let references;
29
+ const restModifiers = rest.slice(typeMatch[0].length).trim();
30
+ const refMatch = restModifiers.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*"?(\w+)"?\s*\)/i);
31
+ if (refMatch) references = {
32
+ table: restModifiers.includes(`"${refMatch[1]}"`) ? refMatch[1] : refMatch[1].toLowerCase(),
33
+ column: restModifiers.includes(`"${refMatch[2]}"`) ? refMatch[2] : refMatch[2].toLowerCase()
34
+ };
35
+ return {
36
+ name,
37
+ type: autoincrement ? "INTEGER" : type,
38
+ nullable: primaryKey ? false : nullable,
39
+ ...defaultValue !== void 0 && { default: defaultValue },
40
+ primaryKey,
41
+ unique,
42
+ ...references && { references }
43
+ };
44
+ }
45
+ function isTableConstraint(line) {
46
+ const upper = line.trim().toUpperCase();
47
+ return upper.startsWith("PRIMARY KEY") || upper.startsWith("UNIQUE") || upper.startsWith("FOREIGN KEY") || upper.startsWith("CONSTRAINT") || upper.startsWith("CHECK");
48
+ }
49
+ function parseTableConstraint(raw) {
50
+ let working = raw.trim().toUpperCase();
51
+ let name;
52
+ const constraintMatch = raw.trim().match(/^CONSTRAINT\s+"?(\w+)"?\s+(.+)$/is);
53
+ if (constraintMatch) {
54
+ name = constraintMatch[1].toLowerCase();
55
+ working = constraintMatch[2].trim().toUpperCase();
56
+ }
57
+ const colsMatch = working.match(/\(\s*(.+?)\s*\)/);
58
+ if (!colsMatch) return null;
59
+ const columns = colsMatch[1].split(",").map((c) => {
60
+ const t = c.trim();
61
+ if (t.startsWith("\"") && t.endsWith("\"")) return t.slice(1, -1);
62
+ return t.replace(/"/g, "").toLowerCase();
63
+ });
64
+ if (working.startsWith("PRIMARY KEY")) return {
65
+ type: "primary_key",
66
+ ...name && { name },
67
+ columns
68
+ };
69
+ if (working.startsWith("UNIQUE")) return {
70
+ type: "unique",
71
+ ...name && { name },
72
+ columns
73
+ };
74
+ if (working.startsWith("FOREIGN KEY")) {
75
+ const refMatch = working.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*(.+?)\s*\)/i);
76
+ if (!refMatch) return null;
77
+ const refColumns = refMatch[2].split(",").map((c) => {
78
+ const t = c.trim();
79
+ if (t.startsWith("\"") && t.endsWith("\"")) return t.slice(1, -1);
80
+ return t.replace(/"/g, "").toLowerCase();
81
+ });
82
+ return {
83
+ type: "foreign_key",
84
+ ...name && { name },
85
+ columns,
86
+ references: {
87
+ table: refMatch[1].toLowerCase(),
88
+ columns: refColumns
89
+ }
90
+ };
91
+ }
92
+ return null;
93
+ }
94
+ function parseCreateTable(sql) {
95
+ const tableMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?("(\w+)"|(\w+))\s*\(([\s\S]+)\)/i);
96
+ if (!tableMatch) return (0, neverthrow.err)(new _tsqx_core.SchemaError("Failed to parse CREATE TABLE statement"));
97
+ const tableName = tableMatch[2] ?? tableMatch[3].toLowerCase();
98
+ const body = tableMatch[4];
99
+ const lines = [];
100
+ let depth = 0;
101
+ let current = "";
102
+ for (const char of body) {
103
+ if (char === "(") depth++;
104
+ else if (char === ")") depth--;
105
+ if (char === "," && depth === 0) {
106
+ lines.push(current.trim());
107
+ current = "";
108
+ } else current += char;
109
+ }
110
+ if (current.trim()) lines.push(current.trim());
111
+ const columns = [];
112
+ const constraints = [];
113
+ for (const line of lines) if (isTableConstraint(line)) {
114
+ const constraint = parseTableConstraint(line);
115
+ if (constraint) constraints.push(constraint);
116
+ } else {
117
+ const column = parseColumnDef(line);
118
+ if (column) columns.push(column);
119
+ }
120
+ const pkConstraint = constraints.find((c) => c.type === "primary_key");
121
+ if (pkConstraint) {
122
+ for (const col of columns) if (pkConstraint.columns.includes(col.name)) {
123
+ col.primaryKey = true;
124
+ col.nullable = false;
125
+ }
126
+ }
127
+ return (0, neverthrow.ok)({
128
+ name: tableName,
129
+ columns,
130
+ constraints
131
+ });
132
+ }
133
+ function parseSchemaFiles(files) {
134
+ const snapshot = {};
135
+ for (const file of files) {
136
+ const matches = stripComments(file.content).match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?\w+"?\s*\([\s\S]*?\);/gi);
137
+ if (!matches) continue;
138
+ for (const statement of matches) {
139
+ const result = parseCreateTable(statement);
140
+ if (result.isErr()) return (0, neverthrow.err)(new _tsqx_core.SchemaError(`Error in ${file.filename}: ${result.error.message}`));
141
+ const table = result.value;
142
+ if (snapshot[table.name]) return (0, neverthrow.err)(new _tsqx_core.SchemaError(`Duplicate table "${table.name}" found in ${file.filename}`));
143
+ snapshot[table.name] = table;
144
+ }
145
+ }
146
+ return (0, neverthrow.ok)(snapshot);
147
+ }
148
+ //#endregion
149
+ //#region src/sqlite/d1/generator.ts
150
+ function quoteIdent(name) {
151
+ if (name !== name.toLowerCase()) return `"${name}"`;
152
+ return name;
153
+ }
154
+ function columnToSQL(col) {
155
+ const parts = [quoteIdent(col.name), col.type];
156
+ if (col.primaryKey) parts.push("PRIMARY KEY");
157
+ if (!col.nullable && !col.primaryKey) parts.push("NOT NULL");
158
+ if (col.unique) parts.push("UNIQUE");
159
+ if (col.default !== void 0) parts.push(`DEFAULT ${col.default}`);
160
+ if (col.references) parts.push(`REFERENCES ${quoteIdent(col.references.table)}(${quoteIdent(col.references.column)})`);
161
+ return parts.join(" ");
162
+ }
163
+ function createTableSQL(table) {
164
+ const cols = table.columns.map((c) => ` ${columnToSQL(c)}`);
165
+ for (const constraint of table.constraints) {
166
+ const name = constraint.name ? `CONSTRAINT ${constraint.name} ` : "";
167
+ if (constraint.type === "primary_key") {
168
+ if (constraint.columns.length > 1) cols.push(` ${name}PRIMARY KEY (${constraint.columns.map(quoteIdent).join(", ")})`);
169
+ } else if (constraint.type === "unique") {
170
+ if (constraint.columns.length > 1) cols.push(` ${name}UNIQUE (${constraint.columns.map(quoteIdent).join(", ")})`);
171
+ } else if (constraint.type === "foreign_key" && constraint.references) cols.push(` ${name}FOREIGN KEY (${constraint.columns.map(quoteIdent).join(", ")}) REFERENCES ${quoteIdent(constraint.references.table)}(${constraint.references.columns.map(quoteIdent).join(", ")})`);
172
+ }
173
+ return `CREATE TABLE ${quoteIdent(table.name)} (\n${cols.join(",\n")}\n);`;
174
+ }
175
+ function operationToSQL(op) {
176
+ switch (op.type) {
177
+ case "create_table": return createTableSQL(op.table);
178
+ case "drop_table": return `DROP TABLE IF EXISTS ${quoteIdent(op.tableName)};`;
179
+ case "add_column": return `ALTER TABLE ${quoteIdent(op.tableName)} ADD COLUMN ${columnToSQL(op.column)};`;
180
+ case "drop_column": return `ALTER TABLE ${quoteIdent(op.tableName)} DROP COLUMN ${quoteIdent(op.columnName)};`;
181
+ case "alter_column": {
182
+ const qt = quoteIdent(op.tableName);
183
+ const qc = quoteIdent(op.columnName);
184
+ return `-- SQLite does not support ALTER COLUMN. To change column "${op.columnName}" in "${op.tableName}", recreate the table.\n-- ALTER TABLE ${qt} ALTER COLUMN ${qc} (not supported);`;
185
+ }
186
+ }
187
+ }
188
+ function generateSQL(operations) {
189
+ if (operations.length === 0) return "";
190
+ return `${operations.map(operationToSQL).filter(Boolean).join("\n\n")}\n`;
191
+ }
192
+ //#endregion
193
+ //#region src/sqlite/d1/types.ts
194
+ function sqlTypeToJsonSchema(sqlType) {
195
+ switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
196
+ case "INTEGER":
197
+ case "INT":
198
+ case "TINYINT":
199
+ case "SMALLINT":
200
+ case "MEDIUMINT":
201
+ case "BIGINT": return { type: "integer" };
202
+ case "REAL":
203
+ case "DOUBLE":
204
+ case "DOUBLE PRECISION":
205
+ case "FLOAT":
206
+ case "NUMERIC":
207
+ case "DECIMAL": return { type: "number" };
208
+ case "BOOLEAN": return { type: "integer" };
209
+ case "TEXT":
210
+ case "CLOB": return { type: "string" };
211
+ case "VARCHAR":
212
+ case "CHARACTER VARYING":
213
+ case "NVARCHAR":
214
+ case "CHARACTER": {
215
+ const lenMatch = sqlType.match(/\((\d+)\)/);
216
+ return lenMatch ? {
217
+ type: "string",
218
+ maxLength: parseInt(lenMatch[1], 10)
219
+ } : { type: "string" };
220
+ }
221
+ case "BLOB": return {
222
+ type: "string",
223
+ format: "byte"
224
+ };
225
+ case "DATE": return {
226
+ type: "string",
227
+ format: "date"
228
+ };
229
+ case "DATETIME":
230
+ case "TIMESTAMP": return {
231
+ type: "string",
232
+ format: "date-time"
233
+ };
234
+ default: return { type: "string" };
235
+ }
236
+ }
237
+ function sqlTypeToTsType(sqlType) {
238
+ switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
239
+ case "INTEGER":
240
+ case "INT":
241
+ case "TINYINT":
242
+ case "SMALLINT":
243
+ case "MEDIUMINT":
244
+ case "BIGINT":
245
+ case "REAL":
246
+ case "DOUBLE":
247
+ case "DOUBLE PRECISION":
248
+ case "FLOAT":
249
+ case "NUMERIC":
250
+ case "DECIMAL": return "number";
251
+ case "BOOLEAN": return "number";
252
+ case "BLOB": return "ArrayBuffer";
253
+ default: return "string";
254
+ }
255
+ }
256
+ //#endregion
257
+ //#region src/sqlite/d1/query-codegen.ts
258
+ function camelCase(str) {
259
+ return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
260
+ }
261
+ function generateParamInterface(query) {
262
+ if (query.params.length === 0) return null;
263
+ return `export interface ${`${(0, _tsqx_core.pascalCase)(query.name)}Params`} {\n${query.params.map((p) => {
264
+ const base = sqlTypeToTsType(p.sqlType);
265
+ const type = p.nullable ? `${base} | null` : base;
266
+ return ` ${p.name}: ${type};`;
267
+ }).join("\n")}\n}`;
268
+ }
269
+ function generateParamSchema(query) {
270
+ if (query.params.length === 0) return null;
271
+ const name = `${(0, _tsqx_core.pascalCase)(query.name)}ParamsSchema`;
272
+ const properties = {};
273
+ const required = [];
274
+ for (const param of query.params) {
275
+ const jsonType = sqlTypeToJsonSchema(param.sqlType);
276
+ if (param.nullable) properties[param.name] = {
277
+ ...jsonType,
278
+ type: Array.isArray(jsonType.type) ? [...jsonType.type, "null"] : [jsonType.type, "null"]
279
+ };
280
+ else {
281
+ properties[param.name] = jsonType;
282
+ required.push(param.name);
283
+ }
284
+ }
285
+ const schema = {
286
+ $schema: "https://json-schema.org/draft/2020-12/schema",
287
+ title: `${(0, _tsqx_core.pascalCase)(query.name)}Params`,
288
+ type: "object",
289
+ properties,
290
+ required,
291
+ additionalProperties: false
292
+ };
293
+ return `export const ${name} = ${JSON.stringify(schema, null, 2)} as const;`;
294
+ }
295
+ function generateResultType(query, snapshot) {
296
+ if (query.command === "exec" || query.command === "execrows" || query.command === "execresult") return null;
297
+ if (!query.returnsTable || !snapshot[query.returnsTable]) return null;
298
+ const table = snapshot[query.returnsTable];
299
+ const typeName = (0, _tsqx_core.pascalCase)(query.returnsTable);
300
+ if (query.returnsColumns.length === table.columns.length && query.returnsColumns.every((c) => table.columns.some((tc) => tc.name === c))) return typeName;
301
+ return `Pick<${typeName}, ${query.returnsColumns.map((c) => `"${c}"`).join(" | ")}>`;
302
+ }
303
+ function generateReturnType(query, resultType) {
304
+ switch (query.command) {
305
+ case "one": return `Promise<${resultType ?? "unknown"} | null>`;
306
+ case "many": return `Promise<${resultType ?? "unknown"}[]>`;
307
+ case "exec": return "Promise<void>";
308
+ case "execrows": return "Promise<number>";
309
+ case "execresult": return "Promise<D1Result<unknown>>";
310
+ default: return "Promise<void>";
311
+ }
312
+ }
313
+ function generateFunctionBody(query) {
314
+ const paramArgs = query.params.map((p) => `params.${p.name}`).join(", ");
315
+ const bindCall = query.params.length > 0 ? `.bind(${paramArgs})` : "";
316
+ switch (query.command) {
317
+ case "one": return ` const result = await db.prepare(sql)${bindCall}.first<${generateResultType(query, {}) ?? "unknown"}>();\n return result ?? null;`;
318
+ case "many": return ` const result = await db.prepare(sql)${bindCall}.all();\n return result.results;`;
319
+ case "exec": return ` await db.prepare(sql)${bindCall}.run();`;
320
+ case "execrows": return ` const result = await db.prepare(sql)${bindCall}.run();\n return result.meta.changes;`;
321
+ case "execresult": return ` return await db.prepare(sql)${bindCall}.run();`;
322
+ default: return ` await db.prepare(sql)${bindCall}.run();`;
323
+ }
324
+ }
325
+ function generateFunction(query, snapshot) {
326
+ const fnName = camelCase(query.name);
327
+ const returnType = generateReturnType(query, generateResultType(query, snapshot));
328
+ const hasParams = query.params.length > 0;
329
+ const paramsType = hasParams ? `${(0, _tsqx_core.pascalCase)(query.name)}Params` : null;
330
+ const args = hasParams ? `db: D1Database, params: ${paramsType}` : "db: D1Database";
331
+ const sql = query.expandedSql.replace(/'/g, "\\'").replace(/\n/g, "\\n");
332
+ const lines = [];
333
+ lines.push(`export async function ${fnName}(${args}): ${returnType} {`);
334
+ lines.push(` const sql = '${sql}';`);
335
+ lines.push(generateFunctionBody(query));
336
+ lines.push("}");
337
+ return lines.join("\n");
338
+ }
339
+ function generateQueryFiles(queries, snapshot) {
340
+ const grouped = /* @__PURE__ */ new Map();
341
+ for (const query of queries) {
342
+ const key = query.sourceFile.replace(/\.sql$/, "");
343
+ if (!grouped.has(key)) grouped.set(key, []);
344
+ grouped.get(key).push(query);
345
+ }
346
+ const files = {};
347
+ for (const [basename, fileQueries] of grouped) {
348
+ const lines = ["// This file is auto-generated by tsqx. Do not edit manually."];
349
+ lines.push("// D1Database type is available globally in Cloudflare Workers");
350
+ lines.push("");
351
+ const tableImports = /* @__PURE__ */ new Set();
352
+ for (const query of fileQueries) if (query.returnsTable && snapshot[query.returnsTable]) tableImports.add(query.returnsTable);
353
+ if (tableImports.size > 0) {
354
+ const imports = [...tableImports].map((t) => `type ${(0, _tsqx_core.pascalCase)(t)}`).join(", ");
355
+ lines.push(`import { ${imports} } from "../../generated";`);
356
+ }
357
+ lines.push("");
358
+ for (const query of fileQueries) {
359
+ const paramInterface = generateParamInterface(query);
360
+ if (paramInterface) {
361
+ lines.push(paramInterface);
362
+ lines.push("");
363
+ }
364
+ const paramSchema = generateParamSchema(query);
365
+ if (paramSchema) {
366
+ lines.push(paramSchema);
367
+ lines.push("");
368
+ }
369
+ lines.push(generateFunction(query, snapshot));
370
+ lines.push("");
371
+ }
372
+ files[`${basename}.ts`] = lines.join("\n");
373
+ }
374
+ return files;
375
+ }
376
+ //#endregion
377
+ //#region src/sqlite/d1/index.ts
378
+ function d1Dialect() {
379
+ return {
380
+ name: "d1",
381
+ parseSchema: parseSchemaFiles,
382
+ generateSQL,
383
+ sqlTypeToJsonSchema,
384
+ sqlTypeToTsType,
385
+ generateQueryCode: generateQueryFiles
386
+ };
387
+ }
388
+ //#endregion
389
+ exports.d1Dialect = d1Dialect;
@@ -0,0 +1,6 @@
1
+ import { Dialect } from "@tsqx/core";
2
+
3
+ //#region src/sqlite/d1/index.d.ts
4
+ declare function d1Dialect(): Dialect;
5
+ //#endregion
6
+ export { d1Dialect };
@@ -0,0 +1,6 @@
1
+ import { Dialect } from "@tsqx/core";
2
+
3
+ //#region src/sqlite/d1/index.d.ts
4
+ declare function d1Dialect(): Dialect;
5
+ //#endregion
6
+ export { d1Dialect };
@@ -0,0 +1,388 @@
1
+ import { SchemaError, pascalCase } from "@tsqx/core";
2
+ import { err, ok } from "neverthrow";
3
+ //#region src/sqlite/d1/parser.ts
4
+ function stripComments(sql) {
5
+ sql = sql.replace(/\/\*[\s\S]*?\*\//g, "");
6
+ sql = sql.replace(/--.*$/gm, "");
7
+ return sql;
8
+ }
9
+ function parseColumnDef(raw) {
10
+ const trimmed = raw.trim();
11
+ if (!trimmed) return null;
12
+ const match = trimmed.match(/^("(\w+)"|(\w+))\s+(.+)$/is);
13
+ if (!match) return null;
14
+ const name = match[2] ?? match[3].toLowerCase();
15
+ const rest = match[4].trim();
16
+ const typeMatch = rest.match(/^(\w+(?:\s+(?:PRECISION|VARYING))?(?:\(\s*\d+(?:\s*,\s*\d+)?\s*\))?)/i);
17
+ if (!typeMatch) return null;
18
+ const type = typeMatch[1].toUpperCase();
19
+ const modifiers = rest.slice(typeMatch[0].length).trim().toUpperCase();
20
+ const nullable = !modifiers.includes("NOT NULL");
21
+ const primaryKey = modifiers.includes("PRIMARY KEY");
22
+ const autoincrement = modifiers.includes("AUTOINCREMENT");
23
+ const unique = modifiers.includes("UNIQUE");
24
+ let defaultValue;
25
+ const defaultMatch = rest.slice(typeMatch[0].length).match(/DEFAULT\s+(.+?)(?:\s+(?:NOT\s+NULL|NULL|PRIMARY\s+KEY|UNIQUE|REFERENCES|CHECK|CONSTRAINT|AUTOINCREMENT)|\s*$)/i);
26
+ if (defaultMatch) defaultValue = defaultMatch[1].trim();
27
+ let references;
28
+ const restModifiers = rest.slice(typeMatch[0].length).trim();
29
+ const refMatch = restModifiers.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*"?(\w+)"?\s*\)/i);
30
+ if (refMatch) references = {
31
+ table: restModifiers.includes(`"${refMatch[1]}"`) ? refMatch[1] : refMatch[1].toLowerCase(),
32
+ column: restModifiers.includes(`"${refMatch[2]}"`) ? refMatch[2] : refMatch[2].toLowerCase()
33
+ };
34
+ return {
35
+ name,
36
+ type: autoincrement ? "INTEGER" : type,
37
+ nullable: primaryKey ? false : nullable,
38
+ ...defaultValue !== void 0 && { default: defaultValue },
39
+ primaryKey,
40
+ unique,
41
+ ...references && { references }
42
+ };
43
+ }
44
+ function isTableConstraint(line) {
45
+ const upper = line.trim().toUpperCase();
46
+ return upper.startsWith("PRIMARY KEY") || upper.startsWith("UNIQUE") || upper.startsWith("FOREIGN KEY") || upper.startsWith("CONSTRAINT") || upper.startsWith("CHECK");
47
+ }
48
+ function parseTableConstraint(raw) {
49
+ let working = raw.trim().toUpperCase();
50
+ let name;
51
+ const constraintMatch = raw.trim().match(/^CONSTRAINT\s+"?(\w+)"?\s+(.+)$/is);
52
+ if (constraintMatch) {
53
+ name = constraintMatch[1].toLowerCase();
54
+ working = constraintMatch[2].trim().toUpperCase();
55
+ }
56
+ const colsMatch = working.match(/\(\s*(.+?)\s*\)/);
57
+ if (!colsMatch) return null;
58
+ const columns = colsMatch[1].split(",").map((c) => {
59
+ const t = c.trim();
60
+ if (t.startsWith("\"") && t.endsWith("\"")) return t.slice(1, -1);
61
+ return t.replace(/"/g, "").toLowerCase();
62
+ });
63
+ if (working.startsWith("PRIMARY KEY")) return {
64
+ type: "primary_key",
65
+ ...name && { name },
66
+ columns
67
+ };
68
+ if (working.startsWith("UNIQUE")) return {
69
+ type: "unique",
70
+ ...name && { name },
71
+ columns
72
+ };
73
+ if (working.startsWith("FOREIGN KEY")) {
74
+ const refMatch = working.match(/REFERENCES\s+"?(\w+)"?\s*\(\s*(.+?)\s*\)/i);
75
+ if (!refMatch) return null;
76
+ const refColumns = refMatch[2].split(",").map((c) => {
77
+ const t = c.trim();
78
+ if (t.startsWith("\"") && t.endsWith("\"")) return t.slice(1, -1);
79
+ return t.replace(/"/g, "").toLowerCase();
80
+ });
81
+ return {
82
+ type: "foreign_key",
83
+ ...name && { name },
84
+ columns,
85
+ references: {
86
+ table: refMatch[1].toLowerCase(),
87
+ columns: refColumns
88
+ }
89
+ };
90
+ }
91
+ return null;
92
+ }
93
+ function parseCreateTable(sql) {
94
+ const tableMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?("(\w+)"|(\w+))\s*\(([\s\S]+)\)/i);
95
+ if (!tableMatch) return err(new SchemaError("Failed to parse CREATE TABLE statement"));
96
+ const tableName = tableMatch[2] ?? tableMatch[3].toLowerCase();
97
+ const body = tableMatch[4];
98
+ const lines = [];
99
+ let depth = 0;
100
+ let current = "";
101
+ for (const char of body) {
102
+ if (char === "(") depth++;
103
+ else if (char === ")") depth--;
104
+ if (char === "," && depth === 0) {
105
+ lines.push(current.trim());
106
+ current = "";
107
+ } else current += char;
108
+ }
109
+ if (current.trim()) lines.push(current.trim());
110
+ const columns = [];
111
+ const constraints = [];
112
+ for (const line of lines) if (isTableConstraint(line)) {
113
+ const constraint = parseTableConstraint(line);
114
+ if (constraint) constraints.push(constraint);
115
+ } else {
116
+ const column = parseColumnDef(line);
117
+ if (column) columns.push(column);
118
+ }
119
+ const pkConstraint = constraints.find((c) => c.type === "primary_key");
120
+ if (pkConstraint) {
121
+ for (const col of columns) if (pkConstraint.columns.includes(col.name)) {
122
+ col.primaryKey = true;
123
+ col.nullable = false;
124
+ }
125
+ }
126
+ return ok({
127
+ name: tableName,
128
+ columns,
129
+ constraints
130
+ });
131
+ }
132
+ function parseSchemaFiles(files) {
133
+ const snapshot = {};
134
+ for (const file of files) {
135
+ const matches = stripComments(file.content).match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?"?\w+"?\s*\([\s\S]*?\);/gi);
136
+ if (!matches) continue;
137
+ for (const statement of matches) {
138
+ const result = parseCreateTable(statement);
139
+ if (result.isErr()) return err(new SchemaError(`Error in ${file.filename}: ${result.error.message}`));
140
+ const table = result.value;
141
+ if (snapshot[table.name]) return err(new SchemaError(`Duplicate table "${table.name}" found in ${file.filename}`));
142
+ snapshot[table.name] = table;
143
+ }
144
+ }
145
+ return ok(snapshot);
146
+ }
147
+ //#endregion
148
+ //#region src/sqlite/d1/generator.ts
149
+ function quoteIdent(name) {
150
+ if (name !== name.toLowerCase()) return `"${name}"`;
151
+ return name;
152
+ }
153
+ function columnToSQL(col) {
154
+ const parts = [quoteIdent(col.name), col.type];
155
+ if (col.primaryKey) parts.push("PRIMARY KEY");
156
+ if (!col.nullable && !col.primaryKey) parts.push("NOT NULL");
157
+ if (col.unique) parts.push("UNIQUE");
158
+ if (col.default !== void 0) parts.push(`DEFAULT ${col.default}`);
159
+ if (col.references) parts.push(`REFERENCES ${quoteIdent(col.references.table)}(${quoteIdent(col.references.column)})`);
160
+ return parts.join(" ");
161
+ }
162
+ function createTableSQL(table) {
163
+ const cols = table.columns.map((c) => ` ${columnToSQL(c)}`);
164
+ for (const constraint of table.constraints) {
165
+ const name = constraint.name ? `CONSTRAINT ${constraint.name} ` : "";
166
+ if (constraint.type === "primary_key") {
167
+ if (constraint.columns.length > 1) cols.push(` ${name}PRIMARY KEY (${constraint.columns.map(quoteIdent).join(", ")})`);
168
+ } else if (constraint.type === "unique") {
169
+ if (constraint.columns.length > 1) cols.push(` ${name}UNIQUE (${constraint.columns.map(quoteIdent).join(", ")})`);
170
+ } else if (constraint.type === "foreign_key" && constraint.references) cols.push(` ${name}FOREIGN KEY (${constraint.columns.map(quoteIdent).join(", ")}) REFERENCES ${quoteIdent(constraint.references.table)}(${constraint.references.columns.map(quoteIdent).join(", ")})`);
171
+ }
172
+ return `CREATE TABLE ${quoteIdent(table.name)} (\n${cols.join(",\n")}\n);`;
173
+ }
174
+ function operationToSQL(op) {
175
+ switch (op.type) {
176
+ case "create_table": return createTableSQL(op.table);
177
+ case "drop_table": return `DROP TABLE IF EXISTS ${quoteIdent(op.tableName)};`;
178
+ case "add_column": return `ALTER TABLE ${quoteIdent(op.tableName)} ADD COLUMN ${columnToSQL(op.column)};`;
179
+ case "drop_column": return `ALTER TABLE ${quoteIdent(op.tableName)} DROP COLUMN ${quoteIdent(op.columnName)};`;
180
+ case "alter_column": {
181
+ const qt = quoteIdent(op.tableName);
182
+ const qc = quoteIdent(op.columnName);
183
+ return `-- SQLite does not support ALTER COLUMN. To change column "${op.columnName}" in "${op.tableName}", recreate the table.\n-- ALTER TABLE ${qt} ALTER COLUMN ${qc} (not supported);`;
184
+ }
185
+ }
186
+ }
187
+ function generateSQL(operations) {
188
+ if (operations.length === 0) return "";
189
+ return `${operations.map(operationToSQL).filter(Boolean).join("\n\n")}\n`;
190
+ }
191
+ //#endregion
192
+ //#region src/sqlite/d1/types.ts
193
+ function sqlTypeToJsonSchema(sqlType) {
194
+ switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
195
+ case "INTEGER":
196
+ case "INT":
197
+ case "TINYINT":
198
+ case "SMALLINT":
199
+ case "MEDIUMINT":
200
+ case "BIGINT": return { type: "integer" };
201
+ case "REAL":
202
+ case "DOUBLE":
203
+ case "DOUBLE PRECISION":
204
+ case "FLOAT":
205
+ case "NUMERIC":
206
+ case "DECIMAL": return { type: "number" };
207
+ case "BOOLEAN": return { type: "integer" };
208
+ case "TEXT":
209
+ case "CLOB": return { type: "string" };
210
+ case "VARCHAR":
211
+ case "CHARACTER VARYING":
212
+ case "NVARCHAR":
213
+ case "CHARACTER": {
214
+ const lenMatch = sqlType.match(/\((\d+)\)/);
215
+ return lenMatch ? {
216
+ type: "string",
217
+ maxLength: parseInt(lenMatch[1], 10)
218
+ } : { type: "string" };
219
+ }
220
+ case "BLOB": return {
221
+ type: "string",
222
+ format: "byte"
223
+ };
224
+ case "DATE": return {
225
+ type: "string",
226
+ format: "date"
227
+ };
228
+ case "DATETIME":
229
+ case "TIMESTAMP": return {
230
+ type: "string",
231
+ format: "date-time"
232
+ };
233
+ default: return { type: "string" };
234
+ }
235
+ }
236
+ function sqlTypeToTsType(sqlType) {
237
+ switch (sqlType.toUpperCase().replace(/\(.+\)/, "").trim()) {
238
+ case "INTEGER":
239
+ case "INT":
240
+ case "TINYINT":
241
+ case "SMALLINT":
242
+ case "MEDIUMINT":
243
+ case "BIGINT":
244
+ case "REAL":
245
+ case "DOUBLE":
246
+ case "DOUBLE PRECISION":
247
+ case "FLOAT":
248
+ case "NUMERIC":
249
+ case "DECIMAL": return "number";
250
+ case "BOOLEAN": return "number";
251
+ case "BLOB": return "ArrayBuffer";
252
+ default: return "string";
253
+ }
254
+ }
255
+ //#endregion
256
+ //#region src/sqlite/d1/query-codegen.ts
257
+ function camelCase(str) {
258
+ return str.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
259
+ }
260
+ function generateParamInterface(query) {
261
+ if (query.params.length === 0) return null;
262
+ return `export interface ${`${pascalCase(query.name)}Params`} {\n${query.params.map((p) => {
263
+ const base = sqlTypeToTsType(p.sqlType);
264
+ const type = p.nullable ? `${base} | null` : base;
265
+ return ` ${p.name}: ${type};`;
266
+ }).join("\n")}\n}`;
267
+ }
268
+ function generateParamSchema(query) {
269
+ if (query.params.length === 0) return null;
270
+ const name = `${pascalCase(query.name)}ParamsSchema`;
271
+ const properties = {};
272
+ const required = [];
273
+ for (const param of query.params) {
274
+ const jsonType = sqlTypeToJsonSchema(param.sqlType);
275
+ if (param.nullable) properties[param.name] = {
276
+ ...jsonType,
277
+ type: Array.isArray(jsonType.type) ? [...jsonType.type, "null"] : [jsonType.type, "null"]
278
+ };
279
+ else {
280
+ properties[param.name] = jsonType;
281
+ required.push(param.name);
282
+ }
283
+ }
284
+ const schema = {
285
+ $schema: "https://json-schema.org/draft/2020-12/schema",
286
+ title: `${pascalCase(query.name)}Params`,
287
+ type: "object",
288
+ properties,
289
+ required,
290
+ additionalProperties: false
291
+ };
292
+ return `export const ${name} = ${JSON.stringify(schema, null, 2)} as const;`;
293
+ }
294
+ function generateResultType(query, snapshot) {
295
+ if (query.command === "exec" || query.command === "execrows" || query.command === "execresult") return null;
296
+ if (!query.returnsTable || !snapshot[query.returnsTable]) return null;
297
+ const table = snapshot[query.returnsTable];
298
+ const typeName = pascalCase(query.returnsTable);
299
+ if (query.returnsColumns.length === table.columns.length && query.returnsColumns.every((c) => table.columns.some((tc) => tc.name === c))) return typeName;
300
+ return `Pick<${typeName}, ${query.returnsColumns.map((c) => `"${c}"`).join(" | ")}>`;
301
+ }
302
+ function generateReturnType(query, resultType) {
303
+ switch (query.command) {
304
+ case "one": return `Promise<${resultType ?? "unknown"} | null>`;
305
+ case "many": return `Promise<${resultType ?? "unknown"}[]>`;
306
+ case "exec": return "Promise<void>";
307
+ case "execrows": return "Promise<number>";
308
+ case "execresult": return "Promise<D1Result<unknown>>";
309
+ default: return "Promise<void>";
310
+ }
311
+ }
312
+ function generateFunctionBody(query) {
313
+ const paramArgs = query.params.map((p) => `params.${p.name}`).join(", ");
314
+ const bindCall = query.params.length > 0 ? `.bind(${paramArgs})` : "";
315
+ switch (query.command) {
316
+ case "one": return ` const result = await db.prepare(sql)${bindCall}.first<${generateResultType(query, {}) ?? "unknown"}>();\n return result ?? null;`;
317
+ case "many": return ` const result = await db.prepare(sql)${bindCall}.all();\n return result.results;`;
318
+ case "exec": return ` await db.prepare(sql)${bindCall}.run();`;
319
+ case "execrows": return ` const result = await db.prepare(sql)${bindCall}.run();\n return result.meta.changes;`;
320
+ case "execresult": return ` return await db.prepare(sql)${bindCall}.run();`;
321
+ default: return ` await db.prepare(sql)${bindCall}.run();`;
322
+ }
323
+ }
324
+ function generateFunction(query, snapshot) {
325
+ const fnName = camelCase(query.name);
326
+ const returnType = generateReturnType(query, generateResultType(query, snapshot));
327
+ const hasParams = query.params.length > 0;
328
+ const paramsType = hasParams ? `${pascalCase(query.name)}Params` : null;
329
+ const args = hasParams ? `db: D1Database, params: ${paramsType}` : "db: D1Database";
330
+ const sql = query.expandedSql.replace(/'/g, "\\'").replace(/\n/g, "\\n");
331
+ const lines = [];
332
+ lines.push(`export async function ${fnName}(${args}): ${returnType} {`);
333
+ lines.push(` const sql = '${sql}';`);
334
+ lines.push(generateFunctionBody(query));
335
+ lines.push("}");
336
+ return lines.join("\n");
337
+ }
338
+ function generateQueryFiles(queries, snapshot) {
339
+ const grouped = /* @__PURE__ */ new Map();
340
+ for (const query of queries) {
341
+ const key = query.sourceFile.replace(/\.sql$/, "");
342
+ if (!grouped.has(key)) grouped.set(key, []);
343
+ grouped.get(key).push(query);
344
+ }
345
+ const files = {};
346
+ for (const [basename, fileQueries] of grouped) {
347
+ const lines = ["// This file is auto-generated by tsqx. Do not edit manually."];
348
+ lines.push("// D1Database type is available globally in Cloudflare Workers");
349
+ lines.push("");
350
+ const tableImports = /* @__PURE__ */ new Set();
351
+ for (const query of fileQueries) if (query.returnsTable && snapshot[query.returnsTable]) tableImports.add(query.returnsTable);
352
+ if (tableImports.size > 0) {
353
+ const imports = [...tableImports].map((t) => `type ${pascalCase(t)}`).join(", ");
354
+ lines.push(`import { ${imports} } from "../../generated";`);
355
+ }
356
+ lines.push("");
357
+ for (const query of fileQueries) {
358
+ const paramInterface = generateParamInterface(query);
359
+ if (paramInterface) {
360
+ lines.push(paramInterface);
361
+ lines.push("");
362
+ }
363
+ const paramSchema = generateParamSchema(query);
364
+ if (paramSchema) {
365
+ lines.push(paramSchema);
366
+ lines.push("");
367
+ }
368
+ lines.push(generateFunction(query, snapshot));
369
+ lines.push("");
370
+ }
371
+ files[`${basename}.ts`] = lines.join("\n");
372
+ }
373
+ return files;
374
+ }
375
+ //#endregion
376
+ //#region src/sqlite/d1/index.ts
377
+ function d1Dialect() {
378
+ return {
379
+ name: "d1",
380
+ parseSchema: parseSchemaFiles,
381
+ generateSQL,
382
+ sqlTypeToJsonSchema,
383
+ sqlTypeToTsType,
384
+ generateQueryCode: generateQueryFiles
385
+ };
386
+ }
387
+ //#endregion
388
+ export { d1Dialect };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsqx/kit",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "exports": {
@@ -13,6 +13,11 @@
13
13
  "import": "./dist/postgres/pg/index.mjs",
14
14
  "require": "./dist/postgres/pg/index.cjs",
15
15
  "types": "./dist/postgres/pg/index.d.ts"
16
+ },
17
+ "./sqlite/d1": {
18
+ "import": "./dist/sqlite/d1/index.mjs",
19
+ "require": "./dist/sqlite/d1/index.cjs",
20
+ "types": "./dist/sqlite/d1/index.d.ts"
16
21
  }
17
22
  },
18
23
  "main": "./dist/index.cjs",
@@ -23,7 +28,7 @@
23
28
  ],
24
29
  "dependencies": {
25
30
  "neverthrow": "^8.2.0",
26
- "@tsqx/core": "^0.0.4"
31
+ "@tsqx/core": "^0.0.5"
27
32
  },
28
33
  "repository": {
29
34
  "type": "git",