@sqg/sqg 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.mjs +142 -12
- package/dist/sqg.mjs +144 -14
- package/dist/templates/better-sqlite3.hbs +1 -1
- package/dist/templates/java-jdbc.hbs +9 -0
- package/dist/templates/python.hbs +15 -9
- package/dist/templates/typescript-duckdb.hbs +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,12 +117,15 @@ console.log(user?.name);
|
|
|
117
117
|
| Annotation | Description |
|
|
118
118
|
|------------|-------------|
|
|
119
119
|
| `-- MIGRATE name` | Schema migration (CREATE TABLE, etc.) |
|
|
120
|
+
| `-- BASELINE name` | Schema created outside SQG (ETL, sibling service). Runs before migrations for type-checking; **not** emitted in `getMigrations()` |
|
|
121
|
+
| `-- BASELINE name :source=pg` | Schema of a `type: postgres` source. Applied natively to a managed testcontainer and attached into DuckDB for introspection (DuckDB generators; needs Docker) |
|
|
120
122
|
| `-- QUERY name` | SELECT query returning rows |
|
|
121
123
|
| `-- QUERY name :one` | Query returning single row or undefined |
|
|
122
124
|
| `-- QUERY name :pluck` | Return single (first) column value |
|
|
123
125
|
| `-- QUERY name :result=Foo` | Name the row type (Java). Add it to ONE query and every same-shape query shares it. Full-table `SELECT *` reuses the `TABLE` row type without annotation. |
|
|
124
126
|
| `-- EXEC name` | INSERT/UPDATE/DELETE (no result rows) |
|
|
125
127
|
| `-- TESTDATA name` | Test data, runs after migrations |
|
|
128
|
+
| `-- TABLE name :appender` | Generate a type-safe bulk insert appender (DuckDB, PostgreSQL) |
|
|
126
129
|
| `@set var = value` | Define parameter with sample value |
|
|
127
130
|
| `${var}` | Reference parameter in query |
|
|
128
131
|
|
package/dist/index.mjs
CHANGED
|
@@ -229,6 +229,7 @@ SQL Annotation Syntax:
|
|
|
229
229
|
-- QUERY <name> [:one] [:pluck] Select query (returns rows)
|
|
230
230
|
-- EXEC <name> [:batch] Execute statement (INSERT/UPDATE/DELETE)
|
|
231
231
|
-- BASELINE <name> Schema created outside SQG (runs before migrations, not tracked)
|
|
232
|
+
-- BASELINE <name> :source=<src> Schema of a 'type: postgres' source (applied to a managed testcontainer, attached into DuckDB)
|
|
232
233
|
-- MIGRATE <name> Schema migration (runs in source order; name is any identifier, e.g. "1" or "add_email")
|
|
233
234
|
-- TESTDATA <name> Test data setup (not generated)
|
|
234
235
|
-- TABLE <name> :appender Table for bulk insert appender (DuckDB, PostgreSQL)
|
|
@@ -242,6 +243,7 @@ Modifiers:
|
|
|
242
243
|
:all Return all rows (default)
|
|
243
244
|
:batch Generate a JDBC batch method for an EXEC (Java only)
|
|
244
245
|
:appender Generate bulk insert appender for TABLE annotation
|
|
246
|
+
:source=Name Mark a BASELINE block as the schema of a postgres source (Name)
|
|
245
247
|
:result=Name Name (and share) the row type (Java only). Annotate ONE query
|
|
246
248
|
with :result=Name — every other query in the same file with the
|
|
247
249
|
same column shape automatically gets the same name; you do NOT
|
|
@@ -1496,6 +1498,19 @@ var PythonGenerator = class extends BaseGenerator {
|
|
|
1496
1498
|
if (value.includes("\n") || value.includes("'") || value.includes("\"")) return `"""\\\n${value}"""`;
|
|
1497
1499
|
return `"${value}"`;
|
|
1498
1500
|
});
|
|
1501
|
+
Handlebars.registerHelper("sqlExpr", (queryHelper) => {
|
|
1502
|
+
const parts = queryHelper.sqlQueryParts;
|
|
1503
|
+
if (!parts.some((p) => typeof p !== "string" && p.name.startsWith("sources_"))) {
|
|
1504
|
+
const value = queryHelper.sqlQuery;
|
|
1505
|
+
if (value.includes("\n") || value.includes("'") || value.includes("\"")) return `"""\\\n${value}"""`;
|
|
1506
|
+
return `"${value}"`;
|
|
1507
|
+
}
|
|
1508
|
+
let out = "f\"\"\"";
|
|
1509
|
+
for (const part of parts) if (typeof part === "string") out += part.replace(/\\/g, "\\\\").replace(/\{/g, "{{").replace(/\}/g, "}}");
|
|
1510
|
+
else if (part.name.startsWith("sources_")) out += ` '{${part.name}}'`;
|
|
1511
|
+
else out += "?";
|
|
1512
|
+
return `${out}"""`;
|
|
1513
|
+
});
|
|
1499
1514
|
Handlebars.registerHelper("pyType", (column) => {
|
|
1500
1515
|
return this.getPyType(column);
|
|
1501
1516
|
});
|
|
@@ -1620,6 +1635,7 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
1620
1635
|
async beforeGenerate(_projectDir, gen, _queries, _tables) {
|
|
1621
1636
|
if (this.typeMapper instanceof TypeScriptTypeMapper) this.typeMapper.safeIntegers = !!gen.config?.safeIntegers;
|
|
1622
1637
|
Handlebars.registerHelper("quote", (value) => this.quote(value));
|
|
1638
|
+
Handlebars.registerHelper("sqlExpr", (queryHelper) => this.sqlExpr(queryHelper));
|
|
1623
1639
|
Handlebars.registerHelper("appendMethod", (column) => {
|
|
1624
1640
|
if (column.type instanceof ListType) return "List";
|
|
1625
1641
|
const typeStr = column.type?.toString().toUpperCase() || "";
|
|
@@ -1772,6 +1788,22 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
1772
1788
|
quote(value) {
|
|
1773
1789
|
return value.includes("\n") || value.includes("'") ? `\`${value}\`` : `'${value}'`;
|
|
1774
1790
|
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Build the SQL expression for a query. Without file `sources`, this is the
|
|
1793
|
+
* plain quoted SQL string (unchanged). When a `${sources_x}` reference is
|
|
1794
|
+
* present it is interpolated into a template literal as a runtime argument
|
|
1795
|
+
* (the value is supplied by the caller, not bound), mirroring the Java
|
|
1796
|
+
* generator — `ATTACH '` + arg + `'` style.
|
|
1797
|
+
*/
|
|
1798
|
+
sqlExpr(queryHelper) {
|
|
1799
|
+
const parts = queryHelper.sqlQueryParts;
|
|
1800
|
+
if (!parts.some((p) => typeof p !== "string" && p.name.startsWith("sources_"))) return this.quote(queryHelper.sqlQuery);
|
|
1801
|
+
let out = "`";
|
|
1802
|
+
for (const part of parts) if (typeof part === "string") out += part.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
1803
|
+
else if (part.name.startsWith("sources_")) out += ` '\${${part.name}}'`;
|
|
1804
|
+
else out += "?";
|
|
1805
|
+
return `${out}\``;
|
|
1806
|
+
}
|
|
1775
1807
|
};
|
|
1776
1808
|
//#endregion
|
|
1777
1809
|
//#region src/generators/typescript-duckdb-generator.ts
|
|
@@ -1878,6 +1910,57 @@ function getGenerator(generator) {
|
|
|
1878
1910
|
}
|
|
1879
1911
|
}
|
|
1880
1912
|
//#endregion
|
|
1913
|
+
//#region src/sources.ts
|
|
1914
|
+
/**
|
|
1915
|
+
* For each `type: postgres` source: start a throwaway Postgres testcontainer,
|
|
1916
|
+
* run the source's schema (the BASELINE blocks tagged `:source=<name>`) natively
|
|
1917
|
+
* against it so introspection sees true Postgres types, and return the connection
|
|
1918
|
+
* info to ATTACH into DuckDB.
|
|
1919
|
+
*
|
|
1920
|
+
* The synthesized ATTACH (done by the DuckDB adapter) and these schema blocks are
|
|
1921
|
+
* not emitted into generated code — at runtime the application attaches the real
|
|
1922
|
+
* production database under the same alias.
|
|
1923
|
+
*/
|
|
1924
|
+
async function preparePostgresSources(sources, queries, reporter) {
|
|
1925
|
+
const containers = [];
|
|
1926
|
+
const attachments = [];
|
|
1927
|
+
const teardown = async () => {
|
|
1928
|
+
for (const container of containers) try {
|
|
1929
|
+
await container.stop();
|
|
1930
|
+
} catch {}
|
|
1931
|
+
};
|
|
1932
|
+
try {
|
|
1933
|
+
for (const source of sources) {
|
|
1934
|
+
reporter?.onContainerStarting?.();
|
|
1935
|
+
const container = await new PostgreSqlContainer(source.image).withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
|
|
1936
|
+
containers.push(container);
|
|
1937
|
+
const connectionUri = container.getConnectionUri();
|
|
1938
|
+
reporter?.onContainerStarted?.(connectionUri);
|
|
1939
|
+
const schemaBlocks = queries.filter((q) => q.isBaseline && q.sourceTarget === source.name);
|
|
1940
|
+
const client = new Client({ connectionString: connectionUri });
|
|
1941
|
+
await client.connect();
|
|
1942
|
+
try {
|
|
1943
|
+
for (const block of schemaBlocks) await client.query(block.rawQuery);
|
|
1944
|
+
} catch (e) {
|
|
1945
|
+
throw new DatabaseError(`Failed to apply schema for postgres source '${source.name}': ${e.message}`, "postgres", `Check that the BASELINE blocks tagged ':source=${source.name}' are valid PostgreSQL.`);
|
|
1946
|
+
} finally {
|
|
1947
|
+
await client.end();
|
|
1948
|
+
}
|
|
1949
|
+
attachments.push({
|
|
1950
|
+
alias: source.name,
|
|
1951
|
+
connectionUri
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
} catch (e) {
|
|
1955
|
+
await teardown();
|
|
1956
|
+
throw e;
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
attachments,
|
|
1960
|
+
teardown
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
//#endregion
|
|
1881
1964
|
//#region src/sqltool.ts
|
|
1882
1965
|
const GENERATED_FILE_COMMENT = "This file is generated by SQG (https://sqg.dev). Do not edit manually.";
|
|
1883
1966
|
const configSchema = z.object({ result: z.record(z.string(), z.string()).optional() });
|
|
@@ -2054,18 +2137,23 @@ var TableHelper = class {
|
|
|
2054
2137
|
return this.generator.typeMapper;
|
|
2055
2138
|
}
|
|
2056
2139
|
};
|
|
2057
|
-
function generateSourceFile(name, queries, tables, templatePath, generator, engine, projectName, config) {
|
|
2140
|
+
function generateSourceFile(name, queries, tables, templatePath, generator, engine, projectName, config, postgresSourceNames = []) {
|
|
2058
2141
|
const templateSrc = readFileSync(templatePath, "utf-8");
|
|
2059
2142
|
const template = Handlebars.compile(templateSrc);
|
|
2060
2143
|
Handlebars.registerHelper("mapType", (column) => generator.mapType(column));
|
|
2061
2144
|
Handlebars.registerHelper("plusOne", (value) => value + 1);
|
|
2062
2145
|
const migrations = queries.filter((q) => q.isMigrate).map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q)));
|
|
2063
2146
|
const tableHelpers = generator.supportsAppenders(engine) ? tables.filter((t) => !t.skipGenerateFunction).map((t) => new TableHelper(t, generator)) : [];
|
|
2147
|
+
const attachers = engine === "duckdb" ? postgresSourceNames.map((sourceName) => ({
|
|
2148
|
+
alias: sourceName,
|
|
2149
|
+
functionName: generator.getFunctionName(`attach_${sourceName}`)
|
|
2150
|
+
})) : [];
|
|
2064
2151
|
return template({
|
|
2065
2152
|
generatedComment: GENERATED_FILE_COMMENT,
|
|
2066
2153
|
migrations,
|
|
2067
2154
|
queries: queries.map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q))),
|
|
2068
2155
|
tables: tableHelpers,
|
|
2156
|
+
attachers,
|
|
2069
2157
|
className: generator.getClassName(name),
|
|
2070
2158
|
projectName,
|
|
2071
2159
|
config
|
|
@@ -2093,9 +2181,11 @@ const ProjectSchema = z.object({
|
|
|
2093
2181
|
})).min(1, "At least one generator is required").describe("Code generators to run")
|
|
2094
2182
|
})).min(1, "At least one SQL configuration is required").describe("SQL file configurations"),
|
|
2095
2183
|
sources: z.array(z.object({
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2184
|
+
type: z.enum(["file", "postgres"]).optional().describe("Source type: 'file' (default) or 'postgres'"),
|
|
2185
|
+
path: z.string().optional().describe("Path to source file for file sources (supports $HOME)"),
|
|
2186
|
+
name: z.string().optional().describe("Variable name (file sources) or DuckDB attach alias (postgres sources)"),
|
|
2187
|
+
image: z.string().optional().describe("Docker image for postgres sources (default: postgres:16-alpine)")
|
|
2188
|
+
}).refine((s) => s.type === "postgres" ? !!s.name : !!s.path, { message: "file sources require 'path'; postgres sources require 'name'" })).optional().describe("External sources: files inlined as variables, or postgres databases attached for introspection")
|
|
2099
2189
|
});
|
|
2100
2190
|
var ExtraVariable = class {
|
|
2101
2191
|
constructor(name, value) {
|
|
@@ -2104,7 +2194,7 @@ var ExtraVariable = class {
|
|
|
2104
2194
|
}
|
|
2105
2195
|
};
|
|
2106
2196
|
function createExtraVariables(sources, suppressLogging = false) {
|
|
2107
|
-
return sources.map((source) => {
|
|
2197
|
+
return sources.filter((source) => source.type !== "postgres").map((source) => {
|
|
2108
2198
|
const path = source.path;
|
|
2109
2199
|
const resolvedPath = path.replace("$HOME", homedir());
|
|
2110
2200
|
const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
|
|
@@ -2112,6 +2202,13 @@ function createExtraVariables(sources, suppressLogging = false) {
|
|
|
2112
2202
|
return new ExtraVariable(varName, `'${resolvedPath}'`);
|
|
2113
2203
|
});
|
|
2114
2204
|
}
|
|
2205
|
+
/** Postgres sources resolved from the project config (with image defaulted). */
|
|
2206
|
+
function getPostgresSources(sources) {
|
|
2207
|
+
return sources.filter((source) => source.type === "postgres").map((source) => ({
|
|
2208
|
+
name: source.name,
|
|
2209
|
+
image: source.image ?? "postgres:16-alpine"
|
|
2210
|
+
}));
|
|
2211
|
+
}
|
|
2115
2212
|
function buildProjectFromCliOptions(options) {
|
|
2116
2213
|
if (!isValidGenerator(options.generator)) {
|
|
2117
2214
|
const similar = findSimilarGenerators(options.generator);
|
|
@@ -2250,7 +2347,7 @@ function assignResultTypeNames(queries, tables, projectDir = process.cwd()) {
|
|
|
2250
2347
|
}
|
|
2251
2348
|
const fullMatch = tryFullTableMatch(group.queries[0], tablesByName);
|
|
2252
2349
|
if (fullMatch) {
|
|
2253
|
-
claim(fullMatch, shape, group.queries[0],
|
|
2350
|
+
claim(fullMatch, shape, group.queries[0], "full-table match");
|
|
2254
2351
|
for (const q of group.queries) q.resultTypeName = fullMatch;
|
|
2255
2352
|
continue;
|
|
2256
2353
|
}
|
|
@@ -2303,14 +2400,14 @@ function validateQueries(queries) {
|
|
|
2303
2400
|
};
|
|
2304
2401
|
}
|
|
2305
2402
|
}
|
|
2306
|
-
async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false) {
|
|
2403
|
+
async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false, postgresSourceNames = []) {
|
|
2307
2404
|
await generator.beforeGenerate(projectDir, gen, queries, tables);
|
|
2308
2405
|
const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
|
|
2309
2406
|
const name = gen.name ?? basename(file, extname(file));
|
|
2310
2407
|
const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.projectName ?? name, {
|
|
2311
2408
|
migrations: true,
|
|
2312
2409
|
...gen.config
|
|
2313
|
-
});
|
|
2410
|
+
}, postgresSourceNames);
|
|
2314
2411
|
if (writeToStdout) {
|
|
2315
2412
|
process.stdout.write(sourceFile);
|
|
2316
2413
|
if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
|
|
@@ -2411,6 +2508,11 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2411
2508
|
const results = [];
|
|
2412
2509
|
try {
|
|
2413
2510
|
const extraVariables = createExtraVariables(project.sources ?? [], writeToStdout);
|
|
2511
|
+
const pgSources = getPostgresSources(project.sources ?? []);
|
|
2512
|
+
const pgSourceNames = new Set(pgSources.map((s) => s.name));
|
|
2513
|
+
if (pgSources.length > 0) {
|
|
2514
|
+
if (!project.sql.some((sql) => sql.gen.some((gen) => getGeneratorEngine(gen.generator) === "duckdb"))) throw new SqgError("Postgres sources require a DuckDB generator (they are attached into DuckDB for introspection)", "CONFIG_VALIDATION_ERROR", "Add a 'typescript/duckdb' or 'java/duckdb' generator, or remove the postgres sources");
|
|
2515
|
+
}
|
|
2414
2516
|
const files = [];
|
|
2415
2517
|
for (const sql of project.sql) {
|
|
2416
2518
|
const gensByEngine = /* @__PURE__ */ new Map();
|
|
@@ -2428,6 +2530,7 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2428
2530
|
const parseResult = parseSQLQueries(fullPath, extraVariables);
|
|
2429
2531
|
queries = parseResult.queries;
|
|
2430
2532
|
tables = parseResult.tables;
|
|
2533
|
+
for (const q of queries) if (q.sourceTarget && !pgSourceNames.has(q.sourceTarget)) throw SqgError.inQuery(`BASELINE block targets unknown postgres source '${q.sourceTarget}'`, "CONFIG_VALIDATION_ERROR", q.id, sqlFile, { suggestion: `Declare a source with 'type: postgres, name: ${q.sourceTarget}' in sqg.yaml` });
|
|
2431
2534
|
} catch (e) {
|
|
2432
2535
|
if (e instanceof SqgError) throw e;
|
|
2433
2536
|
throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
|
|
@@ -2436,10 +2539,17 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2436
2539
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
2437
2540
|
const reporterAny = reporter;
|
|
2438
2541
|
if (reporterAny?.setQueryTotal) reporterAny.setQueryTotal(executableQueries.length);
|
|
2542
|
+
let preparedSources;
|
|
2439
2543
|
try {
|
|
2440
2544
|
const dbEngine = getDatabaseEngine(engine);
|
|
2545
|
+
let attachments;
|
|
2546
|
+
if (engine === "duckdb" && pgSources.length > 0) {
|
|
2547
|
+
ui?.startPhase("Starting postgres source containers...");
|
|
2548
|
+
preparedSources = await preparePostgresSources(pgSources, queries, reporter);
|
|
2549
|
+
attachments = preparedSources.attachments;
|
|
2550
|
+
}
|
|
2441
2551
|
ui?.startPhase(`Initializing ${engine} database...`);
|
|
2442
|
-
await dbEngine.initializeDatabase(queries, reporter);
|
|
2552
|
+
await dbEngine.initializeDatabase(queries, reporter, { attachments });
|
|
2443
2553
|
ui?.startPhase(`Introspecting ${executableQueries.length} queries...`);
|
|
2444
2554
|
await dbEngine.executeQueries(queries, reporter);
|
|
2445
2555
|
if (tables.length > 0) await dbEngine.introspectTables(tables, reporter);
|
|
@@ -2456,6 +2566,8 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2456
2566
|
file: sqlFile,
|
|
2457
2567
|
engine
|
|
2458
2568
|
});
|
|
2569
|
+
} finally {
|
|
2570
|
+
if (preparedSources) await preparedSources.teardown();
|
|
2459
2571
|
}
|
|
2460
2572
|
ui?.startPhase("Generating code...");
|
|
2461
2573
|
const genStart = performance.now();
|
|
@@ -2464,7 +2576,7 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2464
2576
|
const outputPath = await writeGeneratedFile(projectDir, {
|
|
2465
2577
|
...gen,
|
|
2466
2578
|
projectName: project.name
|
|
2467
|
-
}, generator, sqlFile, queries, tables, engine, writeToStdout);
|
|
2579
|
+
}, generator, sqlFile, queries, tables, engine, writeToStdout, pgSources.map((s) => s.name));
|
|
2468
2580
|
if (outputPath !== null) {
|
|
2469
2581
|
files.push(outputPath);
|
|
2470
2582
|
results.push({
|
|
@@ -2537,6 +2649,13 @@ var SQLQuery = class {
|
|
|
2537
2649
|
parameterTypes;
|
|
2538
2650
|
/** Explicit row-type name from `:result=Name` modifier, when present. */
|
|
2539
2651
|
resultTypeOverride;
|
|
2652
|
+
/**
|
|
2653
|
+
* Name of the postgres `source` this block targets, from a `:source=name`
|
|
2654
|
+
* modifier on a BASELINE block. Such blocks define the schema of an external
|
|
2655
|
+
* postgres source and run natively against that source's container during
|
|
2656
|
+
* generation (not against the DuckDB introspection connection).
|
|
2657
|
+
*/
|
|
2658
|
+
sourceTarget;
|
|
2540
2659
|
/** Final row-type identifier assigned by assignResultTypeNames (after introspection). */
|
|
2541
2660
|
resultTypeName;
|
|
2542
2661
|
constructor(filename, id, rawQuery, queryAnonymous, queryNamed, queryPositional, type, isOne, isPluck, isBatch, variables, config, line) {
|
|
@@ -2653,6 +2772,8 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2653
2772
|
const isPluck = modifiers.includes(":pluck");
|
|
2654
2773
|
const isBatch = modifiers.includes(":batch");
|
|
2655
2774
|
const resultTypeOverride = modifiers.find((m) => m.startsWith(":result="))?.slice(8);
|
|
2775
|
+
const sourceTarget = modifiers.find((m) => m.startsWith(":source="))?.slice(8);
|
|
2776
|
+
if (sourceTarget && queryType !== "BASELINE") throw SqgError.inQuery(`The ':source=' modifier is only valid on BASELINE blocks, not ${queryType}`, "SQL_PARSE_ERROR", name, filePath, { suggestion: "Move the schema for a postgres source into a '-- BASELINE <name> :source=<source>' block" });
|
|
2656
2777
|
let configStr = getStr("Config", true);
|
|
2657
2778
|
if (configStr?.endsWith("*/")) configStr = configStr.slice(0, -2);
|
|
2658
2779
|
let config = null;
|
|
@@ -2808,6 +2929,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2808
2929
|
});
|
|
2809
2930
|
const query = new SQLQuery(filePath, name, sqlContentStr, sql.toSqlWithAnonymousPlaceholders(), sql.toSqlWithNamedPlaceholders(), sql.toSqlWithPositionalPlaceholders(), queryType, isOne, isPluck, isBatch, variables, config, annotationLine);
|
|
2810
2931
|
query.resultTypeOverride = resultTypeOverride;
|
|
2932
|
+
query.sourceTarget = sourceTarget;
|
|
2811
2933
|
checkDuplicate(queryType, name);
|
|
2812
2934
|
queries.push(query);
|
|
2813
2935
|
consola.debug(`Added query: ${name} (${queryType})`);
|
|
@@ -2824,7 +2946,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
2824
2946
|
//#endregion
|
|
2825
2947
|
//#region src/db/types.ts
|
|
2826
2948
|
async function initializeDatabase(queries, execQueries, reporter) {
|
|
2827
|
-
const baselineQueries = queries.filter((q) => q.isBaseline);
|
|
2949
|
+
const baselineQueries = queries.filter((q) => q.isBaseline && !q.sourceTarget);
|
|
2828
2950
|
for (const query of baselineQueries) try {
|
|
2829
2951
|
await execQueries(query);
|
|
2830
2952
|
} catch (error) {
|
|
@@ -2880,9 +3002,17 @@ function convertType(type) {
|
|
|
2880
3002
|
const duckdb = new class {
|
|
2881
3003
|
db;
|
|
2882
3004
|
connection;
|
|
2883
|
-
async initializeDatabase(queries, reporter) {
|
|
3005
|
+
async initializeDatabase(queries, reporter, options) {
|
|
2884
3006
|
this.db = await DuckDBInstance.create(":memory:");
|
|
2885
3007
|
this.connection = await this.db.connect();
|
|
3008
|
+
for (const attachment of options?.attachments ?? []) {
|
|
3009
|
+
const sql = `ATTACH '${attachment.connectionUri}' AS ${attachment.alias} (TYPE postgres)`;
|
|
3010
|
+
try {
|
|
3011
|
+
await this.connection.run(sql);
|
|
3012
|
+
} catch (e) {
|
|
3013
|
+
throw new DatabaseError(`Failed to attach source '${attachment.alias}': ${e.message}`, "duckdb", "Check that the postgres source container started and is reachable.");
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
2886
3016
|
await initializeDatabase(queries, async (query) => {
|
|
2887
3017
|
try {
|
|
2888
3018
|
await this.connection.run(query.rawQuery);
|
package/dist/sqg.mjs
CHANGED
|
@@ -221,6 +221,7 @@ SQL Annotation Syntax:
|
|
|
221
221
|
-- QUERY <name> [:one] [:pluck] Select query (returns rows)
|
|
222
222
|
-- EXEC <name> [:batch] Execute statement (INSERT/UPDATE/DELETE)
|
|
223
223
|
-- BASELINE <name> Schema created outside SQG (runs before migrations, not tracked)
|
|
224
|
+
-- BASELINE <name> :source=<src> Schema of a 'type: postgres' source (applied to a managed testcontainer, attached into DuckDB)
|
|
224
225
|
-- MIGRATE <name> Schema migration (runs in source order; name is any identifier, e.g. "1" or "add_email")
|
|
225
226
|
-- TESTDATA <name> Test data setup (not generated)
|
|
226
227
|
-- TABLE <name> :appender Table for bulk insert appender (DuckDB, PostgreSQL)
|
|
@@ -234,6 +235,7 @@ Modifiers:
|
|
|
234
235
|
:all Return all rows (default)
|
|
235
236
|
:batch Generate a JDBC batch method for an EXEC (Java only)
|
|
236
237
|
:appender Generate bulk insert appender for TABLE annotation
|
|
238
|
+
:source=Name Mark a BASELINE block as the schema of a postgres source (Name)
|
|
237
239
|
:result=Name Name (and share) the row type (Java only). Annotate ONE query
|
|
238
240
|
with :result=Name — every other query in the same file with the
|
|
239
241
|
same column shape automatically gets the same name; you do NOT
|
|
@@ -783,6 +785,13 @@ var SQLQuery = class {
|
|
|
783
785
|
parameterTypes;
|
|
784
786
|
/** Explicit row-type name from `:result=Name` modifier, when present. */
|
|
785
787
|
resultTypeOverride;
|
|
788
|
+
/**
|
|
789
|
+
* Name of the postgres `source` this block targets, from a `:source=name`
|
|
790
|
+
* modifier on a BASELINE block. Such blocks define the schema of an external
|
|
791
|
+
* postgres source and run natively against that source's container during
|
|
792
|
+
* generation (not against the DuckDB introspection connection).
|
|
793
|
+
*/
|
|
794
|
+
sourceTarget;
|
|
786
795
|
/** Final row-type identifier assigned by assignResultTypeNames (after introspection). */
|
|
787
796
|
resultTypeName;
|
|
788
797
|
constructor(filename, id, rawQuery, queryAnonymous, queryNamed, queryPositional, type, isOne, isPluck, isBatch, variables, config, line) {
|
|
@@ -899,6 +908,8 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
899
908
|
const isPluck = modifiers.includes(":pluck");
|
|
900
909
|
const isBatch = modifiers.includes(":batch");
|
|
901
910
|
const resultTypeOverride = modifiers.find((m) => m.startsWith(":result="))?.slice(8);
|
|
911
|
+
const sourceTarget = modifiers.find((m) => m.startsWith(":source="))?.slice(8);
|
|
912
|
+
if (sourceTarget && queryType !== "BASELINE") throw SqgError.inQuery(`The ':source=' modifier is only valid on BASELINE blocks, not ${queryType}`, "SQL_PARSE_ERROR", name, filePath, { suggestion: "Move the schema for a postgres source into a '-- BASELINE <name> :source=<source>' block" });
|
|
902
913
|
let configStr = getStr("Config", true);
|
|
903
914
|
if (configStr?.endsWith("*/")) configStr = configStr.slice(0, -2);
|
|
904
915
|
let config = null;
|
|
@@ -1054,6 +1065,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
1054
1065
|
});
|
|
1055
1066
|
const query = new SQLQuery(filePath, name, sqlContentStr, sql.toSqlWithAnonymousPlaceholders(), sql.toSqlWithNamedPlaceholders(), sql.toSqlWithPositionalPlaceholders(), queryType, isOne, isPluck, isBatch, variables, config, annotationLine);
|
|
1056
1067
|
query.resultTypeOverride = resultTypeOverride;
|
|
1068
|
+
query.sourceTarget = sourceTarget;
|
|
1057
1069
|
checkDuplicate(queryType, name);
|
|
1058
1070
|
queries.push(query);
|
|
1059
1071
|
consola.debug(`Added query: ${name} (${queryType})`);
|
|
@@ -1070,7 +1082,7 @@ function parseSQLQueries(filePath, extraVariables) {
|
|
|
1070
1082
|
//#endregion
|
|
1071
1083
|
//#region src/db/types.ts
|
|
1072
1084
|
async function initializeDatabase(queries, execQueries, reporter) {
|
|
1073
|
-
const baselineQueries = queries.filter((q) => q.isBaseline);
|
|
1085
|
+
const baselineQueries = queries.filter((q) => q.isBaseline && !q.sourceTarget);
|
|
1074
1086
|
for (const query of baselineQueries) try {
|
|
1075
1087
|
await execQueries(query);
|
|
1076
1088
|
} catch (error) {
|
|
@@ -1126,9 +1138,17 @@ function convertType(type) {
|
|
|
1126
1138
|
const duckdb = new class {
|
|
1127
1139
|
db;
|
|
1128
1140
|
connection;
|
|
1129
|
-
async initializeDatabase(queries, reporter) {
|
|
1141
|
+
async initializeDatabase(queries, reporter, options) {
|
|
1130
1142
|
this.db = await DuckDBInstance.create(":memory:");
|
|
1131
1143
|
this.connection = await this.db.connect();
|
|
1144
|
+
for (const attachment of options?.attachments ?? []) {
|
|
1145
|
+
const sql = `ATTACH '${attachment.connectionUri}' AS ${attachment.alias} (TYPE postgres)`;
|
|
1146
|
+
try {
|
|
1147
|
+
await this.connection.run(sql);
|
|
1148
|
+
} catch (e) {
|
|
1149
|
+
throw new DatabaseError(`Failed to attach source '${attachment.alias}': ${e.message}`, "duckdb", "Check that the postgres source container started and is reachable.");
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1132
1152
|
await initializeDatabase(queries, async (query) => {
|
|
1133
1153
|
try {
|
|
1134
1154
|
await this.connection.run(query.rawQuery);
|
|
@@ -2726,6 +2746,19 @@ var PythonGenerator = class extends BaseGenerator {
|
|
|
2726
2746
|
if (value.includes("\n") || value.includes("'") || value.includes("\"")) return `"""\\\n${value}"""`;
|
|
2727
2747
|
return `"${value}"`;
|
|
2728
2748
|
});
|
|
2749
|
+
Handlebars.registerHelper("sqlExpr", (queryHelper) => {
|
|
2750
|
+
const parts = queryHelper.sqlQueryParts;
|
|
2751
|
+
if (!parts.some((p) => typeof p !== "string" && p.name.startsWith("sources_"))) {
|
|
2752
|
+
const value = queryHelper.sqlQuery;
|
|
2753
|
+
if (value.includes("\n") || value.includes("'") || value.includes("\"")) return `"""\\\n${value}"""`;
|
|
2754
|
+
return `"${value}"`;
|
|
2755
|
+
}
|
|
2756
|
+
let out = "f\"\"\"";
|
|
2757
|
+
for (const part of parts) if (typeof part === "string") out += part.replace(/\\/g, "\\\\").replace(/\{/g, "{{").replace(/\}/g, "}}");
|
|
2758
|
+
else if (part.name.startsWith("sources_")) out += ` '{${part.name}}'`;
|
|
2759
|
+
else out += "?";
|
|
2760
|
+
return `${out}"""`;
|
|
2761
|
+
});
|
|
2729
2762
|
Handlebars.registerHelper("pyType", (column) => {
|
|
2730
2763
|
return this.getPyType(column);
|
|
2731
2764
|
});
|
|
@@ -2850,6 +2883,7 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
2850
2883
|
async beforeGenerate(_projectDir, gen, _queries, _tables) {
|
|
2851
2884
|
if (this.typeMapper instanceof TypeScriptTypeMapper) this.typeMapper.safeIntegers = !!gen.config?.safeIntegers;
|
|
2852
2885
|
Handlebars.registerHelper("quote", (value) => this.quote(value));
|
|
2886
|
+
Handlebars.registerHelper("sqlExpr", (queryHelper) => this.sqlExpr(queryHelper));
|
|
2853
2887
|
Handlebars.registerHelper("appendMethod", (column) => {
|
|
2854
2888
|
if (column.type instanceof ListType) return "List";
|
|
2855
2889
|
const typeStr = column.type?.toString().toUpperCase() || "";
|
|
@@ -3002,6 +3036,22 @@ var TsGenerator = class extends BaseGenerator {
|
|
|
3002
3036
|
quote(value) {
|
|
3003
3037
|
return value.includes("\n") || value.includes("'") ? `\`${value}\`` : `'${value}'`;
|
|
3004
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* Build the SQL expression for a query. Without file `sources`, this is the
|
|
3041
|
+
* plain quoted SQL string (unchanged). When a `${sources_x}` reference is
|
|
3042
|
+
* present it is interpolated into a template literal as a runtime argument
|
|
3043
|
+
* (the value is supplied by the caller, not bound), mirroring the Java
|
|
3044
|
+
* generator — `ATTACH '` + arg + `'` style.
|
|
3045
|
+
*/
|
|
3046
|
+
sqlExpr(queryHelper) {
|
|
3047
|
+
const parts = queryHelper.sqlQueryParts;
|
|
3048
|
+
if (!parts.some((p) => typeof p !== "string" && p.name.startsWith("sources_"))) return this.quote(queryHelper.sqlQuery);
|
|
3049
|
+
let out = "`";
|
|
3050
|
+
for (const part of parts) if (typeof part === "string") out += part.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${");
|
|
3051
|
+
else if (part.name.startsWith("sources_")) out += ` '\${${part.name}}'`;
|
|
3052
|
+
else out += "?";
|
|
3053
|
+
return `${out}\``;
|
|
3054
|
+
}
|
|
3005
3055
|
};
|
|
3006
3056
|
//#endregion
|
|
3007
3057
|
//#region src/generators/typescript-duckdb-generator.ts
|
|
@@ -3108,6 +3158,57 @@ function getGenerator(generator) {
|
|
|
3108
3158
|
}
|
|
3109
3159
|
}
|
|
3110
3160
|
//#endregion
|
|
3161
|
+
//#region src/sources.ts
|
|
3162
|
+
/**
|
|
3163
|
+
* For each `type: postgres` source: start a throwaway Postgres testcontainer,
|
|
3164
|
+
* run the source's schema (the BASELINE blocks tagged `:source=<name>`) natively
|
|
3165
|
+
* against it so introspection sees true Postgres types, and return the connection
|
|
3166
|
+
* info to ATTACH into DuckDB.
|
|
3167
|
+
*
|
|
3168
|
+
* The synthesized ATTACH (done by the DuckDB adapter) and these schema blocks are
|
|
3169
|
+
* not emitted into generated code — at runtime the application attaches the real
|
|
3170
|
+
* production database under the same alias.
|
|
3171
|
+
*/
|
|
3172
|
+
async function preparePostgresSources(sources, queries, reporter) {
|
|
3173
|
+
const containers = [];
|
|
3174
|
+
const attachments = [];
|
|
3175
|
+
const teardown = async () => {
|
|
3176
|
+
for (const container of containers) try {
|
|
3177
|
+
await container.stop();
|
|
3178
|
+
} catch {}
|
|
3179
|
+
};
|
|
3180
|
+
try {
|
|
3181
|
+
for (const source of sources) {
|
|
3182
|
+
reporter?.onContainerStarting?.();
|
|
3183
|
+
const container = await new PostgreSqlContainer(source.image).withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
|
|
3184
|
+
containers.push(container);
|
|
3185
|
+
const connectionUri = container.getConnectionUri();
|
|
3186
|
+
reporter?.onContainerStarted?.(connectionUri);
|
|
3187
|
+
const schemaBlocks = queries.filter((q) => q.isBaseline && q.sourceTarget === source.name);
|
|
3188
|
+
const client = new Client({ connectionString: connectionUri });
|
|
3189
|
+
await client.connect();
|
|
3190
|
+
try {
|
|
3191
|
+
for (const block of schemaBlocks) await client.query(block.rawQuery);
|
|
3192
|
+
} catch (e) {
|
|
3193
|
+
throw new DatabaseError(`Failed to apply schema for postgres source '${source.name}': ${e.message}`, "postgres", `Check that the BASELINE blocks tagged ':source=${source.name}' are valid PostgreSQL.`);
|
|
3194
|
+
} finally {
|
|
3195
|
+
await client.end();
|
|
3196
|
+
}
|
|
3197
|
+
attachments.push({
|
|
3198
|
+
alias: source.name,
|
|
3199
|
+
connectionUri
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
} catch (e) {
|
|
3203
|
+
await teardown();
|
|
3204
|
+
throw e;
|
|
3205
|
+
}
|
|
3206
|
+
return {
|
|
3207
|
+
attachments,
|
|
3208
|
+
teardown
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
//#endregion
|
|
3111
3212
|
//#region src/sqltool.ts
|
|
3112
3213
|
const GENERATED_FILE_COMMENT = "This file is generated by SQG (https://sqg.dev). Do not edit manually.";
|
|
3113
3214
|
const configSchema = z.object({ result: z.record(z.string(), z.string()).optional() });
|
|
@@ -3284,18 +3385,23 @@ var TableHelper = class {
|
|
|
3284
3385
|
return this.generator.typeMapper;
|
|
3285
3386
|
}
|
|
3286
3387
|
};
|
|
3287
|
-
function generateSourceFile(name, queries, tables, templatePath, generator, engine, projectName, config) {
|
|
3388
|
+
function generateSourceFile(name, queries, tables, templatePath, generator, engine, projectName, config, postgresSourceNames = []) {
|
|
3288
3389
|
const templateSrc = readFileSync(templatePath, "utf-8");
|
|
3289
3390
|
const template = Handlebars.compile(templateSrc);
|
|
3290
3391
|
Handlebars.registerHelper("mapType", (column) => generator.mapType(column));
|
|
3291
3392
|
Handlebars.registerHelper("plusOne", (value) => value + 1);
|
|
3292
3393
|
const migrations = queries.filter((q) => q.isMigrate).map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q)));
|
|
3293
3394
|
const tableHelpers = generator.supportsAppenders(engine) ? tables.filter((t) => !t.skipGenerateFunction).map((t) => new TableHelper(t, generator)) : [];
|
|
3395
|
+
const attachers = engine === "duckdb" ? postgresSourceNames.map((sourceName) => ({
|
|
3396
|
+
alias: sourceName,
|
|
3397
|
+
functionName: generator.getFunctionName(`attach_${sourceName}`)
|
|
3398
|
+
})) : [];
|
|
3294
3399
|
return template({
|
|
3295
3400
|
generatedComment: GENERATED_FILE_COMMENT,
|
|
3296
3401
|
migrations,
|
|
3297
3402
|
queries: queries.map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q))),
|
|
3298
3403
|
tables: tableHelpers,
|
|
3404
|
+
attachers,
|
|
3299
3405
|
className: generator.getClassName(name),
|
|
3300
3406
|
projectName,
|
|
3301
3407
|
config
|
|
@@ -3323,9 +3429,11 @@ const ProjectSchema = z.object({
|
|
|
3323
3429
|
})).min(1, "At least one generator is required").describe("Code generators to run")
|
|
3324
3430
|
})).min(1, "At least one SQL configuration is required").describe("SQL file configurations"),
|
|
3325
3431
|
sources: z.array(z.object({
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3432
|
+
type: z.enum(["file", "postgres"]).optional().describe("Source type: 'file' (default) or 'postgres'"),
|
|
3433
|
+
path: z.string().optional().describe("Path to source file for file sources (supports $HOME)"),
|
|
3434
|
+
name: z.string().optional().describe("Variable name (file sources) or DuckDB attach alias (postgres sources)"),
|
|
3435
|
+
image: z.string().optional().describe("Docker image for postgres sources (default: postgres:16-alpine)")
|
|
3436
|
+
}).refine((s) => s.type === "postgres" ? !!s.name : !!s.path, { message: "file sources require 'path'; postgres sources require 'name'" })).optional().describe("External sources: files inlined as variables, or postgres databases attached for introspection")
|
|
3329
3437
|
});
|
|
3330
3438
|
var ExtraVariable = class {
|
|
3331
3439
|
constructor(name, value) {
|
|
@@ -3334,7 +3442,7 @@ var ExtraVariable = class {
|
|
|
3334
3442
|
}
|
|
3335
3443
|
};
|
|
3336
3444
|
function createExtraVariables(sources, suppressLogging = false) {
|
|
3337
|
-
return sources.map((source) => {
|
|
3445
|
+
return sources.filter((source) => source.type !== "postgres").map((source) => {
|
|
3338
3446
|
const path = source.path;
|
|
3339
3447
|
const resolvedPath = path.replace("$HOME", homedir());
|
|
3340
3448
|
const varName = `sources_${(source.name ?? basename(path, extname(resolvedPath))).replace(/\s+/g, "_")}`;
|
|
@@ -3342,6 +3450,13 @@ function createExtraVariables(sources, suppressLogging = false) {
|
|
|
3342
3450
|
return new ExtraVariable(varName, `'${resolvedPath}'`);
|
|
3343
3451
|
});
|
|
3344
3452
|
}
|
|
3453
|
+
/** Postgres sources resolved from the project config (with image defaulted). */
|
|
3454
|
+
function getPostgresSources(sources) {
|
|
3455
|
+
return sources.filter((source) => source.type === "postgres").map((source) => ({
|
|
3456
|
+
name: source.name,
|
|
3457
|
+
image: source.image ?? "postgres:16-alpine"
|
|
3458
|
+
}));
|
|
3459
|
+
}
|
|
3345
3460
|
function buildProjectFromCliOptions(options) {
|
|
3346
3461
|
if (!isValidGenerator(options.generator)) {
|
|
3347
3462
|
const similar = findSimilarGenerators(options.generator);
|
|
@@ -3480,7 +3595,7 @@ function assignResultTypeNames(queries, tables, projectDir = process.cwd()) {
|
|
|
3480
3595
|
}
|
|
3481
3596
|
const fullMatch = tryFullTableMatch(group.queries[0], tablesByName);
|
|
3482
3597
|
if (fullMatch) {
|
|
3483
|
-
claim(fullMatch, shape, group.queries[0],
|
|
3598
|
+
claim(fullMatch, shape, group.queries[0], "full-table match");
|
|
3484
3599
|
for (const q of group.queries) q.resultTypeName = fullMatch;
|
|
3485
3600
|
continue;
|
|
3486
3601
|
}
|
|
@@ -3533,14 +3648,14 @@ function validateQueries(queries) {
|
|
|
3533
3648
|
};
|
|
3534
3649
|
}
|
|
3535
3650
|
}
|
|
3536
|
-
async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false) {
|
|
3651
|
+
async function writeGeneratedFile(projectDir, gen, generator, file, queries, tables, engine, writeToStdout = false, postgresSourceNames = []) {
|
|
3537
3652
|
await generator.beforeGenerate(projectDir, gen, queries, tables);
|
|
3538
3653
|
const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
|
|
3539
3654
|
const name = gen.name ?? basename(file, extname(file));
|
|
3540
3655
|
const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.projectName ?? name, {
|
|
3541
3656
|
migrations: true,
|
|
3542
3657
|
...gen.config
|
|
3543
|
-
});
|
|
3658
|
+
}, postgresSourceNames);
|
|
3544
3659
|
if (writeToStdout) {
|
|
3545
3660
|
process.stdout.write(sourceFile);
|
|
3546
3661
|
if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
|
|
@@ -3641,6 +3756,11 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
3641
3756
|
const results = [];
|
|
3642
3757
|
try {
|
|
3643
3758
|
const extraVariables = createExtraVariables(project.sources ?? [], writeToStdout);
|
|
3759
|
+
const pgSources = getPostgresSources(project.sources ?? []);
|
|
3760
|
+
const pgSourceNames = new Set(pgSources.map((s) => s.name));
|
|
3761
|
+
if (pgSources.length > 0) {
|
|
3762
|
+
if (!project.sql.some((sql) => sql.gen.some((gen) => getGeneratorEngine(gen.generator) === "duckdb"))) throw new SqgError("Postgres sources require a DuckDB generator (they are attached into DuckDB for introspection)", "CONFIG_VALIDATION_ERROR", "Add a 'typescript/duckdb' or 'java/duckdb' generator, or remove the postgres sources");
|
|
3763
|
+
}
|
|
3644
3764
|
const files = [];
|
|
3645
3765
|
for (const sql of project.sql) {
|
|
3646
3766
|
const gensByEngine = /* @__PURE__ */ new Map();
|
|
@@ -3658,6 +3778,7 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
3658
3778
|
const parseResult = parseSQLQueries(fullPath, extraVariables);
|
|
3659
3779
|
queries = parseResult.queries;
|
|
3660
3780
|
tables = parseResult.tables;
|
|
3781
|
+
for (const q of queries) if (q.sourceTarget && !pgSourceNames.has(q.sourceTarget)) throw SqgError.inQuery(`BASELINE block targets unknown postgres source '${q.sourceTarget}'`, "CONFIG_VALIDATION_ERROR", q.id, sqlFile, { suggestion: `Declare a source with 'type: postgres, name: ${q.sourceTarget}' in sqg.yaml` });
|
|
3661
3782
|
} catch (e) {
|
|
3662
3783
|
if (e instanceof SqgError) throw e;
|
|
3663
3784
|
throw SqgError.inFile(`Failed to parse SQL file: ${e.message}`, "SQL_PARSE_ERROR", sqlFile, { suggestion: "Check SQL syntax and annotation format" });
|
|
@@ -3666,10 +3787,17 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
3666
3787
|
const executableQueries = queries.filter((q) => !q.skipGenerateFunction);
|
|
3667
3788
|
const reporterAny = reporter;
|
|
3668
3789
|
if (reporterAny?.setQueryTotal) reporterAny.setQueryTotal(executableQueries.length);
|
|
3790
|
+
let preparedSources;
|
|
3669
3791
|
try {
|
|
3670
3792
|
const dbEngine = getDatabaseEngine(engine);
|
|
3793
|
+
let attachments;
|
|
3794
|
+
if (engine === "duckdb" && pgSources.length > 0) {
|
|
3795
|
+
ui?.startPhase("Starting postgres source containers...");
|
|
3796
|
+
preparedSources = await preparePostgresSources(pgSources, queries, reporter);
|
|
3797
|
+
attachments = preparedSources.attachments;
|
|
3798
|
+
}
|
|
3671
3799
|
ui?.startPhase(`Initializing ${engine} database...`);
|
|
3672
|
-
await dbEngine.initializeDatabase(queries, reporter);
|
|
3800
|
+
await dbEngine.initializeDatabase(queries, reporter, { attachments });
|
|
3673
3801
|
ui?.startPhase(`Introspecting ${executableQueries.length} queries...`);
|
|
3674
3802
|
await dbEngine.executeQueries(queries, reporter);
|
|
3675
3803
|
if (tables.length > 0) await dbEngine.introspectTables(tables, reporter);
|
|
@@ -3686,6 +3814,8 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
3686
3814
|
file: sqlFile,
|
|
3687
3815
|
engine
|
|
3688
3816
|
});
|
|
3817
|
+
} finally {
|
|
3818
|
+
if (preparedSources) await preparedSources.teardown();
|
|
3689
3819
|
}
|
|
3690
3820
|
ui?.startPhase("Generating code...");
|
|
3691
3821
|
const genStart = performance.now();
|
|
@@ -3694,7 +3824,7 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
3694
3824
|
const outputPath = await writeGeneratedFile(projectDir, {
|
|
3695
3825
|
...gen,
|
|
3696
3826
|
projectName: project.name
|
|
3697
|
-
}, generator, sqlFile, queries, tables, engine, writeToStdout);
|
|
3827
|
+
}, generator, sqlFile, queries, tables, engine, writeToStdout, pgSources.map((s) => s.name));
|
|
3698
3828
|
if (outputPath !== null) {
|
|
3699
3829
|
files.push(outputPath);
|
|
3700
3830
|
results.push({
|
|
@@ -3728,7 +3858,7 @@ async function processProject(projectPath, ui) {
|
|
|
3728
3858
|
//#region src/mcp-server.ts
|
|
3729
3859
|
const server = new Server({
|
|
3730
3860
|
name: "sqg-mcp",
|
|
3731
|
-
version: process.env.npm_package_version ?? "0.
|
|
3861
|
+
version: process.env.npm_package_version ?? "0.21.0"
|
|
3732
3862
|
}, { capabilities: {
|
|
3733
3863
|
tools: {},
|
|
3734
3864
|
resources: {}
|
|
@@ -4199,7 +4329,7 @@ function formatMs(ms) {
|
|
|
4199
4329
|
}
|
|
4200
4330
|
//#endregion
|
|
4201
4331
|
//#region src/sqg.ts
|
|
4202
|
-
const version = process.env.npm_package_version ?? "0.
|
|
4332
|
+
const version = process.env.npm_package_version ?? "0.21.0";
|
|
4203
4333
|
updateNotifier({ pkg: {
|
|
4204
4334
|
name: "@sqg/sqg",
|
|
4205
4335
|
version
|
|
@@ -69,7 +69,7 @@ export class {{className}} {
|
|
|
69
69
|
{{#unless skipGenerateFunction}}
|
|
70
70
|
{{functionName}}({{#each variables}}{{name}}: {{type}}{{#unless @last}}, {{/unless}}{{/each}}): {{> returnType }} {
|
|
71
71
|
const stmt = this.prepare<[{{> paramTypes}}], {{> resultType}}>('{{id}}',
|
|
72
|
-
{{{
|
|
72
|
+
{{{sqlExpr this}}}{{#if isPluck}}, true{{/if}});
|
|
73
73
|
{{> execute}}
|
|
74
74
|
}
|
|
75
75
|
{{/unless}}
|
|
@@ -223,6 +223,15 @@ public class {{className}} {
|
|
|
223
223
|
|
|
224
224
|
{{{declareEnums queries}}}
|
|
225
225
|
|
|
226
|
+
{{#each attachers}}
|
|
227
|
+
/** Attach the "{{alias}}" postgres source under the same alias used during generation. */
|
|
228
|
+
public void {{functionName}}(String connectionString) throws SQLException {
|
|
229
|
+
try (var stmt = connection.createStatement()) {
|
|
230
|
+
stmt.execute("ATTACH '" + connectionString + "' AS {{alias}} (TYPE postgres)");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
{{/each}}
|
|
226
235
|
{{#each queries}}
|
|
227
236
|
{{#unless skipGenerateFunction}}
|
|
228
237
|
{{>columnTypesRecord}}
|
|
@@ -135,6 +135,12 @@ class {{className}}:
|
|
|
135
135
|
{{/if}}
|
|
136
136
|
{{/if}}
|
|
137
137
|
|
|
138
|
+
{{#each attachers}}
|
|
139
|
+
def {{functionName}}(self, connection_string: str) -> None:
|
|
140
|
+
"""Attach the "{{alias}}" postgres source under the same alias used during generation."""
|
|
141
|
+
self._conn.execute(f"ATTACH '{connection_string}' AS {{alias}} (TYPE postgres)")
|
|
142
|
+
|
|
143
|
+
{{/each}}
|
|
138
144
|
{{#each queries}}
|
|
139
145
|
{{#unless skipGenerateFunction}}
|
|
140
146
|
{{#if isQuery}}
|
|
@@ -142,7 +148,7 @@ class {{className}}:
|
|
|
142
148
|
{{#if isOne}}
|
|
143
149
|
def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> {{{pyTypeOrNone (lookup columns 0)}}}:
|
|
144
150
|
row = self._conn.execute(
|
|
145
|
-
{{{
|
|
151
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
146
152
|
).fetchone()
|
|
147
153
|
if row is None:
|
|
148
154
|
return None
|
|
@@ -150,19 +156,19 @@ class {{className}}:
|
|
|
150
156
|
|
|
151
157
|
def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> tuple[Any, ...] | None:
|
|
152
158
|
return self._conn.execute(
|
|
153
|
-
{{{
|
|
159
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
154
160
|
).fetchone()
|
|
155
161
|
|
|
156
162
|
{{else}}
|
|
157
163
|
def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[{{{pyType (lookup columns 0)}}}]:
|
|
158
164
|
rows = self._conn.execute(
|
|
159
|
-
{{{
|
|
165
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
160
166
|
).fetchall()
|
|
161
167
|
return [r[0] for r in rows]
|
|
162
168
|
|
|
163
169
|
def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[tuple[Any, ...]]:
|
|
164
170
|
return self._conn.execute(
|
|
165
|
-
{{{
|
|
171
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
166
172
|
).fetchall()
|
|
167
173
|
|
|
168
174
|
{{/if}}
|
|
@@ -170,7 +176,7 @@ class {{className}}:
|
|
|
170
176
|
{{#if isOne}}
|
|
171
177
|
def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> {{{rowType}}} | None:
|
|
172
178
|
row = self._conn.execute(
|
|
173
|
-
{{{
|
|
179
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
174
180
|
).fetchone()
|
|
175
181
|
if row is None:
|
|
176
182
|
return None
|
|
@@ -178,19 +184,19 @@ class {{className}}:
|
|
|
178
184
|
|
|
179
185
|
def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> tuple[Any, ...] | None:
|
|
180
186
|
return self._conn.execute(
|
|
181
|
-
{{{
|
|
187
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
182
188
|
).fetchone()
|
|
183
189
|
|
|
184
190
|
{{else}}
|
|
185
191
|
def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[{{{rowType}}}]:
|
|
186
192
|
rows = self._conn.execute(
|
|
187
|
-
{{{
|
|
193
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
188
194
|
).fetchall()
|
|
189
195
|
return [{{{constructRow this}}} for row in rows]
|
|
190
196
|
|
|
191
197
|
def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[tuple[Any, ...]]:
|
|
192
198
|
return self._conn.execute(
|
|
193
|
-
{{{
|
|
199
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
194
200
|
).fetchall()
|
|
195
201
|
|
|
196
202
|
{{/if}}
|
|
@@ -198,7 +204,7 @@ class {{className}}:
|
|
|
198
204
|
{{else}}
|
|
199
205
|
def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> None:
|
|
200
206
|
self._conn.execute(
|
|
201
|
-
{{{
|
|
207
|
+
{{{sqlExpr this}}}, {{> params}}
|
|
202
208
|
)
|
|
203
209
|
|
|
204
210
|
{{/if}}
|
|
@@ -58,10 +58,17 @@ export class {{className}} {
|
|
|
58
58
|
);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
{{#each attachers}}
|
|
62
|
+
/** Attach the "{{alias}}" postgres source under the same alias used during generation. */
|
|
63
|
+
async {{functionName}}(connectionString: string): Promise<void> {
|
|
64
|
+
await this.conn.run(`ATTACH '${connectionString}' AS {{alias}} (TYPE postgres)`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
{{/each}}
|
|
61
68
|
{{#each queries}}
|
|
62
69
|
{{#unless skipGenerateFunction}}
|
|
63
70
|
async {{functionName}}({{#each variables}}{{name}}: {{type}}{{#unless @last}}, {{/unless}}{{/each}}): Promise<{{> returnType }}> {
|
|
64
|
-
const sql = {{{
|
|
71
|
+
const sql = {{{sqlExpr this}}};
|
|
65
72
|
{{#if (hasListParams this)}}
|
|
66
73
|
const stmt = await this.conn.prepare(sql);
|
|
67
74
|
{{{bindStatements this}}}
|