@sqg/sqg 0.3.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.
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 SUPPORTED_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 SUPPORTED_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
  }
@@ -406,11 +440,15 @@ function parseSQLQueries(filePath, extraVariables) {
406
440
  const queries = [];
407
441
  const tables = [];
408
442
  const cursor = parser.parse(content).cursor();
443
+ function getLineNumber(position) {
444
+ return content.slice(0, position).split("\n").length;
445
+ }
409
446
  function getStr(nodeName, optional = false) {
410
447
  const node = cursor.node.getChild(nodeName);
411
448
  if (!node) {
412
449
  if (optional) return;
413
- throw new Error(`${nodeName} not found`);
450
+ const lineNumber = getLineNumber(cursor.node.from);
451
+ throw new Error(`Node '${nodeName}' not found at line ${lineNumber}`);
414
452
  }
415
453
  return nodeStr(node);
416
454
  }
@@ -1328,6 +1366,9 @@ var BaseGenerator = class {
1328
1366
  isCompatibleWith(_engine) {
1329
1367
  return true;
1330
1368
  }
1369
+ supportsAppenders(_engine) {
1370
+ return false;
1371
+ }
1331
1372
  functionReturnType(query) {
1332
1373
  if (query.isOne) return this.rowType(query);
1333
1374
  return this.typeMapper.formatListType(this.rowType(query));
@@ -1351,6 +1392,9 @@ var JavaGenerator = class extends BaseGenerator {
1351
1392
  super(template, new JavaTypeMapper());
1352
1393
  this.template = template;
1353
1394
  }
1395
+ supportsAppenders(engine) {
1396
+ return engine === "duckdb";
1397
+ }
1354
1398
  getFunctionName(id) {
1355
1399
  return camelCase$1(id);
1356
1400
  }
@@ -1447,14 +1491,17 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
1447
1491
  const name = `${gen.name}-jdbc`;
1448
1492
  writeGeneratedFile(projectDir, {
1449
1493
  name,
1450
- generator: "java/jdbc",
1494
+ generator: "java/duckdb/jdbc",
1451
1495
  output: gen.output,
1452
1496
  config: gen.config
1453
- }, this.javaGenerator, name, q, tables);
1497
+ }, this.javaGenerator, name, q, tables, "duckdb");
1454
1498
  }
1455
1499
  isCompatibleWith(engine) {
1456
1500
  return engine === "duckdb";
1457
1501
  }
1502
+ supportsAppenders(_engine) {
1503
+ return true;
1504
+ }
1458
1505
  getFilename(sqlFileName) {
1459
1506
  return this.javaGenerator.getFilename(sqlFileName);
1460
1507
  }
@@ -1462,25 +1509,28 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
1462
1509
  return this.javaGenerator.getClassName(name);
1463
1510
  }
1464
1511
  mapType(column) {
1465
- const { type, nullable } = column;
1512
+ const { type } = column;
1466
1513
  if (typeof type === "string") {
1467
1514
  const mappedType = {
1468
1515
  INTEGER: "IntVector",
1516
+ BIGINT: "BigIntVector",
1469
1517
  BOOLEAN: "BitVector",
1470
1518
  DOUBLE: "Float8Vector",
1471
1519
  FLOAT: "Float4Vector",
1472
1520
  VARCHAR: "VarCharVector",
1473
- TEXT: "VarCharVector"
1521
+ TEXT: "VarCharVector",
1522
+ TIMESTAMP: "TimeStampVector",
1523
+ DATE: "DateDayVector",
1524
+ TIME: "TimeMicroVector"
1474
1525
  }[type.toUpperCase()];
1475
1526
  if (!mappedType) consola.warn("(duckdb-arrow) Mapped type is unknown:", type);
1476
1527
  return mappedType ?? "Object";
1477
1528
  }
1478
- const mockColumn = {
1479
- name: "",
1480
- type,
1481
- nullable
1482
- };
1483
- return this.typeMapper.getTypeName(mockColumn);
1529
+ if (type instanceof ListType) return "ListVector";
1530
+ if (type instanceof StructType) return "StructVector";
1531
+ if (type instanceof MapType) return "MapVector";
1532
+ consola.warn("(duckdb-arrow) Unknown complex type:", type);
1533
+ return "Object";
1484
1534
  }
