@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.
- package/README.md +156 -0
- package/dist/sqg.mjs +834 -81
- 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
|
-
|
|
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
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
}
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
891
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
|
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)
|
|
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
|
|
1539
|
-
if (!
|
|
1540
|
-
|
|
1541
|
-
|
|
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.
|
|
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
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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.
|
|
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": {
|