@sqg/sqg 0.1.1 → 0.2.1

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 (3) hide show
  1. package/README.md +156 -0
  2. package/dist/sqg.mjs +834 -81
  3. package/package.json +16 -2
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # SQG - SQL Query Generator
2
+
3
+ Type-safe code generation from SQL. Write SQL, get fully-typed database access code.
4
+
5
+ ## What it does
6
+
7
+ SQG reads annotated `.sql` files, executes queries against real databases to introspect column types, and generates type-safe code to execute the SQL queries.
8
+
9
+ The syntax of the `.sql` file is compatible with [DBeaver](https://dbeaver.io/), this allows to develop the SQL
10
+ queries with it and then generate the code from the same file.
11
+
12
+ ## Features
13
+
14
+ - **Type-safe by design** - Generates fully-typed code with accurate column types inferred from your database
15
+ - **Multiple database engines** - Supports SQLite, DuckDB, and (soon) PostgreSQL
16
+ - **Multiple language targets** - Generate TypeScript or Java code from the same SQL files
17
+ - **Arrow API support** - Can generate Apache Arrow API bindings for DuckDB (Java)
18
+ - **DBeaver compatible** - Works seamlessly with DBeaver for database development and testing
19
+ - **Complex type support** - DuckDB: Handles structs, lists, and maps
20
+ - **Migration management** - Built-in support for schema migrations and test data
21
+
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pnpm add -g @sqg/sqg
27
+ pnpm approve-builds -g # needed for sqlite dependency
28
+ ```
29
+
30
+ Check if the install was successful:
31
+ ```bash
32
+ sqg --help
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### Option 1: Use `sqg init` (Recommended)
38
+
39
+ ```bash
40
+ # Initialize a new project (creates sqg.yaml and queries.sql)
41
+ sqg init
42
+
43
+ # Or with a specific database engine
44
+ sqg init --engine duckdb
45
+
46
+ # Generate code
47
+ sqg sqg.yaml
48
+ ```
49
+
50
+ ### Option 2: Manual Setup
51
+
52
+ 1. Create `sqg.yaml` in your project root:
53
+
54
+ ```yaml
55
+ version: 1
56
+ name: my-project
57
+ sql:
58
+ - engine: sqlite # sqlite, duckdb, or postgres
59
+ files:
60
+ - queries.sql
61
+ gen:
62
+ - generator: typescript/better-sqlite3
63
+ output: src/db.ts
64
+ ```
65
+
66
+ 2. Write your SQL file with annotations
67
+
68
+ For example `queries.sql`:
69
+
70
+ ```sql
71
+ -- MIGRATE createUsersTable
72
+ CREATE TABLE users (id INTEGER PRIMARY KEY,
73
+ name TEXT NOT NULL,
74
+ email TEXT);
75
+
76
+ -- QUERY getUserById :one
77
+ @set id = 1
78
+ SELECT id, name, email FROM users WHERE id = ${id};
79
+
80
+ -- QUERY getUsers
81
+ SELECT id, name, email FROM users;
82
+
83
+ -- EXEC insertUser
84
+ @set name = 'John'
85
+ @set email = 'john@example.com'
86
+ INSERT INTO users (name, email) VALUES (${name}, ${email});
87
+ ```
88
+
89
+ 3. Run SQG to generate code:
90
+
91
+ ```bash
92
+ sqg sqg.yaml
93
+ ```
94
+
95
+ 4. Use the generated code:
96
+
97
+ ```typescript
98
+ import Database from 'better-sqlite3';
99
+ import { Queries } from './db';
100
+
101
+ const db = new Database(':memory:');
102
+ const queries = new Queries(db);
103
+
104
+ // Run migrations
105
+ for (const sql of Queries.getMigrations()) {
106
+ db.exec(sql);
107
+ }
108
+
109
+ // Type-safe queries
110
+ queries.insertUser('Alice', 'alice@example.com');
111
+ const user = queries.getUserById(1);
112
+ console.log(user?.name);
113
+ ```
114
+
115
+ ## SQL Annotations
116
+
117
+ | Annotation | Description |
118
+ |------------|-------------|
119
+ | `-- MIGRATE name` | Schema migration (CREATE TABLE, etc.) |
120
+ | `-- QUERY name` | SELECT query returning rows |
121
+ | `-- QUERY name :one` | Query returning single row or undefined |
122
+ | `-- QUERY name :pluck` | Return single (first) column value |
123
+ | `-- EXEC name` | INSERT/UPDATE/DELETE (no result rows) |
124
+ | `-- TESTDATA name` | Test data, runs after migrations |
125
+ | `@set var = value` | Define parameter with sample value |
126
+ | `${var}` | Reference parameter in query |
127
+
128
+ ## Supported Databases & Generators
129
+
130
+ | Language | Database | API | Generator | Status |
131
+ |----------|----------|-----|-----------|--------|
132
+ | TypeScript | SQLite | better-sqlite3 | `typescript/better-sqlite3` | Tested |
133
+ | TypeScript | DuckDB | @duckdb/node-api | `typescript/duckdb` | Tested |
134
+ | Java | Any (JDBC) | JDBC | `java/jdbc` | Tested |
135
+ | Java | DuckDB | Apache Arrow | `java/duckdb-arrow` | Tested |
136
+ | TypeScript | PostgreSQL | pg (node-postgres) | `typescript/pg` | under development |
137
+
138
+ ## CLI Commands
139
+
140
+ ```bash
141
+ sqg <config> # Generate code from config file
142
+ sqg init # Initialize new project with example files
143
+ sqg init --engine duckdb # Initialize with specific database engine
144
+ sqg --validate <config> # Validate config without generating code
145
+ sqg --format json <config> # Output as JSON (for tooling integration)
146
+ sqg syntax # Show SQL annotation syntax reference
147
+ sqg --help # Show all options
148
+ ```
149
+
150
+ ## Documentation
151
+
152
+ Full documentation at [sqg.dev](https://sqg.dev)
153
+
154
+ ## License
155
+
156
+ Apache-2.0
package/dist/sqg.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { exit } from "node:process";
3
3
  import { Command } from "commander";
4
4
  import consola, { LogLevels } from "consola";
5
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { homedir } from "node:os";
7
7
  import { basename, dirname, extname, join, resolve } from "node:path";
8
8
  import Handlebars from "handlebars";
@@ -20,6 +20,263 @@ import prettierPluginJava from "prettier-plugin-java";
20
20
  import typescriptPlugin from "prettier/parser-typescript";
21
21
  import estree from "prettier/plugins/estree";
22
22
 
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": {
37
+ description: "TypeScript with better-sqlite3 driver",
38
+ compatibleEngines: ["sqlite"],
39
+ extension: ".ts"
40
+ },
41
+ "typescript/duckdb": {
42
+ description: "TypeScript with @duckdb/node-api driver",
43
+ compatibleEngines: ["duckdb"],
44
+ extension: ".ts"
45
+ },
46
+ "java/jdbc": {
47
+ description: "Java with JDBC (SQLite, DuckDB, PostgreSQL)",
48
+ compatibleEngines: [
49
+ "sqlite",
50
+ "duckdb",
51
+ "postgres"
52
+ ],
53
+ extension: ".java"
54
+ },
55
+ "java/duckdb-arrow": {
56
+ description: "Java with DuckDB Arrow API",
57
+ compatibleEngines: ["duckdb"],
58
+ extension: ".java"
59
+ }
60
+ };
61
+ /** List of all generator names for validation */
62
+ const GENERATOR_NAMES = Object.keys(SUPPORTED_GENERATORS);
63
+ /** SQL annotation syntax reference */
64
+ const SQL_SYNTAX_REFERENCE = `
65
+ SQL Annotation Syntax:
66
+ -- QUERY <name> [:one] [:pluck] Select query (returns rows)
67
+ -- EXEC <name> Execute statement (INSERT/UPDATE/DELETE)
68
+ -- MIGRATE <number> Schema migration (run in order)
69
+ -- TESTDATA <name> Test data setup (not generated)
70
+
71
+ @set <varName> = <value> Define a variable
72
+ \${varName} Reference a variable in SQL
73
+
74
+ Modifiers:
75
+ :one Return single row (or null) instead of array
76
+ :pluck Return single column value (requires exactly 1 column)
77
+ :all Return all rows (default)
78
+
79
+ Example:
80
+ -- MIGRATE 1
81
+ CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
82
+
83
+ -- QUERY get_user :one
84
+ @set id = 1
85
+ SELECT * FROM users WHERE id = \${id};
86
+ `.trim();
87
+ /**
88
+ * Find similar generator names for typo suggestions
89
+ */
90
+ function findSimilarGenerators(input) {
91
+ const normalized = input.toLowerCase();
92
+ return GENERATOR_NAMES.filter((name) => {
93
+ const nameLower = name.toLowerCase();
94
+ if (nameLower.includes(normalized) || normalized.includes(nameLower)) return true;
95
+ return [
96
+ normalized.replace("/", "-"),
97
+ normalized.replace("-", "/"),
98
+ normalized.replace("_", "/"),
99
+ normalized.replace("_", "-")
100
+ ].some((v) => nameLower.includes(v) || v.includes(nameLower));
101
+ });
102
+ }
103
+ /**
104
+ * Format generators for CLI help output
105
+ */
106
+ function formatGeneratorsHelp() {
107
+ return Object.entries(SUPPORTED_GENERATORS).map(([name, info]) => ` ${name.padEnd(28)} ${info.description} (${info.compatibleEngines.join(", ")})`).join("\n");
108
+ }
109
+ /**
110
+ * Format engines for CLI help output
111
+ */
112
+ function formatEnginesHelp() {
113
+ return SUPPORTED_ENGINES.map((e) => ` ${e}`).join("\n");
114
+ }
115
+
116
+ //#endregion
117
+ //#region src/errors.ts
118
+ /**
119
+ * Base error class for SQG with structured information
120
+ */
121
+ var SqgError = class SqgError extends Error {
122
+ constructor(message, code, suggestion, context) {
123
+ super(message);
124
+ this.code = code;
125
+ this.suggestion = suggestion;
126
+ this.context = context;
127
+ this.name = "SqgError";
128
+ }
129
+ /**
130
+ * Create error with file context
131
+ */
132
+ static inFile(message, code, file, options) {
133
+ return new SqgError(`${message} in ${options?.line ? `${file}:${options.line}` : file}`, code, options?.suggestion, {
134
+ file,
135
+ line: options?.line,
136
+ ...options?.context
137
+ });
138
+ }
139
+ /**
140
+ * Create error with query context
141
+ */
142
+ static inQuery(message, code, queryName, file, options) {
143
+ return new SqgError(`${message} in query '${queryName}' (${file})`, code, options?.suggestion, {
144
+ file,
145
+ query: queryName,
146
+ sql: options?.sql,
147
+ ...options?.context
148
+ });
149
+ }
150
+ toJSON() {
151
+ return {
152
+ name: this.name,
153
+ code: this.code,
154
+ message: this.message,
155
+ suggestion: this.suggestion,
156
+ context: this.context
157
+ };
158
+ }
159
+ };
160
+ /**
161
+ * Error for configuration issues
162
+ */
163
+ var ConfigError = class extends SqgError {
164
+ constructor(message, suggestion, context) {
165
+ super(message, "CONFIG_VALIDATION_ERROR", suggestion, context);
166
+ this.name = "ConfigError";
167
+ }
168
+ };
169
+ /**
170
+ * Error for invalid generator names
171
+ */
172
+ var InvalidGeneratorError = class extends SqgError {
173
+ constructor(generatorName, validGenerators, suggestion) {
174
+ const similarMsg = suggestion ? ` Did you mean '${suggestion}'?` : "";
175
+ super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators.join(", ")}`, { generator: generatorName });
176
+ this.name = "InvalidGeneratorError";
177
+ }
178
+ };
179
+ /**
180
+ * Error for invalid engine names
181
+ */
182
+ var InvalidEngineError = class extends SqgError {
183
+ constructor(engineName, validEngines) {
184
+ super(`Invalid engine '${engineName}'. Valid engines: ${validEngines.join(", ")}`, "INVALID_ENGINE", `Choose from: ${validEngines.join(", ")}`, { engine: engineName });
185
+ this.name = "InvalidEngineError";
186
+ }
187
+ };
188
+ /**
189
+ * Error for generator/engine compatibility
190
+ */
191
+ var GeneratorEngineMismatchError = class extends SqgError {
192
+ constructor(generator, engine, compatibleEngines) {
193
+ super(`Generator '${generator}' is not compatible with engine '${engine}'`, "GENERATOR_ENGINE_MISMATCH", `Generator '${generator}' works with: ${compatibleEngines.join(", ")}`, {
194
+ generator,
195
+ engine
196
+ });
197
+ this.name = "GeneratorEngineMismatchError";
198
+ }
199
+ };
200
+ /**
201
+ * Error for database initialization/connection issues
202
+ */
203
+ var DatabaseError = class extends SqgError {
204
+ constructor(message, engine, suggestion, context) {
205
+ super(message, "DATABASE_ERROR", suggestion, {
206
+ engine,
207
+ ...context
208
+ });
209
+ this.name = "DatabaseError";
210
+ }
211
+ };
212
+ /**
213
+ * Error for SQL execution issues
214
+ */
215
+ var SqlExecutionError = class extends SqgError {
216
+ constructor(message, queryName, file, sql, originalError) {
217
+ super(`Failed to execute query '${queryName}' in ${file}: ${message}`, "SQL_EXECUTION_ERROR", void 0, {
218
+ query: queryName,
219
+ file,
220
+ sql,
221
+ originalError: originalError?.message
222
+ });
223
+ this.name = "SqlExecutionError";
224
+ }
225
+ };
226
+ /**
227
+ * Error for type mapping issues
228
+ */
229
+ var TypeMappingError = class extends SqgError {
230
+ constructor(message, columnName, queryName, file) {
231
+ const location = queryName && file ? ` in query '${queryName}' (${file})` : "";
232
+ super(`Type mapping error for column '${columnName}'${location}: ${message}`, "TYPE_MAPPING_ERROR", void 0, {
233
+ columnName,
234
+ query: queryName,
235
+ file
236
+ });
237
+ this.name = "TypeMappingError";
238
+ }
239
+ };
240
+ /**
241
+ * Error for file not found
242
+ */
243
+ var FileNotFoundError = class extends SqgError {
244
+ constructor(filePath, searchedFrom) {
245
+ const suggestion = searchedFrom ? `Check that the path is relative to ${searchedFrom}` : "Check that the file path is correct";
246
+ super(`File not found: ${filePath}`, "FILE_NOT_FOUND", suggestion, { file: filePath });
247
+ this.name = "FileNotFoundError";
248
+ }
249
+ };
250
+ /**
251
+ * Format any error for JSON output
252
+ */
253
+ function formatErrorForOutput(err) {
254
+ if (err instanceof SqgError) return {
255
+ status: "error",
256
+ error: {
257
+ code: err.code,
258
+ message: err.message,
259
+ suggestion: err.suggestion,
260
+ context: err.context
261
+ }
262
+ };
263
+ if (err instanceof Error) return {
264
+ status: "error",
265
+ error: {
266
+ code: "UNKNOWN_ERROR",
267
+ message: err.message
268
+ }
269
+ };
270
+ return {
271
+ status: "error",
272
+ error: {
273
+ code: "UNKNOWN_ERROR",
274
+ message: String(err)
275
+ }
276
+ };
277
+ }
278
+
279
+ //#endregion
23
280
  //#region src/parser/sql-parser.ts
24
281
  const parser = LRParser.deserialize({
25
282
  version: 14,
@@ -162,17 +419,16 @@ function parseSQLQueries(filePath, extraVariables) {
162
419
  variables.set(varName, extraVariable.value);
163
420
  return extraVariable.value;
164
421
  }
165
- throw error(`Variable '${varName}' not found`);
422
+ const definedVars = Array.from(variables.keys());
423
+ const suggestion = definedVars.length > 0 ? `Add '@set ${varName} = <value>' before the query. Defined variables: ${definedVars.join(", ")}` : `Add '@set ${varName} = <value>' before the query`;
424
+ throw SqgError.inQuery(`Variable '\${${varName}}' is referenced but not defined`, "MISSING_VARIABLE", name, filePath, { suggestion });
166
425
  }
167
426
  const sqlNode = cursor.node.getChild("SQLBlock");
168
- if (!sqlNode) throw new Error(`'SQLBlock' not found`);
427
+ if (!sqlNode) throw SqgError.inQuery("SQL block not found", "SQL_PARSE_ERROR", name, filePath, { suggestion: "Ensure the query has valid SQL content after the annotation comment" });
169
428
  const sqlContentStr = nodeStr(sqlNode).trim();
170
429
  const sqlCursor = sqlNode.cursor();
171
430
  let from = -1;
172
431
  let to = -1;
173
- function error(message) {
174
- return /* @__PURE__ */ new Error(`${message} in ${filePath} query '${name}': ${message}`);
175
- }
176
432
  class SQLQueryBuilder {
177
433
  sqlParts = [];
178
434
  appendSql(sql$1) {
@@ -214,33 +470,39 @@ function parseSQLQueries(filePath, extraVariables) {
214
470
  }
215
471
  toSqlWithPositionalPlaceholders() {
216
472
  const parameters = [];
217
- return {
218
- parameters,
219
- sqlParts: [],
220
- sql: this.sqlParts.map((part) => {
221
- if (typeof part === "string") return part;
222
- const varName = part.name;
223
- const value = part.value;
473
+ const sqlParts = [];
474
+ for (const part of this.sqlParts) if (typeof part === "string") sqlParts.push(part);
475
+ else {
476
+ const varName = part.name;
477
+ const value = part.value;
478
+ if (varName.startsWith("sources_")) sqlParts.push(part);
479
+ else {
224
480
  let pos = parameters.findIndex((p) => p.name === varName);
225
481
  if (pos < 0) {
226
482
  parameters.push({
227
483
  name: varName,
228
484
  value
229
485
  });
230
- pos = parameters.length - 1;
231
- }
232
- return ` $${pos + 1} `;
233
- }).join("").trim()
486
+ pos = parameters.length;
487
+ } else pos = pos + 1;
488
+ sqlParts.push(`$${pos}`);
489
+ }
490
+ }
491
+ return {
492
+ parameters,
493
+ sqlParts,
494
+ sql: sqlParts.map((part) => typeof part === "string" ? part : ` ${part.value} `).join("").trim()
234
495
  };
235
496
  }
236
497
  toSqlWithNamedPlaceholders() {
498
+ const sqlParts = [];
499
+ for (const part of this.sqlParts) if (typeof part === "string") sqlParts.push(part);
500
+ else if (part.name.startsWith("sources_")) sqlParts.push(part);
501
+ else sqlParts.push(`$${part.name}`);
237
502
  return {
238
503
  parameters: this.parameters(),
239
- sqlParts: [],
240
- sql: this.sqlParts.map((part) => {
241
- if (typeof part === "string") return part;
242
- return `$${part.name}`;
243
- }).join("").trim()
504
+ sqlParts,
505
+ sql: sqlParts.map((part) => typeof part === "string" ? part : `$${part.name}`).join("").trim()
244
506
  };
245
507
  }
246
508
  }
@@ -256,7 +518,7 @@ function parseSQLQueries(filePath, extraVariables) {
256
518
  }
257
519
  if (child.name === "VarRef") {
258
520
  const varRef = nodeStr(child);
259
- if (!varRef.startsWith("${") || !varRef.endsWith("}")) throw error(`Invalid variable reference: ${varRef}`);
521
+ if (!varRef.startsWith("${") || !varRef.endsWith("}")) throw SqgError.inQuery(`Invalid variable reference: ${varRef}`, "SQL_PARSE_ERROR", name, filePath, { suggestion: "Variables should be in the format ${varName}" });
260
522
  const varName = varRef.replace("${", "").replace("}", "");
261
523
  const value = getVariable(varName);
262
524
  if (to > from) sql.appendSql(content.slice(from, to));
@@ -280,7 +542,7 @@ function parseSQLQueries(filePath, extraVariables) {
280
542
  config
281
543
  });
282
544
  const query = new SQLQuery(filePath, name, sqlContentStr, sql.toSqlWithAnonymousPlaceholders(), sql.toSqlWithNamedPlaceholders(), sql.toSqlWithPositionalPlaceholders(), queryType, isOne, isPluck, variables, config);
283
- if (queryNames.has(name)) throw new Error(`Duplicate query name in ${filePath}: ${name}`);
545
+ if (queryNames.has(name)) throw SqgError.inFile(`Duplicate query name '${name}'`, "DUPLICATE_QUERY", filePath, { suggestion: `Rename one of the queries to have a unique name` });
284
546
  queryNames.add(name);
285
547
  queries.push(query);
286
548
  consola.debug(`Added query: ${name} (${queryType})`);
@@ -322,12 +584,16 @@ const duckdb = new class {
322
584
  this.db = await DuckDBInstance.create(":memory:");
323
585
  this.connection = await this.db.connect();
324
586
  await initializeDatabase(queries, async (query) => {
325
- await this.connection.run(query.rawQuery);
587
+ try {
588
+ await this.connection.run(query.rawQuery);
589
+ } catch (e) {
590
+ throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
591
+ }
326
592
  });
327
593
  }
328
594
  async executeQueries(queries) {
329
595
  const connection = this.connection;
330
- if (!connection) throw new Error("Database not initialized");
596
+ if (!connection) throw new DatabaseError("DuckDB connection not initialized", "duckdb", "This is an internal error. Check that migrations completed successfully.");
331
597
  try {
332
598
  const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
333
599
  for (const query of executableQueries) {
@@ -400,8 +666,8 @@ const duckdb = new class {
400
666
  //#endregion
401
667
  //#region src/db/postgres.ts
402
668
  const databaseName = "sqg-db-temp";
403
- const connectionString = "postgresql://sqg:secret@localhost:15432/sqg-db";
404
- const connectionStringTemp = `postgresql://sqg:secret@localhost:15432/${databaseName}`;
669
+ const connectionString = process.env.SQG_POSTGRES_URL || "postgresql://sqg:secret@localhost:15432/sqg-db";
670
+ const connectionStringTemp = process.env.SQG_POSTGRES_URL ? process.env.SQG_POSTGRES_URL.replace(/\/[^/]+$/, `/${databaseName}`) : `postgresql://sqg:secret@localhost:15432/${databaseName}`;
405
671
  const typeIdToName = /* @__PURE__ */ new Map();
406
672
  for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
407
673
  const postgres = new class {
@@ -410,23 +676,35 @@ const postgres = new class {
410
676
  async initializeDatabase(queries) {
411
677
  this.dbInitial = new Client({ connectionString });
412
678
  this.db = new Client({ connectionString: connectionStringTemp });
413
- await this.dbInitial.connect();
679
+ try {
680
+ await this.dbInitial.connect();
681
+ } catch (e) {
682
+ 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.`);
683
+ }
414
684
  try {
415
685
  await this.dbInitial.query(`DROP DATABASE "${databaseName}";`);
416
686
  } catch (error) {}
417
687
  try {
418
688
  await this.dbInitial.query(`CREATE DATABASE "${databaseName}";`);
419
689
  } catch (error) {
420
- consola.error("Error creating database:", error);
690
+ throw new DatabaseError(`Failed to create temporary database: ${error.message}`, "postgres", "Check PostgreSQL user permissions to create databases");
691
+ }
692
+ try {
693
+ await this.db.connect();
694
+ } catch (e) {
695
+ throw new DatabaseError(`Failed to connect to temporary database: ${e.message}`, "postgres");
421
696
  }
422
- await this.db.connect();
423
697
  await initializeDatabase(queries, async (query) => {
424
- await this.db.query(query.rawQuery);
698
+ try {
699
+ await this.db.query(query.rawQuery);
700
+ } catch (e) {
701
+ throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
702
+ }
425
703
  });
426
704
  }
427
705
  async executeQueries(queries) {
428
706
  const db = this.db;
429
- if (!db) throw new Error("Database not initialized");
707
+ if (!db) throw new DatabaseError("PostgreSQL database not initialized", "postgres", "This is an internal error. Check that migrations completed successfully.");
430
708
  try {
431
709
  const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
432
710
  for (const query of executableQueries) {
@@ -488,14 +766,18 @@ const sqlite = new class {
488
766
  async initializeDatabase(queries) {
489
767
  const db = new BetterSqlite3(":memory:");
490
768
  await initializeDatabase(queries, (query) => {
491
- db.exec(query.rawQuery);
769
+ try {
770
+ db.exec(query.rawQuery);
771
+ } catch (e) {
772
+ throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
773
+ }
492
774
  return Promise.resolve();
493
775
  });
494
776
  this.db = db;
495
777
  }
496
778
  executeQueries(queries) {
497
779
  const db = this.db;
498
- if (!db) throw new Error("Database not initialized");
780
+ if (!db) throw new DatabaseError("SQLite database not initialized", "sqlite", "This is an internal error. Migrations may have failed silently.");
499
781
  try {
500
782
  const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
501
783
  for (const query of executableQueries) {
@@ -582,7 +864,7 @@ var TypeMapper = class {
582
864
  }
583
865
  if (column.type instanceof StructType) return path + this.formatStructTypeName(column.name);
584
866
  if (column.type instanceof MapType) return path + this.formatMapTypeName(column.name);
585
- if (!column.type) throw new Error(`Expected ColumnType ${JSON.stringify(column)}`);
867
+ if (!column.type) throw new TypeMappingError(`Missing type information`, column.name);
586
868
  return this.mapPrimitiveType(column.type.toString(), column.nullable);
587
869
  }
588
870
  /**
@@ -887,8 +1169,15 @@ var TypeScriptTypeMapper = class extends TypeMapper {
887
1169
  return ` ${field.name}: ${fieldType};`;
888
1170
  }).join("\n")}\n };\n}`;
889
1171
  }
890
- parseValue(column, value) {
891
- return "/// TODO: parseValue";
1172
+ /**
1173
+ * Generates code to parse/convert a raw DuckDB value to the target TypeScript type.
1174
+ * DuckDB returns complex types with specific wrapper structures that need to be preserved.
1175
+ */
1176
+ parseValue(column, value, path = "") {
1177
+ if (column.type instanceof ListType) return value;
1178
+ if (column.type instanceof StructType) return value;
1179
+ if (column.type instanceof MapType) return value;
1180
+ return value;
892
1181
  }
893
1182
  };
894
1183
 
@@ -1292,7 +1581,10 @@ function getGenerator(generator) {
1292
1581
  case "java/duckdb-arrow": return new JavaDuckDBArrowGenerator("templates/java-duckdb-arrow.hbs");
1293
1582
  case "typescript/better-sqlite3": return new TsGenerator("templates/better-sqlite3.hbs");
1294
1583
  case "typescript/duckdb": return new TsDuckDBGenerator("templates/typescript-duckdb.hbs");
1295
- default: throw new Error(`Unsupported generator: ${generator}`);
1584
+ default: {
1585
+ const similar = findSimilarGenerators(generator);
1586
+ throw new InvalidGeneratorError(generator, [...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
1587
+ }
1296
1588
  }
1297
1589
  }
1298
1590
 
@@ -1426,24 +1718,27 @@ function generateSourceFile(name, queries, templatePath, generator, config) {
1426
1718
  allowProtoMethodsByDefault: true
1427
1719
  });
1428
1720
  }
1721
+ /**
1722
+ * Project configuration schema with descriptions for validation messages
1723
+ */
1429
1724
  const ProjectSchema = z.object({
1430
- version: z.number(),
1431
- name: z.string(),
1725
+ version: z.number().describe("Configuration version (currently 1)"),
1726
+ name: z.string().min(1, "Project name is required").describe("Project name used for generated class names"),
1432
1727
  sql: z.array(z.object({
1433
- engine: z.string(),
1434
- files: z.array(z.string()),
1728
+ engine: z.enum(SUPPORTED_ENGINES).describe(`Database engine: ${SUPPORTED_ENGINES.join(", ")}`),
1729
+ files: z.array(z.string().min(1)).min(1, "At least one SQL file is required").describe("SQL files to process"),
1435
1730
  gen: z.array(z.object({
1436
- generator: z.string(),
1437
- name: z.string().optional(),
1438
- template: z.string().optional(),
1439
- output: z.string(),
1440
- config: z.any().optional()
1441
- }))
1442
- })),
1731
+ generator: z.enum(GENERATOR_NAMES).describe(`Code generator: ${GENERATOR_NAMES.join(", ")}`),
1732
+ name: z.string().optional().describe("Override the generated class/module name"),
1733
+ template: z.string().optional().describe("Custom Handlebars template path"),
1734
+ output: z.string().min(1, "Output path is required").describe("Output file or directory path"),
1735
+ config: z.any().optional().describe("Generator-specific configuration")
1736
+ })).min(1, "At least one generator is required").describe("Code generators to run")
1737
+ })).min(1, "At least one SQL configuration is required").describe("SQL file configurations"),
1443
1738
  sources: z.array(z.object({
1444
- path: z.string(),
1445
- name: z.string().optional()
1446
- })).optional()
1739
+ path: z.string().describe("Path to source file (supports $HOME)"),
1740
+ name: z.string().optional().describe("Variable name override")
1741
+ })).optional().describe("External source files to include as variables")
1447
1742
  });