1485
1535
  mapParameterType(type, nullable) {
1486
1536
  return this.typeMapper.getTypeName({
@@ -1688,6 +1738,9 @@ var TsDuckDBGenerator = class extends TsGenerator {
1688
1738
  constructor(template) {
1689
1739
  super(template);
1690
1740
  }
1741
+ supportsAppenders(_engine) {
1742
+ return true;
1743
+ }
1691
1744
  async beforeGenerate(projectDir, gen, queries, tables) {
1692
1745
  await super.beforeGenerate(projectDir, gen, queries, tables);
1693
1746
  Handlebars.registerHelper("tsType", (column) => {
@@ -1735,22 +1788,31 @@ var TsDuckDBGenerator = class extends TsGenerator {
1735
1788
 
1736
1789
  //#endregion
1737
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
+ */
1738
1795
  function getGenerator(generator) {
1739
- switch (generator) {
1740
- case "java/jdbc": return new JavaGenerator("templates/java-jdbc.hbs");
1741
- case "java/duckdb-arrow": return new JavaDuckDBArrowGenerator("templates/java-duckdb-arrow.hbs");
1742
- case "typescript/better-sqlite3": return new TsGenerator("templates/better-sqlite3.hbs");
1743
- case "typescript/duckdb": return new TsDuckDBGenerator("templates/typescript-duckdb.hbs");
1744
- default: {
1745
- const similar = findSimilarGenerators(generator);
1746
- 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}`);
1747
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);
1748
1810
  }
1749
1811
  }
1750
1812
 
1751
1813
  //#endregion
1752
1814
  //#region src/sqltool.ts
1753
- const GENERATED_FILE_COMMENT = "This file is generated by SQG. Do not edit manually.";
1815
+ const GENERATED_FILE_COMMENT = "This file is generated by SQG (https://sqg.dev). Do not edit manually.";
1754
1816
  const configSchema = z.object({ result: z.record(z.string(), z.string()).optional() });
1755
1817
  var Config = class Config {
1756
1818
  constructor(result) {
@@ -1894,13 +1956,13 @@ var TableHelper = class {
1894
1956
  return this.generator.typeMapper;
1895
1957
  }
1896
1958
  };
1897
- function generateSourceFile(name, queries, tables, templatePath, generator, config) {
1959
+ function generateSourceFile(name, queries, tables, templatePath, generator, engine, config) {
1898
1960
  const templateSrc = readFileSync(templatePath, "utf-8");
1899
1961
  const template = Handlebars.compile(templateSrc);
1900
1962
  Handlebars.registerHelper("mapType", (column) => generator.mapType(column));
1901
1963
  Handlebars.registerHelper("plusOne", (value) => value + 1);
1902
1964
  const migrations = queries.filter((q) => q.isMigrate).map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q)));
1903
- const tableHelpers = tables.filter((t) => !t.skipGenerateFunction).map((t) => new TableHelper(t, generator));
1965
+ const tableHelpers = generator.supportsAppenders(engine) ? tables.filter((t) => !t.skipGenerateFunction).map((t) => new TableHelper(t, generator)) : [];
1904
1966
  return template({
1905
1967
  generatedComment: GENERATED_FILE_COMMENT,
1906
1968
  migrations,
@@ -1913,6 +1975,8 @@ function generateSourceFile(name, queries, tables, templatePath, generator, conf
1913
1975
  allowProtoMethodsByDefault: true
1914
1976
  });
1915
1977
  }
1978
+ /** All valid generator strings for schema validation */
1979
+ const validGenerators = [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES];
1916
1980
  /**
1917
1981
  * Project configuration schema with descriptions for validation messages
1918
1982
  */
@@ -1920,10 +1984,9 @@ const ProjectSchema = z.object({
1920
1984
  version: z.number().describe("Configuration version (currently 1)"),
1921
1985
  name: z.string().min(1, "Project name is required").describe("Project name used for generated class names"),
1922
1986
  sql: z.array(z.object({
1923
- engine: z.enum(SUPPORTED_ENGINES).describe(`Database engine: ${SUPPORTED_ENGINES.join(", ")}`),
1924
1987
  files: z.array(z.string().min(1)).min(1, "At least one SQL file is required").describe("SQL files to process"),
1925
1988
  gen: z.array(z.object({
1926
- 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(", ")}`),
1927
1990
  name: z.string().optional().describe("Override the generated class/module name"),
1928
1991
  template: z.string().optional().describe("Custom Handlebars template path"),
1929
1992
  output: z.string().min(1, "Output path is required").describe("Output file or directory path"),
@@ -1941,15 +2004,36 @@ var ExtraVariable = class {
1941
2004
  this.value = value;
1942
2005
  }
1943
2006
  };
1944
- function createExtraVariables(sources) {
2007
+ function createExtraVariables(sources, suppressLogging = false) {
1945
2008
  return sources.map((source) => {
1946
2009
  const path = source.path;
1947
2010
  const resolvedPath = path.replace("$HOME", homedir());
1948
2011
  const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
1949
- consola.info("Extra variable:", varName, resolvedPath);
2012
+ if (!suppressLogging) consola.info("Extra variable:", varName, resolvedPath);
1950
2013
  return new ExtraVariable(varName, `'${resolvedPath}'`);
1951
2014
  });
1952
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
+ }
1953
2037
  /**
1954
2038
  * Parse and validate project configuration with helpful error messages
1955
2039
  */
@@ -1971,12 +2055,12 @@ function parseProjectConfig(filePath) {
1971
2055
  const obj = parsed;
1972
2056
  if (obj.sql && Array.isArray(obj.sql)) for (let i = 0; i < obj.sql.length; i++) {
1973
2057
  const sqlConfig = obj.sql[i];
1974
- if (sqlConfig.engine && !SUPPORTED_ENGINES.includes(sqlConfig.engine)) throw new InvalidEngineError(String(sqlConfig.engine), [...SUPPORTED_ENGINES]);
1975
2058
  if (sqlConfig.gen && Array.isArray(sqlConfig.gen)) for (let j = 0; j < sqlConfig.gen.length; j++) {
1976
2059
  const genConfig = sqlConfig.gen[j];
1977
- if (genConfig.generator && !GENERATOR_NAMES.includes(genConfig.generator)) {
2060
+ if (genConfig.generator && !isValidGenerator(String(genConfig.generator))) {
1978
2061
  const similar = findSimilarGenerators(String(genConfig.generator));
1979
- 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);
1980
2064
  }
1981
2065
  }
1982
2066
  }
@@ -2020,11 +2104,16 @@ function validateQueries(queries) {
2020
2104
  };
2021
2105
  }
2022
2106
  }
2023
- async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables = []) {
2107
+ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false) {
2024
2108
  await generator.beforeGenerate(projectDir, gen, queries, tables);
2025
2109
  const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
2026
2110
  const name = gen.name ?? basename(file, extname(file));
2027
- const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, gen.config);
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
+ }
2028
2117
  const outputPath = getOutputPath(projectDir, name, gen, generator);
2029
2118
  writeFileSync(outputPath, sourceFile);
2030
2119
  consola.success(`Generated ${outputPath}`);
@@ -2032,33 +2121,11 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
2032
2121
  return outputPath;
2033
2122
  }
2034
2123
  /**
2035
- * Validate project configuration without executing queries
2124
+ * Validate project configuration from a Project object without executing queries
2036
2125
  * Use this for pre-flight checks before generation
2037
2126
  */
2038
- async function validateProject(projectPath) {
2127
+ async function validateProjectFromConfig(project, projectDir) {
2039
2128
  const errors = [];
2040
- const projectDir = resolve(dirname(projectPath));
2041
- let project;
2042
- try {
2043
- project = parseProjectConfig(projectPath);
2044
- } catch (e) {
2045
- if (e instanceof SqgError) return {
2046
- valid: false,
2047
- errors: [{
2048
- code: e.code,
2049
- message: e.message,
2050
- suggestion: e.suggestion,
2051
- context: e.context
2052
- }]
2053
- };
2054
- return {
2055
- valid: false,
2056
- errors: [{
2057
- code: "UNKNOWN_ERROR",
2058
- message: String(e)
2059
- }]
2060
- };
2061
- }
2062
2129
  const sqlFiles = [];
2063
2130
  const generators = [];
2064
2131
  for (const sql of project.sql) for (const sqlFile of sql.files) {
@@ -2072,15 +2139,15 @@ async function validateProject(projectPath) {
2072
2139
  });
2073
2140
  for (const gen of sql.gen) {
2074
2141
  generators.push(gen.generator);
2075
- if (!SUPPORTED_GENERATORS[gen.generator].compatibleEngines.includes(sql.engine)) errors.push({
2076
- code: "GENERATOR_ENGINE_MISMATCH",
2077
- message: `Generator '${gen.generator}' is not compatible with engine '${sql.engine}'`,
2078
- suggestion: `For '${sql.engine}', use one of: ${Object.entries(SUPPORTED_GENERATORS).filter(([_, info]) => info.compatibleEngines.includes(sql.engine)).map(([name]) => name).join(", ")}`,
2079
- context: {
2080
- generator: gen.generator,
2081
- engine: sql.engine
2082
- }
2083
- });
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
+ }
2084
2151
  }
2085
2152
  }
2086
2153
  return {
@@ -2095,53 +2162,96 @@ async function validateProject(projectPath) {
2095
2162
  };
2096
2163
  }
2097
2164
  /**
2098
- * 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
2099
2167
  */
2100
- async function processProject(projectPath) {
2168
+ async function validateProject(projectPath) {
2101
2169
  const projectDir = resolve(dirname(projectPath));
2102
- const project = parseProjectConfig(projectPath);
2103
- const extraVariables = createExtraVariables(project.sources ?? []);
2104
- if (extraVariables.length > 0) consola.info("Extra variables:", extraVariables);
2105
- const files = [];
2106
- for (const sql of project.sql) {
2107
- for (const gen of sql.gen) {
2108
- const generatorInfo = SUPPORTED_GENERATORS[gen.generator];
2109
- if (!generatorInfo.compatibleEngines.includes(sql.engine)) throw new GeneratorEngineMismatchError(gen.generator, sql.engine, generatorInfo.compatibleEngines);
2110
- }
2111
- for (const sqlFile of sql.files) {
2112
- const fullPath = join(projectDir, sqlFile);
2113
- if (!existsSync(fullPath)) throw new FileNotFoundError(fullPath, projectDir);
2114
- let queries;
2115
- let tables;
2116
- try {
2117
- const parseResult = parseSQLQueries(fullPath, extraVariables);
2118
- queries = parseResult.queries;
2119
- tables = parseResult.tables;
2120
- } catch (e) {
2121
- if (e instanceof SqgError) throw e;
2122
- throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
2123
- }
2124
- try {
2125
- const dbEngine = getDatabaseEngine(sql.engine);
2126
- await dbEngine.initializeDatabase(queries);
2127
- await dbEngine.executeQueries(queries);
2128
- if (tables.length > 0) await dbEngine.introspectTables(tables);
2129
- validateQueries(queries);
2130
- await dbEngine.close();
2131
- } catch (e) {
2132
- if (e instanceof SqgError) throw e;
2133
- throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${sql.engine}'`, {
2134
- file: sqlFile,
2135
- engine: sql.engine
2136
- });
2137
- }
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();
2138
2204
  for (const gen of sql.gen) {
2139
- const outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries, tables);
2140
- 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
+ }
2141
2242
  }
2142
2243
  }
2244
+ return files;
2245
+ } finally {
2246
+ if (writeToStdout) consola.level = originalLevel;
2143
2247
  }
2144
- 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);
2145
2255
  }
2146
2256
 
2147
2257
  //#endregion
@@ -2150,14 +2260,10 @@ async function processProject(projectPath) {
2150
2260
  * SQG Project Initialization - Creates new SQG projects with example files
2151
2261
  */
2152
2262
  /**
2153
- * Get the default generator for an engine
2263
+ * Get the default generator for a language preference
2154
2264
  */
2155
- function getDefaultGenerator(engine) {
2156
- return {
2157
- sqlite: "typescript/better-sqlite3",
2158
- duckdb: "typescript/duckdb",
2159
- postgres: "java/jdbc"
2160
- }[engine];
2265
+ function getDefaultGenerator() {
2266
+ return "typescript/sqlite";
2161
2267
  }
2162
2268
  /**
2163
2269
  * Generate example SQL content based on engine
@@ -2355,21 +2461,8 @@ DELETE FROM posts WHERE id = \${id};
2355
2461
  /**
2356
2462
  * Generate sqg.yaml configuration
2357
2463
  */
2358
- function getConfigYaml(engine, generator, output) {
2359
- SUPPORTED_GENERATORS[generator];
2360
- const config = {
2361
- version: 1,
2362
- name: "my-project",
2363
- sql: [{
2364
- engine,
2365
- files: ["queries.sql"],
2366
- gen: [{
2367
- generator,
2368
- output: output.endsWith("/") ? output : `${output}/`
2369
- }]
2370
- }]
2371
- };
2372
- if (generator.startsWith("java/")) config.sql[0].gen[0].config = { package: "generated" };
2464
+ function getConfigYaml(generator, output) {
2465
+ const isJava = parseGenerator(generator).language === "java";
2373
2466
  return `# SQG Configuration
2374
2467
  # Generated by: sqg init
2375
2468
  # Documentation: https://sqg.dev
@@ -2378,12 +2471,11 @@ version: 1
2378
2471
  name: my-project
2379
2472
 
2380
2473
  sql:
2381
- - engine: ${engine}
2382
- files:
2474
+ - files:
2383
2475
  - queries.sql
2384
2476
  gen:
2385
2477
  - generator: ${generator}
2386
- output: ${output.endsWith("/") ? output : `${output}/`}${generator.startsWith("java/") ? `
2478
+ output: ${output.endsWith("/") ? output : `${output}/`}${isJava ? `
2387
2479
  config:
2388
2480
  package: generated` : ""}
2389
2481
  `;
@@ -2392,18 +2484,13 @@ sql:
2392
2484
  * Initialize a new SQG project
2393
2485
  */
2394
2486
  async function initProject(options) {
2395
- const engine = options.engine || "sqlite";
2487
+ const generator = options.generator || getDefaultGenerator();
2396
2488
  const output = options.output || "./generated";
2397
- if (!SUPPORTED_ENGINES.includes(engine)) throw new InvalidEngineError(engine, [...SUPPORTED_ENGINES]);
2398
- let generator;
2399
- if (options.generator) {
2400
- if (!(options.generator in SUPPORTED_GENERATORS)) {
2401
- const similar = findSimilarGenerators(options.generator);
2402
- throw new InvalidGeneratorError(options.generator, Object.keys(SUPPORTED_GENERATORS), similar.length > 0 ? similar[0] : void 0);
2403
- }
2404
- generator = options.generator;
2405
- 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(", ")}`);
2406
- } 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;
2407
2494
  const configPath = "sqg.yaml";
2408
2495
  const sqlPath = "queries.sql";
2409
2496
  if (!options.force) {
@@ -2414,16 +2501,16 @@ async function initProject(options) {
2414
2501
  mkdirSync(output, { recursive: true });
2415
2502
  consola.success(`Created output directory: ${output}`);
2416
2503
  }
2417
- writeFileSync(configPath, getConfigYaml(engine, generator, output));
2504
+ writeFileSync(configPath, getConfigYaml(generator, output));
2418
2505
  consola.success(`Created ${configPath}`);
2419
2506
  writeFileSync(sqlPath, getExampleSql(engine));
2420
2507
  consola.success(`Created ${sqlPath}`);
2421
2508
  consola.box(`
2422
2509
  SQG project initialized!
2423
2510
 
2424
- Engine: ${engine}
2425
2511
  Generator: ${generator}
2426
- Output: ${output}
2512
+ Engine: ${engine}
2513
+ Output: ${output}
2427
2514
 
2428
2515
  Next steps:
2429
2516
  1. Edit queries.sql to add your SQL queries
@@ -2436,46 +2523,81 @@ Documentation: https://sqg.dev
2436
2523
 
2437
2524
  //#endregion
2438
2525
  //#region src/sqg.ts
2439
- const version = process.env.npm_package_version ?? "0.3.0";
2526
+ const version = process.env.npm_package_version ?? "0.5.0";
2440
2527
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
2441
2528
  consola.level = LogLevels.info;
2442
2529
  const program = new Command().name("sqg").description(`${description}
2443
2530
 
2444
2531
  Generate type-safe database access code from annotated SQL files.
2445
2532
 
2446
- Supported Engines:
2447
- ${formatEnginesHelp()}
2448
-
2449
2533
  Supported Generators:
2450
- ${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();
2451
- 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) => {
2452
2539
  const opts = thisCommand.opts();
2453
2540
  if (opts.verbose) consola.level = LogLevels.debug;
2454
2541
  if (opts.format === "json") consola.level = LogLevels.silent;
2455
2542
  }).action(async (projectPath, options) => {
2456
2543
  try {
2457
- if (options.validate) {
2458
- const result = await validateProject(projectPath);
2459
- if (options.format === "json") console.log(JSON.stringify(result, null, 2));
2460
- else if (result.valid) {
2461
- consola.success("Configuration is valid");
2462
- consola.info(`Project: ${result.project?.name}`);
2463
- consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
2464
- consola.info(`Generators: ${result.generators?.join(", ")}`);
2465
- } else {
2466
- consola.error("Validation failed");
2467
- for (const error of result.errors || []) {
2468
- consola.error(` ${error.message}`);
2469
- 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
+ }
2470
2569
  }
2570
+ exit(result.valid ? 0 : 1);
2471
2571
  }
2472
- exit(result.valid ? 0 : 1);
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
+ }
2592
+ }
2593
+ exit(result.valid ? 0 : 1);
2594
+ }
2595
+ const files = await processProject(projectPath);
2596
+ if (options.format === "json") console.log(JSON.stringify({
2597
+ status: "success",
2598
+ generatedFiles: files
2599
+ }));
2473
2600
  }
2474
- const files = await processProject(projectPath);
2475
- if (options.format === "json") console.log(JSON.stringify({
2476
- status: "success",
2477
- generatedFiles: files
2478
- }));
2479
2601
  } catch (err) {
2480
2602
  if (options.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
2481
2603
  else if (err instanceof SqgError) {
@@ -2486,7 +2608,7 @@ program.argument("<project>", "Path to the project YAML config (sqg.yaml)").hook
2486
2608
  exit(1);
2487
2609
  }
2488
2610
  });
2489
- program.command("init").description("Initialize a new SQG project with example configuration").option("-e, --engine <engine>", `Database engine (${SUPPORTED_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) => {
2490
2612
  const parentOpts = program.opts();
2491
2613
  try {
2492
2614
  await initProject(options);
@@ -69,7 +69,7 @@ public class {{className}} {
69
69
  public record {{rowType}}({{#each columns}}{{type}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
70
70
  {{else}}
71
71
  public record {{rowType}}(PreparedStatement statement, RootAllocator allocator, ArrowReader reader,
72
- {{#each columns}}{{mapType this}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) implements AutoCloseable {
72
+ {{#each columns}}{{{mapType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) implements AutoCloseable {
73
73
  public boolean loadNextBatch() throws IOException {
74
74
  return reader.loadNextBatch();
75
75
  }
@@ -99,7 +99,7 @@ int
99
99
  {{#*inline "readVectors"}}
100
100
  var root = reader.getVectorSchemaRoot();
101
101
 
102
- return new {{rowType}}(stmt, allocator, reader, {{#each columns}}({{mapType this}})root.getVector("{{name}}"){{#unless @last}}, {{/unless}}{{/each}});
102
+ return new {{rowType}}(stmt, allocator, reader, {{#each columns}}({{{mapType this}}})root.getVector("{{name}}"){{#unless @last}}, {{/unless}}{{/each}});
103
103
 
104
104
  {{/inline~}}
105
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.3.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": {