@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 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 { camelCase, isNotNil, pascalCase, sortBy } from "es-toolkit";
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: my-project
570
+ name: ${projectName}
574
571
 
575
572
  sql:
576
573
  - files:
@@ -583,11 +580,70 @@ sql:
583
580
  `;
584
581
  }
585
582
  /**
586
- * Initialize a new SQG project
583
+ * Run interactive wizard when no --generator flag is provided
587
584
  */
588
- async function initProject(options) {
589
- const generator = options.generator || getDefaultGenerator();
590
- const output = options.output || "./generated";
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 (!options.force) {
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
- mkdirSync(output, { recursive: true });
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
- consola.success(`Created ${sqlPath}`);
610
- consola.box(`
611
- SQG project initialized!
612
-
613
- Generator: ${generator}
614
- Engine: ${engine}
615
- Output: ${output}
616
-
617
- Next steps:
618
- 1. Edit queries.sql to add your SQL queries
619
- 2. Run: sqg sqg.yaml
620
- 3. Import the generated code from ${output}
621
-
622
- Documentation: https://sqg.dev
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.info(`Parsing SQL file: ${filePath}`);
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.info(`Total queries parsed: ${queries.length}, tables: ${tables.length}`);
939
- consola.info(`Query names: ${queries.map((q) => q.id).join(", ")}`);
940
- if (tables.length > 0) consola.info(`Table names: ${tables.map((t) => t.id).join(", ")}`);
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
- consola.success("Database initialized successfully");
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
- consola.info(`Executing query: ${query.id}`);
1075
+ reporter?.onQueryStart?.(query.id);
1011
1076
  await this.executeQuery(connection, query);
1012
- if (query.isQuery) {}
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
- consola.info(`Introspecting table schema: ${table.tableName}`);
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
- consola.success(`Introspected table: ${table.tableName} (${table.columns.length} columns)`);
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
- async startContainer() {
1188
- consola.info("Starting PostgreSQL container via testcontainers...");
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
- consola.success(`PostgreSQL container started at: ${connectionUri}`);
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
- async initializeDatabase(queries) {
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
- consola.debug(`Executing query: ${query.id}`);
1326
+ reporter?.onQueryStart?.(query.id);
1244
1327
  await this.executeQuery(db, query);
1245
- consola.success(`Query ${query.id} executed successfully`);
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 typeName = this.getTypeName(oid);
1271
- paramTypes.set(statement.parameters[i].name, typeName);
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.getTypeName(field.dataTypeID);
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
- consola.info(`Introspecting table schema: ${table.tableName}`);
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
- consola.success(`Introspected table: ${table.tableName} (${table.columns.length} columns)`);
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
- consola.info(`Executing query: ${query.id}`);
1436
+ reporter?.onQueryStart?.(query.id);
1353
1437
  this.executeQuery(db, query);
1354
- if (query.isQuery) {}
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
- consola.info(`Introspecting table schema: ${table.tableName}`);
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
- consola.success(`Introspected table: ${table.tableName} (${table.columns.length} columns)`);
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$1(id);
1952
+ return camelCase(id);
1865
1953
  }
1866
1954
  getFilename(sqlFileName) {
1867
- return `${pascalCase$1(sqlFileName)}.java`;
1955
+ return `${pascalCase(sqlFileName)}.java`;
1868
1956
  }
1869
1957
  getClassName(name) {
1870
- return pascalCase$1(name);
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$1(id);
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$1(name);
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
- if (rawType instanceof ListType) {
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.info("Extra variable:", varName, resolvedPath);
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
- await dbEngine.initializeDatabase(queries);
2758
- await dbEngine.executeQueries(queries);
2759
- if (tables.length > 0) await dbEngine.introspectTables(tables);
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) files.push(outputPath);
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.12.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.12.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.info;
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
- consola.error(err.message);
3209
- if (err.suggestion) consola.info(`Suggestion: ${err.suggestion}`);
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(", ")})`, "typescript/sqlite").option("-o, --output <dir>", "Output directory for generated files", "./generated").option("-f, --force", "Overwrite existing files").action(async (options) => {
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 if (err instanceof SqgError) {
3226
- consola.error(err.message);
3227
- if (err.suggestion) consola.info(`Suggestion: ${err.suggestion}`);
3228
- } else consola.error(err);
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.12.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": {