1448
1743
  var ExtraVariable = class {
1449
1744
  constructor(name, value) {
@@ -1460,13 +1755,39 @@ function createExtraVariables(sources) {
1460
1755
  return new ExtraVariable(varName, `'${resolvedPath}'`);
1461
1756
  });
1462
1757
  }
1758
+ /**
1759
+ * Parse and validate project configuration with helpful error messages
1760
+ */
1463
1761
  function parseProjectConfig(filePath) {
1464
- const content = readFileSync(filePath, "utf-8");
1465
- const result = ProjectSchema.safeParse(YAML.parse(content));
1466
- if (!result.success) {
1467
- const prettyError = z.prettifyError(result.error);
1468
- throw new Error(`Error parsing project file ${filePath}:\n${prettyError}`);
1762
+ if (!existsSync(filePath)) throw new FileNotFoundError(filePath, process.cwd());
1763
+ let content;
1764
+ try {
1765
+ content = readFileSync(filePath, "utf-8");
1766
+ } catch (e) {
1767
+ throw new SqgError(`Cannot read config file: ${filePath}`, "CONFIG_PARSE_ERROR", "Check file permissions and that the path is correct");
1768
+ }
1769
+ let parsed;
1770
+ try {
1771
+ parsed = YAML.parse(content);
1772
+ } catch (e) {
1773
+ throw new SqgError(`Invalid YAML syntax in ${filePath}: ${e.message}`, "CONFIG_PARSE_ERROR", "Check YAML syntax - common issues: incorrect indentation, missing colons, unquoted special characters");
1774
+ }
1775
+ if (parsed && typeof parsed === "object") {
1776
+ const obj = parsed;
1777
+ if (obj.sql && Array.isArray(obj.sql)) for (let i = 0; i < obj.sql.length; i++) {
1778
+ const sqlConfig = obj.sql[i];
1779
+ if (sqlConfig.engine && !SUPPORTED_ENGINES.includes(sqlConfig.engine)) throw new InvalidEngineError(String(sqlConfig.engine), [...SUPPORTED_ENGINES]);
1780
+ if (sqlConfig.gen && Array.isArray(sqlConfig.gen)) for (let j = 0; j < sqlConfig.gen.length; j++) {
1781
+ const genConfig = sqlConfig.gen[j];
1782
+ if (genConfig.generator && !GENERATOR_NAMES.includes(genConfig.generator)) {
1783
+ const similar = findSimilarGenerators(String(genConfig.generator));
1784
+ throw new InvalidGeneratorError(String(genConfig.generator), [...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
1785
+ }
1786
+ }
1787
+ }
1469
1788
  }
1789
+ const result = ProjectSchema.safeParse(parsed);
1790
+ if (!result.success) throw new ConfigError(`Configuration error in ${filePath}:\n${z.prettifyError(result.error)}`, "Check the configuration format against the documentation at https://sqg.dev", { file: filePath });
1470
1791
  return result.data;
1471
1792
  }
1472
1793
  function detectParameterType(value) {
@@ -1491,7 +1812,7 @@ function getOutputPath(projectDir, sqlFileName, gen, generator) {
1491
1812
  }
1492
1813
  function validateQueries(queries) {
1493
1814
  for (const query of queries) {
1494
- if (query.isQuery && query.isPluck && query.columns.length !== 1) throw new Error(`Query ${query.id} in ${query.filename} has the ':pluck: option, must have exactly one column, but has ${query.columns.length} columns`);
1815
+ if (query.isQuery && query.isPluck && query.columns.length !== 1) throw SqgError.inQuery(`':pluck' modifier requires exactly 1 column, but query has ${query.columns.length} columns`, "VALIDATION_ERROR", query.id, query.filename, { suggestion: query.columns.length === 0 ? "Ensure the query returns at least one column" : `Remove ':pluck' or select only one column. Current columns: ${query.columns.map((c) => c.name).join(", ")}` });
1495
1816
  const columns = query.columns.map((col) => {
1496
1817
  const configColumn = query.config?.getColumnInfo(col.name);
1497
1818
  if (configColumn) return configColumn;
@@ -1515,44 +1836,476 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries) {
1515
1836
  await generator.afterGenerate(outputPath);
1516
1837
  return outputPath;
1517
1838
  }
1839
+ /**
1840
+ * Validate project configuration without executing queries
1841
+ * Use this for pre-flight checks before generation
1842
+ */
1843
+ async function validateProject(projectPath) {
1844
+ const errors = [];
1845
+ const projectDir = resolve(dirname(projectPath));
1846
+ let project;
1847
+ try {
1848
+ project = parseProjectConfig(projectPath);
1849
+ } catch (e) {
1850
+ if (e instanceof SqgError) return {
1851
+ valid: false,
1852
+ errors: [{
1853
+ code: e.code,
1854
+ message: e.message,
1855
+ suggestion: e.suggestion,
1856
+ context: e.context
1857
+ }]
1858
+ };
1859
+ return {
1860
+ valid: false,
1861
+ errors: [{
1862
+ code: "UNKNOWN_ERROR",
1863
+ message: String(e)
1864
+ }]
1865
+ };
1866
+ }
1867
+ const sqlFiles = [];
1868
+ const generators = [];
1869
+ for (const sql of project.sql) for (const sqlFile of sql.files) {
1870
+ const fullPath = join(projectDir, sqlFile);
1871
+ sqlFiles.push(sqlFile);
1872
+ if (!existsSync(fullPath)) errors.push({
1873
+ code: "FILE_NOT_FOUND",
1874
+ message: `SQL file not found: ${sqlFile}`,
1875
+ suggestion: `Check that ${sqlFile} exists relative to ${projectDir}`,
1876
+ context: { file: fullPath }
1877
+ });
1878
+ for (const gen of sql.gen) {
1879
+ generators.push(gen.generator);
1880
+ if (!SUPPORTED_GENERATORS[gen.generator].compatibleEngines.includes(sql.engine)) errors.push({
1881
+ code: "GENERATOR_ENGINE_MISMATCH",
1882
+ message: `Generator '${gen.generator}' is not compatible with engine '${sql.engine}'`,
1883
+ suggestion: `For '${sql.engine}', use one of: ${Object.entries(SUPPORTED_GENERATORS).filter(([_, info]) => info.compatibleEngines.includes(sql.engine)).map(([name]) => name).join(", ")}`,
1884
+ context: {
1885
+ generator: gen.generator,
1886
+ engine: sql.engine
1887
+ }
1888
+ });
1889
+ }
1890
+ }
1891
+ return {
1892
+ valid: errors.length === 0,
1893
+ project: {
1894
+ name: project.name,
1895
+ version: project.version
1896
+ },
1897
+ sqlFiles: [...new Set(sqlFiles)],
1898
+ generators: [...new Set(generators)],
1899
+ errors: errors.length > 0 ? errors : void 0
1900
+ };
1901
+ }
1902
+ /**
1903
+ * Process a project configuration and generate code
1904
+ */
1518
1905
  async function processProject(projectPath) {
1519
1906
  const projectDir = resolve(dirname(projectPath));
1520
1907
  const project = parseProjectConfig(projectPath);
1521
1908
  const extraVariables = createExtraVariables(project.sources ?? []);
1522
1909
  if (extraVariables.length > 0) consola.info("Extra variables:", extraVariables);
1523
1910
  const files = [];
1524
- for (const sql of project.sql) for (const sqlFile of sql.files) {
1525
- let queries;
1526
- try {
1527
- queries = parseSQLQueries(join(projectDir, sqlFile), extraVariables);
1528
- const dbEngine = getDatabaseEngine(sql.engine);
1529
- await dbEngine.initializeDatabase(queries);
1530
- await dbEngine.executeQueries(queries);
1531
- validateQueries(queries);
1532
- await dbEngine.close();
1533
- } catch (e) {
1534
- consola.error(`Error processing SQL file ${sqlFile}: ${e}`);
1535
- throw e;
1536
- }
1911
+ for (const sql of project.sql) {
1537
1912
  for (const gen of sql.gen) {
1538
- const generator = getGenerator(gen.generator);
1539
- if (!generator.isCompatibleWith(sql.engine)) throw new Error(`File ${sqlFile}: Generator ${gen.generator} is not compatible with engine ${sql.engine}`);
1540
- const outputPath = await writeGeneratedFile(projectDir, gen, generator, sqlFile, queries);
1541
- files.push(outputPath);
1913
+ const generatorInfo = SUPPORTED_GENERATORS[gen.generator];
1914
+ if (!generatorInfo.compatibleEngines.includes(sql.engine)) throw new GeneratorEngineMismatchError(gen.generator, sql.engine, generatorInfo.compatibleEngines);
1915
+ }
1916
+ for (const sqlFile of sql.files) {
1917
+ const fullPath = join(projectDir, sqlFile);
1918
+ if (!existsSync(fullPath)) throw new FileNotFoundError(fullPath, projectDir);
1919
+ let queries;
1920
+ try {
1921
+ queries = parseSQLQueries(fullPath, extraVariables);
1922
+ } catch (e) {
1923
+ if (e instanceof SqgError) throw e;
1924
+ throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
1925
+ }
1926
+ try {
1927
+ const dbEngine = getDatabaseEngine(sql.engine);
1928
+ await dbEngine.initializeDatabase(queries);
1929
+ await dbEngine.executeQueries(queries);
1930
+ validateQueries(queries);
1931
+ await dbEngine.close();
1932
+ } catch (e) {
1933
+ if (e instanceof SqgError) throw e;
1934
+ throw new SqgError(`Database error processing ${sqlFile}: ${e.message}`, "DATABASE_ERROR", `Check that the SQL is valid for engine '${sql.engine}'`, {
1935
+ file: sqlFile,
1936
+ engine: sql.engine
1937
+ });
1938
+ }
1939
+ for (const gen of sql.gen) {
1940
+ const outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries);
1941
+ files.push(outputPath);
1942
+ }
1542
1943
  }
1543
1944
  }
1544
1945
  return files;
1545
1946
  }
1546
1947
 
1948
+ //#endregion
1949
+ //#region src/init.ts
1950
+ /**
1951
+ * SQG Project Initialization - Creates new SQG projects with example files
1952
+ */
1953
+ /**
1954
+ * Get the default generator for an engine
1955
+ */
1956
+ function getDefaultGenerator(engine) {
1957
+ return {
1958
+ sqlite: "typescript/better-sqlite3",
1959
+ duckdb: "typescript/duckdb",
1960
+ postgres: "java/jdbc"
1961
+ }[engine];
1962
+ }
1963
+ /**
1964
+ * Generate example SQL content based on engine
1965
+ */
1966
+ function getExampleSql(engine) {
1967
+ return {
1968
+ sqlite: `-- MIGRATE 1
1969
+ -- Create the users table (SQG Example - https://sqg.dev/guides/sql-syntax/)
1970
+ CREATE TABLE users (
1971
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1972
+ name TEXT NOT NULL,
1973
+ email TEXT UNIQUE NOT NULL,
1974
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
1975
+ );
1976
+
1977
+ -- MIGRATE 2
1978
+ CREATE TABLE posts (
1979
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1980
+ user_id INTEGER NOT NULL REFERENCES users(id),
1981
+ title TEXT NOT NULL,
1982
+ content TEXT,
1983
+ published INTEGER DEFAULT 0,
1984
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
1985
+ );
1986
+
1987
+ -- TESTDATA seed_data
1988
+ INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
1989
+ INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
1990
+ INSERT INTO posts (user_id, title, content, published) VALUES (1, 'Hello World', 'My first post!', 1);
1991
+ `,
1992
+ duckdb: `-- MIGRATE 1
1993
+ -- Create the users table (SQG Example - https://sqg.dev/guides/sql-syntax/)
1994
+ CREATE TABLE users (
1995
+ id INTEGER PRIMARY KEY,
1996
+ name VARCHAR NOT NULL,
1997
+ email VARCHAR UNIQUE NOT NULL,
1998
+ metadata STRUCT(role VARCHAR, active BOOLEAN),
1999
+ tags VARCHAR[],
2000
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
2001
+ );
2002
+
2003
+ -- MIGRATE 2
2004
+ CREATE TABLE posts (
2005
+ id INTEGER PRIMARY KEY,
2006
+ user_id INTEGER NOT NULL REFERENCES users(id),
2007
+ title VARCHAR NOT NULL,
2008
+ content VARCHAR,
2009
+ published BOOLEAN DEFAULT FALSE,
2010
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
2011
+ );
2012
+
2013
+ -- TESTDATA seed_data
2014
+ INSERT INTO users (id, name, email, metadata, tags)
2015
+ VALUES (1, 'Alice', 'alice@example.com', {'role': 'admin', 'active': true}, ['developer', 'lead']);
2016
+ INSERT INTO users (id, name, email, metadata, tags)
2017
+ VALUES (2, 'Bob', 'bob@example.com', {'role': 'user', 'active': true}, ['developer']);
2018
+ INSERT INTO posts (id, user_id, title, content, published)
2019
+ VALUES (1, 1, 'Hello World', 'My first post!', TRUE);
2020
+ `,
2021
+ postgres: `-- MIGRATE 1
2022
+ -- Create the users table (SQG Example - https://sqg.dev/guides/sql-syntax/)
2023
+ CREATE TABLE users (
2024
+ id SERIAL PRIMARY KEY,
2025
+ name TEXT NOT NULL,
2026
+ email TEXT UNIQUE NOT NULL,
2027
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
2028
+ );
2029
+
2030
+ -- MIGRATE 2
2031
+ CREATE TABLE posts (
2032
+ id SERIAL PRIMARY KEY,
2033
+ user_id INTEGER NOT NULL REFERENCES users(id),
2034
+ title TEXT NOT NULL,
2035
+ content TEXT,
2036
+ published BOOLEAN DEFAULT FALSE,
2037
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
2038
+ );
2039
+
2040
+ -- TESTDATA seed_data
2041
+ INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
2042
+ INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
2043
+ INSERT INTO posts (user_id, title, content, published) VALUES (1, 'Hello World', 'My first post!', TRUE);
2044
+ `
2045
+ }[engine] + `
2046
+ -- QUERY list_users
2047
+ SELECT id, name, email, created_at
2048
+ FROM users
2049
+ ORDER BY created_at DESC;
2050
+
2051
+ -- QUERY get_user_by_id :one
2052
+ @set id = 1
2053
+ SELECT id, name, email, created_at
2054
+ FROM users
2055
+ WHERE id = \${id};
2056
+
2057
+ -- QUERY get_user_by_email :one
2058
+ @set email = 'alice@example.com'
2059
+ SELECT id, name, email, created_at
2060
+ FROM users
2061
+ WHERE email = \${email};
2062
+
2063
+ -- QUERY count_users :one :pluck
2064
+ SELECT COUNT(*) FROM users;
2065
+
2066
+ -- QUERY list_posts_by_user
2067
+ @set user_id = 1
2068
+ SELECT p.id, p.title, p.content, p.published, p.created_at
2069
+ FROM posts p
2070
+ WHERE p.user_id = \${user_id}
2071
+ ORDER BY p.created_at DESC;
2072
+
2073
+ -- QUERY list_published_posts
2074
+ SELECT
2075
+ p.id,
2076
+ p.title,
2077
+ p.content,
2078
+ p.created_at,
2079
+ u.name as author_name,
2080
+ u.email as author_email
2081
+ FROM posts p
2082
+ JOIN users u ON p.user_id = u.id
2083
+ WHERE p.published = 1
2084
+ ORDER BY p.created_at DESC;
2085
+ ` + {
2086
+ sqlite: `
2087
+ -- EXEC create_user
2088
+ @set name = 'New User'
2089
+ @set email = 'new@example.com'
2090
+ INSERT INTO users (name, email)
2091
+ VALUES (\${name}, \${email});
2092
+
2093
+ -- EXEC create_post
2094
+ @set user_id = 1
2095
+ @set title = 'New Post'
2096
+ @set content = 'Post content here'
2097
+ INSERT INTO posts (user_id, title, content)
2098
+ VALUES (\${user_id}, \${title}, \${content});
2099
+
2100
+ -- EXEC publish_post
2101
+ @set id = 1
2102
+ UPDATE posts SET published = 1 WHERE id = \${id};
2103
+
2104
+ -- EXEC delete_post
2105
+ @set id = 1
2106
+ DELETE FROM posts WHERE id = \${id};
2107
+ `,
2108
+ duckdb: `
2109
+ -- EXEC create_user
2110
+ @set id = 100
2111
+ @set name = 'New User'
2112
+ @set email = 'new@example.com'
2113
+ INSERT INTO users (id, name, email)
2114
+ VALUES (\${id}, \${name}, \${email});
2115
+
2116
+ -- EXEC create_post
2117
+ @set id = 100
2118
+ @set user_id = 1
2119
+ @set title = 'New Post'
2120
+ @set content = 'Post content here'
2121
+ INSERT INTO posts (id, user_id, title, content)
2122
+ VALUES (\${id}, \${user_id}, \${title}, \${content});
2123
+
2124
+ -- EXEC publish_post
2125
+ @set id = 1
2126
+ UPDATE posts SET published = TRUE WHERE id = \${id};
2127
+
2128
+ -- EXEC delete_post
2129
+ @set id = 1
2130
+ DELETE FROM posts WHERE id = \${id};
2131
+ `,
2132
+ postgres: `
2133
+ -- EXEC create_user
2134
+ @set name = 'New User'
2135
+ @set email = 'new@example.com'
2136
+ INSERT INTO users (name, email)
2137
+ VALUES (\${name}, \${email});
2138
+
2139
+ -- EXEC create_post
2140
+ @set user_id = 1
2141
+ @set title = 'New Post'
2142
+ @set content = 'Post content here'
2143
+ INSERT INTO posts (user_id, title, content)
2144
+ VALUES (\${user_id}, \${title}, \${content});
2145
+
2146
+ -- EXEC publish_post
2147
+ @set id = 1
2148
+ UPDATE posts SET published = TRUE WHERE id = \${id};
2149
+
2150
+ -- EXEC delete_post
2151
+ @set id = 1
2152
+ DELETE FROM posts WHERE id = \${id};
2153
+ `
2154
+ }[engine];
2155
+ }
2156
+ /**
2157
+ * Generate sqg.yaml configuration
2158
+ */
2159
+ function getConfigYaml(engine, generator, output) {
2160
+ SUPPORTED_GENERATORS[generator];
2161
+ const config = {
2162
+ version: 1,
2163
+ name: "my-project",
2164
+ sql: [{
2165
+ engine,
2166
+ files: ["queries.sql"],
2167
+ gen: [{
2168
+ generator,
2169
+ output: output.endsWith("/") ? output : `${output}/`
2170
+ }]
2171
+ }]
2172
+ };
2173
+ if (generator.startsWith("java/")) config.sql[0].gen[0].config = { package: "generated" };
2174
+ return `# SQG Configuration
2175
+ # Generated by: sqg init
2176
+ # Documentation: https://sqg.dev
2177
+
2178
+ version: 1
2179
+ name: my-project
2180
+
2181
+ sql:
2182
+ - engine: ${engine}
2183
+ files:
2184
+ - queries.sql
2185
+ gen:
2186
+ - generator: ${generator}
2187
+ output: ${output.endsWith("/") ? output : `${output}/`}${generator.startsWith("java/") ? `
2188
+ config:
2189
+ package: generated` : ""}
2190
+ `;
2191
+ }
2192
+ /**
2193
+ * Initialize a new SQG project
2194
+ */
2195
+ async function initProject(options) {
2196
+ const engine = options.engine || "sqlite";
2197
+ const output = options.output || "./generated";
2198
+ if (!SUPPORTED_ENGINES.includes(engine)) throw new InvalidEngineError(engine, [...SUPPORTED_ENGINES]);
2199
+ let generator;
2200
+ if (options.generator) {
2201
+ if (!(options.generator in SUPPORTED_GENERATORS)) {
2202
+ const similar = findSimilarGenerators(options.generator);
2203
+ throw new InvalidGeneratorError(options.generator, Object.keys(SUPPORTED_GENERATORS), similar.length > 0 ? similar[0] : void 0);
2204
+ }
2205
+ generator = options.generator;
2206
+ 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(", ")}`);
2207
+ } else generator = getDefaultGenerator(engine);
2208
+ const configPath = "sqg.yaml";
2209
+ const sqlPath = "queries.sql";
2210
+ if (!options.force) {
2211
+ if (existsSync(configPath)) throw new SqgError(`File already exists: ${configPath}`, "VALIDATION_ERROR", "Use --force to overwrite existing files");
2212
+ if (existsSync(sqlPath)) throw new SqgError(`File already exists: ${sqlPath}`, "VALIDATION_ERROR", "Use --force to overwrite existing files");
2213
+ }
2214
+ if (!existsSync(output)) {
2215
+ mkdirSync(output, { recursive: true });
2216
+ consola.success(`Created output directory: ${output}`);
2217
+ }
2218
+ writeFileSync(configPath, getConfigYaml(engine, generator, output));
2219
+ consola.success(`Created ${configPath}`);
2220
+ writeFileSync(sqlPath, getExampleSql(engine));
2221
+ consola.success(`Created ${sqlPath}`);
2222
+ consola.box(`
2223
+ SQG project initialized!
2224
+
2225
+ Engine: ${engine}
2226
+ Generator: ${generator}
2227
+ Output: ${output}
2228
+
2229
+ Next steps:
2230
+ 1. Edit queries.sql to add your SQL queries
2231
+ 2. Run: sqg sqg.yaml
2232
+ 3. Import the generated code from ${output}
2233
+
2234
+ Documentation: https://sqg.dev
2235
+ `);
2236
+ }
2237
+
1547
2238
  //#endregion
1548
2239
  //#region src/sqg.ts
1549
- const version = process.env.npm_package_version ?? "0.1.1";
2240
+ const version = process.env.npm_package_version ?? "0.2.1";
1550
2241
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
1551
2242
  consola.level = LogLevels.info;
1552
- const program = new Command().name("sqg").description(description).version(version, "-v, --version", "output the version number").option("--verbose", "Enable debug logging").argument("<project>", "Path to the project YAML config").showHelpAfterError().showSuggestionAfterError().hook("preAction", (thisCommand) => {
1553
- if (thisCommand.opts().verbose) consola.level = LogLevels.debug;
1554
- }).action(async (projectPath) => {
1555
- await processProject(projectPath);
2243
+ const program = new Command().name("sqg").description(`${description}
2244
+
2245
+ Generate type-safe database access code from annotated SQL files.
2246
+
2247
+ Supported Engines:
2248
+ ${formatEnginesHelp()}
2249
+
2250
+ Supported Generators:
2251
+ ${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();
2252
+ program.argument("<project>", "Path to the project YAML config (sqg.yaml)").hook("preAction", (thisCommand) => {
2253
+ const opts = thisCommand.opts();
2254
+ if (opts.verbose) consola.level = LogLevels.debug;
2255
+ if (opts.format === "json") consola.level = LogLevels.silent;
2256
+ }).action(async (projectPath, options) => {
2257
+ try {
2258
+ if (options.validate) {
2259
+ const result = await validateProject(projectPath);
2260
+ if (options.format === "json") console.log(JSON.stringify(result, null, 2));
2261
+ else if (result.valid) {
2262
+ consola.success("Configuration is valid");
2263
+ consola.info(`Project: ${result.project?.name}`);
2264
+ consola.info(`SQL files: ${result.sqlFiles?.join(", ")}`);
2265
+ consola.info(`Generators: ${result.generators?.join(", ")}`);
2266
+ } else {
2267
+ consola.error("Validation failed");
2268
+ for (const error of result.errors || []) {
2269
+ consola.error(` ${error.message}`);
2270
+ if (error.suggestion) consola.info(` Suggestion: ${error.suggestion}`);
2271
+ }
2272
+ }
2273
+ exit(result.valid ? 0 : 1);
2274
+ }
2275
+ const files = await processProject(projectPath);
2276
+ if (options.format === "json") console.log(JSON.stringify({
2277
+ status: "success",
2278
+ generatedFiles: files
2279
+ }));
2280
+ } catch (err) {
2281
+ if (options.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
2282
+ else if (err instanceof SqgError) {
2283
+ consola.error(err.message);
2284
+ if (err.suggestion) consola.info(`Suggestion: ${err.suggestion}`);
2285
+ if (err.context && options.verbose) consola.debug("Context:", err.context);
2286
+ } else consola.error(err);
2287
+ exit(1);
2288
+ }
2289
+ });
2290
+ 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) => {
2291
+ const parentOpts = program.opts();
2292
+ try {
2293
+ await initProject(options);
2294
+ if (parentOpts.format === "json") console.log(JSON.stringify({
2295
+ status: "success",
2296
+ message: "Project initialized"
2297
+ }));
2298
+ } catch (err) {
2299
+ if (parentOpts.format === "json") console.log(JSON.stringify(formatErrorForOutput(err)));
2300
+ else if (err instanceof SqgError) {
2301
+ consola.error(err.message);
2302
+ if (err.suggestion) consola.info(`Suggestion: ${err.suggestion}`);
2303
+ } else consola.error(err);
2304
+ exit(1);
2305
+ }
2306
+ });
2307
+ program.command("syntax").description("Show SQL annotation syntax reference").action(() => {
2308
+ console.log(SQL_SYNTAX_REFERENCE);
1556
2309
  });
1557
2310
  if (process.argv.length <= 2) {
1558
2311
  program.outputHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,21 @@
20
20
  "bugs": {
21
21
  "url": "https://github.com/sqg-dev/sqg/issues"
22
22
  },
23
- "keywords": [],
23
+ "keywords": [
24
+ "sql",
25
+ "codegen",
26
+ "code-generation",
27
+ "typescript",
28
+ "java",
29
+ "sqlite",
30
+ "duckdb",
31
+ "postgres",
32
+ "postgresql",
33
+ "type-safe",
34
+ "database",
35
+ "orm-alternative",
36
+ "query-builder"
37
+ ],
24
38
  "author": "Uwe Maurer",
25
39
  "license": "Apache-2.0",
26
40
  "dependencies": {