@barishnamazov/gsql 0.1.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/README.md +203 -0
- package/dist/cli.js +174 -0
- package/dist/index.js +33 -0
- package/package.json +50 -0
- package/src/cli.ts +206 -0
- package/src/compiler.ts +73 -0
- package/src/generator.ts +547 -0
- package/src/index.ts +11 -0
- package/src/lexer.ts +636 -0
- package/src/parser.ts +1241 -0
- package/src/types.ts +165 -0
package/src/generator.ts
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSQL Code Generator
|
|
3
|
+
*
|
|
4
|
+
* Transforms the AST into PostgreSQL-compatible SQL.
|
|
5
|
+
* Handles concept instantiation, mixin resolution, and template expansion.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
GSQLProgram,
|
|
10
|
+
TopLevelDeclaration,
|
|
11
|
+
ExtensionDecl,
|
|
12
|
+
FunctionDecl,
|
|
13
|
+
SchemaDecl,
|
|
14
|
+
ConceptDecl,
|
|
15
|
+
EnumDecl,
|
|
16
|
+
Instantiation,
|
|
17
|
+
PerInstanceIndex,
|
|
18
|
+
SchemaBodyItem,
|
|
19
|
+
ColumnDef,
|
|
20
|
+
IndexDef,
|
|
21
|
+
CheckDef,
|
|
22
|
+
TriggerDef,
|
|
23
|
+
} from "./types.ts";
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Utility Functions
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Convert PascalCase or camelCase to snake_case
|
|
31
|
+
*
|
|
32
|
+
* Examples:
|
|
33
|
+
* - "MyTarget" -> "my_target"
|
|
34
|
+
* - "Author" -> "author"
|
|
35
|
+
* - "UserID" -> "user_id"
|
|
36
|
+
* - "HTTPServer" -> "http_server"
|
|
37
|
+
* - "myVariableName" -> "my_variable_name"
|
|
38
|
+
*
|
|
39
|
+
* Handles edge cases:
|
|
40
|
+
* - Consecutive uppercase letters: splits before lowercase (HTTPServer -> http_server)
|
|
41
|
+
* - Mixed case: inserts underscores between transitions (myVarName -> my_var_name)
|
|
42
|
+
*/
|
|
43
|
+
function toSnakeCase(str: string): string {
|
|
44
|
+
return str
|
|
45
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") // Handle consecutive caps (HTTPServer -> HTTP_Server)
|
|
46
|
+
.replace(/([a-z\d])([A-Z])/g, "$1_$2") // Handle camelCase transitions (myVar -> my_Var)
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Generator Context
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
interface GeneratorContext {
|
|
55
|
+
/** Map of concept names to their definitions */
|
|
56
|
+
concepts: Map<string, ConceptDecl>;
|
|
57
|
+
|
|
58
|
+
/** Map of schema names to their definitions (for mixins) */
|
|
59
|
+
schemas: Map<string, SchemaDecl>;
|
|
60
|
+
|
|
61
|
+
/** Map of enum names to their definitions */
|
|
62
|
+
enums: Map<string, EnumDecl>;
|
|
63
|
+
|
|
64
|
+
/** Map of table names to their resolved schema names (for references) */
|
|
65
|
+
tableToSchema: Map<string, string>;
|
|
66
|
+
|
|
67
|
+
/** Template substitutions for current instantiation */
|
|
68
|
+
templateSubs: Map<string, string>;
|
|
69
|
+
|
|
70
|
+
/** Generated enum SQL */
|
|
71
|
+
enumSql: string[];
|
|
72
|
+
|
|
73
|
+
/** Generated table SQL */
|
|
74
|
+
tableSql: string[];
|
|
75
|
+
|
|
76
|
+
/** Generated index SQL */
|
|
77
|
+
indexSql: string[];
|
|
78
|
+
|
|
79
|
+
/** Generated trigger SQL */
|
|
80
|
+
triggerSql: string[];
|
|
81
|
+
|
|
82
|
+
/** Extension SQL */
|
|
83
|
+
extensionSql: string[];
|
|
84
|
+
|
|
85
|
+
/** Function SQL */
|
|
86
|
+
functionSql: string[];
|
|
87
|
+
|
|
88
|
+
/** Per-instance index SQL (added at the end) */
|
|
89
|
+
perInstanceIndexSql: string[];
|
|
90
|
+
|
|
91
|
+
/** Track which enums have been generated */
|
|
92
|
+
generatedEnums: Set<string>;
|
|
93
|
+
|
|
94
|
+
/** Track which tables have been generated */
|
|
95
|
+
generatedTables: Set<string>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createContext(): GeneratorContext {
|
|
99
|
+
return {
|
|
100
|
+
concepts: new Map(),
|
|
101
|
+
schemas: new Map(),
|
|
102
|
+
enums: new Map(),
|
|
103
|
+
tableToSchema: new Map(),
|
|
104
|
+
templateSubs: new Map(),
|
|
105
|
+
enumSql: [],
|
|
106
|
+
tableSql: [],
|
|
107
|
+
indexSql: [],
|
|
108
|
+
triggerSql: [],
|
|
109
|
+
extensionSql: [],
|
|
110
|
+
functionSql: [],
|
|
111
|
+
perInstanceIndexSql: [],
|
|
112
|
+
generatedEnums: new Set(),
|
|
113
|
+
generatedTables: new Set(),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// First Pass: Collect Definitions
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
function collectDefinitions(ast: GSQLProgram, ctx: GeneratorContext): void {
|
|
122
|
+
for (const decl of ast.declarations) {
|
|
123
|
+
switch (decl.type) {
|
|
124
|
+
case "ConceptDecl":
|
|
125
|
+
ctx.concepts.set(decl.name, decl);
|
|
126
|
+
break;
|
|
127
|
+
case "SchemaDecl":
|
|
128
|
+
ctx.schemas.set(decl.name, decl);
|
|
129
|
+
break;
|
|
130
|
+
case "EnumDecl":
|
|
131
|
+
ctx.enums.set(decl.name, decl);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Template Expansion
|
|
139
|
+
// ============================================================================
|
|
140
|
+
|
|
141
|
+
function expandTemplate(name: string, ctx: GeneratorContext): string {
|
|
142
|
+
const templateMatch = name.match(/^\{([^}]+)\}(.*)$/);
|
|
143
|
+
if (templateMatch) {
|
|
144
|
+
const [, param, suffix] = templateMatch;
|
|
145
|
+
let replacement: string | undefined;
|
|
146
|
+
for (const [key, value] of ctx.templateSubs) {
|
|
147
|
+
if (key === param) {
|
|
148
|
+
replacement = value;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return (replacement ?? toSnakeCase(param ?? "")) + (suffix ?? "");
|
|
153
|
+
}
|
|
154
|
+
return name;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Escape special regex characters in a string
|
|
159
|
+
*/
|
|
160
|
+
function escapeRegExp(str: string): string {
|
|
161
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function expandCheckExpression(expr: string, ctx: GeneratorContext): string {
|
|
165
|
+
let result = expr;
|
|
166
|
+
|
|
167
|
+
for (const [param, value] of ctx.templateSubs) {
|
|
168
|
+
const escapedParam = escapeRegExp(param);
|
|
169
|
+
const pattern = new RegExp(`\\{${escapedParam}\\}`, "g");
|
|
170
|
+
result = result.replace(pattern, value);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result = result.replace(/(\w+)::(\w+)/g, "'$2'::$1");
|
|
174
|
+
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// SQL Generation Helpers
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
function generateEnumSql(enumDecl: EnumDecl, ctx: GeneratorContext): void {
|
|
183
|
+
const name = enumDecl.name;
|
|
184
|
+
|
|
185
|
+
if (ctx.generatedEnums.has(name)) return;
|
|
186
|
+
ctx.generatedEnums.add(name);
|
|
187
|
+
|
|
188
|
+
const values = enumDecl.values.map((v) => `'${v}'`).join(", ");
|
|
189
|
+
ctx.enumSql.push(`DO $$ BEGIN
|
|
190
|
+
CREATE TYPE ${name} AS ENUM (${values});
|
|
191
|
+
EXCEPTION
|
|
192
|
+
WHEN duplicate_object THEN null;
|
|
193
|
+
END $$;`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function generateColumnSql(col: ColumnDef, ctx: GeneratorContext): string {
|
|
197
|
+
const name = expandTemplate(col.name, ctx);
|
|
198
|
+
const dataType = col.dataType.toUpperCase();
|
|
199
|
+
|
|
200
|
+
const parts: string[] = [` ${name} ${dataType}`];
|
|
201
|
+
const postConstraints: string[] = [];
|
|
202
|
+
|
|
203
|
+
for (const constraint of col.constraints) {
|
|
204
|
+
switch (constraint.type) {
|
|
205
|
+
case "PrimaryKey":
|
|
206
|
+
parts.push("PRIMARY KEY");
|
|
207
|
+
break;
|
|
208
|
+
case "NotNull":
|
|
209
|
+
parts.push("NOT NULL");
|
|
210
|
+
break;
|
|
211
|
+
case "Unique":
|
|
212
|
+
parts.push("UNIQUE");
|
|
213
|
+
break;
|
|
214
|
+
case "Default":
|
|
215
|
+
parts.push(`DEFAULT ${constraint.value ?? "NULL"}`);
|
|
216
|
+
break;
|
|
217
|
+
case "Reference": {
|
|
218
|
+
let tableName = constraint.table ?? "";
|
|
219
|
+
if (ctx.tableToSchema.has(tableName)) {
|
|
220
|
+
tableName = ctx.tableToSchema.get(tableName) ?? tableName;
|
|
221
|
+
}
|
|
222
|
+
postConstraints.push(`REFERENCES ${tableName}(${constraint.column ?? "id"})`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
case "Check":
|
|
226
|
+
parts.push(`CHECK (${expandCheckExpression(constraint.value ?? "", ctx)})`);
|
|
227
|
+
break;
|
|
228
|
+
case "OnDelete":
|
|
229
|
+
postConstraints.push(`ON DELETE ${constraint.action ?? "NO ACTION"}`);
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return [...parts, ...postConstraints].join(" ");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function generateIndexSql(tableName: string, index: IndexDef, ctx: GeneratorContext): string {
|
|
238
|
+
const columns = index.columns.map((c) => expandTemplate(c, ctx)).join(", ");
|
|
239
|
+
const columnNames = index.columns.map((c) => expandTemplate(c, ctx)).join("_");
|
|
240
|
+
const indexName = `idx_${tableName}_${columnNames}`;
|
|
241
|
+
const unique = index.unique === true ? "UNIQUE " : "";
|
|
242
|
+
const using = index.using ? `USING ${index.using} ` : "";
|
|
243
|
+
|
|
244
|
+
return `CREATE ${unique}INDEX ${indexName} ON ${tableName} ${using}(${columns});`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function generateCheckSql(_tableName: string, check: CheckDef, ctx: GeneratorContext): string {
|
|
248
|
+
const expr = expandCheckExpression(check.expression, ctx);
|
|
249
|
+
return ` CHECK (${expr})`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function generateTriggerSql(
|
|
253
|
+
tableName: string,
|
|
254
|
+
trigger: TriggerDef,
|
|
255
|
+
_ctx: GeneratorContext
|
|
256
|
+
): string {
|
|
257
|
+
const timing = trigger.timing.toUpperCase();
|
|
258
|
+
const event = trigger.event.toUpperCase();
|
|
259
|
+
const forEach = trigger.forEach === "row" ? "FOR EACH ROW" : "FOR EACH STATEMENT";
|
|
260
|
+
|
|
261
|
+
return `CREATE TRIGGER ${trigger.name}_${tableName}
|
|
262
|
+
${timing} ${event} ON ${tableName}
|
|
263
|
+
${forEach} EXECUTE FUNCTION ${trigger.executeFunction}();`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Schema Resolution
|
|
268
|
+
// ============================================================================
|
|
269
|
+
|
|
270
|
+
interface ResolvedSchema {
|
|
271
|
+
tableName: string;
|
|
272
|
+
columns: ColumnDef[];
|
|
273
|
+
indexes: IndexDef[];
|
|
274
|
+
checks: CheckDef[];
|
|
275
|
+
triggers: TriggerDef[];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveMixins(schema: SchemaDecl, ctx: GeneratorContext): SchemaBodyItem[] {
|
|
279
|
+
const items: SchemaBodyItem[] = [];
|
|
280
|
+
|
|
281
|
+
for (const mixinName of schema.mixins) {
|
|
282
|
+
const mixin = ctx.schemas.get(mixinName);
|
|
283
|
+
if (mixin) {
|
|
284
|
+
items.push(...resolveMixins(mixin, ctx));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
items.push(...schema.members);
|
|
289
|
+
|
|
290
|
+
return items;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function resolveSchema(
|
|
294
|
+
schema: SchemaDecl,
|
|
295
|
+
tableName: string,
|
|
296
|
+
ctx: GeneratorContext
|
|
297
|
+
): ResolvedSchema {
|
|
298
|
+
const allItems = resolveMixins(schema, ctx);
|
|
299
|
+
|
|
300
|
+
const columns: ColumnDef[] = [];
|
|
301
|
+
const indexes: IndexDef[] = [];
|
|
302
|
+
const checks: CheckDef[] = [];
|
|
303
|
+
const triggers: TriggerDef[] = [];
|
|
304
|
+
|
|
305
|
+
for (const item of allItems) {
|
|
306
|
+
switch (item.type) {
|
|
307
|
+
case "ColumnDef":
|
|
308
|
+
columns.push(item);
|
|
309
|
+
break;
|
|
310
|
+
case "IndexDef":
|
|
311
|
+
indexes.push(item);
|
|
312
|
+
break;
|
|
313
|
+
case "CheckDef":
|
|
314
|
+
checks.push(item);
|
|
315
|
+
break;
|
|
316
|
+
case "TriggerDef":
|
|
317
|
+
triggers.push(item);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { tableName, columns, indexes, checks, triggers };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Table Generation
|
|
327
|
+
// ============================================================================
|
|
328
|
+
|
|
329
|
+
function generateTableSql(resolved: ResolvedSchema, ctx: GeneratorContext): void {
|
|
330
|
+
if (ctx.generatedTables.has(resolved.tableName)) return;
|
|
331
|
+
ctx.generatedTables.add(resolved.tableName);
|
|
332
|
+
|
|
333
|
+
const columnsSql = resolved.columns.map((c) => generateColumnSql(c, ctx));
|
|
334
|
+
const checksSql = resolved.checks.map((c) => generateCheckSql(resolved.tableName, c, ctx));
|
|
335
|
+
|
|
336
|
+
const allColumnLines = [...columnsSql, ...checksSql];
|
|
337
|
+
const tableBody = allColumnLines.join(",\n");
|
|
338
|
+
|
|
339
|
+
ctx.tableSql.push(`CREATE TABLE ${resolved.tableName} (\n${tableBody}\n);`);
|
|
340
|
+
|
|
341
|
+
for (const index of resolved.indexes) {
|
|
342
|
+
ctx.indexSql.push(generateIndexSql(resolved.tableName, index, ctx));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (const trigger of resolved.triggers) {
|
|
346
|
+
ctx.triggerSql.push(generateTriggerSql(resolved.tableName, trigger, ctx));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================================================
|
|
351
|
+
// Declaration Processing
|
|
352
|
+
// ============================================================================
|
|
353
|
+
|
|
354
|
+
function processExtension(decl: ExtensionDecl, ctx: GeneratorContext): void {
|
|
355
|
+
ctx.extensionSql.push(`CREATE EXTENSION IF NOT EXISTS "${decl.name}";`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function processFunction(decl: FunctionDecl, ctx: GeneratorContext): void {
|
|
359
|
+
ctx.functionSql.push(`CREATE OR REPLACE FUNCTION ${decl.name}()
|
|
360
|
+
RETURNS TRIGGER AS $$
|
|
361
|
+
BEGIN
|
|
362
|
+
${decl.body}
|
|
363
|
+
END;
|
|
364
|
+
$$ LANGUAGE plpgsql;`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function processEnum(decl: EnumDecl, ctx: GeneratorContext): void {
|
|
368
|
+
generateEnumSql(decl, ctx);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function processStandaloneSchema(decl: SchemaDecl, ctx: GeneratorContext): void {
|
|
372
|
+
// Standalone schemas are stored for mixin resolution
|
|
373
|
+
ctx.schemas.set(decl.name, decl);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function processInstantiation(decl: Instantiation, ctx: GeneratorContext): void {
|
|
377
|
+
const concept = ctx.concepts.get(decl.conceptName);
|
|
378
|
+
|
|
379
|
+
if (!concept) {
|
|
380
|
+
const schema = ctx.schemas.get(decl.conceptName);
|
|
381
|
+
if (schema) {
|
|
382
|
+
const target = decl.targets[0];
|
|
383
|
+
if (target) {
|
|
384
|
+
const tableName = target.tableName;
|
|
385
|
+
ctx.tableToSchema.set(schema.name, tableName);
|
|
386
|
+
const resolved = resolveSchema(schema, tableName, ctx);
|
|
387
|
+
generateTableSql(resolved, ctx);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
ctx.templateSubs.clear();
|
|
394
|
+
for (let i = 0; i < concept.typeParams.length; i++) {
|
|
395
|
+
const param = concept.typeParams[i];
|
|
396
|
+
const arg = decl.typeArgs[i];
|
|
397
|
+
if (param && arg) {
|
|
398
|
+
// If alias is provided, use it as-is (preserve case)
|
|
399
|
+
// If no alias, use snake_cased version of the parameter name
|
|
400
|
+
ctx.templateSubs.set(param, arg.alias ?? toSnakeCase(param));
|
|
401
|
+
// Also map the type parameter to the actual table name for foreign key resolution
|
|
402
|
+
ctx.tableToSchema.set(param, arg.tableName);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Get the schemas in the concept in order
|
|
407
|
+
const conceptSchemas = concept.members.filter((m): m is SchemaDecl => m.type === "SchemaDecl");
|
|
408
|
+
|
|
409
|
+
// Map target names to schema positions
|
|
410
|
+
// First, create schema-to-table mapping
|
|
411
|
+
const schemaToTable = new Map<string, { name: string; alias?: string }>();
|
|
412
|
+
|
|
413
|
+
for (let i = 0; i < decl.targets.length && i < conceptSchemas.length; i++) {
|
|
414
|
+
const target = decl.targets[i];
|
|
415
|
+
const schema = conceptSchemas[i];
|
|
416
|
+
if (target && schema) {
|
|
417
|
+
schemaToTable.set(schema.name, { name: target.tableName, alias: target.alias });
|
|
418
|
+
ctx.tableToSchema.set(schema.name, target.tableName);
|
|
419
|
+
|
|
420
|
+
// Also add template substitution for self-references like {Assessments}_id
|
|
421
|
+
// If alias is provided, use it as-is. Otherwise, use snake_cased schema name
|
|
422
|
+
if (target.alias) {
|
|
423
|
+
ctx.templateSubs.set(schema.name, target.alias);
|
|
424
|
+
} else {
|
|
425
|
+
ctx.templateSubs.set(schema.name, toSnakeCase(schema.name));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Generate enums from concept
|
|
431
|
+
const conceptEnums = concept.members.filter((m): m is EnumDecl => m.type === "EnumDecl");
|
|
432
|
+
for (const e of conceptEnums) {
|
|
433
|
+
generateEnumSql(e, ctx);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Generate tables from concept schemas
|
|
437
|
+
for (const schema of conceptSchemas) {
|
|
438
|
+
const mapping = schemaToTable.get(schema.name);
|
|
439
|
+
if (mapping) {
|
|
440
|
+
const resolved = resolveSchema(schema, mapping.name, ctx);
|
|
441
|
+
generateTableSql(resolved, ctx);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function processPerInstanceIndex(decl: PerInstanceIndex, ctx: GeneratorContext): void {
|
|
447
|
+
const columns = decl.columns.join(", ");
|
|
448
|
+
const indexName = `idx_${decl.tableName}_${decl.columns.join("_")}`;
|
|
449
|
+
const unique = decl.unique === true ? "UNIQUE " : "";
|
|
450
|
+
const using = decl.using ? `USING ${decl.using} ` : "";
|
|
451
|
+
|
|
452
|
+
ctx.perInstanceIndexSql.push(
|
|
453
|
+
`CREATE ${unique}INDEX ${indexName} ON ${decl.tableName} ${using}(${columns});`
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ============================================================================
|
|
458
|
+
// Main Generate Function
|
|
459
|
+
// ============================================================================
|
|
460
|
+
|
|
461
|
+
export function generate(ast: GSQLProgram): string {
|
|
462
|
+
const ctx = createContext();
|
|
463
|
+
|
|
464
|
+
// First pass: collect all definitions
|
|
465
|
+
collectDefinitions(ast, ctx);
|
|
466
|
+
|
|
467
|
+
// Second pass: process declarations in order
|
|
468
|
+
for (const decl of ast.declarations) {
|
|
469
|
+
processDeclaration(decl, ctx);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Assemble final SQL
|
|
473
|
+
const sections: string[] = [];
|
|
474
|
+
|
|
475
|
+
if (ctx.extensionSql.length > 0) {
|
|
476
|
+
sections.push(ctx.extensionSql.join("\n\n"));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (ctx.functionSql.length > 0) {
|
|
480
|
+
sections.push(ctx.functionSql.join("\n\n"));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (ctx.enumSql.length > 0) {
|
|
484
|
+
sections.push(ctx.enumSql.join("\n\n"));
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Tables with their indexes and triggers interleaved
|
|
488
|
+
const tableWithIndexes: string[] = [];
|
|
489
|
+
for (const table of ctx.tableSql) {
|
|
490
|
+
if (table) {
|
|
491
|
+
tableWithIndexes.push(table);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Find matching indexes and triggers (by extracting table name)
|
|
495
|
+
const tableMatch = table.match(/CREATE TABLE (\w+)/);
|
|
496
|
+
if (tableMatch) {
|
|
497
|
+
const tableName = tableMatch[1] ?? "";
|
|
498
|
+
for (const idx of ctx.indexSql) {
|
|
499
|
+
if (idx.includes(` ON ${tableName} `)) {
|
|
500
|
+
tableWithIndexes.push(idx);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (const trg of ctx.triggerSql) {
|
|
504
|
+
if (trg.includes(` ON ${tableName}`)) {
|
|
505
|
+
tableWithIndexes.push(trg);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (tableWithIndexes.length > 0) {
|
|
512
|
+
sections.push(tableWithIndexes.join("\n\n"));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Per-instance indexes at the end
|
|
516
|
+
if (ctx.perInstanceIndexSql.length > 0) {
|
|
517
|
+
sections.push(ctx.perInstanceIndexSql.join("\n\n"));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return "-- Generated SQL from Schema DSL\n\n" + sections.join("\n\n") + "\n";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function processDeclaration(decl: TopLevelDeclaration, ctx: GeneratorContext): void {
|
|
524
|
+
switch (decl.type) {
|
|
525
|
+
case "ExtensionDecl":
|
|
526
|
+
processExtension(decl, ctx);
|
|
527
|
+
break;
|
|
528
|
+
case "FunctionDecl":
|
|
529
|
+
processFunction(decl, ctx);
|
|
530
|
+
break;
|
|
531
|
+
case "EnumDecl":
|
|
532
|
+
processEnum(decl, ctx);
|
|
533
|
+
break;
|
|
534
|
+
case "SchemaDecl":
|
|
535
|
+
processStandaloneSchema(decl, ctx);
|
|
536
|
+
break;
|
|
537
|
+
case "ConceptDecl":
|
|
538
|
+
// Concepts are pre-collected, no processing needed here
|
|
539
|
+
break;
|
|
540
|
+
case "Instantiation":
|
|
541
|
+
processInstantiation(decl, ctx);
|
|
542
|
+
break;
|
|
543
|
+
case "PerInstanceIndex":
|
|
544
|
+
processPerInstanceIndex(decl, ctx);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSQL - Generic SQL Compiler
|
|
3
|
+
*
|
|
4
|
+
* A compiler for GSQL, bringing parametric polymorphism to SQL schemas.
|
|
5
|
+
* Compiles GSQL source to PostgreSQL-compatible SQL.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { compile, compileToSQL } from "./compiler.ts";
|
|
9
|
+
export { parse } from "./parser.ts";
|
|
10
|
+
export { generate } from "./generator.ts";
|
|
11
|
+
export type { GSQLProgram, CompileResult, CompileError } from "./types.ts";
|