@sqg/sqg 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/dist/sqg.mjs +343 -240
  2. package/package.json +1 -1
package/dist/sqg.mjs CHANGED
@@ -21,45 +21,128 @@ import typescriptPlugin from "prettier/parser-typescript";
21
21
  import estree from "prettier/plugins/estree";
22
22
 
23
23
  //#region src/constants.ts
24
- /**
25
- * SQG Constants - Centralized definitions for supported engines and generators
26
- * This file enables self-documenting CLI help and validation.
27
- */
28
- /** Supported database engines */
29
- const DB_ENGINES = [
30
- "sqlite",
31
- "duckdb",
32
- "postgres"
33
- ];
34
- /** Supported code generators with their descriptions */
35
- const SUPPORTED_GENERATORS = {
36
- "typescript/better-sqlite3": {
24
+ /** All supported generators with their full specification */
25
+ const GENERATORS = {
26
+ "typescript/sqlite/better-sqlite3": {
27
+ language: "typescript",
28
+ engine: "sqlite",
29
+ driver: "better-sqlite3",
37
30
  description: "TypeScript with better-sqlite3 driver",
38
- compatibleEngines: ["sqlite"],
39
- extension: ".ts"
31
+ extension: ".ts",
32
+ template: "better-sqlite3.hbs"
40
33
  },
41
- "typescript/duckdb": {
34
+ "typescript/duckdb/node-api": {
35
+ language: "typescript",
36
+ engine: "duckdb",
37
+ driver: "node-api",
42
38
  description: "TypeScript with @duckdb/node-api driver",
43
- compatibleEngines: ["duckdb"],
44
- extension: ".ts"
39
+ extension: ".ts",
40
+ template: "typescript-duckdb.hbs"
41
+ },
42
+ "java/sqlite/jdbc": {
43
+ language: "java",
44
+ engine: "sqlite",
45
+ driver: "jdbc",
46
+ description: "Java with JDBC for SQLite",
47
+ extension: ".java",
48
+ template: "java-jdbc.hbs"
45
49
  },
46
- "java/jdbc": {
47
- description: "Java with JDBC (SQLite, DuckDB, PostgreSQL)",
48
- compatibleEngines: [
49
- "sqlite",
50
- "duckdb",
51
- "postgres"
52
- ],
53
- extension: ".java"
50
+ "java/duckdb/jdbc": {
51
+ language: "java",
52
+ engine: "duckdb",
53
+ driver: "jdbc",
54
+ description: "Java with JDBC for DuckDB",
55
+ extension: ".java",
56
+ template: "java-jdbc.hbs"
54
57
  },
55
- "java/duckdb-arrow": {
58
+ "java/duckdb/arrow": {
59
+ language: "java",
60
+ engine: "duckdb",
61
+ driver: "arrow",
56
62
  description: "Java with DuckDB Arrow API",
57
- compatibleEngines: ["duckdb"],
58
- extension: ".java"
63
+ extension: ".java",
64
+ template: "java-duckdb-arrow.hbs"
65
+ },
66
+ "java/postgres/jdbc": {
67
+ language: "java",
68
+ engine: "postgres",
69
+ driver: "jdbc",
70
+ description: "Java with JDBC for PostgreSQL",
71
+ extension: ".java",
72
+ template: "java-jdbc.hbs"
59
73
  }
60
74
  };
61
- /** List of all generator names for validation */
62
- const GENERATOR_NAMES = Object.keys(SUPPORTED_GENERATORS);
75
+ /** Default drivers for language/engine combinations */
76
+ const DEFAULT_DRIVERS = {
77
+ "typescript/sqlite": "better-sqlite3",
78
+ "typescript/duckdb": "node-api",
79
+ "java/sqlite": "jdbc",
80
+ "java/duckdb": "jdbc",
81
+ "java/postgres": "jdbc"
82
+ };
83
+ /** List of all full generator names */
84
+ const GENERATOR_NAMES = Object.keys(GENERATORS);
85
+ /** List of short generator names (language/engine) */
86
+ const SHORT_GENERATOR_NAMES = Object.keys(DEFAULT_DRIVERS);
87
+ /**
88
+ * Resolve a generator string to its full form.
89
+ * Accepts both short (language/engine) and full (language/engine/driver) formats.
90
+ */
91
+ function resolveGenerator(generator) {
92
+ if (generator in GENERATORS) return generator;
93
+ if (generator in DEFAULT_DRIVERS) return `${generator}/${DEFAULT_DRIVERS[generator]}`;
94
+ return generator;
95
+ }
96
+ /**
97
+ * Parse a generator string and return its info.
98
+ * Throws if the generator is invalid.
99
+ */
100
+ function parseGenerator(generator) {
101
+ const info = GENERATORS[resolveGenerator(generator)];
102
+ if (!info) throw new Error(`Invalid generator: ${generator}`);
103
+ return info;
104
+ }
105
+ /**
106
+ * Check if a generator string is valid (either short or full form).
107
+ */
108
+ function isValidGenerator(generator) {
109
+ return resolveGenerator(generator) in GENERATORS;
110
+ }
111
+ /**
112
+ * Get the database engine for a generator.
113
+ */
114
+ function getGeneratorEngine(generator) {
115
+ return parseGenerator(generator).engine;
116
+ }
117
+ /**
118
+ * Find similar generator names for typo suggestions.
119
+ */
120
+ function findSimilarGenerators(input) {
121
+ const normalized = input.toLowerCase();
122
+ return [...GENERATOR_NAMES, ...SHORT_GENERATOR_NAMES].filter((name) => {
123
+ const nameLower = name.toLowerCase();
124
+ if (nameLower.includes(normalized) || normalized.includes(nameLower)) return true;
125
+ return [
126
+ normalized.replace(/\//g, "-"),
127
+ normalized.replace(/-/g, "/"),
128
+ normalized.replace(/_/g, "/"),
129
+ normalized.replace(/_/g, "-")
130
+ ].some((v) => nameLower.includes(v) || v.includes(nameLower));
131
+ });
132
+ }
133
+ /**
134
+ * Format generators for CLI help output.
135
+ */
136
+ function formatGeneratorsHelp() {
137
+ const lines = [];
138
+ for (const shortName of SHORT_GENERATOR_NAMES) {
139
+ const fullName = `${shortName}/${DEFAULT_DRIVERS[shortName]}`;
140
+ const info = GENERATORS[fullName];
141
+ lines.push(` ${shortName.padEnd(24)} ${info.description} (default)`);
142
+ for (const [generatorName, generatorInfo] of Object.entries(GENERATORS)) if (generatorName.startsWith(`${shortName}/`) && generatorName !== fullName) lines.push(` ${generatorName.padEnd(24)} ${generatorInfo.description}`);
143
+ }
144
+ return lines.join("\n");
145
+ }
63
146
  /** SQL annotation syntax reference */
64
147
  const SQL_SYNTAX_REFERENCE = `
65
148
  SQL Annotation Syntax:
@@ -88,34 +171,6 @@ Example:
88
171
 
89
172
  -- TABLE users :appender
90
173
  `.trim();
91
- /**
92
- * Find similar generator names for typo suggestions
93
- */
94
- function findSimilarGenerators(input) {
95
- const normalized = input.toLowerCase();
96
- return GENERATOR_NAMES.filter((name) => {
97
- const nameLower = name.toLowerCase();
98
- if (nameLower.includes(normalized) || normalized.includes(nameLower)) return true;
99
- return [
100
- normalized.replace("/", "-"),
101
- normalized.replace("-", "/"),
102
- normalized.replace("_", "/"),
103
- normalized.replace("_", "-")
104
- ].some((v) => nameLower.includes(v) || v.includes(nameLower));
105
- });
106
- }
107
- /**
108
- * Format generators for CLI help output
109
- */
110
- function formatGeneratorsHelp() {
111
- return Object.entries(SUPPORTED_GENERATORS).map(([name, info]) => ` ${name.padEnd(28)} ${info.description} (${info.compatibleEngines.join(", ")})`).join("\n");
112
- }
113
- /**
114
- * Format engines for CLI help output
115
- */
116
- function formatEnginesHelp() {
117
- return DB_ENGINES.map((e) => ` ${e}`).join("\n");
118
- }
119
174
 
120
175
  //#endregion
121
176
  //#region src/errors.ts
@@ -174,41 +229,20 @@ var ConfigError = class extends SqgError {
174
229
  * Error for invalid generator names
175
230
  */
176
231
  var InvalidGeneratorError = class extends SqgError {
177
- constructor(generatorName, validGenerators, suggestion) {
232
+ constructor(generatorName, validGenerators$1, suggestion) {
178
233
  const similarMsg = suggestion ? ` Did you mean '${suggestion}'?` : "";
179
- super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators.join(", ")}`, { generator: generatorName });
234
+ super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators$1.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators$1.join(", ")}`, { generator: generatorName });
180
235
  this.name = "InvalidGeneratorError";
181
236
  }
182
237
  };
183
238
  /**
184
- * Error for invalid engine names
185
- */
186
- var InvalidEngineError = class extends SqgError {
187
- constructor(engineName, validEngines) {
188
- super(`Invalid engine '${engineName}'. Valid engines: ${validEngines.join(", ")}`, "INVALID_ENGINE", `Choose from: ${validEngines.join(", ")}`, { engine: engineName });
189
- this.name = "InvalidEngineError";
190
- }
191
- };
192
- /**
193
- * Error for generator/engine compatibility
194
- */
195
- var GeneratorEngineMismatchError = class extends SqgError {
196
- constructor(generator, engine, compatibleEngines) {
197
- super(`Generator '${generator}' is not compatible with engine '${engine}'`, "GENERATOR_ENGINE_MISMATCH", `Generator '${generator}' works with: ${compatibleEngines.join(", ")}`, {
198
- generator,
199
- engine
200
- });
201
- this.name = "GeneratorEngineMismatchError";
202
- }
203
- };
204
- /**
205
239
  * Error for database initialization/connection issues
206
240
  */
207
241
  var DatabaseError = class extends SqgError {
208
242
  constructor(message, engine, suggestion, context) {
209
243
  super(message, "DATABASE_ERROR", suggestion, {
210
- engine,
211
- ...context
244
+ ...context,
245
+ engine
212
246
  });
213
247
  this.name = "DatabaseError";
214
248
  }
@@ -1457,7 +1491,7 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
1457
1491
  const name = `${gen.name}-jdbc`;
1458
1492
  writeGeneratedFile(projectDir, {
1459
1493
  name,
1460
- generator: "java/jdbc",
1494
+ generator: "java/duckdb/jdbc",
1461
1495
  output: gen.output,
1462
1496
  config: gen.config
1463
1497
  }, this.javaGenerator, name, q, tables, "duckdb");
@@ -1754,16 +1788,25 @@ var TsDuckDBGenerator = class extends TsGenerator {
1754
1788
 
1755
1789
  //#endregion
1756
1790
  //#region src/generators/index.ts
1791
+ /**
1792
+ * Get a generator instance for the given generator.
1793
+ * Accepts both short (language/engine) and full (language/engine/driver) formats.
1794
+ */
1757
1795
  function getGenerator(generator) {
1758
- switch (generator) {
1759
- case "java/jdbc": return new JavaGenerator("templates/java-jdbc.hbs");
1760
- case "java/duckdb-arrow": return new JavaDuckDBArrowGenerator("templates/java-duckdb-arrow.hbs");
1761
- case "typescript/better-sqlite3": return new TsGenerator("templates/better-sqlite3.hbs");
1762
- case "typescript/duckdb": return new TsDuckDBGenerator("templates/typescript-duckdb.hbs");
1763
- default: {
1764
- const similar = findSimilarGenerators(generator);
1765
- throw new InvalidGeneratorError(generator, [...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
1796
+ const fullGenerator = resolveGenerator(generator);
1797
+ try {
1798
+ const info = parseGenerator(generator);
1799
+ const key = `${info.language}/${info.driver}`;
1800
+ switch (key) {
1801
+ case "typescript/better-sqlite3": return new TsGenerator(`templates/${info.template}`);
1802
+ case "typescript/node-api": return new TsDuckDBGenerator(`templates/${info.template}`);
1803
+ case "java/jdbc": return new JavaGenerator(`templates/${info.template}`);
1804
+ case "java/arrow": return new JavaDuckDBArrowGenerator(`templates/${info.template}`);
1805
+ default: throw new Error(`No generator class for ${key}`);
1766
1806
  }
1807
+ } catch {
1808
+ const similar = findSimilarGenerators(generator);
1809
+ throw new InvalidGeneratorError(fullGenerator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
1767
1810
  }
1768
1811
  }
1769
1812
 
@@ -1932,6 +1975,8 @@ function generateSourceFile(name, queries, tables, templatePath, generator, engi
1932
1975
  allowProtoMethodsByDefault: true
1933
1976
  });
1934
1977
  }
1978
+ /** All valid generator strings for schema validation */
1979
+ const validGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
1935
1980
  /**
1936
1981
  * Project configuration schema with descriptions for validation messages
1937
1982
  */
@@ -1939,10 +1984,9 @@ const ProjectSchema = z.object({
1939
1984
  version: z.number().describe("Configuration version (currently 1)"),
1940
1985
  name: z.string().min(1, "Project name is required").describe("Project name used for generated class names"),
1941
1986
  sql: z.array(z.object({
1942
- engine: z.enum(DB_ENGINES).describe(`Database engine: ${DB_ENGINES.join(", ")}`),
1943
1987
  files: z.array(z.string().min(1)).min(1, "At least one SQL file is required").describe("SQL files to process"),
1944
1988
  gen: z.array(z.object({
1945
- generator: z.enum(GENERATOR_NAMES).describe(`Code generator: ${GENERATOR_NAMES.join(", ")}`),
1989
+ generator: z.string().refine((val) => isValidGenerator(val), { message: `Invalid generator. Valid generators: ${validGenerators.join(", ")}` }).describe(`Code generation generator: ${SHORT_GENERATOR_NAMES.join(", ")}`),
1946
1990
  name: z.string().optional().describe("Override the generated class/module name"),
1947
1991
  template: z.string().optional().describe("Custom Handlebars template path"),
1948
1992
  output: z.string().min(1, "Output path is required").describe("Output file or directory path"),
@@ -1960,15 +2004,36 @@ var ExtraVariable = class {
1960
2004
  this.value = value;
1961
2005
  }
1962
2006
  };
1963
- function createExtraVariables(sources) {
2007
+ function createExtraVariables(sources, suppressLogging = false) {
1964
2008
  return sources.map((source) => {
1965
2009
  const path = source.path;
1966
2010
  const resolvedPath = path.replace("$HOME", homedir());
1967
2011
  const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
1968
- consola.info("Extra variable:", varName, resolvedPath);
2012
+ if (!suppressLogging) consola.info("Extra variable:", varName, resolvedPath);
1969
2013
  return new ExtraVariable(varName, `'${resolvedPath}'`);
1970
2014
  });
1971
2015
  }
2016
+ function buildProjectFromCliOptions(options) {
2017
+ if (!isValidGenerator(options.generator)) {
2018
+ const similar = findSimilarGenerators(options.generator);
2019
+ const allGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
2020
+ throw new InvalidGeneratorError(options.generator, allGenerators, similar.length > 0 ? similar[0] : void 0);
2021
+ }
2022
+ const generatorInfo = parseGenerator(options.generator);
2023
+ const genConfig = {
2024
+ generator: options.generator,
2025
+ output: options.output || "."
2026
+ };
2027
+ if (generatorInfo.language === "java") genConfig.config = { package: "generated" };
2028
+ return {
2029
+ version: 1,
2030
+ name: options.name || "generated",
2031
+ sql: [{
2032
+ files: options.files,
2033
+ gen: [genConfig]
2034
+ }]
2035
+ };
2036
+ }
1972
2037
  /**
1973
2038
  * Parse and validate project configuration with helpful error messages
1974
2039
  */
@@ -1990,12 +2055,12 @@ function parseProjectConfig(filePath) {
1990
2055
  const obj = parsed;
1991
2056
  if (obj.sql && Array.isArray(obj.sql)) for (let i = 0; i < obj.sql.length; i++) {
1992
2057
  const sqlConfig = obj.sql[i];
1993
- if (sqlConfig.engine && !DB_ENGINES.includes(sqlConfig.engine)) throw new InvalidEngineError(String(sqlConfig.engine), [...DB_ENGINES]);
1994
2058
  if (sqlConfig.gen && Array.isArray(sqlConfig.gen)) for (let j = 0; j < sqlConfig.gen.length; j++) {
1995
2059
  const genConfig = sqlConfig.gen[j];
1996
- if (genConfig.generator && !GENERATOR_NAMES.includes(genConfig.generator)) {
2060
+ if (genConfig.generator && !isValidGenerator(String(genConfig.generator))) {
1997
2061
  const similar = findSimilarGenerators(String(genConfig.generator));
1998
- throw new InvalidGeneratorError(String(genConfig.generator), [...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
2062
+ const allGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
2063
+ throw new InvalidGeneratorError(String(genConfig.generator), allGenerators, similar.length > 0 ? similar[0] : void 0);
1999
2064
  }
2000
2065
  }
2001
2066
  }
@@ -2039,11 +2104,16 @@ function validateQueries(queries) {
2039
2104
  };
2040
2105
  }
2041
2106
  }
2042
- async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine) {
2107
+ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false) {
2043
2108
  await generator.beforeGenerate(projectDir, gen, queries, tables);
2044
2109
  const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
2045
2110
  const name = gen.name ?? basename(file, extname(file));
2046
2111
  const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.config);
2112
+ if (writeToStdout) {
2113
+ process.stdout.write(sourceFile);
2114
+ if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
2115
+ return null;
2116
+ }
2047
2117
  const outputPath = getOutputPath(projectDir, name, gen, generator);
2048
2118
  writeFileSync(outputPath, sourceFile);
2049
2119
  consola.success(`Generated ${outputPath}`);
@@ -2051,33 +2121,11 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
2051
2121
  return outputPath;
2052
2122
  }
2053
2123
  /**
2054
- * Validate project configuration without executing queries
2124
+ * Validate project configuration from a Project object without executing queries
2055
2125
  * Use this for pre-flight checks before generation
2056
2126
  */
2057
- async function validateProject(projectPath) {
2127
+ async function validateProjectFromConfig(project, projectDir) {
2058
2128
  const errors = [];
2059
- const projectDir = resolve(dirname(projectPath));
2060
- let project;
2061
- try {
2062
- project = parseProjectConfig(projectPath);
2063
- } catch (e) {
2064
- if (e instanceof SqgError) return {
2065
- valid: false,
2066
- errors: [{
2067
- code: e.code,
2068
- message: e.message,
2069
- suggestion: e.suggestion,
2070
- context: e.context
2071
- }]
2072
- };
2073
- return {
2074
- valid: false,
2075
- errors: [{
2076
- code: "UNKNOWN_ERROR",
2077
- message: String(e)
2078
- }]
2079
- };
2080
- }
2081
2129
  const sqlFiles = [];
2082
2130
  const generators = [];
2083
2131
  for (const sql of project.sql) for (const sqlFile of sql.files) {
@@ -2091,15 +2139,15 @@ async function validateProject(projectPath) {
2091
2139
  });
2092
2140
  for (const gen of sql.gen) {
2093
2141
  generators.push(gen.generator);
2094
- if (!SUPPORTED_GENERATORS[gen.generator].compatibleEngines.includes(sql.engine)) errors.push({
2095
- code: "GENERATOR_ENGINE_MISMATCH",
2096
- message: `Generator '${gen.generator}' is not compatible with engine '${sql.engine}'`,
2097
- suggestion: `For '${sql.engine}', use one of: ${Object.entries(SUPPORTED_GENERATORS).filter(([_, info]) => info.compatibleEngines.includes(sql.engine)).map(([name]) => name).join(", ")}`,
2098
- context: {
2099
- generator: gen.generator,
2100
- engine: sql.engine
2101
- }
2102
- });
2142
+ if (!isValidGenerator(gen.generator)) {
2143
+ const similar = findSimilarGenerators(gen.generator);
2144
+ errors.push({
2145
+ code: "INVALID_GENERATOR",
2146
+ message: `Invalid generator '${gen.generator}'`,
2147
+ suggestion: similar.length > 0 ? `Did you mean '${similar[0]}'?` : `Valid generators: ${SHORT_GENERATOR_NAMES.join(", ")}`,
2148
+ context: { generator: gen.generator }
2149
+ });
2150
+ }
2103
2151
  }
2104
2152
  }
2105
2153
  return {
@@ -2114,53 +2162,96 @@ async function validateProject(projectPath) {
2114
2162
  };
2115
2163
  }
2116
2164
  /**
2117
- * Process a project configuration and generate code
2165
+ * Validate project configuration from a YAML file without executing queries
2166
+ * Use this for pre-flight checks before generation
2118
2167
  */
2119
- async function processProject(projectPath) {
2168
+ async function validateProject(projectPath) {
2120
2169
  const projectDir = resolve(dirname(projectPath));
2121
- const project = parseProjectConfig(projectPath);
2122
- const extraVariables = createExtraVariables(project.sources ?? []);
2123
- if (extraVariables.length > 0) consola.info("Extra variables:", extraVariables);
2124
- const files = [];
2125
- for (const sql of project.sql) {
2126
- for (const gen of sql.gen) {
2127
- const generatorInfo = SUPPORTED_GENERATORS[gen.generator];
2128
- if (!generatorInfo.compatibleEngines.includes(sql.engine)) throw new GeneratorEngineMismatchError(gen.generator, sql.engine, generatorInfo.compatibleEngines);
2129
- }
2130
- for (const sqlFile of sql.files) {
2131
- const fullPath = join(projectDir, sqlFile);
2132
- if (!existsSync(fullPath)) throw new FileNotFoundError(fullPath, projectDir);
2133
- let queries;
2134
- let tables;
2135
- try {
2136
- const parseResult = parseSQLQueries(fullPath, extraVariables);
2137
- queries = parseResult.queries;
2138
- tables = parseResult.tables;
2139
- } catch (e) {
2140
- if (e instanceof SqgError) throw e;
2141
- throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
2142
- }
2143
- try {
2144
- const dbEngine = getDatabaseEngine(sql.engine);
2145
- await dbEngine.initializeDatabase(queries);
2146
- await dbEngine.executeQueries(queries);
2147
- if (tables.length > 0) await dbEngine.introspectTables(tables);
2148
- validateQueries(queries);
2149
- await dbEngine.close();
2150
- } catch (e) {
2151
- if (e instanceof SqgError) throw e;
2152
- throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${sql.engine}'`, {
2153
- file: sqlFile,
2154
- engine: sql.engine
2155
- });
2156
- }
2170
+ let project;
2171
+ try {
2172
+ project = parseProjectConfig(projectPath);
2173
+ } catch (e) {
2174
+ if (e instanceof SqgError) return {
2175
+ valid: false,
2176
+ errors: [{
2177
+ code: e.code,
2178
+ message: e.message,
2179
+ suggestion: e.suggestion,
2180
+ context: e.context
2181
+ }]
2182
+ };
2183
+ return {
2184
+ valid: false,
2185
+ errors: [{
2186
+ code: "UNKNOWN_ERROR",
2187
+ message: String(e)
2188
+ }]
2189
+ };
2190
+ }
2191
+ return await validateProjectFromConfig(project, projectDir);
2192
+ }
2193
+ /**
2194
+ * Process a project configuration and generate code from a Project object
2195
+ */
2196
+ async function processProjectFromConfig(project, projectDir, writeToStdout = false) {
2197
+ const originalLevel = consola.level;
2198
+ if (writeToStdout) consola.level = LogLevels.silent;
2199
+ try {
2200
+ const extraVariables = createExtraVariables(project.sources ?? [], writeToStdout);
2201
+ const files = [];
2202
+ for (const sql of project.sql) {
2203
+ const gensByEngine = /* @__PURE__ */ new Map();
2157
2204
  for (const gen of sql.gen) {
2158
- const outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries, tables, sql.engine);
2159
- files.push(outputPath);
2205
+ const engine = getGeneratorEngine(gen.generator);
2206
+ if (!gensByEngine.has(engine)) gensByEngine.set(engine, []);
2207
+ gensByEngine.get(engine).push(gen);
2208
+ }
2209
+ for (const sqlFile of sql.files) {
2210
+ const fullPath = join(projectDir, sqlFile);
2211
+ if (!existsSync(fullPath)) throw new FileNotFoundError(fullPath, projectDir);
2212
+ let queries;
2213
+ let tables;
2214
+ try {
2215
+ const parseResult = parseSQLQueries(fullPath, extraVariables);
2216
+ queries = parseResult.queries;
2217
+ tables = parseResult.tables;
2218
+ } catch (e) {
2219
+ if (e instanceof SqgError) throw e;
2220
+ throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
2221
+ }
2222
+ for (const [engine, gens] of gensByEngine) {
2223
+ try {
2224
+ const dbEngine = getDatabaseEngine(engine);
2225
+ await dbEngine.initializeDatabase(queries);
2226
+ await dbEngine.executeQueries(queries);
2227
+ if (tables.length > 0) await dbEngine.introspectTables(tables);
2228
+ validateQueries(queries);
2229
+ await dbEngine.close();
2230
+ } catch (e) {
2231
+ if (e instanceof SqgError) throw e;
2232
+ throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${engine}'`, {
2233
+ file: sqlFile,
2234
+ engine
2235
+ });
2236
+ }
2237
+ for (const gen of gens) {
2238
+ const outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries, tables, engine, writeToStdout);
2239
+ if (outputPath !== null) files.push(outputPath);
2240
+ }
2241
+ }
2160
2242
  }
2161
2243
  }
2244
+ return files;
2245
+ } finally {
2246
+ if (writeToStdout) consola.level = originalLevel;
2162
2247
  }
2163
- return files;
2248
+ }
2249
+ /**
2250
+ * Process a project configuration and generate code from a YAML file
2251
+ */
2252
+ async function processProject(projectPath) {
2253
+ const projectDir = resolve(dirname(projectPath));
2254
+ return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false);
2164
2255
  }
2165
2256
 
2166
2257
  //#endregion
@@ -2169,14 +2260,10 @@ async function processProject(projectPath) {
2169
2260
  * SQG Project Initialization - Creates new SQG projects with example files
2170
2261
  */
2171
2262
  /**
2172
- * Get the default generator for an engine
2263
+ * Get the default generator for a language preference
2173
2264
  */
2174
- function getDefaultGenerator(engine) {
2175
- return {
2176
- sqlite: "typescript/better-sqlite3",
2177
- duckdb: "typescript/duckdb",
2178
- postgres: "java/jdbc"
2179
- }[engine];
2265
+ function getDefaultGenerator() {
2266
+ return "typescript/sqlite";
2180
2267
  }
2181
2268
  /**
2182
2269
  * Generate example SQL content based on engine
@@ -2374,21 +2461,8 @@ DELETE FROM posts WHERE id = \${id};
2374
2461
  /**
2375
2462
  * Generate sqg.yaml configuration
2376
2463
  */
2377
- function getConfigYaml(engine, generator, output) {
2378
- SUPPORTED_GENERATORS[generator];
2379
- const config = {
2380
- version: 1,
2381
- name: "my-project",
2382
- sql: [{
2383
- engine,
2384
- files: ["queries.sql"],
2385
- gen: [{
2386
- generator,
2387
- output: output.endsWith("/") ? output : `${output}/`
2388
- }]
2389
- }]
2390
- };
2391
- if (generator.startsWith("java/")) config.sql[0].gen[0].config = { package: "generated" };
2464
+ function getConfigYaml(generator, output) {
2465
+ const isJava = parseGenerator(generator).language === "java";
2392
2466
  return `# SQG Configuration
2393
2467
  # Generated by: sqg init
2394
2468
  # Documentation: https://sqg.dev
@@ -2397,12 +2471,11 @@ version: 1
2397
2471
  name: my-project
2398
2472
 
2399
2473
  sql:
2400
- - engine: ${engine}
2401
- files:
2474
+ - files:
2402
2475
  - queries.sql
2403
2476
  gen:
2404
2477
  - generator: ${generator}
2405
- output: ${output.endsWith("/") ? output : `${output}/`}${generator.startsWith("java/") ? `
2478
+ output: ${output.endsWith("/") ? output : `${output}/`}${isJava ? `
2406
2479
  config:
2407
2480
  package: generated` : ""}
2408
2481
  `;
@@ -2411,18 +2484,13 @@ sql:
2411
2484
  * Initialize a new SQG project
2412
2485
  */
2413
2486
  async function initProject(options) {
2414
- const engine = options.engine || "sqlite";
2487
+ const generator = options.generator || getDefaultGenerator();
2415
2488
  const output = options.output || "./generated";
2416
- if (!DB_ENGINES.includes(engine)) throw new InvalidEngineError(engine, [...DB_ENGINES]);
2417
- let generator;
2418
- if (options.generator) {
2419
- if (!(options.generator in SUPPORTED_GENERATORS)) {
2420
- const similar = findSimilarGenerators(options.generator);
2421
- throw new InvalidGeneratorError(options.generator, Object.keys(SUPPORTED_GENERATORS), similar.length > 0 ? similar[0] : void 0);
2422
- }
2423
- generator = options.generator;
2424
- if (!SUPPORTED_GENERATORS[generator].compatibleEngines.includes(engine)) throw new SqgError(`Generator '${generator}' is not compatible with engine '${engine}'`, "GENERATOR_ENGINE_MISMATCH", `For '${engine}', use one of: ${Object.entries(SUPPORTED_GENERATORS).filter(([_, info]) => info.compatibleEngines.includes(engine)).map(([name]) => name).join(", ")}`);
2425
- } else generator = getDefaultGenerator(engine);
2489
+ if (!isValidGenerator(generator)) {
2490
+ const similar = findSimilarGenerators(generator);
2491
+ throw new InvalidGeneratorError(generator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
2492
+ }
2493
+ const engine = parseGenerator(generator).engine;
2426
2494
  const configPath = "sqg.yaml";
2427
2495
  const sqlPath = "queries.sql";
2428
2496
  if (!options.force) {
@@ -2433,16 +2501,16 @@ async function initProject(options) {
2433
2501
  mkdirSync(output, { recursive: true });
2434
2502
  consola.success(`Created output directory: ${output}`);
2435
2503
  }
2436
- writeFileSync(configPath, getConfigYaml(engine, generator, output));
2504
+ writeFileSync(configPath, getConfigYaml(generator, output));
2437
2505
  consola.success(`Created ${configPath}`);
2438
2506
  writeFileSync(sqlPath, getExampleSql(engine));
2439
2507
  consola.success(`Created ${sqlPath}`);
2440
2508
  consola.box(`
2441
2509
  SQG project initialized!
2442
2510
 
2443
- Engine: ${engine}
2444
2511
  Generator: ${generator}
2445
- Output: ${output}
2512
+ Engine: ${engine}
2513
+ Output: ${output}
2446
2514
 
2447
2515
  Next steps:
2448
2516
  1. Edit queries.sql to add your SQL queries
@@ -2455,46 +2523,81 @@ Documentation: https://sqg.dev
2455
2523
 
2456
2524
  //#endregion
2457
2525
  //#region src/sqg.ts
2458
- const version = process.env.npm_package_version ?? "0.4.0";
2526
+ const version = process.env.npm_package_version ?? "0.5.0";
2459
2527
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
2460
2528
  consola.level = LogLevels.info;
2461
2529
  const program = new Command().name("sqg").description(`${description}
2462
2530
 
2463
2531
  Generate type-safe database access code from annotated SQL files.
2464
2532
 
2465
- Supported Engines:
2466
- ${formatEnginesHelp()}
2467
-
2468
2533
  Supported Generators:
2469
- ${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").showHelpAfterError().showSuggestionAfterError();
2470
- program.argument("<project>", "Path to the project YAML config (sqg.yaml)").hook("preAction", (thisCommand) => {
2534
+ ${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 = []) => {
2535
+ prev.push(val);
2536
+ return prev;
2537
+ }).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();
2538
+ program.argument("[project]", "Path to the project YAML config (sqg.yaml) or omit to use CLI options").hook("preAction", (thisCommand) => {
2471
2539
  const opts = thisCommand.opts();
2472
2540
  if (opts.verbose) consola.level = LogLevels.debug;
2473
2541
  if (opts.format === "json") consola.level = LogLevels.silent;
2474
2542
  }).action(async (projectPath, options) => {
2475
2543
  try {
2476
- if (options.validate) {
2477
- const result = await validateProject(projectPath);
2478
- if (options.format === "json") console.log(JSON.stringify(result, null, 2));
2479
- else if (result.valid) {
2480
- consola.success("Configuration is valid");
2481
- consola.info(`Project: ${result.project?.name}`);
2482
- consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
2483
- consola.info(`Generators: ${result.generators?.join(", ")}`);
2484
- } else {
2485
- consola.error("Validation failed");
2486
- for (const error of result.errors || []) {
2487
- consola.error(` ${error.message}`);
2488
- if (error.suggestion) consola.info(` Suggestion: ${error.suggestion}`);
2544
+ if (!projectPath) {
2545
+ if (!options.generator) throw new SqgError("Missing required option: --generator", "CONFIG_VALIDATION_ERROR", `Specify a code generation generator: ${SHORT_GENERATOR_NAMES.join(", ")}`);
2546
+ if (!options.file || options.file.length === 0) throw new SqgError("Missing required option: --file", "CONFIG_VALIDATION_ERROR", "Specify at least one SQL file with --file <path>");
2547
+ const project = buildProjectFromCliOptions({
2548
+ generator: options.generator,
2549
+ files: options.file,
2550
+ output: options.output,
2551
+ name: options.name
2552
+ });
2553
+ const projectDir = process.cwd();
2554
+ const writeToStdout = !options.output;
2555
+ if (options.validate) {
2556
+ const result = await validateProjectFromConfig(project, projectDir);
2557
+ if (options.format === "json") console.log(JSON.stringify(result, null, 2));
2558
+ else if (result.valid) {
2559
+ consola.success("Configuration is valid");
2560
+ consola.info(`Project: ${result.project?.name}`);
2561
+ consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
2562
+ consola.info(`Generators: ${result.generators?.join(", ")}`);
2563
+ } else {
2564
+ consola.error("Validation failed");
2565
+ for (const error of result.errors || []) {
2566
+ consola.error(` ${error.message}`);
2567
+ if (error.suggestion) consola.info(` Suggestion: ${error.suggestion}`);
2568
+ }
2569
+ }
2570
+ exit(result.valid ? 0 : 1);
2571
+ }
2572
+ const files = await processProjectFromConfig(project, projectDir, writeToStdout);
2573
+ if (options.format === "json" && !writeToStdout) console.log(JSON.stringify({
2574
+ status: "success",
2575
+ generatedFiles: files
2576
+ }));
2577
+ } else {
2578
+ if (options.validate) {
2579
+ const result = await validateProject(projectPath);
2580
+ if (options.format === "json") console.log(JSON.stringify(result, null, 2));
2581
+ else if (result.valid) {
2582
+ consola.success("Configuration is valid");
2583
+ consola.info(`Project: ${result.project?.name}`);
2584
+ consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
2585
+ consola.info(`Generators: ${result.generators?.join(", ")}`);
2586
+ } else {
2587
+ consola.error("Validation failed");
2588
+ for (const error of result.errors || []) {
2589
+ consola.error(` ${error.message}`);
2590
+ if (error.suggestion) consola.info(` Suggestion: ${error.suggestion}`);
2591
+ }
2489
2592
  }
2593
+ exit(result.valid ? 0 : 1);
2490
2594
  }
2491
- exit(result.valid ? 0 : 1);
2595
+ const files = await processProject(projectPath);
2596
+ if (options.format === "json") console.log(JSON.stringify({
2597
+ status: "success",
2598
+ generatedFiles: files
2599
+ }));
2492
2600
  }
2493
- const files = await processProject(projectPath);
2494
- if (options.format === "json") console.log(JSON.stringify({
2495
- status: "success",
2496
- generatedFiles: files
2497
- }));
2498
2601
  } catch (err) {
2499
2602
  if (options.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
2500
2603
  else if (err instanceof SqgError) {
@@ -2505,7 +2608,7 @@ program.argument("<project>", "Path to the project YAML config (sqg.yaml)").hook
2505
2608
  exit(1);
2506
2609
  }
2507
2610
  });
2508
- program.command("init").description("Initialize a new SQG project with example configuration").option("-e, --engine <engine>", `Database engine (${DB_ENGINES.join(", ")})`, "sqlite").option("-g, --generator <generator>", `Code generator (${GENERATOR_NAMES.join(", ")})`).option("-o, --output <dir>", "Output directory for generated files", "./generated").option("-f, --force", "Overwrite existing files").action(async (options) => {
2611
+ 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) => {
2509
2612
  const parentOpts = program.opts();
2510
2613
  try {
2511
2614
  await initProject(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
5
5
  "type": "module",
6
6
  "bin": {