@sqg/sqg 0.11.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 +561 -180
- package/dist/templates/java-jdbc.hbs +4 -0
- package/package.json +9 -4
package/dist/sqg.mjs
CHANGED
|
@@ -2,27 +2,32 @@
|
|
|
2
2
|
import { exit } from "node:process";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import consola, { LogLevels } from "consola";
|
|
5
|
+
import updateNotifier from "update-notifier";
|
|
6
|
+
import pc from "picocolors";
|
|
5
7
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import * as clack from "@clack/prompts";
|
|
6
9
|
import { randomUUID } from "node:crypto";
|
|
7
10
|
import { homedir, tmpdir } from "node:os";
|
|
8
|
-
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
11
|
+
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
9
12
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
14
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
15
|
import YAML from "yaml";
|
|
16
|
+
import { camelCase, pascalCase } from "es-toolkit/string";
|
|
13
17
|
import Handlebars from "handlebars";
|
|
14
18
|
import { z } from "zod";
|
|
15
19
|
import { DuckDBEnumType, DuckDBInstance, DuckDBListType, DuckDBMapType, DuckDBStructType } from "@duckdb/node-api";
|
|
16
20
|
import { LRParser } from "@lezer/lr";
|
|
17
|
-
import {
|
|
21
|
+
import { isNotNil, sortBy } from "es-toolkit";
|
|
22
|
+
import { PostgreSqlContainer } from "@testcontainers/postgresql";
|
|
18
23
|
import { Client } from "pg";
|
|
19
24
|
import types from "pg-types";
|
|
20
25
|
import BetterSqlite3 from "better-sqlite3";
|
|
21
|
-
import { camelCase as camelCase$1, pascalCase as pascalCase$1 } from "es-toolkit/string";
|
|
22
26
|
import prettier from "prettier/standalone";
|
|
23
27
|
import prettierPluginJava from "prettier-plugin-java";
|
|
24
28
|
import typescriptPlugin from "prettier/parser-typescript";
|
|
25
29
|
import estree from "prettier/plugins/estree";
|
|
30
|
+
import yoctoSpinner from "yocto-spinner";
|
|
26
31
|
|
|
27
32
|
//#region src/constants.ts
|
|
28
33
|
/**
|
|
@@ -360,12 +365,6 @@ function formatErrorForOutput(err) {
|
|
|
360
365
|
* SQG Project Initialization - Creates new SQG projects with example files
|
|
361
366
|
*/
|
|
362
367
|
/**
|
|
363
|
-
* Get the default generator for a language preference
|
|
364
|
-
*/
|
|
365
|
-
function getDefaultGenerator() {
|
|
366
|
-
return "typescript/sqlite";
|
|
367
|
-
}
|
|
368
|
-
/**
|
|
369
368
|
* Generate example SQL content based on engine
|
|
370
369
|
*/
|
|
371
370
|
function getExampleSql(engine) {
|
|
@@ -561,14 +560,14 @@ DELETE FROM posts WHERE id = \${id};
|
|
|
561
560
|
/**
|
|
562
561
|
* Generate sqg.yaml configuration
|
|
563
562
|
*/
|
|
564
|
-
function getConfigYaml(generator, output) {
|
|
563
|
+
function getConfigYaml(generator, output, projectName) {
|
|
565
564
|
const isJava = parseGenerator(generator).language === "java";
|
|
566
565
|
return `# SQG Configuration
|
|
567
566
|
# Generated by: sqg init
|
|
568
567
|
# Documentation: https://sqg.dev
|
|
569
568
|
|
|
570
569
|
version: 1
|
|
571
|
-
name:
|
|
570
|
+
name: ${projectName}
|
|
572
571
|
|
|
573
572
|
sql:
|
|
574
573
|
- files:
|
|
@@ -581,11 +580,70 @@ sql:
|
|
|
581
580
|
`;
|
|
582
581
|
}
|
|
583
582
|
/**
|
|
584
|
-
*
|
|
583
|
+
* Run interactive wizard when no --generator flag is provided
|
|
585
584
|
*/
|
|
586
|
-
async function
|
|
587
|
-
|
|
588
|
-
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;
|
|
589
647
|
if (!isValidGenerator(generator)) {
|
|
590
648
|
const similar = findSimilarGenerators(generator);
|
|
591
649
|
throw new InvalidGeneratorError(generator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
|
|
@@ -593,32 +651,40 @@ async function initProject(options) {
|
|
|
593
651
|
const engine = parseGenerator(generator).engine;
|
|
594
652
|
const configPath = "sqg.yaml";
|
|
595
653
|
const sqlPath = "queries.sql";
|
|
596
|
-
if (!
|
|
654
|
+
if (!force) {
|
|
597
655
|
if (existsSync(configPath)) throw new SqgError(`File already exists: ${configPath}`, "VALIDATION_ERROR", "Use --force to overwrite existing files");
|
|
598
656
|
if (existsSync(sqlPath)) throw new SqgError(`File already exists: ${sqlPath}`, "VALIDATION_ERROR", "Use --force to overwrite existing files");
|
|
599
657
|
}
|
|
600
|
-
if (!existsSync(output)) {
|
|
601
|
-
|
|
602
|
-
consola.success(`Created output directory: ${output}`);
|
|
603
|
-
}
|
|
604
|
-
writeFileSync(configPath, getConfigYaml(generator, output));
|
|
605
|
-
consola.success(`Created ${configPath}`);
|
|
658
|
+
if (!existsSync(output)) mkdirSync(output, { recursive: true });
|
|
659
|
+
writeFileSync(configPath, getConfigYaml(generator, output, projectName));
|
|
606
660
|
writeFileSync(sqlPath, getExampleSql(engine));
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
SQG project
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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");
|
|
622
688
|
}
|
|
623
689
|
|
|
624
690
|
//#endregion
|
|
@@ -670,8 +736,9 @@ var MapType = class {
|
|
|
670
736
|
}
|
|
671
737
|
};
|
|
672
738
|
var EnumType = class {
|
|
673
|
-
constructor(values) {
|
|
739
|
+
constructor(values, name) {
|
|
674
740
|
this.values = values;
|
|
741
|
+
this.name = name;
|
|
675
742
|
}
|
|
676
743
|
toString() {
|
|
677
744
|
return `ENUM(${this.values.map((v) => `'${v}'`).join(", ")})`;
|
|
@@ -744,7 +811,7 @@ var TableInfo = class {
|
|
|
744
811
|
};
|
|
745
812
|
function parseSQLQueries(filePath, extraVariables) {
|
|
746
813
|
const content = readFileSync(filePath, "utf-8");
|
|
747
|
-
consola.
|
|
814
|
+
consola.debug(`Parsing SQL file: ${filePath}`);
|
|
748
815
|
consola.debug(`File start: ${content.slice(0, 200)}`);
|
|
749
816
|
const queries = [];
|
|
750
817
|
const tables = [];
|
|
@@ -933,9 +1000,9 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
933
1000
|
consola.debug(`Added query: ${name} (${queryType})`);
|
|
934
1001
|
}
|
|
935
1002
|
while (cursor.next());
|
|
936
|
-
consola.
|
|
937
|
-
consola.
|
|
938
|
-
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(", ")}`);
|
|
939
1006
|
return {
|
|
940
1007
|
queries,
|
|
941
1008
|
tables
|
|
@@ -944,7 +1011,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
944
1011
|
|
|
945
1012
|
//#endregion
|
|
946
1013
|
//#region src/db/types.ts
|
|
947
|
-
async function initializeDatabase(queries, execQueries) {
|
|
1014
|
+
async function initializeDatabase(queries, execQueries, reporter) {
|
|
948
1015
|
const migrationQueries = queries.filter((q) => q.isMigrate);
|
|
949
1016
|
sortBy(migrationQueries, [(q) => Number(q.id.split("_")[1])]);
|
|
950
1017
|
for (const query of migrationQueries) try {
|
|
@@ -961,7 +1028,7 @@ async function initializeDatabase(queries, execQueries) {
|
|
|
961
1028
|
throw error;
|
|
962
1029
|
}
|
|
963
1030
|
if (migrationQueries.length + testdataQueries.length === 0) consola.warn("No migration or testdata queries found");
|
|
964
|
-
|
|
1031
|
+
reporter?.onDatabaseInitialized?.();
|
|
965
1032
|
}
|
|
966
1033
|
|
|
967
1034
|
//#endregion
|
|
@@ -988,7 +1055,7 @@ function convertType(type) {
|
|
|
988
1055
|
const duckdb = new class {
|
|
989
1056
|
db;
|
|
990
1057
|
connection;
|
|
991
|
-
async initializeDatabase(queries) {
|
|
1058
|
+
async initializeDatabase(queries, reporter) {
|
|
992
1059
|
this.db = await DuckDBInstance.create(":memory:");
|
|
993
1060
|
this.connection = await this.db.connect();
|
|
994
1061
|
await initializeDatabase(queries, async (query) => {
|
|
@@ -997,18 +1064,17 @@ const duckdb = new class {
|
|
|
997
1064
|
} catch (e) {
|
|
998
1065
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
999
1066
|
}
|
|
1000
|
-
});
|
|
1067
|
+
}, reporter);
|
|
1001
1068
|
}
|
|
1002
|
-
async executeQueries(queries) {
|
|
1069
|
+
async executeQueries(queries, reporter) {
|
|
1003
1070
|
const connection = this.connection;
|
|
1004
1071
|
if (!connection) throw new DatabaseError("DuckDB connection not initialized", "duckdb", "This is an internal error. Check that migrations completed successfully.");
|
|
1005
1072
|
try {
|
|
1006
1073
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1007
1074
|
for (const query of executableQueries) {
|
|
1008
|
-
|
|
1075
|
+
reporter?.onQueryStart?.(query.id);
|
|
1009
1076
|
await this.executeQuery(connection, query);
|
|
1010
|
-
|
|
1011
|
-
consola.success(`Query ${query.id} executed successfully`);
|
|
1077
|
+
reporter?.onQueryComplete?.(query.id);
|
|
1012
1078
|
}
|
|
1013
1079
|
} catch (error) {
|
|
1014
1080
|
consola.error("Error executing queries:", error.message);
|
|
@@ -1056,11 +1122,11 @@ const duckdb = new class {
|
|
|
1056
1122
|
throw error;
|
|
1057
1123
|
}
|
|
1058
1124
|
}
|
|
1059
|
-
async introspectTables(tables) {
|
|
1125
|
+
async introspectTables(tables, reporter) {
|
|
1060
1126
|
const connection = this.connection;
|
|
1061
1127
|
if (!connection) throw new DatabaseError("DuckDB connection not initialized", "duckdb", "This is an internal error. Check that migrations completed successfully.");
|
|
1062
1128
|
for (const table of tables) {
|
|
1063
|
-
|
|
1129
|
+
reporter?.onTableStart?.(table.tableName);
|
|
1064
1130
|
try {
|
|
1065
1131
|
const descRows = (await connection.runAndReadAll(`DESCRIBE ${table.tableName}`)).getRows();
|
|
1066
1132
|
const nullabilityMap = /* @__PURE__ */ new Map();
|
|
@@ -1074,7 +1140,7 @@ const duckdb = new class {
|
|
|
1074
1140
|
nullable: nullabilityMap.get(name) ?? true
|
|
1075
1141
|
}));
|
|
1076
1142
|
consola.debug(`Table ${table.tableName} columns:`, table.columns);
|
|
1077
|
-
|
|
1143
|
+
reporter?.onTableComplete?.(table.tableName, table.columns.length);
|
|
1078
1144
|
} catch (error) {
|
|
1079
1145
|
consola.error(`Failed to introspect table '${table.tableName}':`, error);
|
|
1080
1146
|
throw error;
|
|
@@ -1088,103 +1154,178 @@ const duckdb = new class {
|
|
|
1088
1154
|
|
|
1089
1155
|
//#endregion
|
|
1090
1156
|
//#region src/db/postgres.ts
|
|
1091
|
-
const
|
|
1092
|
-
let containerInstance = null;
|
|
1093
|
-
async function startTestContainer() {
|
|
1094
|
-
if (containerInstance) return containerInstance.getConnectionUri();
|
|
1095
|
-
consola.info("Starting PostgreSQL container via testcontainers...");
|
|
1096
|
-
const { PostgreSqlContainer } = await import("@testcontainers/postgresql");
|
|
1097
|
-
containerInstance = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
|
|
1098
|
-
const connectionUri = containerInstance.getConnectionUri();
|
|
1099
|
-
consola.success(`PostgreSQL container started at: ${connectionUri}`);
|
|
1100
|
-
return connectionUri;
|
|
1101
|
-
}
|
|
1102
|
-
async function stopTestContainer() {
|
|
1103
|
-
if (containerInstance) {
|
|
1104
|
-
consola.info("Stopping PostgreSQL container...");
|
|
1105
|
-
await containerInstance.stop();
|
|
1106
|
-
containerInstance = null;
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
async function getConnectionString() {
|
|
1110
|
-
if (process.env.SQG_POSTGRES_URL) return process.env.SQG_POSTGRES_URL;
|
|
1111
|
-
return await startTestContainer();
|
|
1112
|
-
}
|
|
1113
|
-
function getTempConnectionString(baseUrl) {
|
|
1114
|
-
return baseUrl.replace(/\/[^/]+$/, `/${databaseName}`);
|
|
1115
|
-
}
|
|
1157
|
+
const tempDatabaseName = "sqg-db-temp";
|
|
1116
1158
|
const typeIdToName = /* @__PURE__ */ new Map();
|
|
1117
1159
|
for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
`);
|
|
1127
|
-
dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1128
|
-
for (const row of result.rows) {
|
|
1129
|
-
const oid = row.oid;
|
|
1130
|
-
let typeName = row.typname;
|
|
1131
|
-
if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
|
|
1132
|
-
else typeName = typeName.toUpperCase();
|
|
1133
|
-
dynamicTypeCache.set(oid, typeName);
|
|
1160
|
+
/**
|
|
1161
|
+
* External database mode: connects directly to the user's database.
|
|
1162
|
+
* Uses a single transaction for the entire session and rolls back on close,
|
|
1163
|
+
* so the database is never modified. Individual queries use savepoints.
|
|
1164
|
+
*/
|
|
1165
|
+
var ExternalDbMode = class {
|
|
1166
|
+
constructor(connectionString) {
|
|
1167
|
+
this.connectionString = connectionString;
|
|
1134
1168
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1169
|
+
async connect() {
|
|
1170
|
+
const db = new Client({ connectionString: this.connectionString });
|
|
1171
|
+
try {
|
|
1172
|
+
await db.connect();
|
|
1173
|
+
} catch (e) {
|
|
1174
|
+
throw new DatabaseError(`Failed to connect to PostgreSQL: ${e.message}`, "postgres", `Check that PostgreSQL is running and accessible at ${this.connectionString}.`);
|
|
1175
|
+
}
|
|
1176
|
+
await db.query("BEGIN");
|
|
1177
|
+
return db;
|
|
1178
|
+
}
|
|
1179
|
+
async wrapQuery(db, fn) {
|
|
1180
|
+
try {
|
|
1181
|
+
await db.query("SAVEPOINT sqg_query");
|
|
1182
|
+
return await fn();
|
|
1183
|
+
} finally {
|
|
1184
|
+
await db.query("ROLLBACK TO SAVEPOINT sqg_query");
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async close(db) {
|
|
1188
|
+
await db.query("ROLLBACK");
|
|
1189
|
+
await db.end();
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
/**
|
|
1193
|
+
* Temp database mode: creates a temporary database for SQG to work in.
|
|
1194
|
+
* Connects to the provided server first (dbInitial) to CREATE the temp DB,
|
|
1195
|
+
* then connects to the temp DB for all operations.
|
|
1196
|
+
* On close, drops the temp DB and optionally stops the testcontainer.
|
|
1197
|
+
*/
|
|
1198
|
+
var TempDbMode = class {
|
|
1142
1199
|
dbInitial;
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
this.dbInitial = new Client({ connectionString });
|
|
1150
|
-
this.db = new Client({ connectionString: connectionStringTemp });
|
|
1200
|
+
container = null;
|
|
1201
|
+
constructor(connectionString, container) {
|
|
1202
|
+
this.connectionString = connectionString;
|
|
1203
|
+
this.container = container;
|
|
1204
|
+
}
|
|
1205
|
+
async connect() {
|
|
1206
|
+
this.dbInitial = new Client({ connectionString: this.connectionString });
|
|
1151
1207
|
try {
|
|
1152
1208
|
await this.dbInitial.connect();
|
|
1153
1209
|
} catch (e) {
|
|
1154
|
-
throw new DatabaseError(`Failed to connect to PostgreSQL: ${e.message}`, "postgres", `Check that PostgreSQL is running and accessible at ${connectionString}. Set SQG_POSTGRES_URL environment variable to use a different connection string.`);
|
|
1210
|
+
throw new DatabaseError(`Failed to connect to PostgreSQL: ${e.message}`, "postgres", `Check that PostgreSQL is running and accessible at ${this.connectionString}. Set SQG_POSTGRES_URL environment variable to use a different connection string.`);
|
|
1155
1211
|
}
|
|
1156
1212
|
try {
|
|
1157
|
-
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${
|
|
1213
|
+
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}";`);
|
|
1158
1214
|
} catch (error) {}
|
|
1159
1215
|
try {
|
|
1160
|
-
await this.dbInitial.query(`CREATE DATABASE "${
|
|
1216
|
+
await this.dbInitial.query(`CREATE DATABASE "${tempDatabaseName}";`);
|
|
1161
1217
|
} catch (error) {
|
|
1162
1218
|
throw new DatabaseError(`Failed to create temporary database: ${error.message}`, "postgres", "Check PostgreSQL user permissions to create databases");
|
|
1163
1219
|
}
|
|
1220
|
+
const db = new Client({ connectionString: this.connectionString.replace(/\/[^/]+$/, `/${tempDatabaseName}`) });
|
|
1164
1221
|
try {
|
|
1165
|
-
await
|
|
1222
|
+
await db.connect();
|
|
1166
1223
|
} catch (e) {
|
|
1167
1224
|
throw new DatabaseError(`Failed to connect to temporary database: ${e.message}`, "postgres");
|
|
1168
1225
|
}
|
|
1226
|
+
return db;
|
|
1227
|
+
}
|
|
1228
|
+
async wrapQuery(db, fn) {
|
|
1229
|
+
try {
|
|
1230
|
+
await db.query("BEGIN");
|
|
1231
|
+
return await fn();
|
|
1232
|
+
} finally {
|
|
1233
|
+
await db.query("ROLLBACK");
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
async close(db) {
|
|
1237
|
+
await db.end();
|
|
1238
|
+
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}"`);
|
|
1239
|
+
await this.dbInitial.end();
|
|
1240
|
+
if (this.container) {
|
|
1241
|
+
await this.container.stop();
|
|
1242
|
+
this.container = null;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
const postgres = new class {
|
|
1247
|
+
db;
|
|
1248
|
+
mode;
|
|
1249
|
+
dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1250
|
+
enumTypeCache = /* @__PURE__ */ new Map();
|
|
1251
|
+
async startContainer(reporter) {
|
|
1252
|
+
reporter?.onContainerStarting?.();
|
|
1253
|
+
const container = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
|
|
1254
|
+
const connectionUri = container.getConnectionUri();
|
|
1255
|
+
reporter?.onContainerStarted?.(connectionUri);
|
|
1256
|
+
return {
|
|
1257
|
+
connectionUri,
|
|
1258
|
+
container
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
async loadTypeCache(db) {
|
|
1262
|
+
const result = await db.query(`
|
|
1263
|
+
SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype,
|
|
1264
|
+
e.enumlabel
|
|
1265
|
+
FROM pg_type t
|
|
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'
|
|
1268
|
+
WHERE t.typtype IN ('b', 'e', 'r', 'c') -- base, enum, range, composite
|
|
1269
|
+
OR t.typelem != 0 -- array types
|
|
1270
|
+
ORDER BY t.oid, e.enumsortorder
|
|
1271
|
+
`);
|
|
1272
|
+
this.dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1273
|
+
this.enumTypeCache = /* @__PURE__ */ new Map();
|
|
1274
|
+
const enumsByOid = /* @__PURE__ */ new Map();
|
|
1275
|
+
for (const row of result.rows) {
|
|
1276
|
+
const oid = row.oid;
|
|
1277
|
+
let typeName = row.typname;
|
|
1278
|
+
if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
|
|
1279
|
+
else typeName = typeName.toUpperCase();
|
|
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
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
for (const [oid, { name, values }] of enumsByOid) this.enumTypeCache.set(oid, new EnumType(values, name));
|
|
1290
|
+
}
|
|
1291
|
+
getTypeName(dataTypeID) {
|
|
1292
|
+
const cached = this.dynamicTypeCache.get(dataTypeID);
|
|
1293
|
+
if (cached) return cached;
|
|
1294
|
+
return typeIdToName.get(dataTypeID) || `type_${dataTypeID}`;
|
|
1295
|
+
}
|
|
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) {
|
|
1302
|
+
const externalUrl = process.env.SQG_POSTGRES_URL;
|
|
1303
|
+
this.dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1304
|
+
this.enumTypeCache = /* @__PURE__ */ new Map();
|
|
1305
|
+
if (externalUrl) this.mode = new ExternalDbMode(externalUrl);
|
|
1306
|
+
else {
|
|
1307
|
+
const { connectionUri, container } = await this.startContainer(reporter);
|
|
1308
|
+
this.mode = new TempDbMode(connectionUri, container);
|
|
1309
|
+
}
|
|
1310
|
+
this.db = await this.mode.connect();
|
|
1169
1311
|
await initializeDatabase(queries, async (query) => {
|
|
1170
1312
|
try {
|
|
1171
1313
|
await this.db.query(query.rawQuery);
|
|
1172
1314
|
} catch (e) {
|
|
1173
1315
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
1174
1316
|
}
|
|
1175
|
-
});
|
|
1176
|
-
await loadTypeCache(this.db);
|
|
1317
|
+
}, reporter);
|
|
1318
|
+
await this.loadTypeCache(this.db);
|
|
1177
1319
|
}
|
|
1178
|
-
async executeQueries(queries) {
|
|
1320
|
+
async executeQueries(queries, reporter) {
|
|
1179
1321
|
const db = this.db;
|
|
1180
1322
|
if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
|
|
1181
1323
|
try {
|
|
1182
1324
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1183
1325
|
for (const query of executableQueries) {
|
|
1184
|
-
|
|
1326
|
+
reporter?.onQueryStart?.(query.id);
|
|
1185
1327
|
await this.executeQuery(db, query);
|
|
1186
|
-
|
|
1187
|
-
consola.success(`Query ${query.id} executed successfully`);
|
|
1328
|
+
reporter?.onQueryComplete?.(query.id);
|
|
1188
1329
|
}
|
|
1189
1330
|
} catch (error) {
|
|
1190
1331
|
consola.error("Error executing queries:", error.message);
|
|
@@ -1194,7 +1335,7 @@ const postgres = new class {
|
|
|
1194
1335
|
async executeQuery(db, query) {
|
|
1195
1336
|
const statement = query.queryPositional;
|
|
1196
1337
|
try {
|
|
1197
|
-
consola.
|
|
1338
|
+
consola.debug("Query:", statement.sql);
|
|
1198
1339
|
const parameterValues = statement.parameters.map((p) => {
|
|
1199
1340
|
const value = p.value;
|
|
1200
1341
|
if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
|
|
@@ -1208,8 +1349,9 @@ const postgres = new class {
|
|
|
1208
1349
|
if (paramTypeResult.rows.length === statement.parameters.length) {
|
|
1209
1350
|
const paramTypes = /* @__PURE__ */ new Map();
|
|
1210
1351
|
for (let i = 0; i < statement.parameters.length; i++) {
|
|
1211
|
-
const
|
|
1212
|
-
|
|
1352
|
+
const oid = Number(paramTypeResult.rows[i].oid);
|
|
1353
|
+
const colType = this.getColumnType(oid);
|
|
1354
|
+
paramTypes.set(statement.parameters[i].name, colType);
|
|
1213
1355
|
}
|
|
1214
1356
|
query.parameterTypes = paramTypes;
|
|
1215
1357
|
consola.debug("Parameter types:", Object.fromEntries(paramTypes));
|
|
@@ -1217,20 +1359,14 @@ const postgres = new class {
|
|
|
1217
1359
|
} catch (e) {
|
|
1218
1360
|
consola.debug(`Parameter type introspection failed for ${query.id}, using heuristic:`, e.message);
|
|
1219
1361
|
}
|
|
1220
|
-
|
|
1221
|
-
try {
|
|
1222
|
-
await db.query("BEGIN");
|
|
1223
|
-
result = await db.query(statement.sql, parameterValues);
|
|
1224
|
-
} finally {
|
|
1225
|
-
await db.query("ROLLBACK");
|
|
1226
|
-
}
|
|
1362
|
+
const result = await this.mode.wrapQuery(db, () => db.query(statement.sql, parameterValues));
|
|
1227
1363
|
if (query.isQuery) {
|
|
1228
1364
|
const columnNames = result.fields.map((field) => field.name);
|
|
1229
1365
|
const columnTypes = result.fields.map((field) => {
|
|
1230
|
-
return
|
|
1366
|
+
return this.getColumnType(field.dataTypeID);
|
|
1231
1367
|
});
|
|
1232
1368
|
consola.debug("Columns:", columnNames);
|
|
1233
|
-
consola.debug("Types:", columnTypes);
|
|
1369
|
+
consola.debug("Types:", columnTypes.map((t) => t.toString()));
|
|
1234
1370
|
query.columns = columnNames.map((name, index) => ({
|
|
1235
1371
|
name,
|
|
1236
1372
|
type: columnTypes[index],
|
|
@@ -1247,11 +1383,11 @@ const postgres = new class {
|
|
|
1247
1383
|
throw error;
|
|
1248
1384
|
}
|
|
1249
1385
|
}
|
|
1250
|
-
async introspectTables(tables) {
|
|
1386
|
+
async introspectTables(tables, reporter) {
|
|
1251
1387
|
const db = this.db;
|
|
1252
1388
|
if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
|
|
1253
1389
|
for (const table of tables) {
|
|
1254
|
-
|
|
1390
|
+
reporter?.onTableStart?.(table.tableName);
|
|
1255
1391
|
try {
|
|
1256
1392
|
table.columns = (await db.query(`SELECT column_name, data_type, is_nullable
|
|
1257
1393
|
FROM information_schema.columns
|
|
@@ -1261,7 +1397,7 @@ const postgres = new class {
|
|
|
1261
1397
|
type: row.data_type.toUpperCase(),
|
|
1262
1398
|
nullable: row.is_nullable === "YES"
|
|
1263
1399
|
}));
|
|
1264
|
-
|
|
1400
|
+
reporter?.onTableComplete?.(table.tableName, table.columns.length);
|
|
1265
1401
|
} catch (error) {
|
|
1266
1402
|
consola.error(`Failed to introspect table '${table.tableName}':`, error);
|
|
1267
1403
|
throw error;
|
|
@@ -1269,10 +1405,9 @@ const postgres = new class {
|
|
|
1269
1405
|
}
|
|
1270
1406
|
}
|
|
1271
1407
|
async close() {
|
|
1272
|
-
await this.
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
if (this.usingTestContainer) await stopTestContainer();
|
|
1408
|
+
await this.mode.close(this.db);
|
|
1409
|
+
this.dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1410
|
+
this.enumTypeCache = /* @__PURE__ */ new Map();
|
|
1276
1411
|
}
|
|
1277
1412
|
}();
|
|
1278
1413
|
|
|
@@ -1280,7 +1415,7 @@ const postgres = new class {
|
|
|
1280
1415
|
//#region src/db/sqlite.ts
|
|
1281
1416
|
const sqlite = new class {
|
|
1282
1417
|
db;
|
|
1283
|
-
async initializeDatabase(queries) {
|
|
1418
|
+
async initializeDatabase(queries, reporter) {
|
|
1284
1419
|
const db = new BetterSqlite3(":memory:");
|
|
1285
1420
|
await initializeDatabase(queries, (query) => {
|
|
1286
1421
|
try {
|
|
@@ -1289,37 +1424,36 @@ const sqlite = new class {
|
|
|
1289
1424
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
1290
1425
|
}
|
|
1291
1426
|
return Promise.resolve();
|
|
1292
|
-
});
|
|
1427
|
+
}, reporter);
|
|
1293
1428
|
this.db = db;
|
|
1294
1429
|
}
|
|
1295
|
-
executeQueries(queries) {
|
|
1430
|
+
executeQueries(queries, reporter) {
|
|
1296
1431
|
const db = this.db;
|
|
1297
1432
|
if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
|
|
1298
1433
|
try {
|
|
1299
1434
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
1300
1435
|
for (const query of executableQueries) {
|
|
1301
|
-
|
|
1436
|
+
reporter?.onQueryStart?.(query.id);
|
|
1302
1437
|
this.executeQuery(db, query);
|
|
1303
|
-
|
|
1304
|
-
consola.success(`Query ${query.id} executed successfully`);
|
|
1438
|
+
reporter?.onQueryComplete?.(query.id);
|
|
1305
1439
|
}
|
|
1306
1440
|
} catch (error) {
|
|
1307
1441
|
consola.error("Error executing queries:", error.message);
|
|
1308
1442
|
throw error;
|
|
1309
1443
|
}
|
|
1310
1444
|
}
|
|
1311
|
-
introspectTables(tables) {
|
|
1445
|
+
introspectTables(tables, reporter) {
|
|
1312
1446
|
const db = this.db;
|
|
1313
1447
|
if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
|
|
1314
1448
|
for (const table of tables) {
|
|
1315
|
-
|
|
1449
|
+
reporter?.onTableStart?.(table.tableName);
|
|
1316
1450
|
const info = this.getTableInfo(db, table.tableName);
|
|
1317
1451
|
table.columns = Array.from(info.values()).map((col) => ({
|
|
1318
1452
|
name: col.name,
|
|
1319
1453
|
type: col.type || "TEXT",
|
|
1320
1454
|
nullable: col.notnull === 0 && col.pk === 0
|
|
1321
1455
|
}));
|
|
1322
|
-
|
|
1456
|
+
reporter?.onTableComplete?.(table.tableName, table.columns.length);
|
|
1323
1457
|
}
|
|
1324
1458
|
}
|
|
1325
1459
|
close() {
|
|
@@ -1534,6 +1668,10 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1534
1668
|
"false",
|
|
1535
1669
|
"null"
|
|
1536
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
|
+
}
|
|
1537
1675
|
mapPrimitiveType(type, _nullable) {
|
|
1538
1676
|
const upperType = type.toString().toUpperCase();
|
|
1539
1677
|
const mappedType = this.typeMap[upperType];
|
|
@@ -1585,6 +1723,7 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1585
1723
|
return name;
|
|
1586
1724
|
}
|
|
1587
1725
|
parseValue(column, value, path) {
|
|
1726
|
+
if (column.type instanceof EnumType && column.type.name) return `${pascalCase(column.type.name)}.fromValue((String)${value})`;
|
|
1588
1727
|
if (column.type instanceof ListType) {
|
|
1589
1728
|
const elementType = this.getTypeName({
|
|
1590
1729
|
name: column.name,
|
|
@@ -1810,13 +1949,13 @@ var JavaGenerator = class extends BaseGenerator {
|
|
|
1810
1949
|
return engine === "duckdb";
|
|
1811
1950
|
}
|
|
1812
1951
|
getFunctionName(id) {
|
|
1813
|
-
return camelCase
|
|
1952
|
+
return camelCase(id);
|
|
1814
1953
|
}
|
|
1815
1954
|
getFilename(sqlFileName) {
|
|
1816
|
-
return `${pascalCase
|
|
1955
|
+
return `${pascalCase(sqlFileName)}.java`;
|
|
1817
1956
|
}
|
|
1818
1957
|
getClassName(name) {
|
|
1819
|
-
return pascalCase
|
|
1958
|
+
return pascalCase(name);
|
|
1820
1959
|
}
|
|
1821
1960
|
partsToString(parts) {
|
|
1822
1961
|
const stringParts = [];
|
|
@@ -1861,6 +2000,56 @@ var JavaGenerator = class extends BaseGenerator {
|
|
|
1861
2000
|
Handlebars.registerHelper("appenderType", (column) => {
|
|
1862
2001
|
return this.mapType(column);
|
|
1863
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
|
+
});
|
|
1864
2053
|
Handlebars.registerHelper("readColumns", (queryHelper) => {
|
|
1865
2054
|
const query = queryHelper.query;
|
|
1866
2055
|
if (queryHelper.isPluck) return this.readColumn({
|
|
@@ -2003,7 +2192,7 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
2003
2192
|
super(template, new TypeScriptTypeMapper());
|
|
2004
2193
|
}
|
|
2005
2194
|
getFunctionName(id) {
|
|
2006
|
-
return camelCase
|
|
2195
|
+
return camelCase(id);
|
|
2007
2196
|
}
|
|
2008
2197
|
isCompatibleWith(_engine) {
|
|
2009
2198
|
return true;
|
|
@@ -2012,7 +2201,7 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
2012
2201
|
return `${sqlFileName}.ts`;
|
|
2013
2202
|
}
|
|
2014
2203
|
getClassName(name) {
|
|
2015
|
-
return pascalCase
|
|
2204
|
+
return pascalCase(name);
|
|
2016
2205
|
}
|
|
2017
2206
|
async beforeGenerate(_projectDir, _gen, _queries, _tables) {
|
|
2018
2207
|
Handlebars.registerHelper("quote", (value) => this.quote(value));
|
|
@@ -2354,7 +2543,12 @@ var SqlQueryHelper = class {
|
|
|
2354
2543
|
const rawType = this.query.parameterTypes?.get(param.name);
|
|
2355
2544
|
let isArray = false;
|
|
2356
2545
|
let arrayBaseType = null;
|
|
2357
|
-
|
|
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) {
|
|
2358
2552
|
isArray = true;
|
|
2359
2553
|
arrayBaseType = rawType.baseType.toString();
|
|
2360
2554
|
} else if (typeof rawType === "string" && rawType.startsWith("_")) {
|
|
@@ -2365,7 +2559,9 @@ var SqlQueryHelper = class {
|
|
|
2365
2559
|
name: param.name,
|
|
2366
2560
|
type: vars.get(param.name),
|
|
2367
2561
|
isArray,
|
|
2368
|
-
arrayBaseType
|
|
2562
|
+
arrayBaseType,
|
|
2563
|
+
isEnum,
|
|
2564
|
+
enumClassName
|
|
2369
2565
|
};
|
|
2370
2566
|
});
|
|
2371
2567
|
}
|
|
@@ -2487,7 +2683,7 @@ function createExtraVariables(sources, suppressLogging = false) {
|
|
|
2487
2683
|
const path = source.path;
|
|
2488
2684
|
const resolvedPath = path.replace("$HOME", homedir());
|
|
2489
2685
|
const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
|
|
2490
|
-
if (!suppressLogging) consola.
|
|
2686
|
+
if (!suppressLogging) consola.debug("Extra variable:", varName, resolvedPath);
|
|
2491
2687
|
return new ExtraVariable(varName, `'${resolvedPath}'`);
|
|
2492
2688
|
});
|
|
2493
2689
|
}
|
|
@@ -2597,7 +2793,6 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
|
|
|
2597
2793
|
}
|
|
2598
2794
|
const outputPath = getOutputPath(projectDir, name, gen, generator);
|
|
2599
2795
|
writeFileSync(outputPath, sourceFile);
|
|
2600
|
-
consola.success(`Generated ${outputPath}`);
|
|
2601
2796
|
await generator.afterGenerate(outputPath);
|
|
2602
2797
|
return outputPath;
|
|
2603
2798
|
}
|
|
@@ -2671,12 +2866,24 @@ async function validateProject(projectPath) {
|
|
|
2671
2866
|
}
|
|
2672
2867
|
return await validateProjectFromConfig(project, projectDir);
|
|
2673
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
|
+
}
|
|
2674
2878
|
/**
|
|
2675
2879
|
* Process a project configuration and generate code from a Project object
|
|
2676
2880
|
*/
|
|
2677
|
-
async function processProjectFromConfig(project, projectDir, writeToStdout = false) {
|
|
2881
|
+
async function processProjectFromConfig(project, projectDir, writeToStdout = false, ui) {
|
|
2678
2882
|
const originalLevel = consola.level;
|
|
2679
2883
|
if (writeToStdout) consola.level = LogLevels.silent;
|
|
2884
|
+
const totalStart = performance.now();
|
|
2885
|
+
const reporter = ui?.createReporter();
|
|
2886
|
+
const results = [];
|
|
2680
2887
|
try {
|
|
2681
2888
|
const extraVariables = createExtraVariables(project.sources ?? [], writeToStdout);
|
|
2682
2889
|
const files = [];
|
|
@@ -2701,31 +2908,52 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2701
2908
|
throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
|
|
2702
2909
|
}
|
|
2703
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);
|
|
2704
2914
|
try {
|
|
2705
2915
|
const dbEngine = getDatabaseEngine(engine);
|
|
2706
|
-
|
|
2707
|
-
await dbEngine.
|
|
2708
|
-
|
|
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`);
|
|
2709
2922
|
validateQueries(queries);
|
|
2710
2923
|
await dbEngine.close();
|
|
2711
2924
|
} catch (e) {
|
|
2925
|
+
ui?.failPhase(`Failed to process ${sqlFile}`);
|
|
2712
2926
|
if (e instanceof SqgError) throw e;
|
|
2713
2927
|
throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${engine}'`, {
|
|
2714
2928
|
file: sqlFile,
|
|
2715
2929
|
engine
|
|
2716
2930
|
});
|
|
2717
2931
|
}
|
|
2932
|
+
ui?.startPhase("Generating code...");
|
|
2933
|
+
const genStart = performance.now();
|
|
2718
2934
|
for (const gen of gens) {
|
|
2719
2935
|
const generator = getGenerator(gen.generator);
|
|
2720
2936
|
const outputPath = await writeGeneratedFile(projectDir, {
|
|
2721
2937
|
...gen,
|
|
2722
2938
|
projectName: project.name
|
|
2723
2939
|
}, generator, sqlFile, queries, tables, engine, writeToStdout);
|
|
2724
|
-
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
|
+
}
|
|
2725
2951
|
}
|
|
2952
|
+
ui?.succeedPhase(`Generated ${gens.length} ${gens.length === 1 ? "file" : "files"} from ${sqlFile}`);
|
|
2726
2953
|
}
|
|
2727
2954
|
}
|
|
2728
2955
|
}
|
|
2956
|
+
ui?.summary(results, performance.now() - totalStart);
|
|
2729
2957
|
return files;
|
|
2730
2958
|
} finally {
|
|
2731
2959
|
if (writeToStdout) consola.level = originalLevel;
|
|
@@ -2734,16 +2962,16 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2734
2962
|
/**
|
|
2735
2963
|
* Process a project configuration and generate code from a YAML file
|
|
2736
2964
|
*/
|
|
2737
|
-
async function processProject(projectPath) {
|
|
2965
|
+
async function processProject(projectPath, ui) {
|
|
2738
2966
|
const projectDir = resolve(dirname(projectPath));
|
|
2739
|
-
return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false);
|
|
2967
|
+
return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false, ui);
|
|
2740
2968
|
}
|
|
2741
2969
|
|
|
2742
2970
|
//#endregion
|
|
2743
2971
|
//#region src/mcp-server.ts
|
|
2744
2972
|
const server = new Server({
|
|
2745
2973
|
name: "sqg-mcp",
|
|
2746
|
-
version: process.env.npm_package_version ?? "0.
|
|
2974
|
+
version: process.env.npm_package_version ?? "0.13.0"
|
|
2747
2975
|
}, { capabilities: {
|
|
2748
2976
|
tools: {},
|
|
2749
2977
|
resources: {}
|
|
@@ -3070,11 +3298,154 @@ async function startMcpServer() {
|
|
|
3070
3298
|
console.error("SQG MCP server running on stdio");
|
|
3071
3299
|
}
|
|
3072
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
|
+
|
|
3073
3439
|
//#endregion
|
|
3074
3440
|
//#region src/sqg.ts
|
|
3075
|
-
const version = process.env.npm_package_version ?? "0.
|
|
3441
|
+
const version = process.env.npm_package_version ?? "0.13.0";
|
|
3442
|
+
updateNotifier({ pkg: {
|
|
3443
|
+
name: "@sqg/sqg",
|
|
3444
|
+
version
|
|
3445
|
+
} }).notify({ message: "Update available {currentVersion} → {latestVersion}" });
|
|
3076
3446
|
const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
|
|
3077
|
-
consola.level = LogLevels.
|
|
3447
|
+
consola.level = LogLevels.warn;
|
|
3448
|
+
const BRANDING = `\n ${pc.bold(pc.blue("SQG"))} ${pc.dim(`v${version}`)}\n`;
|
|
3078
3449
|
const program = new Command().name("sqg").description(`${description}
|
|
3079
3450
|
|
|
3080
3451
|
Generate type-safe database access code from annotated SQL files.
|
|
@@ -3083,12 +3454,20 @@ Supported Generators:
|
|
|
3083
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 = []) => {
|
|
3084
3455
|
prev.push(val);
|
|
3085
3456
|
return prev;
|
|
3086
|
-
}).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();
|
|
3087
3458
|
program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omit to use CLI options").hook("preAction", (thisCommand) => {
|
|
3088
3459
|
const opts = thisCommand.opts();
|
|
3089
3460
|
if (opts.verbose) consola.level = LogLevels.debug;
|
|
3090
3461
|
if (opts.format === "json") consola.level = LogLevels.silent;
|
|
3091
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();
|
|
3092
3471
|
try {
|
|
3093
3472
|
if (!projectPath) {
|
|
3094
3473
|
if (!options.generator) throw new SqgError("Missing required option: --generator", "CONFIG_VALIDATION_ERROR", `Specify a code generation generator: ${SHORT_GENERATOR_NAMES.join(", ")}`);
|
|
@@ -3100,7 +3479,6 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3100
3479
|
name: options.name
|
|
3101
3480
|
});
|
|
3102
3481
|
const projectDir = process.cwd();
|
|
3103
|
-
const writeToStdout = !options.output;
|
|
3104
3482
|
if (options.validate) {
|
|
3105
3483
|
const result = await validateProjectFromConfig(project, projectDir);
|
|
3106
3484
|
if (options.format === "json") console.log(JSON.stringify(result, null, 2));
|
|
@@ -3118,7 +3496,7 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3118
3496
|
}
|
|
3119
3497
|
exit(result.valid ? 0 : 1);
|
|
3120
3498
|
}
|
|
3121
|
-
const files = await processProjectFromConfig(project, projectDir, writeToStdout);
|
|
3499
|
+
const files = await processProjectFromConfig(project, projectDir, writeToStdout, ui);
|
|
3122
3500
|
if (options.format === "json" && !writeToStdout) console.log(JSON.stringify({
|
|
3123
3501
|
status: "success",
|
|
3124
3502
|
generatedFiles: files
|
|
@@ -3141,7 +3519,7 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3141
3519
|
}
|
|
3142
3520
|
exit(result.valid ? 0 : 1);
|
|
3143
3521
|
}
|
|
3144
|
-
const files = await processProject(projectPath);
|
|
3522
|
+
const files = await processProject(projectPath, ui);
|
|
3145
3523
|
if (options.format === "json") console.log(JSON.stringify({
|
|
3146
3524
|
status: "success",
|
|
3147
3525
|
generatedFiles: files
|
|
@@ -3149,15 +3527,13 @@ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omi
|
|
|
3149
3527
|
}
|
|
3150
3528
|
} catch (err) {
|
|
3151
3529
|
if (options.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
|
|
3152
|
-
else if (err instanceof SqgError)
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
if (err.context && options.verbose) consola.debug("Context:", err.context);
|
|
3156
|
-
} 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);
|
|
3157
3533
|
exit(1);
|
|
3158
3534
|
}
|
|
3159
3535
|
});
|
|
3160
|
-
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) => {
|
|
3161
3537
|
const parentOpts = program.opts();
|
|
3162
3538
|
try {
|
|
3163
3539
|
await initProject(options);
|
|
@@ -3167,10 +3543,15 @@ program.command("init").description("Initialize a new SQG project with example c
|
|
|
3167
3543
|
}));
|
|
3168
3544
|
} catch (err) {
|
|
3169
3545
|
if (parentOpts.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
|
|
3170
|
-
else
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
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
|
+
}
|
|
3174
3555
|
exit(1);
|
|
3175
3556
|
}
|
|
3176
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,12 +39,15 @@
|
|
|
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",
|
|
45
46
|
"@lezer/common": "^1.5.0",
|
|
46
47
|
"@lezer/generator": "^1.8.0",
|
|
47
48
|
"@lezer/lr": "^1.4.6",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
50
|
+
"@testcontainers/postgresql": "^10.21.0",
|
|
48
51
|
"better-sqlite3": "^12.5.0",
|
|
49
52
|
"commander": "^14.0.2",
|
|
50
53
|
"consola": "^3.4.2",
|
|
@@ -53,12 +56,13 @@
|
|
|
53
56
|
"handlebars": "^4.7.8",
|
|
54
57
|
"pg": "^8.16.3",
|
|
55
58
|
"pg-types": "^4.1.0",
|
|
56
|
-
"
|
|
59
|
+
"picocolors": "^1.1.1",
|
|
57
60
|
"prettier": "^3.7.4",
|
|
58
61
|
"prettier-plugin-java": "^2.7.7",
|
|
62
|
+
"update-notifier": "^7.3.1",
|
|
59
63
|
"yaml": "^2.8.2",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
64
|
+
"yocto-spinner": "^1.1.0",
|
|
65
|
+
"zod": "^4.3.5"
|
|
62
66
|
},
|
|
63
67
|
"devDependencies": {
|
|
64
68
|
"@libsql/client": "^0.17.0",
|
|
@@ -66,6 +70,7 @@
|
|
|
66
70
|
"@types/better-sqlite3": "^7.6.13",
|
|
67
71
|
"@types/node": "^25.0.3",
|
|
68
72
|
"@types/pg": "^8.16.0",
|
|
73
|
+
"@types/update-notifier": "^6.0.8",
|
|
69
74
|
"@vitest/ui": "^4.0.16",
|
|
70
75
|
"tsdown": "0.18.0",
|
|
71
76
|
"tsx": "^4.21.0",
|