@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 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
- const connectionString = process.env.SQG_POSTGRES_URL || "postgresql://sqg:secret@localhost:15432/sqg-db";
1098
- const connectionStringTemp = process.env.SQG_POSTGRES_URL ? process.env.SQG_POSTGRES_URL.replace(/\/[^/]+$/, `/${databaseName}`) : `postgresql://sqg:secret@localhost:15432/${databaseName}`;
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, statement.parameters);
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 typeIdToName.get(field.dataTypeID) || `type_${field.dataTypeID}`;
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
- INT4: "Integer"
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
- console.warn("Mapped type is unknown:", type);
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
- INT4: "number"
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
- console.warn("Mapped type is unknown:", type);
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 outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries, tables, engine, writeToStdout);
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.7.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.7.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
- public static List<String> getMigrations() {
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}}
@@ -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.7.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",