@sqg/sqg 0.12.0 → 0.13.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/dist/sqg.mjs +440 -114
- package/dist/templates/java-jdbc.hbs +4 -0
- package/package.json +4 -1
package/dist/sqg.mjs
CHANGED
|
@@ -3,28 +3,31 @@ import { exit } from "node:process";
|
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import consola, { LogLevels } from "consola";
|
|
5
5
|
import updateNotifier from "update-notifier";
|
|
6
|
+
import pc from "picocolors";
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import * as clack from "@clack/prompts";
|
|
7
9
|
import { randomUUID } from "node:crypto";
|
|
8
10
|
import { homedir, tmpdir } from "node:os";
|
|
9
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
10
12
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
11
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
12
14
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
13
15
|
import YAML from "yaml";
|
|
16
|
+
import { camelCase, pascalCase } from "es-toolkit/string";
|
|
14
17
|
import Handlebars from "handlebars";
|
|
15
18
|
import { z } from "zod";
|
|
16
19
|
import { DuckDBEnumType, DuckDBInstance, DuckDBListType, DuckDBMapType, DuckDBStructType } from "@duckdb/node-api";
|
|
17
20
|
import { LRParser } from "@lezer/lr";
|
|
18
|
-
import {
|
|
21
|
+
import { isNotNil, sortBy } from "es-toolkit";
|
|
22
|
+
import { PostgreSqlContainer } from "@testcontainers/postgresql";
|
|
19
23
|
import { Client } from "pg";
|
|
20
24
|
import types from "pg-types";
|
|
21
|
-
import { PostgreSqlContainer } from "@testcontainers/postgresql";
|
|
22
25
|
import BetterSqlite3 from "better-sqlite3";
|
|
23
|
-
import { camelCase as camelCase$1, pascalCase as pascalCase$1 } from "es-toolkit/string";
|
|
24
26
|
import prettier from "prettier/standalone";
|
|
25
27
|
import prettierPluginJava from "prettier-plugin-java";
|
|
26
28
|
import typescriptPlugin from "prettier/parser-typescript";
|
|
27
29
|
import estree from "prettier/plugins/estree";
|
|
30
|
+
import yoctoSpinner from "yocto-spinner";
|
|
28
31
|
|
|
29
32
|
//#region src/constants.ts
|
|
30
33
|
/**
|
|
@@ -362,12 +365,6 @@ function formatErrorForOutput(err) {
|
|
|
362
365
|
* SQG Project Initialization - Creates new SQG projects with example files
|
|
363
366
|
*/
|
|
364
367
|
/**
|
|
365
|
-
* Get the default generator for a language preference
|
|
366
|
-
*/
|
|
367
|
-
function getDefaultGenerator() {
|
|
368
|
-
return "typescript/sqlite";
|
|
369
|
-
}
|
|
370
|
-
/**
|
|
371
368
|
* Generate example SQL content based on engine
|
|
372
369
|
*/
|
|
373
370
|
function getExampleSql(engine) {
|
|
@@ -563,14 +560,14 @@ DELETE FROM posts WHERE id = \${id};
|
|
|
563
560
|
/**
|
|
564
561
|
* Generate sqg.yaml configuration
|
|
565
562
|
*/
|
|
566
|
-
function getConfigYaml(generator, output) {
|
|
563
|
+
function getConfigYaml(generator, output, projectName) {
|
|
567
564
|
const isJava = parseGenerator(generator).language === "java";
|
|
568
565
|
return `# SQG Configuration
|
|
569
566
|
# Generated by: sqg init
|
|
570
567
|
# Documentation: https://sqg.dev
|
|
571
568
|
|
|
572
569
|
version: 1
|
|
573
|
-
name:
|
|
570
|
+
name: ${projectName}
|
|
574
571
|
|
|
575
572
|
sql:
|
|
576
573
|
- files:
|
|
@@ -583,11 +580,70 @@ sql:
|
|
|
583
580
|
`;
|
|
584
581
|
}
|
|
585
582
|
/**
|
|
586
|
-
*
|
|
583
|
+
* Run interactive wizard when no --generator flag is provided
|
|
587
584
|
*/
|
|
588
|
-
async function
|
|
589
|
-
|
|
590
|
-
const
|
|
585
|
+
async function runInteractiveInit(options) {
|
|
586
|
+
clack.intro(pc.bold("Create a new SQG project"));
|
|
587
|
+
const projectName = await clack.text({
|
|
588
|
+
message: "Project name",
|
|
589
|
+
placeholder: "my-project",
|
|
590
|
+
defaultValue: "my-project",
|
|
591
|
+
validate: (value) => {
|
|
592
|
+
if (!value?.trim()) return "Project name is required";
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
if (clack.isCancel(projectName)) {
|
|
596
|
+
clack.cancel("Operation cancelled");
|
|
597
|
+
process.exit(0);
|
|
598
|
+
}
|
|
599
|
+
const generatorOptions = [];
|
|
600
|
+
for (const shortName of SHORT_GENERATOR_NAMES) {
|
|
601
|
+
const info = GENERATORS[`${shortName}/${Object.entries(GENERATORS).find(([k]) => k.startsWith(`${shortName}/`))?.[1]?.driver}`];
|
|
602
|
+
if (info) generatorOptions.push({
|
|
603
|
+
value: shortName,
|
|
604
|
+
label: shortName,
|
|
605
|
+
hint: info.description
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
const generator = await clack.select({
|
|
609
|
+
message: "Generator",
|
|
610
|
+
options: generatorOptions
|
|
611
|
+
});
|
|
612
|
+
if (clack.isCancel(generator)) {
|
|
613
|
+
clack.cancel("Operation cancelled");
|
|
614
|
+
process.exit(0);
|
|
615
|
+
}
|
|
616
|
+
const output = await clack.text({
|
|
617
|
+
message: "Output directory",
|
|
618
|
+
placeholder: "./generated",
|
|
619
|
+
defaultValue: options.output || "./generated"
|
|
620
|
+
});
|
|
621
|
+
if (clack.isCancel(output)) {
|
|
622
|
+
clack.cancel("Operation cancelled");
|
|
623
|
+
process.exit(0);
|
|
624
|
+
}
|
|
625
|
+
clack.log.step("Files to create:");
|
|
626
|
+
clack.log.message(` ${pc.dim("sqg.yaml")} Project configuration`);
|
|
627
|
+
clack.log.message(` ${pc.dim("queries.sql")} Example SQL queries`);
|
|
628
|
+
clack.log.message(` ${pc.dim(`${output}/`)} Output directory`);
|
|
629
|
+
const confirm = await clack.confirm({ message: "Create files?" });
|
|
630
|
+
if (clack.isCancel(confirm) || !confirm) {
|
|
631
|
+
clack.cancel("Operation cancelled");
|
|
632
|
+
process.exit(0);
|
|
633
|
+
}
|
|
634
|
+
await createProjectFiles({
|
|
635
|
+
generator,
|
|
636
|
+
output,
|
|
637
|
+
force: options.force,
|
|
638
|
+
projectName
|
|
639
|
+
});
|
|
640
|
+
clack.outro(`Done! Run: ${pc.bold("sqg sqg.yaml")}`);
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Create project files (shared between interactive and non-interactive modes)
|
|
644
|
+
*/
|
|
645
|
+
async function createProjectFiles(options) {
|
|
646
|
+
const { generator, output, force, projectName } = options;
|
|
591
647
|
if (!isValidGenerator(generator)) {
|
|
592
648
|
const similar = findSimilarGenerators(generator);
|
|
593
649
|
throw new InvalidGeneratorError(generator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
|
|
@@ -595,32 +651,40 @@ async function initProject(options) {
|
|
|
595
651
|
const engine = parseGenerator(generator).engine;
|
|
596
652
|
const configPath = "sqg.yaml";
|
|
597
653
|
const sqlPath = "queries.sql";
|
|
598
|
-
if (!
|
|
654
|
+
if (!force) {
|
|
599
655
|
if (existsSync(configPath)) throw new SqgError(`File already exists: ${configPath}`, "VALIDATION_ERROR", "Use --force to overwrite existing files");
|
|
600
656
|
if (existsSync(sqlPath)) throw new SqgError(`File already exists: ${sqlPath}`, "VALIDATION_ERROR", "Use --force to overwrite existing files");
|
|
601
657
|
}
|
|
602
|
-
if (!existsSync(output)) {
|
|
603
|
-
|
|
604
|
-
consola.success(`Created output directory: ${output}`);
|
|
605
|
-
}
|
|
606
|
-
writeFileSync(configPath, getConfigYaml(generator, output));
|
|
607
|
-
consola.success(`Created ${configPath}`);
|
|
658
|
+
if (!existsSync(output)) mkdirSync(output, { recursive: true });
|
|
659
|
+
writeFileSync(configPath, getConfigYaml(generator, output, projectName));
|
|
608
660
|
writeFileSync(sqlPath, getExampleSql(engine));
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
SQG project
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Initialize a new SQG project
|
|
664
|
+
*/
|
|
665
|
+
async function initProject(options) {
|
|
666
|
+
if (!options.generator && process.stdin.isTTY) {
|
|
667
|
+
await runInteractiveInit(options);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const generator = options.generator || "typescript/sqlite";
|
|
671
|
+
const output = options.output || "./generated";
|
|
672
|
+
await createProjectFiles({
|
|
673
|
+
generator,
|
|
674
|
+
output,
|
|
675
|
+
force: options.force,
|
|
676
|
+
projectName: "my-project"
|
|
677
|
+
});
|
|
678
|
+
const generatorInfo = parseGenerator(generator);
|
|
679
|
+
clack.intro(pc.bold("SQG project initialized!"));
|
|
680
|
+
clack.log.info(`Generator: ${generator}`);
|
|
681
|
+
clack.log.info(`Engine: ${generatorInfo.engine}`);
|
|
682
|
+
clack.log.info(`Output: ${output}`);
|
|
683
|
+
clack.log.step("Next steps:");
|
|
684
|
+
clack.log.message(" 1. Edit queries.sql to add your SQL queries");
|
|
685
|
+
clack.log.message(" 2. Run: sqg sqg.yaml");
|
|
686
|
+
clack.log.message(` 3. Import the generated code from ${output}`);
|
|
687
|
+
clack.outro("Documentation: https://sqg.dev");
|
|
624
688
|
}
|
|
625
689
|
|
|
626
690
|
//#endregion
|
|
@@ -672,8 +736,9 @@ var MapType = class {
|
|
|
672
736
|
}
|
|
673
737
|
};
|
|
674
738
|
var EnumType = class {
|
|
675
|
-
constructor(values) {
|
|
739
|
+
constructor(values, name) {
|
|
676
740
|
this.values = values;
|
|
741
|
+
this.name = name;
|
|
677
742
|
}
|
|
678
743
|
toString() {
|
|
679
744
|
return `ENUM(${this.values.map((v) => `'${v}'`).join(", ")})`;
|
|
@@ -746,7 +811,7 @@ var TableInfo = class {
|
|
|
746
811
|
};
|
|
747
812
|
function parseSQLQueries(filePath, extraVariables) {
|
|
748
813
|
const content = readFileSync(filePath, "utf-8");
|
|
749
|
-
consola.
|
|
814
|
+
consola.debug(`Parsing SQL file: ${filePath}`);
|
|
750
815
|
consola.debug(`File start: ${content.slice(0, 200)}`);
|
|
751
816
|
const queries = [];
|
|
752
817
|
const tables = [];
|
|
@@ -935,9 +1000,9 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
935
1000
|
consola.debug(`Added query: ${name} (${queryType})`);
|
|
936
1001
|
}
|
|
937
1002
|
while (cursor.next());
|
|
938
|
-
consola.
|
|
939
|
-
consola.
|
|
940
|
-
if (tables.length > 0) consola.
|
|
1003
|
+
consola.debug(`Total queries parsed: ${queries.length}, tables: ${tables.length}`);
|
|
1004
|
+
consola.debug(`Query names: ${queries.map((q) => q.id).join(", ")}`);
|
|
1005
|
+
if (tables.length > 0) consola.debug(`Table names: ${tables.map((t) => t.id).join(", ")}`);
|
|
941
1006
|
return {
|
|
942
1007
|
queries,
|
|
943
1008
|
tables
|
|
@@ -946,7 +1011,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
946
1011
|
|
|
947
1012
|
//#endregion
|
|
948
1013
|
//#region src/db/types.ts
|
|
949
|
-
async function initializeDatabase(queries, execQueries) {
|
|
1014
|
+
async function initializeDatabase(queries, execQueries, reporter) {
|
|
950
1015
|
const migrationQueries = queries.filter((q) => q.isMigrate);
|
|
951
1016
|
sortBy(migrationQueries, [(q) => Number(q.id.split("_")[1])]);
|
|
952
1017
|
for (const query of migrationQueries) try {
|
|
@@ -963,7 +1028,7 @@ async function initializeDatabase(queries, execQueries) {
|
|
|
963
1028
|
throw error;
|
|
964
1029
|
}
|
|
965
1030
|
if (migrationQueries.length + testdataQueries.length === 0) consola.warn("No migration or testdata queries found");
|
|
966
|
-
|
|
1031
|
+
reporter?.onDatabaseInitialized?.();
|
|
967
1032
|
}
|
|
968
1033
|
|
|
969
1034
|
//#endregion
|
|
@@ -990,7 +1055,7 @@ function convertType(type) {
|
|
|
990
1055
|
const duckdb = new class {
|
|
991
1056
|
db;
|
|
992
1057
|
connection;
|
|
993
|
-
async initializeDatabase(queries) {
|
|
1058
|
+
async initializeDatabase(queries, reporter) {
|
|
994
1059
|
this.db = await DuckDBInstance.create(":memory:");
|
|
995
1060
|
this.connection = await this.db.connect();
|
|
996
1061
|
await initializeDatabase(queries, async (query) => {
|
|
@@ -999,18 +1064,17 @@ const duckdb = new class {
|
|
|
999
1064
|
} catch (e) {
|
|
1000
1065
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
1001
1066
|
}
|
|
1002
|
-
});
|
|
1067
|
+
}, reporter);
|
|
1003
1068
|
}
|
|
1004
|
-
async executeQueries(queries) {
|
|
1069
|
+
async executeQueries(queries, reporter) {
|
|
1005
1070
|
const connection = this.connection;
|
|
1006
1071
|
if (!connection) throw new DatabaseError("DuckDB connection not initialized", "duckdb", "This is an internal error. Check that migrations completed successfully.");
|
|
1007
1072
|
try {
|
|
1008
1073
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1009
1074
|
for (const query of executableQueries) {
|
|
1010
|
-
|
|
1075
|
+
reporter?.onQueryStart?.(query.id);
|
|
1011
1076
|
await this.executeQuery(connection, query);
|
|
1012
|
-
|
|
1013
|
-
consola.success(`Query ${query.id} executed successfully`);
|
|
1077
|
+
reporter?.onQueryComplete?.(query.id);
|
|
1014
1078
|
}
|
|
1015
1079
|
} catch (error) {
|
|
1016
1080
|
consola.error("Error executing queries:", error.message);
|
|
@@ -1058,11 +1122,11 @@ const duckdb = new class {
|
|
|
1058
1122
|
throw error;
|
|
1059
1123
|
}
|
|
1060
1124
|
}
|
|
1061
|
-
async introspectTables(tables) {
|
|
1125
|
+
async introspectTables(tables, reporter) {
|
|
1062
1126
|
const connection = this.connection;
|
|
1063
1127
|
if (!connection) throw new DatabaseError("DuckDB connection not initialized", "duckdb", "This is an internal error. Check that migrations completed successfully.");
|
|
1064
1128
|
for (const table of tables) {
|
|
1065
|
-
|
|
1129
|
+
reporter?.onTableStart?.(table.tableName);
|
|
1066
1130
|
try {
|
|
1067
1131
|
const descRows = (await connection.runAndReadAll(`DESCRIBE ${table.tableName}`)).getRows();
|
|
1068
1132
|
const nullabilityMap = /* @__PURE__ */ new Map();
|
|
@@ -1076,7 +1140,7 @@ const duckdb = new class {
|
|
|
1076
1140
|
nullable: nullabilityMap.get(name) ?? true
|
|
1077
1141
|
}));
|
|
1078
1142
|
consola.debug(`Table ${table.tableName} columns:`, table.columns);
|
|
1079
|
-
|
|
1143
|
+
reporter?.onTableComplete?.(table.tableName, table.columns.length);
|
|
1080
1144
|
} catch (error) {
|
|
1081
1145
|
consola.error(`Failed to introspect table '${table.tableName}':`, error);
|
|
1082
1146
|
throw error;
|
|
@@ -1174,7 +1238,6 @@ var TempDbMode = class {
|
|
|
1174
1238
|
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}"`);
|
|
1175
1239
|
await this.dbInitial.end();
|
|
1176
1240
|
if (this.container) {
|
|
1177
|
-
consola.info("Stopping PostgreSQL container...");
|
|
1178
1241
|
await this.container.stop();
|
|
1179
1242
|
this.container = null;
|
|
1180
1243
|
}
|
|
@@ -1184,11 +1247,12 @@ const postgres = new class {
|
|
|
1184
1247
|
db;
|
|
1185
1248
|
mode;
|
|
1186
1249
|
dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1187
|
-
|
|
1188
|
-
|
|
1250
|
+
enumTypeCache = /* @__PURE__ */ new Map();
|
|
1251
|
+
async startContainer(reporter) {
|
|
1252
|
+
reporter?.onContainerStarting?.();
|
|
1189
1253
|
const container = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
|
|
1190
1254
|
const connectionUri = container.getConnectionUri();
|
|
1191
|
-
|
|
1255
|
+
reporter?.onContainerStarted?.(connectionUri);
|
|
1192
1256
|
return {
|
|
1193
1257
|
connectionUri,
|
|
1194
1258
|
container
|
|
@@ -1196,32 +1260,51 @@ const postgres = new class {
|
|
|
1196
1260
|
}
|
|
1197
1261
|
async loadTypeCache(db) {
|
|
1198
1262
|
const result = await db.query(`
|
|
1199
|
-
SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype
|
|
1263
|
+
SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype,
|
|
1264
|
+
e.enumlabel
|
|
1200
1265
|
FROM pg_type t
|
|
1201
1266
|
LEFT JOIN pg_type et ON t.typelem = et.oid
|
|
1267
|
+
LEFT JOIN pg_enum e ON t.oid = e.enumtypid AND t.typtype = 'e'
|
|
1202
1268
|
WHERE t.typtype IN ('b', 'e', 'r', 'c') -- base, enum, range, composite
|
|
1203
1269
|
OR t.typelem != 0 -- array types
|
|
1270
|
+
ORDER BY t.oid, e.enumsortorder
|
|
1204
1271
|
`);
|
|
1205
1272
|
this.dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1273
|
+
this.enumTypeCache = /* @__PURE__ */ new Map();
|
|
1274
|
+
const enumsByOid = /* @__PURE__ */ new Map();
|
|
1206
1275
|
for (const row of result.rows) {
|
|
1207
1276
|
const oid = row.oid;
|
|
1208
1277
|
let typeName = row.typname;
|
|
1209
1278
|
if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
|
|
1210
1279
|
else typeName = typeName.toUpperCase();
|
|
1211
1280
|
this.dynamicTypeCache.set(oid, typeName);
|
|
1281
|
+
if (row.enumlabel) {
|
|
1282
|
+
if (!enumsByOid.has(oid)) enumsByOid.set(oid, {
|
|
1283
|
+
name: row.typname,
|
|
1284
|
+
values: []
|
|
1285
|
+
});
|
|
1286
|
+
enumsByOid.get(oid).values.push(row.enumlabel);
|
|
1287
|
+
}
|
|
1212
1288
|
}
|
|
1289
|
+
for (const [oid, { name, values }] of enumsByOid) this.enumTypeCache.set(oid, new EnumType(values, name));
|
|
1213
1290
|
}
|
|
1214
1291
|
getTypeName(dataTypeID) {
|
|
1215
1292
|
const cached = this.dynamicTypeCache.get(dataTypeID);
|
|
1216
1293
|
if (cached) return cached;
|
|
1217
1294
|
return typeIdToName.get(dataTypeID) || `type_${dataTypeID}`;
|
|
1218
1295
|
}
|
|
1219
|
-
|
|
1296
|
+
getColumnType(dataTypeID) {
|
|
1297
|
+
const enumType = this.enumTypeCache.get(dataTypeID);
|
|
1298
|
+
if (enumType) return enumType;
|
|
1299
|
+
return this.getTypeName(dataTypeID);
|
|
1300
|
+
}
|
|
1301
|
+
async initializeDatabase(queries, reporter) {
|
|
1220
1302
|
const externalUrl = process.env.SQG_POSTGRES_URL;
|
|
1221
1303
|
this.dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1304
|
+
this.enumTypeCache = /* @__PURE__ */ new Map();
|
|
1222
1305
|
if (externalUrl) this.mode = new ExternalDbMode(externalUrl);
|
|
1223
1306
|
else {
|
|
1224
|
-
const { connectionUri, container } = await this.startContainer();
|
|
1307
|
+
const { connectionUri, container } = await this.startContainer(reporter);
|
|
1225
1308
|
this.mode = new TempDbMode(connectionUri, container);
|
|
1226
1309
|
}
|
|
1227
1310
|
this.db = await this.mode.connect();
|
|
@@ -1231,18 +1314,18 @@ const postgres = new class {
|
|
|
1231
1314
|
} catch (e) {
|
|
1232
1315
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
1233
1316
|
}
|
|
1234
|
-
});
|
|
1317
|
+
}, reporter);
|
|
1235
1318
|
await this.loadTypeCache(this.db);
|
|
1236
1319
|
}
|
|
1237
|
-
async executeQueries(queries) {
|
|
1320
|
+
async executeQueries(queries, reporter) {
|
|
1238
1321
|
const db = this.db;
|
|
1239
1322
|
if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
|
|
1240
1323
|
try {
|
|
1241
1324
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1242
1325
|
for (const query of executableQueries) {
|
|
1243
|
-
|
|
1326
|
+
reporter?.onQueryStart?.(query.id);
|
|
1244
1327
|
await this.executeQuery(db, query);
|
|
1245
|
-
|
|
1328
|
+
reporter?.onQueryComplete?.(query.id);
|
|
1246
1329
|
}
|
|
1247
1330
|
} catch (error) {
|
|
1248
1331
|
consola.error("Error executing queries:", error.message);
|
|
@@ -1267,8 +1350,8 @@ const postgres = new class {
|
|
|
1267
1350
|
const paramTypes = /* @__PURE__ */ new Map();
|
|
1268
1351
|
for (let i = 0; i < statement.parameters.length; i++) {
|
|
1269
1352
|
const oid = Number(paramTypeResult.rows[i].oid);
|
|
1270
|
-
const
|
|
1271
|
-
paramTypes.set(statement.parameters[i].name,
|
|
1353
|
+
const colType = this.getColumnType(oid);
|
|
1354
|
+
paramTypes.set(statement.parameters[i].name, colType);
|
|
1272
1355
|
}
|
|
1273
1356
|
query.parameterTypes = paramTypes;
|
|
1274
1357
|
consola.debug("Parameter types:", Object.fromEntries(paramTypes));
|
|
@@ -1280,10 +1363,10 @@ const postgres = new class {
|
|
|
1280
1363
|
if (query.isQuery) {
|
|
1281
1364
|
const columnNames = result.fields.map((field) => field.name);
|
|
1282
1365
|
const columnTypes = result.fields.map((field) => {
|
|
1283
|
-
return this.
|
|
1366
|
+
return this.getColumnType(field.dataTypeID);
|
|
1284
1367
|
});
|
|
1285
1368
|
consola.debug("Columns:", columnNames);
|
|
1286
|
-
consola.debug("Types:", columnTypes);
|
|
1369
|
+
consola.debug("Types:", columnTypes.map((t) => t.toString()));
|
|
1287
1370
|
query.columns = columnNames.map((name, index) => ({
|
|
1288
1371
|
name,
|
|
1289
1372
|
type: columnTypes[index],
|
|
@@ -1300,11 +1383,11 @@ const postgres = new class {
|
|
|
1300
1383
|
throw error;
|
|
1301
1384
|
}
|
|
1302
1385
|
}
|
|
1303
|
-
async introspectTables(tables) {
|
|
1386
|
+
async introspectTables(tables, reporter) {
|
|
1304
1387
|
const db = this.db;
|
|
1305
1388
|
if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
|
|
1306
1389
|
for (const table of tables) {
|
|
1307
|
-
|
|
1390
|
+
reporter?.onTableStart?.(table.tableName);
|
|
1308
1391
|
try {
|
|
1309
1392
|
table.columns = (await db.query(`SELECT column_name, data_type, is_nullable
|
|
1310
1393
|
FROM information_schema.columns
|
|
@@ -1314,7 +1397,7 @@ const postgres = new class {
|
|
|
1314
1397
|
type: row.data_type.toUpperCase(),
|
|
1315
1398
|
nullable: row.is_nullable === "YES"
|
|
1316
1399
|
}));
|
|
1317
|
-
|
|
1400
|
+
reporter?.onTableComplete?.(table.tableName, table.columns.length);
|
|
1318
1401
|
} catch (error) {
|
|
1319
1402
|
consola.error(`Failed to introspect table '${table.tableName}':`, error);
|
|
1320
1403
|
throw error;
|
|
@@ -1324,6 +1407,7 @@ const postgres = new class {
|
|
|
1324
1407
|
async close() {
|
|
1325
1408
|
await this.mode.close(this.db);
|
|
1326
1409
|
this.dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1410
|
+
this.enumTypeCache = /* @__PURE__ */ new Map();
|
|
1327
1411
|
}
|
|
1328
1412
|
}();
|
|
1329
1413
|
|
|
@@ -1331,7 +1415,7 @@ const postgres = new class {
|
|
|
1331
1415
|
//#region src/db/sqlite.ts
|
|
1332
1416
|
const sqlite = new class {
|
|
1333
1417
|
db;
|
|
1334
|
-
async initializeDatabase(queries) {
|
|
1418
|
+
async initializeDatabase(queries, reporter) {
|
|
1335
1419
|
const db = new BetterSqlite3(":memory:");
|
|
1336
1420
|
await initializeDatabase(queries, (query) => {
|
|
1337
1421
|
try {
|
|
@@ -1340,37 +1424,36 @@ const sqlite = new class {
|
|
|
1340
1424
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
1341
1425
|
}
|
|
1342
1426
|
return Promise.resolve();
|
|
1343
|
-
});
|
|
1427
|
+
}, reporter);
|
|
1344
1428
|
this.db = db;
|
|
1345
1429
|
}
|
|
1346
|
-
executeQueries(queries) {
|
|
1430
|
+
executeQueries(queries, reporter) {
|
|
1347
1431
|
const db = this.db;
|
|
1348
1432
|
if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
|
|
1349
1433
|
try {
|
|
1350
1434
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1351
1435
|
for (const query of executableQueries) {
|
|
1352
|
-
|
|
1436
|
+
reporter?.onQueryStart?.(query.id);
|
|
1353
1437
|
this.executeQuery(db, query);
|
|
1354
|
-
|
|
1355
|
-
consola.success(`Query ${query.id} executed successfully`);
|
|
1438
|
+
reporter?.onQueryComplete?.(query.id);
|
|
1356
1439
|
}
|
|
1357
1440
|
} catch (error) {
|
|
1358
1441
|
consola.error("Error executing queries:", error.message);
|
|
1359
1442
|
throw error;
|
|
1360
1443
|
}
|
|
1361
1444
|
}
|
|
1362
|
-
introspectTables(tables) {
|
|
1445
|
+
introspectTables(tables, reporter) {
|
|
1363
1446
|
const db = this.db;
|
|
1364
1447
|
if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
|
|
1365
1448
|
for (const table of tables) {
|
|
1366
|
-
|
|
1449
|
+
reporter?.onTableStart?.(table.tableName);
|
|
1367
1450
|
const info = this.getTableInfo(db, table.tableName);
|
|
1368
1451
|
table.columns = Array.from(info.values()).map((col) => ({
|
|
1369
1452
|
name: col.name,
|
|
1370
1453
|
type: col.type || "TEXT",
|
|
1371
1454
|
nullable: col.notnull === 0 && col.pk === 0
|
|
1372
1455
|
}));
|
|
1373
|
-
|
|
1456
|
+
reporter?.onTableComplete?.(table.tableName, table.columns.length);
|
|
1374
1457
|
}
|
|
1375
1458
|
}
|
|
1376
1459
|
close() {
|
|
@@ -1585,6 +1668,10 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1585
1668
|
"false",
|
|
1586
1669
|
"null"
|
|
1587
1670
|
]);
|
|
1671
|
+
getTypeName(column, path = "") {
|
|
1672
|
+
if (column.type instanceof EnumType && column.type.name) return path + pascalCase(column.type.name);
|
|
1673
|
+
return super.getTypeName(column, path);
|
|
1674
|
+
}
|
|
1588
1675
|
mapPrimitiveType(type, _nullable) {
|
|
1589
1676
|
const upperType = type.toString().toUpperCase();
|
|
1590
1677
|
const mappedType = this.typeMap[upperType];
|
|
@@ -1636,6 +1723,7 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1636
1723
|
return name;
|
|
1637
1724
|
}
|
|
1638
1725
|
parseValue(column, value, path) {
|
|
1726
|
+
if (column.type instanceof EnumType && column.type.name) return `${pascalCase(column.type.name)}.fromValue((String)${value})`;
|
|
1639
1727
|
if (column.type instanceof ListType) {
|
|
1640
1728
|
const elementType = this.getTypeName({
|
|
1641
1729
|
name: column.name,
|
|
@@ -1861,13 +1949,13 @@ var JavaGenerator = class extends BaseGenerator {
|
|
|
1861
1949
|
return engine === "duckdb";
|
|
1862
1950
|
}
|
|
1863
1951
|
getFunctionName(id) {
|
|
1864
|
-
return camelCase
|
|
1952
|
+
return camelCase(id);
|
|
1865
1953
|
}
|
|
1866
1954
|
getFilename(sqlFileName) {
|
|
1867
|
-
return `${pascalCase
|
|
1955
|
+
return `${pascalCase(sqlFileName)}.java`;
|
|
1868
1956
|
}
|
|
1869
1957
|
getClassName(name) {
|
|
1870
|
-
return pascalCase
|
|
1958
|
+
return pascalCase(name);
|
|
1871
1959
|
}
|
|
1872
1960
|
partsToString(parts) {
|
|
1873
1961
|
const stringParts = [];
|
|
@@ -1912,6 +2000,56 @@ var JavaGenerator = class extends BaseGenerator {
|
|
|
1912
2000
|
Handlebars.registerHelper("appenderType", (column) => {
|
|
1913
2001
|
return this.mapType(column);
|
|
1914
2002
|
});
|
|
2003
|
+
Handlebars.registerHelper("declareEnums", (queryHelpers) => {
|
|
2004
|
+
const enumTypes = /* @__PURE__ */ new Map();
|
|
2005
|
+
for (const qh of queryHelpers) {
|
|
2006
|
+
for (const col of qh.query.columns) if (col.type instanceof EnumType && col.type.name) enumTypes.set(col.type.name, col.type);
|
|
2007
|
+
if (qh.query.parameterTypes) {
|
|
2008
|
+
for (const colType of qh.query.parameterTypes.values()) if (colType instanceof EnumType && colType.name) enumTypes.set(colType.name, colType);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (enumTypes.size === 0) return "";
|
|
2012
|
+
const parts = [];
|
|
2013
|
+
for (const [, enumType] of enumTypes) {
|
|
2014
|
+
const enumName = pascalCase(enumType.name);
|
|
2015
|
+
const sanitized = enumType.values.map((v) => {
|
|
2016
|
+
let ident = v.toUpperCase().replace(/[^A-Za-z0-9_]/g, "_");
|
|
2017
|
+
if (ident.length > 0 && /^[0-9]/.test(ident)) ident = `_${ident}`;
|
|
2018
|
+
if (ident.length === 0) ident = "_EMPTY";
|
|
2019
|
+
return ident;
|
|
2020
|
+
});
|
|
2021
|
+
const usedIdents = /* @__PURE__ */ new Set();
|
|
2022
|
+
const finalIdents = sanitized.map((base) => {
|
|
2023
|
+
if (!usedIdents.has(base)) {
|
|
2024
|
+
usedIdents.add(base);
|
|
2025
|
+
return base;
|
|
2026
|
+
}
|
|
2027
|
+
let counter = 2;
|
|
2028
|
+
let candidate = `${base}_${counter}`;
|
|
2029
|
+
while (usedIdents.has(candidate)) {
|
|
2030
|
+
counter++;
|
|
2031
|
+
candidate = `${base}_${counter}`;
|
|
2032
|
+
}
|
|
2033
|
+
usedIdents.add(candidate);
|
|
2034
|
+
return candidate;
|
|
2035
|
+
});
|
|
2036
|
+
const entries = enumType.values.map((v, i) => `${finalIdents[i]}("${v}")`);
|
|
2037
|
+
parts.push(`public enum ${enumName} {
|
|
2038
|
+
${entries.join(", ")};
|
|
2039
|
+
private final String value;
|
|
2040
|
+
private static final java.util.Map<String, ${enumName}> BY_VALUE =
|
|
2041
|
+
java.util.Map.ofEntries(java.util.Arrays.stream(values()).map(v -> java.util.Map.entry(v.value, v)).toArray(java.util.Map.Entry[]::new));
|
|
2042
|
+
${enumName}(String value) { this.value = value; }
|
|
2043
|
+
public String getValue() { return value; }
|
|
2044
|
+
public static ${enumName} fromValue(String value) {
|
|
2045
|
+
${enumName} result = BY_VALUE.get(value);
|
|
2046
|
+
if (result == null) throw new IllegalArgumentException("Unknown value: " + value);
|
|
2047
|
+
return result;
|
|
2048
|
+
}
|
|
2049
|
+
}`);
|
|
2050
|
+
}
|
|
2051
|
+
return new Handlebars.SafeString(parts.join("\n\n "));
|
|
2052
|
+
});
|
|
1915
2053
|
Handlebars.registerHelper("readColumns", (queryHelper) => {
|
|
1916
2054
|
const query = queryHelper.query;
|
|
1917
2055
|
if (queryHelper.isPluck) return this.readColumn({
|
|
@@ -2054,7 +2192,7 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
2054
2192
|
super(template, new TypeScriptTypeMapper());
|
|
2055
2193
|
}
|
|
2056
2194
|
getFunctionName(id) {
|
|
2057
|
-
return camelCase
|
|
2195
|
+
return camelCase(id);
|
|
2058
2196
|
}
|
|
2059
2197
|
isCompatibleWith(_engine) {
|
|
2060
2198
|
return true;
|
|
@@ -2063,7 +2201,7 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
2063
2201
|
return `${sqlFileName}.ts`;
|
|
2064
2202
|
}
|
|
2065
2203
|
getClassName(name) {
|
|
2066
|
-
return pascalCase
|
|
2204
|
+
return pascalCase(name);
|
|
2067
2205
|
}
|
|
2068
2206
|
async beforeGenerate(_projectDir, _gen, _queries, _tables) {
|
|
2069
2207
|
Handlebars.registerHelper("quote", (value) => this.quote(value));
|
|
@@ -2405,7 +2543,12 @@ var SqlQueryHelper = class {
|
|
|
2405
2543
|
const rawType = this.query.parameterTypes?.get(param.name);
|
|
2406
2544
|
let isArray = false;
|
|
2407
2545
|
let arrayBaseType = null;
|
|
2408
|
-
|
|
2546
|
+
let isEnum = false;
|
|
2547
|
+
let enumClassName = null;
|
|
2548
|
+
if (rawType instanceof EnumType && rawType.name) {
|
|
2549
|
+
isEnum = true;
|
|
2550
|
+
enumClassName = pascalCase(rawType.name);
|
|
2551
|
+
} else if (rawType instanceof ListType) {
|
|
2409
2552
|
isArray = true;
|
|
2410
2553
|
arrayBaseType = rawType.baseType.toString();
|
|
2411
2554
|
} else if (typeof rawType === "string" && rawType.startsWith("_")) {
|
|
@@ -2416,7 +2559,9 @@ var SqlQueryHelper = class {
|
|
|
2416
2559
|
name: param.name,
|
|
2417
2560
|
type: vars.get(param.name),
|
|
2418
2561
|
isArray,
|
|
2419
|
-
arrayBaseType
|
|
2562
|
+
arrayBaseType,
|
|
2563
|
+
isEnum,
|
|
2564
|
+
enumClassName
|
|
2420
2565
|
};
|
|
2421
2566
|
});
|
|
2422
2567
|
}
|
|
@@ -2538,7 +2683,7 @@ function createExtraVariables(sources, suppressLogging = false) {
|
|
|
2538
2683
|
const path = source.path;
|
|
2539
2684
|
const resolvedPath = path.replace("$HOME", homedir());
|
|
2540
2685
|
const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
|
|
2541
|
-
if (!suppressLogging) consola.
|
|
2686
|
+
if (!suppressLogging) consola.debug("Extra variable:", varName, resolvedPath);
|
|
2542
2687
|
return new ExtraVariable(varName, `'${resolvedPath}'`);
|
|
2543
2688
|
});
|
|
2544
2689
|
}
|
|
@@ -2648,7 +2793,6 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
|
|
|
2648
2793
|
}
|
|
2649
2794
|
const outputPath = getOutputPath(projectDir, name, gen, generator);
|
|
2650
2795
|
writeFileSync(outputPath, sourceFile);
|
|
2651
|
-
consola.success(`Generated ${outputPath}`);
|
|
2652
2796
|
await generator.afterGenerate(outputPath);
|
|
2653
2797
|
return outputPath;
|
|
2654
2798
|
}
|
|
@@ -2722,12 +2866,24 @@ async function validateProject(projectPath) {
|
|
|
2722
2866
|
}
|
|
2723
2867
|
return await validateProjectFromConfig(project, projectDir);
|
|
2724
2868
|
}
|
|
2869
|
+
/** Count enum types across queries */
|
|
2870
|
+
function countEnums(queries) {
|
|
2871
|
+
const enumNames = /* @__PURE__ */ new Set();
|
|
2872
|
+
for (const query of queries) {
|
|
2873
|
+
if (!query.isQuery) continue;
|
|
2874
|
+
for (const col of query.columns) if (col.type instanceof EnumType && col.type.name) enumNames.add(col.type.name);
|
|
2875
|
+
}
|
|
2876
|
+
return enumNames.size;
|
|
2877
|
+
}
|
|
2725
2878
|
/**
|
|
2726
2879
|
* Process a project configuration and generate code from a Project object
|
|
2727
2880
|
*/
|
|
2728
|
-
async function processProjectFromConfig(project, projectDir, writeToStdout = false) {
|
|
2881
|
+
async function processProjectFromConfig(project, projectDir, writeToStdout = false, ui) {
|
|
2729
2882
|
const originalLevel = consola.level;
|
|
2730
2883
|
if (writeToStdout) consola.level = LogLevels.silent;
|
|
2884
|
+
const totalStart = performance.now();
|
|
2885
|
+
const reporter = ui?.createReporter();
|
|
2886
|
+
const results = [];
|
|
2731
2887
|
try {
|
|
2732
2888
|
const extraVariables = createExtraVariables(project.sources ?? [], writeToStdout);
|
|
2733
2889
|
const files = [];
|
|
@@ -2752,31 +2908,52 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2752
2908
|
throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
|
|
2753
2909
|
}
|
|
2754
2910
|
for (const [engine, gens] of gensByEngine) {
|
|
2911
|
+
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
2912
|
+
const reporterAny = reporter;
|
|
2913
|
+
if (reporterAny?.setQueryTotal) reporterAny.setQueryTotal(executableQueries.length);
|
|
2755
2914
|
try {
|
|
2756
2915
|
const dbEngine = getDatabaseEngine(engine);
|
|
2757
|
-
|
|
2758
|
-
await dbEngine.
|
|
2759
|
-
|
|
2916
|
+
ui?.startPhase(`Initializing ${engine} database...`);
|
|
2917
|
+
await dbEngine.initializeDatabase(queries, reporter);
|
|
2918
|
+
ui?.startPhase(`Introspecting ${executableQueries.length} queries...`);
|
|
2919
|
+
await dbEngine.executeQueries(queries, reporter);
|
|
2920
|
+
if (tables.length > 0) await dbEngine.introspectTables(tables, reporter);
|
|
2921
|
+
ui?.succeedPhase(`Introspected ${executableQueries.length} queries`);
|
|
2760
2922
|
validateQueries(queries);
|
|
2761
2923
|
await dbEngine.close();
|
|
2762
2924
|
} catch (e) {
|
|
2925
|
+
ui?.failPhase(`Failed to process ${sqlFile}`);
|
|
2763
2926
|
if (e instanceof SqgError) throw e;
|
|
2764
2927
|
throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${engine}'`, {
|
|
2765
2928
|
file: sqlFile,
|
|
2766
2929
|
engine
|
|
2767
2930
|
});
|
|
2768
2931
|
}
|
|
2932
|
+
ui?.startPhase("Generating code...");
|
|
2933
|
+
const genStart = performance.now();
|
|
2769
2934
|
for (const gen of gens) {
|
|
2770
2935
|
const generator = getGenerator(gen.generator);
|
|
2771
2936
|
const outputPath = await writeGeneratedFile(projectDir, {
|
|
2772
2937
|
...gen,
|
|
2773
2938
|
projectName: project.name
|
|
2774
2939
|
}, generator, sqlFile, queries, tables, engine, writeToStdout);
|
|
2775
|
-
if (outputPath !== null)
|
|
2940
|
+
if (outputPath !== null) {
|
|
2941
|
+
files.push(outputPath);
|
|
2942
|
+
results.push({
|
|
2943
|
+
outputPath,
|
|
2944
|
+
queryCount: executableQueries.length,
|
|
2945
|
+
enumCount: countEnums(queries),
|
|
2946
|
+
sqlFile,
|
|
2947
|
+
generator: gen.generator,
|
|
2948
|
+
elapsedMs: performance.now() - genStart
|
|
2949
|
+
});
|
|
2950
|
+
}
|
|
2776
2951
|
}
|
|
2952
|
+
ui?.succeedPhase(`Generated ${gens.length} ${gens.length === 1 ? "file" : "files"} from ${sqlFile}`);
|
|
2777
2953
|
}
|
|
2778
2954
|
}
|
|
2779
2955
|
}
|
|
2956
|
+
ui?.summary(results, performance.now() - totalStart);
|
|
2780
2957
|
return files;
|
|
2781
2958
|
} finally {
|
|
2782
2959
|
if (writeToStdout) consola.level = originalLevel;
|
|
@@ -2785,16 +2962,16 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2785
2962
|
/**
|
|
2786
2963
|
* Process a project configuration and generate code from a YAML file
|
|
2787
2964
|
*/
|
|
2788
|
-
async function processProject(projectPath) {
|
|
2965
|
+
async function processProject(projectPath, ui) {
|
|
2789
2966
|
const projectDir = resolve(dirname(projectPath));
|
|
2790
|
-
return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false);
|
|
2967
|
+
return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false, ui);
|
|
2791
2968
|
}
|
|
2792
2969
|
|
|
2793
2970
|
//#endregion
|
|
2794
2971
|
//#region src/mcp-server.ts
|
|
2795
2972
|
const server = new Server({
|
|
2796
2973
|
name: "sqg-mcp",
|
|
2797
|
-
version: process.env.npm_package_version ?? "0.
|
|
2974
|
+
version: process.env.npm_package_version ?? "0.13.0"
|
|
2798
2975
|
}, { capabilities: {
|
|
2799
2976
|
tools: {},
|
|
2800
2977
|
resources: {}
|
|
@@ -3121,15 +3298,154 @@ async function startMcpServer() {
|
|
|
3121
3298
|
console.error("SQG MCP server running on stdio");
|
|
3122
3299
|
}
|
|
3123
3300
|
|
|
3301
|
+
//#endregion
|
|
3302
|
+
//#region src/ui.ts
|
|
3303
|
+
/**
|
|
3304
|
+
* SQG UI Module - Centralized terminal output with spinners, timing, and beautiful errors
|
|
3305
|
+
*/
|
|
3306
|
+
var UI = class {
|
|
3307
|
+
spinner = null;
|
|
3308
|
+
silent;
|
|
3309
|
+
verbose;
|
|
3310
|
+
phaseStart = 0;
|
|
3311
|
+
version;
|
|
3312
|
+
constructor(options) {
|
|
3313
|
+
this.silent = options.format === "json" || options.isStdout === true;
|
|
3314
|
+
this.verbose = options.verbose === true;
|
|
3315
|
+
this.version = options.version || "";
|
|
3316
|
+
}
|
|
3317
|
+
/** Print colored header */
|
|
3318
|
+
header() {
|
|
3319
|
+
if (this.silent) return;
|
|
3320
|
+
const logo = pc.bold(pc.blue("SQG"));
|
|
3321
|
+
const ver = this.version ? ` ${pc.dim(`v${this.version}`)}` : "";
|
|
3322
|
+
this.log(`\n ${logo}${ver}\n`);
|
|
3323
|
+
}
|
|
3324
|
+
/** Create a ProgressReporter for DB adapters */
|
|
3325
|
+
createReporter() {
|
|
3326
|
+
if (this.silent) return {};
|
|
3327
|
+
let queryCount = 0;
|
|
3328
|
+
let queryTotal = 0;
|
|
3329
|
+
return {
|
|
3330
|
+
onQueryStart: (id) => {
|
|
3331
|
+
queryCount++;
|
|
3332
|
+
if (this.verbose) this.log(` ${pc.dim(`Executing query: ${id}`)}`);
|
|
3333
|
+
else if (this.spinner) this.spinner.text = `Introspecting queries... (${queryCount}/${queryTotal || "?"})`;
|
|
3334
|
+
},
|
|
3335
|
+
onQueryComplete: (id) => {
|
|
3336
|
+
if (this.verbose) this.log(` ${pc.green("+")} ${pc.dim(id)}`);
|
|
3337
|
+
},
|
|
3338
|
+
onTableStart: (name) => {
|
|
3339
|
+
if (this.verbose) this.log(` ${pc.dim(`Introspecting table: ${name}`)}`);
|
|
3340
|
+
},
|
|
3341
|
+
onTableComplete: (name, columnCount) => {
|
|
3342
|
+
if (this.verbose) this.log(` ${pc.green("+")} ${pc.dim(`${name} (${columnCount} columns)`)}`);
|
|
3343
|
+
},
|
|
3344
|
+
onContainerStarting: () => {
|
|
3345
|
+
this.startPhase("Starting PostgreSQL container...");
|
|
3346
|
+
},
|
|
3347
|
+
onContainerStarted: (_uri) => {
|
|
3348
|
+
this.succeedPhase("PostgreSQL ready");
|
|
3349
|
+
},
|
|
3350
|
+
onDatabaseInitialized: () => {
|
|
3351
|
+
if (this.verbose) this.log(` ${pc.green("+")} ${pc.dim("Database initialized")}`);
|
|
3352
|
+
},
|
|
3353
|
+
setQueryTotal: (total) => {
|
|
3354
|
+
queryTotal = total;
|
|
3355
|
+
queryCount = 0;
|
|
3356
|
+
}
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
/** Start a phase with a spinner */
|
|
3360
|
+
startPhase(label) {
|
|
3361
|
+
if (this.silent) return;
|
|
3362
|
+
this.stopSpinner();
|
|
3363
|
+
this.phaseStart = performance.now();
|
|
3364
|
+
this.spinner = yoctoSpinner({ text: label }).start();
|
|
3365
|
+
}
|
|
3366
|
+
/** Complete a phase successfully */
|
|
3367
|
+
succeedPhase(label) {
|
|
3368
|
+
if (this.silent) return;
|
|
3369
|
+
const elapsed = this.phaseStart ? performance.now() - this.phaseStart : 0;
|
|
3370
|
+
const time = elapsed > 100 ? pc.dim(` (${formatMs(elapsed)})`) : "";
|
|
3371
|
+
if (this.spinner) {
|
|
3372
|
+
this.spinner.success(`${label}${time}`);
|
|
3373
|
+
this.spinner = null;
|
|
3374
|
+
} else this.log(`${pc.green("+")} ${label}${time}`);
|
|
3375
|
+
}
|
|
3376
|
+
/** Fail a phase */
|
|
3377
|
+
failPhase(label) {
|
|
3378
|
+
if (this.silent) return;
|
|
3379
|
+
if (this.spinner) {
|
|
3380
|
+
this.spinner.error(label);
|
|
3381
|
+
this.spinner = null;
|
|
3382
|
+
} else this.log(`${pc.red("x")} ${label}`);
|
|
3383
|
+
}
|
|
3384
|
+
/** Display generation summary */
|
|
3385
|
+
summary(results, totalMs) {
|
|
3386
|
+
if (this.silent || results.length === 0) return;
|
|
3387
|
+
this.log("");
|
|
3388
|
+
for (const r of results) {
|
|
3389
|
+
const parts = [];
|
|
3390
|
+
if (r.queryCount > 0) parts.push(`${r.queryCount} ${r.queryCount === 1 ? "query" : "queries"}`);
|
|
3391
|
+
if (r.enumCount > 0) parts.push(`${r.enumCount} ${r.enumCount === 1 ? "enum" : "enums"}`);
|
|
3392
|
+
this.log(` ${pc.dim("->")} ${dimPath(r.outputPath)} ${pc.dim(`(${parts.join(", ")})`)}`);
|
|
3393
|
+
}
|
|
3394
|
+
this.log("");
|
|
3395
|
+
this.log(` ${pc.green("done")} ${pc.dim(`in ${formatMs(totalMs)}`)}`);
|
|
3396
|
+
}
|
|
3397
|
+
/** Display a formatted error */
|
|
3398
|
+
error(err) {
|
|
3399
|
+
if (this.silent) return;
|
|
3400
|
+
this.stopSpinner();
|
|
3401
|
+
const isSqg = "code" in err && "suggestion" in err;
|
|
3402
|
+
const message = err.message;
|
|
3403
|
+
const suggestion = isSqg ? err.suggestion : void 0;
|
|
3404
|
+
const code = isSqg ? err.code : void 0;
|
|
3405
|
+
this.log("");
|
|
3406
|
+
this.log(` ${pc.red(pc.bold("ERROR"))} ${message}`);
|
|
3407
|
+
if (suggestion) {
|
|
3408
|
+
this.log("");
|
|
3409
|
+
this.log(` ${pc.dim("Suggestion:")} ${suggestion}`);
|
|
3410
|
+
}
|
|
3411
|
+
if (code && this.verbose) this.log(` ${pc.dim("Code:")} ${code}`);
|
|
3412
|
+
this.log("");
|
|
3413
|
+
}
|
|
3414
|
+
stopSpinner() {
|
|
3415
|
+
if (this.spinner) {
|
|
3416
|
+
this.spinner.stop();
|
|
3417
|
+
this.spinner = null;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
log(msg) {
|
|
3421
|
+
process.stderr.write(`${msg}\n`);
|
|
3422
|
+
}
|
|
3423
|
+
};
|
|
3424
|
+
/** Format path with dim directory and bright filename, relative to cwd when it's a subpath */
|
|
3425
|
+
function dimPath(fullPath) {
|
|
3426
|
+
const rel = relative(process.cwd(), fullPath);
|
|
3427
|
+
const display = rel && !rel.startsWith("..") ? rel : fullPath;
|
|
3428
|
+
const dir = dirname(display);
|
|
3429
|
+
const file = basename(display);
|
|
3430
|
+
if (dir === ".") return file;
|
|
3431
|
+
return `${pc.dim(`${dir}/`)}${file}`;
|
|
3432
|
+
}
|
|
3433
|
+
/** Format milliseconds as human-readable */
|
|
3434
|
+
function formatMs(ms) {
|
|
3435
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
3436
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3124
3439
|
//#endregion
|
|
3125
3440
|
//#region src/sqg.ts
|
|
3126
|
-
const version = process.env.npm_package_version ?? "0.
|
|
3441
|
+
const version = process.env.npm_package_version ?? "0.13.0";
|
|
3127
3442
|
updateNotifier({ pkg: {
|
|
3128
3443
|
name: "@sqg/sqg",
|
|
3129
3444
|
version
|
|
3130
3445
|
} }).notify({ message: "Update available {currentVersion} → {latestVersion}" });
|
|
3131
3446
|
const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
|
|
3132
|
-
consola.level = LogLevels.
|
|
3447
|
+
consola.level = LogLevels.warn;
|
|
3448
|
+
const BRANDING = `\n ${pc.bold(pc.blue("SQG"))} ${pc.dim(`v${version}`)}\n`;
|
|
3133
3449
|
const program = new Command().name("sqg").description(`${description}
|
|
3134
3450
|
|
|
3135
3451
|
Generate type-safe database access code from annotated SQL files.
|
|
@@ -3138,12 +3454,20 @@ Supported Generators:
|
|
|
3138
3454
|
${formatGeneratorsHelp()}`).version(version, "-v, --version", "output the version number").option("--verbose", "Enable debug logging (shows SQL execution details)").option("--format <format>", "Output format: text (default) or json", "text").option("--validate", "Validate configuration without generating code").option("--generator <generator>", `Code generation generator (${SHORT_GENERATOR_NAMES.join(", ")})`).option("--file <file>", "SQL file path (can be repeated)", (val, prev = []) => {
|
|
3139
3455
|
prev.push(val);
|
|
3140
3456
|
return prev;
|
|
3141
|
-
}).option("--output <path>", "Output file or directory path (optional, if omitted writes to stdout)").option("--name <name>", "Project name (optional, defaults to 'generated')").showHelpAfterError().showSuggestionAfterError();
|
|
3457
|
+
}).option("--output <path>", "Output file or directory path (optional, if omitted writes to stdout)").option("--name <name>", "Project name (optional, defaults to 'generated')").addHelpText("before", BRANDING).showHelpAfterError().showSuggestionAfterError();
|
|
3142
3458
|
program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omit to use CLI options").hook("preAction", (thisCommand) => {
|
|
3143
3459
|
const opts = thisCommand.opts();
|
|
3144
3460
|
if (opts.verbose) consola.level = LogLevels.debug;
|
|
3145
3461
|
if (opts.format === "json") consola.level = LogLevels.silent;
|
|
3146
3462
|
}).action(async (projectPath, options) => {
|
|
3463
|
+
const writeToStdout = !projectPath && !options.output;
|
|
3464
|
+
const ui = new UI({
|
|
3465
|
+
format: options.format,
|
|
3466
|
+
verbose: options.verbose,
|
|
3467
|
+
isStdout: writeToStdout,
|
|
3468
|
+
version
|
|
3469
|
+
});
|
|
3470
|
+
ui.header();
|
|
3147
3471
|
try {
|
|
3148
3472
|
if (!projectPath) {
|
|
3149
3473
|
if (!options.generator) throw new SqgError("Missing required option: --generator", "CONFIG_VALIDATION_ERROR", `Specify a code generation generator: ${SHORT_GENERATOR_NAMES.join(", ")}`);
|
|
@@ -3155,7 +3479,6 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3155
3479
|
name: options.name
|
|
3156
3480
|
});
|
|
3157
3481
|
const projectDir = process.cwd();
|
|
3158
|
-
const writeToStdout = !options.output;
|
|
3159
3482
|
if (options.validate) {
|
|
3160
3483
|
const result = await validateProjectFromConfig(project, projectDir);
|
|
3161
3484
|
if (options.format === "json") console.log(JSON.stringify(result, null, 2));
|
|
@@ -3173,7 +3496,7 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3173
3496
|
}
|
|
3174
3497
|
exit(result.valid ? 0 : 1);
|
|
3175
3498
|
}
|
|
3176
|
-
const files = await processProjectFromConfig(project, projectDir, writeToStdout);
|
|
3499
|
+
const files = await processProjectFromConfig(project, projectDir, writeToStdout, ui);
|
|
3177
3500
|
if (options.format === "json" && !writeToStdout) console.log(JSON.stringify({
|
|
3178
3501
|
status: "success",
|
|
3179
3502
|
generatedFiles: files
|
|
@@ -3196,7 +3519,7 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3196
3519
|
}
|
|
3197
3520
|
exit(result.valid ? 0 : 1);
|
|
3198
3521
|
}
|
|
3199
|
-
const files = await processProject(projectPath);
|
|
3522
|
+
const files = await processProject(projectPath, ui);
|
|
3200
3523
|
if (options.format === "json") console.log(JSON.stringify({
|
|
3201
3524
|
status: "success",
|
|
3202
3525
|
generatedFiles: files
|
|
@@ -3204,15 +3527,13 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3204
3527
|
}
|
|
3205
3528
|
} catch (err) {
|
|
3206
3529
|
if (options.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
|
|
3207
|
-
else if (err instanceof SqgError)
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
if (err.context && options.verbose) consola.debug("Context:", err.context);
|
|
3211
|
-
} else consola.error(err);
|
|
3530
|
+
else if (err instanceof SqgError) ui.error(err);
|
|
3531
|
+
else if (err instanceof Error) ui.error(err);
|
|
3532
|
+
else consola.error(err);
|
|
3212
3533
|
exit(1);
|
|
3213
3534
|
}
|
|
3214
3535
|
});
|
|
3215
|
-
program.command("init").description("Initialize a new SQG project with example configuration").option("-t, --generator <generator>", `Code generation generator (${SHORT_GENERATOR_NAMES.join(", ")})
|
|
3536
|
+
program.command("init").description("Initialize a new SQG project with example configuration").option("-t, --generator <generator>", `Code generation generator (${SHORT_GENERATOR_NAMES.join(", ")}). Omit for interactive mode`).option("-o, --output <dir>", "Output directory for generated files", "./generated").option("-f, --force", "Overwrite existing files").action(async (options) => {
|
|
3216
3537
|
const parentOpts = program.opts();
|
|
3217
3538
|
try {
|
|
3218
3539
|
await initProject(options);
|
|
@@ -3222,10 +3543,15 @@ program.command("init").description("Initialize a new SQG project with example c
|
|
|
3222
3543
|
}));
|
|
3223
3544
|
} catch (err) {
|
|
3224
3545
|
if (parentOpts.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
|
|
3225
|
-
else
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3546
|
+
else {
|
|
3547
|
+
const ui = new UI({
|
|
3548
|
+
format: parentOpts.format,
|
|
3549
|
+
verbose: parentOpts.verbose
|
|
3550
|
+
});
|
|
3551
|
+
if (err instanceof SqgError) ui.error(err);
|
|
3552
|
+
else if (err instanceof Error) ui.error(err);
|
|
3553
|
+
else consola.error(err);
|
|
3554
|
+
}
|
|
3229
3555
|
exit(1);
|
|
3230
3556
|
}
|
|
3231
3557
|
});
|
|
@@ -201,6 +201,8 @@ public class {{className}} {
|
|
|
201
201
|
}
|
|
202
202
|
{{/if}}
|
|
203
203
|
|
|
204
|
+
{{{declareEnums queries}}}
|
|
205
|
+
|
|
204
206
|
{{#each queries}}
|
|
205
207
|
{{#unless skipGenerateFunction}}
|
|
206
208
|
{{>columnTypesRecord}}
|
|
@@ -213,6 +215,7 @@ public class {{className}} {
|
|
|
213
215
|
public Stream<{{rowType}}> {{functionName}}Stream({{#each variables}}{{{type}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
|
|
214
216
|
var stmt = connection.prepareStatement({{{partsToString sqlQueryParts}}});
|
|
215
217
|
{{#each parameters}}{{#if isArray}}stmt.setArray({{plusOne @index}}, connection.createArrayOf("{{arrayBaseType}}", {{name}}.toArray()));
|
|
218
|
+
{{else if isEnum}}stmt.setObject({{plusOne @index}}, {{name}}.getValue());
|
|
216
219
|
{{else}}stmt.setObject({{plusOne @index}}, {{name}});
|
|
217
220
|
{{/if}}{{/each}}
|
|
218
221
|
var rs = stmt.executeQuery();
|
|
@@ -329,6 +332,7 @@ int
|
|
|
329
332
|
|
|
330
333
|
{{#*inline "execute"}}
|
|
331
334
|
{{#each parameters}}{{#if isArray}}stmt.setArray({{plusOne @index}}, connection.createArrayOf("{{arrayBaseType}}", {{name}}.toArray()));
|
|
335
|
+
{{else if isEnum}}stmt.setObject({{plusOne @index}}, {{name}}.getValue());
|
|
332
336
|
{{else}}stmt.setObject({{plusOne @index}}, {{name}});
|
|
333
337
|
{{/if}}{{/each}}
|
|
334
338
|
{{#if isQuery}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqg/sqg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"license": "Apache-2.0",
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@biomejs/biome": "2.3.3",
|
|
42
|
+
"@clack/prompts": "^1.1.0",
|
|
42
43
|
"@duckdb/node-api": "1.4.3-r.2",
|
|
43
44
|
"@duckdb/node-bindings-linux-x64": "1.4.3-r.2",
|
|
44
45
|
"@lezer-unofficial/printer": "^1.0.1",
|
|
@@ -55,10 +56,12 @@
|
|
|
55
56
|
"handlebars": "^4.7.8",
|
|
56
57
|
"pg": "^8.16.3",
|
|
57
58
|
"pg-types": "^4.1.0",
|
|
59
|
+
"picocolors": "^1.1.1",
|
|
58
60
|
"prettier": "^3.7.4",
|
|
59
61
|
"prettier-plugin-java": "^2.7.7",
|
|
60
62
|
"update-notifier": "^7.3.1",
|
|
61
63
|
"yaml": "^2.8.2",
|
|
64
|
+
"yocto-spinner": "^1.1.0",
|
|
62
65
|
"zod": "^4.3.5"
|
|
63
66
|
},
|
|
64
67
|
"devDependencies": {
|