@sqg/sqg 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/sqg.mjs +138 -19
- package/dist/templates/better-sqlite3.hbs +31 -1
- package/dist/templates/java-duckdb-arrow.hbs +11 -1
- package/dist/templates/java-jdbc.hbs +63 -1
- package/dist/templates/libsql.hbs +40 -0
- package/dist/templates/node-sqlite.hbs +33 -0
- package/dist/templates/turso.hbs +35 -0
- package/dist/templates/typescript-duckdb.hbs +37 -1
- package/package.json +2 -1
package/dist/sqg.mjs
CHANGED
|
@@ -680,6 +680,8 @@ var EnumType = class {
|
|
|
680
680
|
var SQLQuery = class {
|
|
681
681
|
columns;
|
|
682
682
|
allColumns;
|
|
683
|
+
/** Database-reported parameter types (variable name → SQL type name), set by database adapters */
|
|
684
|
+
parameterTypes;
|
|
683
685
|
constructor(filename, id, rawQuery, queryAnonymous, queryNamed, queryPositional, type, isOne, isPluck, variables, config) {
|
|
684
686
|
this.filename = filename;
|
|
685
687
|
this.id = id;
|
|
@@ -1094,14 +1096,63 @@ const duckdb = new class {
|
|
|
1094
1096
|
//#endregion
|
|
1095
1097
|
//#region src/db/postgres.ts
|
|
1096
1098
|
const databaseName = "sqg-db-temp";
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
+
let containerInstance = null;
|
|
1100
|
+
async function startTestContainer() {
|
|
1101
|
+
if (containerInstance) return containerInstance.getConnectionUri();
|
|
1102
|
+
consola.info("Starting PostgreSQL container via testcontainers...");
|
|
1103
|
+
const { PostgreSqlContainer } = await import("@testcontainers/postgresql");
|
|
1104
|
+
containerInstance = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
|
|
1105
|
+
const connectionUri = containerInstance.getConnectionUri();
|
|
1106
|
+
consola.success(`PostgreSQL container started at: ${connectionUri}`);
|
|
1107
|
+
return connectionUri;
|
|
1108
|
+
}
|
|
1109
|
+
async function stopTestContainer() {
|
|
1110
|
+
if (containerInstance) {
|
|
1111
|
+
consola.info("Stopping PostgreSQL container...");
|
|
1112
|
+
await containerInstance.stop();
|
|
1113
|
+
containerInstance = null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
async function getConnectionString() {
|
|
1117
|
+
if (process.env.SQG_POSTGRES_URL) return process.env.SQG_POSTGRES_URL;
|
|
1118
|
+
return await startTestContainer();
|
|
1119
|
+
}
|
|
1120
|
+
function getTempConnectionString(baseUrl) {
|
|
1121
|
+
return baseUrl.replace(/\/[^/]+$/, `/${databaseName}`);
|
|
1122
|
+
}
|
|
1099
1123
|
const typeIdToName = /* @__PURE__ */ new Map();
|
|
1100
1124
|
for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
|
|
1125
|
+
let dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1126
|
+
async function loadTypeCache(db) {
|
|
1127
|
+
const result = await db.query(`
|
|
1128
|
+
SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype
|
|
1129
|
+
FROM pg_type t
|
|
1130
|
+
LEFT JOIN pg_type et ON t.typelem = et.oid
|
|
1131
|
+
WHERE t.typtype IN ('b', 'e', 'r', 'c') -- base, enum, range, composite
|
|
1132
|
+
OR t.typelem != 0 -- array types
|
|
1133
|
+
`);
|
|
1134
|
+
dynamicTypeCache = /* @__PURE__ */ new Map();
|
|
1135
|
+
for (const row of result.rows) {
|
|
1136
|
+
const oid = row.oid;
|
|
1137
|
+
let typeName = row.typname;
|
|
1138
|
+
if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
|
|
1139
|
+
else typeName = typeName.toUpperCase();
|
|
1140
|
+
dynamicTypeCache.set(oid, typeName);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
function getTypeName(dataTypeID) {
|
|
1144
|
+
const cached = dynamicTypeCache.get(dataTypeID);
|
|
1145
|
+
if (cached) return cached;
|
|
1146
|
+
return typeIdToName.get(dataTypeID) || `type_${dataTypeID}`;
|
|
1147
|
+
}
|
|
1101
1148
|
const postgres = new class {
|
|
1102
1149
|
dbInitial;
|
|
1103
1150
|
db;
|
|
1151
|
+
usingTestContainer = false;
|
|
1104
1152
|
async initializeDatabase(queries) {
|
|
1153
|
+
const connectionString = await getConnectionString();
|
|
1154
|
+
const connectionStringTemp = getTempConnectionString(connectionString);
|
|
1155
|
+
this.usingTestContainer = containerInstance !== null;
|
|
1105
1156
|
this.dbInitial = new Client({ connectionString });
|
|
1106
1157
|
this.db = new Client({ connectionString: connectionStringTemp });
|
|
1107
1158
|
try {
|
|
@@ -1110,7 +1161,7 @@ const postgres = new class {
|
|
|
1110
1161
|
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.`);
|
|
1111
1162
|
}
|
|
1112
1163
|
try {
|
|
1113
|
-
await this.dbInitial.query(`DROP DATABASE "${databaseName}";`);
|
|
1164
|
+
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${databaseName}";`);
|
|
1114
1165
|
} catch (error) {}
|
|
1115
1166
|
try {
|
|
1116
1167
|
await this.dbInitial.query(`CREATE DATABASE "${databaseName}";`);
|
|
@@ -1129,6 +1180,7 @@ const postgres = new class {
|
|
|
1129
1180
|
throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
|
|
1130
1181
|
}
|
|
1131
1182
|
});
|
|
1183
|
+
await loadTypeCache(this.db);
|
|
1132
1184
|
}
|
|
1133
1185
|
async executeQueries(queries) {
|
|
1134
1186
|
const db = this.db;
|
|
@@ -1150,17 +1202,39 @@ const postgres = new class {
|
|
|
1150
1202
|
const statement = query.queryPositional;
|
|
1151
1203
|
try {
|
|
1152
1204
|
consola.info("Query:", statement.sql);
|
|
1205
|
+
const parameterValues = statement.parameters.map((p) => {
|
|
1206
|
+
const value = p.value;
|
|
1207
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
|
|
1208
|
+
return value;
|
|
1209
|
+
});
|
|
1210
|
+
if (statement.parameters.length > 0) try {
|
|
1211
|
+
await db.query(`DEALLOCATE ALL`);
|
|
1212
|
+
await db.query(`PREPARE sqg_param_check AS ${statement.sql}`);
|
|
1213
|
+
const paramTypeResult = await db.query(`SELECT unnest(parameter_types)::oid AS oid FROM pg_prepared_statements WHERE name = 'sqg_param_check'`);
|
|
1214
|
+
await db.query(`DEALLOCATE sqg_param_check`);
|
|
1215
|
+
if (paramTypeResult.rows.length === statement.parameters.length) {
|
|
1216
|
+
const paramTypes = /* @__PURE__ */ new Map();
|
|
1217
|
+
for (let i = 0; i < statement.parameters.length; i++) {
|
|
1218
|
+
const typeName = getTypeName(Number(paramTypeResult.rows[i].oid));
|
|
1219
|
+
paramTypes.set(statement.parameters[i].name, typeName);
|
|
1220
|
+
}
|
|
1221
|
+
query.parameterTypes = paramTypes;
|
|
1222
|
+
consola.debug("Parameter types:", Object.fromEntries(paramTypes));
|
|
1223
|
+
}
|
|
1224
|
+
} catch (e) {
|
|
1225
|
+
consola.debug(`Parameter type introspection failed for ${query.id}, using heuristic:`, e.message);
|
|
1226
|
+
}
|
|
1153
1227
|
let result;
|
|
1154
1228
|
try {
|
|
1155
1229
|
await db.query("BEGIN");
|
|
1156
|
-
result = await db.query(statement.sql,
|
|
1230
|
+
result = await db.query(statement.sql, parameterValues);
|
|
1157
1231
|
} finally {
|
|
1158
1232
|
await db.query("ROLLBACK");
|
|
1159
1233
|
}
|
|
1160
1234
|
if (query.isQuery) {
|
|
1161
1235
|
const columnNames = result.fields.map((field) => field.name);
|
|
1162
1236
|
const columnTypes = result.fields.map((field) => {
|
|
1163
|
-
return
|
|
1237
|
+
return getTypeName(field.dataTypeID);
|
|
1164
1238
|
});
|
|
1165
1239
|
consola.debug("Columns:", columnNames);
|
|
1166
1240
|
consola.debug("Types:", columnTypes);
|
|
@@ -1203,8 +1277,9 @@ const postgres = new class {
|
|
|
1203
1277
|
}
|
|
1204
1278
|
async close() {
|
|
1205
1279
|
await this.db.end();
|
|
1206
|
-
await this.dbInitial.query(`DROP DATABASE "${databaseName}"`);
|
|
1280
|
+
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${databaseName}"`);
|
|
1207
1281
|
await this.dbInitial.end();
|
|
1282
|
+
if (this.usingTestContainer) await stopTestContainer();
|
|
1208
1283
|
}
|
|
1209
1284
|
}();
|
|
1210
1285
|
|
|
@@ -1396,7 +1471,20 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1396
1471
|
INTERVAL: "String",
|
|
1397
1472
|
BIT: "String",
|
|
1398
1473
|
BIGNUM: "BigDecimal",
|
|
1399
|
-
|
|
1474
|
+
INT2: "Short",
|
|
1475
|
+
INT4: "Integer",
|
|
1476
|
+
INT8: "Long",
|
|
1477
|
+
FLOAT4: "Float",
|
|
1478
|
+
FLOAT8: "Double",
|
|
1479
|
+
NUMERIC: "BigDecimal",
|
|
1480
|
+
BOOL: "Boolean",
|
|
1481
|
+
BYTEA: "byte[]",
|
|
1482
|
+
TIMESTAMPTZ: "OffsetDateTime",
|
|
1483
|
+
JSON: "String",
|
|
1484
|
+
JSONB: "String",
|
|
1485
|
+
OID: "Long",
|
|
1486
|
+
SERIAL: "Integer",
|
|
1487
|
+
BIGSERIAL: "Long"
|
|
1400
1488
|
};
|
|
1401
1489
|
static javaReservedKeywords = new Set([
|
|
1402
1490
|
"abstract",
|
|
@@ -1457,12 +1545,15 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1457
1545
|
const upperType = type.toString().toUpperCase();
|
|
1458
1546
|
const mappedType = this.typeMap[upperType];
|
|
1459
1547
|
if (mappedType) return mappedType;
|
|
1548
|
+
if (upperType.startsWith("_")) {
|
|
1549
|
+
const baseType = upperType.substring(1);
|
|
1550
|
+
return `List<${this.typeMap[baseType] || "Object"}>`;
|
|
1551
|
+
}
|
|
1460
1552
|
if (upperType.startsWith("DECIMAL(") || upperType.startsWith("NUMERIC(")) return "BigDecimal";
|
|
1461
1553
|
if (upperType.startsWith("ENUM(")) return "String";
|
|
1462
1554
|
if (upperType.startsWith("UNION(")) return "Object";
|
|
1463
1555
|
if (/\[\d+\]/.test(upperType)) return "Object";
|
|
1464
|
-
|
|
1465
|
-
return "Object";
|
|
1556
|
+
return "String";
|
|
1466
1557
|
}
|
|
1467
1558
|
formatListType(elementType) {
|
|
1468
1559
|
return `List<${elementType}>`;
|
|
@@ -1515,8 +1606,13 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
|
|
|
1515
1606
|
const fieldType = this.getTypeName(column);
|
|
1516
1607
|
const upperType = column.type?.toString().toUpperCase() ?? "";
|
|
1517
1608
|
if (upperType === "TIMESTAMP" || upperType === "DATETIME") return `toLocalDateTime((java.sql.Timestamp)${value})`;
|
|
1609
|
+
if (upperType === "TIMESTAMPTZ") return `toOffsetDateTime((java.sql.Timestamp)${value})`;
|
|
1518
1610
|
if (upperType === "DATE") return `toLocalDate((java.sql.Date)${value})`;
|
|
1519
1611
|
if (upperType === "TIME") return `toLocalTime((java.sql.Time)${value})`;
|
|
1612
|
+
if (upperType.startsWith("_")) {
|
|
1613
|
+
const baseType = upperType.substring(1);
|
|
1614
|
+
return `arrayToList((Array)${value}, ${this.typeMap[baseType] || "Object"}[].class)`;
|
|
1615
|
+
}
|
|
1520
1616
|
return `(${fieldType})${value}`;
|
|
1521
1617
|
}
|
|
1522
1618
|
getInnermostType(type) {
|
|
@@ -1584,12 +1680,30 @@ var TypeScriptTypeMapper = class extends TypeMapper {
|
|
|
1584
1680
|
INTERVAL: "{ months: number; days: number; micros: bigint }",
|
|
1585
1681
|
BIT: "{ data: Uint8Array }",
|
|
1586
1682
|
BIGNUM: "bigint",
|
|
1587
|
-
|
|
1683
|
+
INT2: "number",
|
|
1684
|
+
INT4: "number",
|
|
1685
|
+
INT8: "bigint",
|
|
1686
|
+
FLOAT4: "number",
|
|
1687
|
+
FLOAT8: "number",
|
|
1688
|
+
NUMERIC: "string",
|
|
1689
|
+
BOOL: "boolean",
|
|
1690
|
+
BYTEA: "Buffer",
|
|
1691
|
+
TIMESTAMPTZ: "Date",
|
|
1692
|
+
JSON: "unknown",
|
|
1693
|
+
JSONB: "unknown",
|
|
1694
|
+
OID: "number",
|
|
1695
|
+
SERIAL: "number",
|
|
1696
|
+
BIGSERIAL: "bigint"
|
|
1588
1697
|
};
|
|
1589
1698
|
mapPrimitiveType(type, nullable) {
|
|
1590
1699
|
const upperType = type.toUpperCase();
|
|
1591
1700
|
const mappedType = this.typeMap[upperType];
|
|
1592
1701
|
if (mappedType) return nullable ? `${mappedType} | null` : mappedType;
|
|
1702
|
+
if (upperType.startsWith("_")) {
|
|
1703
|
+
const baseType = upperType.substring(1);
|
|
1704
|
+
const arrayType = `${this.typeMap[baseType] || "unknown"}[]`;
|
|
1705
|
+
return nullable ? `${arrayType} | null` : arrayType;
|
|
1706
|
+
}
|
|
1593
1707
|
if (upperType.startsWith("DECIMAL(") || upperType.startsWith("NUMERIC(")) {
|
|
1594
1708
|
const baseType = "{ width: number; scale: number; value: bigint }";
|
|
1595
1709
|
return nullable ? `${baseType} | null` : baseType;
|
|
@@ -1614,8 +1728,7 @@ var TypeScriptTypeMapper = class extends TypeMapper {
|
|
|
1614
1728
|
}
|
|
1615
1729
|
}
|
|
1616
1730
|
if (/\[\d+\]/.test(upperType)) return "{ items: unknown[] }";
|
|
1617
|
-
|
|
1618
|
-
return "unknown";
|
|
1731
|
+
return nullable ? "string | null" : "string";
|
|
1619
1732
|
}
|
|
1620
1733
|
formatListType(elementType) {
|
|
1621
1734
|
return `{ items: (${elementType})[] }`;
|
|
@@ -1800,7 +1913,8 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
|
|
|
1800
1913
|
name,
|
|
1801
1914
|
generator: "java/duckdb/jdbc",
|
|
1802
1915
|
output: gen.output,
|
|
1803
|
-
config: gen.config
|
|
1916
|
+
config: gen.config,
|
|
1917
|
+
projectName: gen.projectName
|
|
1804
1918
|
}, this.javaGenerator, name, q, tables, "duckdb");
|
|
1805
1919
|
}
|
|
1806
1920
|
isCompatibleWith(engine) {
|
|
@@ -2209,7 +2323,7 @@ var SqlQueryHelper = class {
|
|
|
2209
2323
|
get variables() {
|
|
2210
2324
|
return Array.from(this.query.variables.entries()).map(([name, value]) => ({
|
|
2211
2325
|
name,
|
|
2212
|
-
type: this.generator.mapParameterType(detectParameterType(value), false)
|
|
2326
|
+
type: this.generator.mapParameterType(this.query.parameterTypes?.get(name) ?? detectParameterType(value), false)
|
|
2213
2327
|
}));
|
|
2214
2328
|
}
|
|
2215
2329
|
get sqlQuery() {
|
|
@@ -2266,7 +2380,7 @@ var TableHelper = class {
|
|
|
2266
2380
|
return this.generator.typeMapper;
|
|
2267
2381
|
}
|
|
2268
2382
|
};
|
|
2269
|
-
function generateSourceFile(name, queries, tables, templatePath, generator, engine, config) {
|
|
2383
|
+
function generateSourceFile(name, queries, tables, templatePath, generator, engine, projectName, config) {
|
|
2270
2384
|
const templateSrc = readFileSync(templatePath, "utf-8");
|
|
2271
2385
|
const template = Handlebars.compile(templateSrc);
|
|
2272
2386
|
Handlebars.registerHelper("mapType", (column) => generator.mapType(column));
|
|
@@ -2279,6 +2393,7 @@ function generateSourceFile(name, queries, tables, templatePath, generator, engi
|
|
|
2279
2393
|
queries: queries.map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q))),
|
|
2280
2394
|
tables: tableHelpers,
|
|
2281
2395
|
className: generator.getClassName(name),
|
|
2396
|
+
projectName,
|
|
2282
2397
|
config
|
|
2283
2398
|
}, {
|
|
2284
2399
|
allowProtoPropertiesByDefault: true,
|
|
@@ -2418,7 +2533,7 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
|
|
|
2418
2533
|
await generator.beforeGenerate(projectDir, gen, queries, tables);
|
|
2419
2534
|
const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
|
|
2420
2535
|
const name = gen.name ?? basename(file, extname(file));
|
|
2421
|
-
const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.config);
|
|
2536
|
+
const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.projectName ?? name, gen.config);
|
|
2422
2537
|
if (writeToStdout) {
|
|
2423
2538
|
process.stdout.write(sourceFile);
|
|
2424
2539
|
if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
|
|
@@ -2545,7 +2660,11 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
|
|
|
2545
2660
|
});
|
|
2546
2661
|
}
|
|
2547
2662
|
for (const gen of gens) {
|
|
2548
|
-
const
|
|
2663
|
+
const generator = getGenerator(gen.generator);
|
|
2664
|
+
const outputPath = await writeGeneratedFile(projectDir, {
|
|
2665
|
+
...gen,
|
|
2666
|
+
projectName: project.name
|
|
2667
|
+
}, generator, sqlFile, queries, tables, engine, writeToStdout);
|
|
2549
2668
|
if (outputPath !== null) files.push(outputPath);
|
|
2550
2669
|
}
|
|
2551
2670
|
}
|
|
@@ -2568,7 +2687,7 @@ async function processProject(projectPath) {
|
|
|
2568
2687
|
//#region src/mcp-server.ts
|
|
2569
2688
|
const server = new Server({
|
|
2570
2689
|
name: "sqg-mcp",
|
|
2571
|
-
version: process.env.npm_package_version ?? "0.
|
|
2690
|
+
version: process.env.npm_package_version ?? "0.9.0"
|
|
2572
2691
|
}, { capabilities: {
|
|
2573
2692
|
tools: {},
|
|
2574
2693
|
resources: {}
|
|
@@ -2897,7 +3016,7 @@ async function startMcpServer() {
|
|
|
2897
3016
|
|
|
2898
3017
|
//#endregion
|
|
2899
3018
|
//#region src/sqg.ts
|
|
2900
|
-
const version = process.env.npm_package_version ?? "0.
|
|
3019
|
+
const version = process.env.npm_package_version ?? "0.9.0";
|
|
2901
3020
|
const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
|
|
2902
3021
|
consola.level = LogLevels.info;
|
|
2903
3022
|
const program = new Command().name("sqg").description(`${description}
|
|
@@ -18,7 +18,7 @@ export class {{className}} {
|
|
|
18
18
|
return stmt as Statement<BindParameters, Result>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
static getMigrations(): string[] {
|
|
21
|
+
static getMigrations(): string[] {
|
|
22
22
|
return [
|
|
23
23
|
{{#each migrations}}
|
|
24
24
|
{{{quote sqlQuery}}},
|
|
@@ -26,6 +26,36 @@ export class {{className}} {
|
|
|
26
26
|
];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
{{#if config.migrations}}
|
|
30
|
+
static applyMigrations(db: Database, projectName = '{{projectName}}'): void {
|
|
31
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _sqg_migrations (
|
|
32
|
+
project TEXT NOT NULL,
|
|
33
|
+
migration_id TEXT NOT NULL,
|
|
34
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
35
|
+
PRIMARY KEY (project, migration_id)
|
|
36
|
+
)`);
|
|
37
|
+
const runMigrations = db.transaction(() => {
|
|
38
|
+
const applied = new Set(
|
|
39
|
+
db.prepare('SELECT migration_id FROM _sqg_migrations WHERE project = ?')
|
|
40
|
+
.pluck().all(projectName) as string[]
|
|
41
|
+
);
|
|
42
|
+
const migrations: [string, string][] = [
|
|
43
|
+
{{#each migrations}}
|
|
44
|
+
['{{{id}}}', {{{quote sqlQuery}}}],
|
|
45
|
+
{{/each}}
|
|
46
|
+
];
|
|
47
|
+
for (const [id, sql] of migrations) {
|
|
48
|
+
if (!applied.has(id)) {
|
|
49
|
+
db.exec(sql);
|
|
50
|
+
db.prepare('INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)')
|
|
51
|
+
.run(projectName, id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
runMigrations.immediate();
|
|
56
|
+
}
|
|
57
|
+
{{/if}}
|
|
58
|
+
|
|
29
59
|
static getQueryNames(): Map<string, keyof {{className}}> {
|
|
30
60
|
return new Map([
|
|
31
61
|
{{#each queries}} {{#unless skipGenerateFunction}}
|
|
@@ -29,10 +29,20 @@ public class {{className}} {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
public static List<String> getMigrations() {
|
|
32
|
+
public static List<String> getMigrations() {
|
|
33
33
|
return {{className}}Jdbc.getMigrations();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
{{#if config.migrations}}
|
|
37
|
+
public static void applyMigrations(Connection connection) throws SQLException {
|
|
38
|
+
{{className}}Jdbc.applyMigrations(connection);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public static void applyMigrations(Connection connection, String projectName) throws SQLException {
|
|
42
|
+
{{className}}Jdbc.applyMigrations(connection, projectName);
|
|
43
|
+
}
|
|
44
|
+
{{/if}}
|
|
45
|
+
|
|
36
46
|
{{#each queries}}
|
|
37
47
|
{{#unless skipGenerateFunction}}
|
|
38
48
|
{{#if isOne}}
|
|
@@ -13,6 +13,7 @@ import java.time.Instant;
|
|
|
13
13
|
import java.time.LocalDate;
|
|
14
14
|
import java.time.LocalDateTime;
|
|
15
15
|
import java.time.LocalTime;
|
|
16
|
+
import java.time.OffsetDateTime;
|
|
16
17
|
import java.time.OffsetTime;
|
|
17
18
|
import java.util.ArrayList;
|
|
18
19
|
import java.util.Arrays;
|
|
@@ -65,6 +66,10 @@ public class {{className}} {
|
|
|
65
66
|
private static LocalTime toLocalTime(java.sql.Time t) {
|
|
66
67
|
return t != null ? t.toLocalTime() : null;
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
private static OffsetDateTime toOffsetDateTime(java.sql.Timestamp ts) {
|
|
71
|
+
return ts != null ? ts.toInstant().atOffset(java.time.ZoneOffset.UTC) : null;
|
|
72
|
+
}
|
|
68
73
|
|
|
69
74
|
private static <K> List<K> arrayToList(
|
|
70
75
|
Array array,
|
|
@@ -129,10 +134,67 @@ public class {{className}} {
|
|
|
129
134
|
{{/each}}
|
|
130
135
|
);
|
|
131
136
|
|
|
132
|
-
|
|
137
|
+
{{#if config.migrations}}
|
|
138
|
+
private static final List<String> migrationIds = List.of(
|
|
139
|
+
{{#each migrations}}"{{id}}"{{#unless @last}},{{/unless}}
|
|
140
|
+
{{/each}}
|
|
141
|
+
);
|
|
142
|
+
{{/if}}
|
|
143
|
+
|
|
144
|
+
public static List<String> getMigrations() {
|
|
133
145
|
return migrations;
|
|
134
146
|
}
|
|
135
147
|
|
|
148
|
+
{{#if config.migrations}}
|
|
149
|
+
public static void applyMigrations(Connection connection) throws SQLException {
|
|
150
|
+
applyMigrations(connection, "{{projectName}}");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public static void applyMigrations(Connection connection, String projectName) throws SQLException {
|
|
154
|
+
try (var stmt = connection.createStatement()) {
|
|
155
|
+
stmt.execute("""
|
|
156
|
+
CREATE TABLE IF NOT EXISTS _sqg_migrations (
|
|
157
|
+
project TEXT NOT NULL,
|
|
158
|
+
migration_id TEXT NOT NULL,
|
|
159
|
+
applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
160
|
+
PRIMARY KEY (project, migration_id)
|
|
161
|
+
)""");
|
|
162
|
+
}
|
|
163
|
+
boolean wasAutoCommit = connection.getAutoCommit();
|
|
164
|
+
connection.setAutoCommit(false);
|
|
165
|
+
try {
|
|
166
|
+
var applied = new java.util.HashSet<String>();
|
|
167
|
+
try (var stmt = connection.prepareStatement("SELECT migration_id FROM _sqg_migrations WHERE project = ?")) {
|
|
168
|
+
stmt.setString(1, projectName);
|
|
169
|
+
try (var rs = stmt.executeQuery()) {
|
|
170
|
+
while (rs.next()) {
|
|
171
|
+
applied.add(rs.getString(1));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (int i = 0; i < migrations.size(); i++) {
|
|
176
|
+
var id = migrationIds.get(i);
|
|
177
|
+
if (!applied.contains(id)) {
|
|
178
|
+
try (var stmt = connection.createStatement()) {
|
|
179
|
+
stmt.execute(migrations.get(i));
|
|
180
|
+
}
|
|
181
|
+
try (var stmt = connection.prepareStatement("INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)")) {
|
|
182
|
+
stmt.setString(1, projectName);
|
|
183
|
+
stmt.setString(2, id);
|
|
184
|
+
stmt.executeUpdate();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
connection.commit();
|
|
189
|
+
} catch (SQLException e) {
|
|
190
|
+
connection.rollback();
|
|
191
|
+
throw e;
|
|
192
|
+
} finally {
|
|
193
|
+
connection.setAutoCommit(wasAutoCommit);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
{{/if}}
|
|
197
|
+
|
|
136
198
|
{{#each queries}}
|
|
137
199
|
{{#unless skipGenerateFunction}}
|
|
138
200
|
{{>columnTypesRecord}}
|
|
@@ -17,6 +17,46 @@ export class {{className}} {
|
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
{{#if config.migrations}}
|
|
21
|
+
static async applyMigrations(client: Client, projectName = '{{projectName}}'): Promise<void> {
|
|
22
|
+
await client.execute({
|
|
23
|
+
sql: `CREATE TABLE IF NOT EXISTS _sqg_migrations (
|
|
24
|
+
project TEXT NOT NULL,
|
|
25
|
+
migration_id TEXT NOT NULL,
|
|
26
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
27
|
+
PRIMARY KEY (project, migration_id)
|
|
28
|
+
)`,
|
|
29
|
+
args: [],
|
|
30
|
+
});
|
|
31
|
+
const tx = await client.transaction('write');
|
|
32
|
+
try {
|
|
33
|
+
const result = await tx.execute({
|
|
34
|
+
sql: 'SELECT migration_id FROM _sqg_migrations WHERE project = ?',
|
|
35
|
+
args: [projectName],
|
|
36
|
+
});
|
|
37
|
+
const applied = new Set(result.rows.map(r => r.migration_id as string));
|
|
38
|
+
const migrations: [string, string][] = [
|
|
39
|
+
{{#each migrations}}
|
|
40
|
+
['{{{id}}}', {{{quote sqlQuery}}}],
|
|
41
|
+
{{/each}}
|
|
42
|
+
];
|
|
43
|
+
for (const [id, sql] of migrations) {
|
|
44
|
+
if (!applied.has(id)) {
|
|
45
|
+
await tx.execute({ sql, args: [] });
|
|
46
|
+
await tx.execute({
|
|
47
|
+
sql: 'INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)',
|
|
48
|
+
args: [projectName, id],
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
await tx.commit();
|
|
53
|
+
} catch (e) {
|
|
54
|
+
await tx.rollback();
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
{{/if}}
|
|
59
|
+
|
|
20
60
|
static getQueryNames(): Map<string, keyof {{className}}> {
|
|
21
61
|
return new Map([
|
|
22
62
|
{{#each queries}} {{#unless skipGenerateFunction}}
|
|
@@ -28,6 +28,39 @@ export class {{className}} {
|
|
|
28
28
|
];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
{{#if config.migrations}}
|
|
32
|
+
static applyMigrations(db: DatabaseSync, projectName = '{{projectName}}'): void {
|
|
33
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _sqg_migrations (
|
|
34
|
+
project TEXT NOT NULL,
|
|
35
|
+
migration_id TEXT NOT NULL,
|
|
36
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
PRIMARY KEY (project, migration_id)
|
|
38
|
+
)`);
|
|
39
|
+
db.exec('BEGIN IMMEDIATE');
|
|
40
|
+
try {
|
|
41
|
+
const rows = db.prepare('SELECT migration_id FROM _sqg_migrations WHERE project = ?')
|
|
42
|
+
.all(projectName) as { migration_id: string }[];
|
|
43
|
+
const applied = new Set(rows.map(r => r.migration_id));
|
|
44
|
+
const migrations: [string, string][] = [
|
|
45
|
+
{{#each migrations}}
|
|
46
|
+
['{{{id}}}', {{{quote sqlQuery}}}],
|
|
47
|
+
{{/each}}
|
|
48
|
+
];
|
|
49
|
+
for (const [id, sql] of migrations) {
|
|
50
|
+
if (!applied.has(id)) {
|
|
51
|
+
db.exec(sql);
|
|
52
|
+
db.prepare('INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)')
|
|
53
|
+
.run(projectName, id);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
db.exec('COMMIT');
|
|
57
|
+
} catch (e) {
|
|
58
|
+
db.exec('ROLLBACK');
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
{{/if}}
|
|
63
|
+
|
|
31
64
|
static getQueryNames(): Map<string, keyof {{className}}> {
|
|
32
65
|
return new Map([
|
|
33
66
|
{{#each queries}} {{#unless skipGenerateFunction}}
|
package/dist/templates/turso.hbs
CHANGED
|
@@ -31,6 +31,41 @@ export class {{className}} {
|
|
|
31
31
|
];
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
{{#if config.migrations}}
|
|
35
|
+
static async applyMigrations(db: Database, projectName = '{{projectName}}'): Promise<void> {
|
|
36
|
+
const createStmt = await db.prepare(`CREATE TABLE IF NOT EXISTS _sqg_migrations (
|
|
37
|
+
project TEXT NOT NULL,
|
|
38
|
+
migration_id TEXT NOT NULL,
|
|
39
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
40
|
+
PRIMARY KEY (project, migration_id)
|
|
41
|
+
)`);
|
|
42
|
+
await createStmt.run();
|
|
43
|
+
const tx = await db.transaction('write');
|
|
44
|
+
try {
|
|
45
|
+
const selectStmt = await tx.prepare('SELECT migration_id FROM _sqg_migrations WHERE project = ?');
|
|
46
|
+
const rows = await selectStmt.all(projectName) as { migration_id: string }[];
|
|
47
|
+
const applied = new Set(rows.map(r => r.migration_id));
|
|
48
|
+
const migrations: [string, string][] = [
|
|
49
|
+
{{#each migrations}}
|
|
50
|
+
['{{{id}}}', {{{quote sqlQuery}}}],
|
|
51
|
+
{{/each}}
|
|
52
|
+
];
|
|
53
|
+
for (const [id, sql] of migrations) {
|
|
54
|
+
if (!applied.has(id)) {
|
|
55
|
+
const execStmt = await tx.prepare(sql);
|
|
56
|
+
await execStmt.run();
|
|
57
|
+
const insertStmt = await tx.prepare('INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)');
|
|
58
|
+
await insertStmt.run(projectName, id);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
await tx.commit();
|
|
62
|
+
} catch (e) {
|
|
63
|
+
await tx.rollback();
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
{{/if}}
|
|
68
|
+
|
|
34
69
|
static getQueryNames(): Map<string, keyof {{className}}> {
|
|
35
70
|
return new Map([
|
|
36
71
|
{{#each queries}} {{#unless skipGenerateFunction}}
|
|
@@ -5,7 +5,7 @@ export class {{className}} {
|
|
|
5
5
|
|
|
6
6
|
constructor(private conn: DuckDBConnection) {}
|
|
7
7
|
|
|
8
|
-
static getMigrations(): string[] {
|
|
8
|
+
static getMigrations(): string[] {
|
|
9
9
|
return [
|
|
10
10
|
{{#each migrations}}
|
|
11
11
|
{{{quote sqlQuery}}},
|
|
@@ -13,6 +13,42 @@ export class {{className}} {
|
|
|
13
13
|
];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
{{#if config.migrations}}
|
|
17
|
+
static async applyMigrations(conn: DuckDBConnection, projectName = '{{projectName}}'): Promise<void> {
|
|
18
|
+
await conn.run(`CREATE TABLE IF NOT EXISTS _sqg_migrations (
|
|
19
|
+
project TEXT NOT NULL,
|
|
20
|
+
migration_id TEXT NOT NULL,
|
|
21
|
+
applied_at TIMESTAMP NOT NULL DEFAULT now(),
|
|
22
|
+
PRIMARY KEY (project, migration_id)
|
|
23
|
+
)`);
|
|
24
|
+
await conn.run('BEGIN');
|
|
25
|
+
try {
|
|
26
|
+
const result = await conn.runAndReadAll(
|
|
27
|
+
'SELECT migration_id FROM _sqg_migrations WHERE project = $1', [projectName]
|
|
28
|
+
);
|
|
29
|
+
const applied = new Set(result.getRows().map((row) => row[0] as string));
|
|
30
|
+
const migrations: [string, string][] = [
|
|
31
|
+
{{#each migrations}}
|
|
32
|
+
['{{{id}}}', {{{quote sqlQuery}}}],
|
|
33
|
+
{{/each}}
|
|
34
|
+
];
|
|
35
|
+
for (const [id, sql] of migrations) {
|
|
36
|
+
if (!applied.has(id)) {
|
|
37
|
+
await conn.run(sql);
|
|
38
|
+
await conn.run(
|
|
39
|
+
'INSERT INTO _sqg_migrations (project, migration_id) VALUES ($1, $2)',
|
|
40
|
+
[projectName, id]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
await conn.run('COMMIT');
|
|
45
|
+
} catch (e) {
|
|
46
|
+
await conn.run('ROLLBACK');
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
{{/if}}
|
|
51
|
+
|
|
16
52
|
static getQueryNames(): Map<string, keyof {{className}}> {
|
|
17
53
|
return new Map([
|
|
18
54
|
{{#each queries}} {{#unless skipGenerateFunction}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqg/sqg",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"handlebars": "^4.7.8",
|
|
54
54
|
"pg": "^8.16.3",
|
|
55
55
|
"pg-types": "^4.1.0",
|
|
56
|
+
"@testcontainers/postgresql": "^10.21.0",
|
|
56
57
|
"prettier": "^3.7.4",
|
|
57
58
|
"prettier-plugin-java": "^2.7.7",
|
|
58
59
|
"yaml": "^2.8.2",
|