@sqg/sqg 0.6.0 → 0.8.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
@@ -55,6 +55,22 @@ const GENERATORS = {
55
55
  extension: ".ts",
56
56
  template: "node-sqlite.hbs"
57
57
  },
58
+ "typescript/sqlite/libsql": {
59
+ language: "typescript",
60
+ engine: "sqlite",
61
+ driver: "libsql",
62
+ description: "TypeScript with @libsql/client (Turso)",
63
+ extension: ".ts",
64
+ template: "libsql.hbs"
65
+ },
66
+ "typescript/sqlite/turso": {
67
+ language: "typescript",
68
+ engine: "sqlite",
69
+ driver: "turso",
70
+ description: "TypeScript with Turso (limbo) native driver",
71
+ extension: ".ts",
72
+ template: "turso.hbs"
73
+ },
58
74
  "typescript/duckdb/node-api": {
59
75
  language: "typescript",
60
76
  engine: "duckdb",
@@ -1078,14 +1094,63 @@ const duckdb = new class {
1078
1094
  //#endregion
1079
1095
  //#region src/db/postgres.ts
1080
1096
  const databaseName = "sqg-db-temp";
1081
- const connectionString = process.env.SQG_POSTGRES_URL || "postgresql://sqg:secret@localhost:15432/sqg-db";
1082
- const connectionStringTemp = process.env.SQG_POSTGRES_URL ? process.env.SQG_POSTGRES_URL.replace(/\/[^/]+$/, `/${databaseName}`) : `postgresql://sqg:secret@localhost:15432/${databaseName}`;
1097
+ let containerInstance = null;
1098
+ async function startTestContainer() {
1099
+ if (containerInstance) return containerInstance.getConnectionUri();
1100
+ consola.info("Starting PostgreSQL container via testcontainers...");
1101
+ const { PostgreSqlContainer } = await import("@testcontainers/postgresql");
1102
+ containerInstance = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
1103
+ const connectionUri = containerInstance.getConnectionUri();
1104
+ consola.success(`PostgreSQL container started at: ${connectionUri}`);
1105
+ return connectionUri;
1106
+ }
1107
+ async function stopTestContainer() {
1108
+ if (containerInstance) {
1109
+ consola.info("Stopping PostgreSQL container...");
1110
+ await containerInstance.stop();
1111
+ containerInstance = null;
1112
+ }
1113
+ }
1114
+ async function getConnectionString() {
1115
+ if (process.env.SQG_POSTGRES_URL) return process.env.SQG_POSTGRES_URL;
1116
+ return await startTestContainer();
1117
+ }
1118
+ function getTempConnectionString(baseUrl) {
1119
+ return baseUrl.replace(/\/[^/]+$/, `/${databaseName}`);
1120
+ }
1083
1121
  const typeIdToName = /* @__PURE__ */ new Map();
1084
1122
  for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
1123
+ let dynamicTypeCache = /* @__PURE__ */ new Map();
1124
+ async function loadTypeCache(db) {
1125
+ const result = await db.query(`
1126
+ SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype
1127
+ FROM pg_type t
1128
+ LEFT JOIN pg_type et ON t.typelem = et.oid
1129
+ WHERE t.typtype IN ('b', 'e', 'r', 'c') -- base, enum, range, composite
1130
+ OR t.typelem != 0 -- array types
1131
+ `);
1132
+ dynamicTypeCache = /* @__PURE__ */ new Map();
1133
+ for (const row of result.rows) {
1134
+ const oid = row.oid;
1135
+ let typeName = row.typname;
1136
+ if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
1137
+ else typeName = typeName.toUpperCase();
1138
+ dynamicTypeCache.set(oid, typeName);
1139
+ }
1140
+ }
1141
+ function getTypeName(dataTypeID) {
1142
+ const cached = dynamicTypeCache.get(dataTypeID);
1143
+ if (cached) return cached;
1144
+ return typeIdToName.get(dataTypeID) || `type_${dataTypeID}`;
1145
+ }
1085
1146
  const postgres = new class {
1086
1147
  dbInitial;
1087
1148
  db;
1149
+ usingTestContainer = false;
1088
1150
  async initializeDatabase(queries) {
1151
+ const connectionString = await getConnectionString();
1152
+ const connectionStringTemp = getTempConnectionString(connectionString);
1153
+ this.usingTestContainer = containerInstance !== null;
1089
1154
  this.dbInitial = new Client({ connectionString });
1090
1155
  this.db = new Client({ connectionString: connectionStringTemp });
1091
1156
  try {
@@ -1094,7 +1159,7 @@ const postgres = new class {
1094
1159
  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.`);
1095
1160
  }
1096
1161
  try {
1097
- await this.dbInitial.query(`DROP DATABASE "${databaseName}";`);
1162
+ await this.dbInitial.query(`DROP DATABASE IF EXISTS "${databaseName}";`);
1098
1163
  } catch (error) {}
1099
1164
  try {
1100
1165
  await this.dbInitial.query(`CREATE DATABASE "${databaseName}";`);
@@ -1113,6 +1178,7 @@ const postgres = new class {
1113
1178
  throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
1114
1179
  }
1115
1180
  });
1181
+ await loadTypeCache(this.db);
1116
1182
  }
1117
1183
  async executeQueries(queries) {
1118
1184
  const db = this.db;
@@ -1134,17 +1200,22 @@ const postgres = new class {
1134
1200
  const statement = query.queryPositional;
1135
1201
  try {
1136
1202
  consola.info("Query:", statement.sql);
1203
+ const parameterValues = statement.parameters.map((p) => {
1204
+ const value = p.value;
1205
+ if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
1206
+ return value;
1207
+ });
1137
1208
  let result;
1138
1209
  try {
1139
1210
  await db.query("BEGIN");
1140
- result = await db.query(statement.sql, statement.parameters);
1211
+ result = await db.query(statement.sql, parameterValues);
1141
1212
  } finally {
1142
1213
  await db.query("ROLLBACK");
1143
1214
  }
1144
1215
  if (query.isQuery) {
1145
1216
  const columnNames = result.fields.map((field) => field.name);
1146
1217
  const columnTypes = result.fields.map((field) => {
1147
- return typeIdToName.get(field.dataTypeID) || `type_${field.dataTypeID}`;
1218
+ return getTypeName(field.dataTypeID);
1148
1219
  });
1149
1220
  consola.debug("Columns:", columnNames);
1150
1221
  consola.debug("Types:", columnTypes);
@@ -1187,8 +1258,9 @@ const postgres = new class {
1187
1258
  }
1188
1259
  async close() {
1189
1260
  await this.db.end();
1190
- await this.dbInitial.query(`DROP DATABASE "${databaseName}"`);
1261
+ await this.dbInitial.query(`DROP DATABASE IF EXISTS "${databaseName}"`);
1191
1262
  await this.dbInitial.end();
1263
+ if (this.usingTestContainer) await stopTestContainer();
1192
1264
  }
1193
1265
  }();
1194
1266
 
@@ -1380,7 +1452,20 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
1380
1452
  INTERVAL: "String",
1381
1453
  BIT: "String",
1382
1454
  BIGNUM: "BigDecimal",
1383
- INT4: "Integer"
1455
+ INT2: "Short",
1456
+ INT4: "Integer",
1457
+ INT8: "Long",
1458
+ FLOAT4: "Float",
1459
+ FLOAT8: "Double",
1460
+ NUMERIC: "BigDecimal",
1461
+ BOOL: "Boolean",
1462
+ BYTEA: "byte[]",
1463
+ TIMESTAMPTZ: "OffsetDateTime",
1464
+ JSON: "String",
1465
+ JSONB: "String",
1466
+ OID: "Long",
1467
+ SERIAL: "Integer",
1468
+ BIGSERIAL: "Long"
1384
1469
  };
1385
1470
  static javaReservedKeywords = new Set([
1386
1471
  "abstract",
@@ -1441,12 +1526,15 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
1441
1526
  const upperType = type.toString().toUpperCase();
1442
1527
  const mappedType = this.typeMap[upperType];
1443
1528
  if (mappedType) return mappedType;
1529
+ if (upperType.startsWith("_")) {
1530
+ const baseType = upperType.substring(1);
1531
+ return `List<${this.typeMap[baseType] || "Object"}>`;
1532
+ }
1444
1533
  if (upperType.startsWith("DECIMAL(") || upperType.startsWith("NUMERIC(")) return "BigDecimal";
1445
1534
  if (upperType.startsWith("ENUM(")) return "String";
1446
1535
  if (upperType.startsWith("UNION(")) return "Object";
1447
1536
  if (/\[\d+\]/.test(upperType)) return "Object";
1448
- console.warn("Mapped type is unknown:", type);
1449
- return "Object";
1537
+ return "String";
1450
1538
  }
1451
1539
  formatListType(elementType) {
1452
1540
  return `List<${elementType}>`;
@@ -1499,8 +1587,13 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
1499
1587
  const fieldType = this.getTypeName(column);
1500
1588
  const upperType = column.type?.toString().toUpperCase() ?? "";
1501
1589
  if (upperType === "TIMESTAMP" || upperType === "DATETIME") return `toLocalDateTime((java.sql.Timestamp)${value})`;
1590
+ if (upperType === "TIMESTAMPTZ") return `toOffsetDateTime((java.sql.Timestamp)${value})`;
1502
1591
  if (upperType === "DATE") return `toLocalDate((java.sql.Date)${value})`;
1503
1592
  if (upperType === "TIME") return `toLocalTime((java.sql.Time)${value})`;
1593
+ if (upperType.startsWith("_")) {
1594
+ const baseType = upperType.substring(1);
1595
+ return `arrayToList((Array)${value}, ${this.typeMap[baseType] || "Object"}[].class)`;
1596
+ }
1504
1597
  return `(${fieldType})${value}`;
1505
1598
  }
1506
1599
  getInnermostType(type) {
@@ -1568,12 +1661,30 @@ var TypeScriptTypeMapper = class extends TypeMapper {
1568
1661
  INTERVAL: "{ months: number; days: number; micros: bigint }",
1569
1662
  BIT: "{ data: Uint8Array }",
1570
1663
  BIGNUM: "bigint",
1571
- INT4: "number"
1664
+ INT2: "number",
1665
+ INT4: "number",
1666
+ INT8: "bigint",
1667
+ FLOAT4: "number",
1668
+ FLOAT8: "number",
1669
+ NUMERIC: "string",
1670
+ BOOL: "boolean",
1671
+ BYTEA: "Buffer",
1672
+ TIMESTAMPTZ: "Date",
1673
+ JSON: "unknown",
1674
+ JSONB: "unknown",
1675
+ OID: "number",
1676
+ SERIAL: "number",
1677
+ BIGSERIAL: "bigint"
1572
1678
  };
1573
1679
  mapPrimitiveType(type, nullable) {
1574
1680
  const upperType = type.toUpperCase();
1575
1681
  const mappedType = this.typeMap[upperType];
1576
1682
  if (mappedType) return nullable ? `${mappedType} | null` : mappedType;
1683
+ if (upperType.startsWith("_")) {
1684
+ const baseType = upperType.substring(1);
1685
+ const arrayType = `${this.typeMap[baseType] || "unknown"}[]`;
1686
+ return nullable ? `${arrayType} | null` : arrayType;
1687
+ }
1577
1688
  if (upperType.startsWith("DECIMAL(") || upperType.startsWith("NUMERIC(")) {
1578
1689
  const baseType = "{ width: number; scale: number; value: bigint }";
1579
1690
  return nullable ? `${baseType} | null` : baseType;
@@ -1598,8 +1709,7 @@ var TypeScriptTypeMapper = class extends TypeMapper {
1598
1709
  }
1599
1710
  }
1600
1711
  if (/\[\d+\]/.test(upperType)) return "{ items: unknown[] }";
1601
- console.warn("Mapped type is unknown:", type);
1602
- return "unknown";
1712
+ return nullable ? "string | null" : "string";
1603
1713
  }
1604
1714
  formatListType(elementType) {
1605
1715
  return `{ items: (${elementType})[] }`;
@@ -1784,7 +1894,8 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
1784
1894
  name,
1785
1895
  generator: "java/duckdb/jdbc",
1786
1896
  output: gen.output,
1787
- config: gen.config
1897
+ config: gen.config,
1898
+ projectName: gen.projectName
1788
1899
  }, this.javaGenerator, name, q, tables, "duckdb");
1789
1900
  }
1790
1901
  isCompatibleWith(engine) {
@@ -2090,7 +2201,9 @@ function getGenerator(generator) {
2090
2201
  const key = `${info.language}/${info.driver}`;
2091
2202
  switch (key) {
2092
2203
  case "typescript/better-sqlite3":
2093
- case "typescript/node": return new TsGenerator(`templates/${info.template}`);
2204
+ case "typescript/node":
2205
+ case "typescript/libsql":
2206
+ case "typescript/turso": return new TsGenerator(`templates/${info.template}`);
2094
2207
  case "typescript/node-api": return new TsDuckDBGenerator(`templates/${info.template}`);
2095
2208
  case "java/jdbc": return new JavaGenerator(`templates/${info.template}`);
2096
2209
  case "java/arrow": return new JavaDuckDBArrowGenerator(`templates/${info.template}`);
@@ -2248,7 +2361,7 @@ var TableHelper = class {
2248
2361
  return this.generator.typeMapper;
2249
2362
  }
2250
2363
  };
2251
- function generateSourceFile(name, queries, tables, templatePath, generator, engine, config) {
2364
+ function generateSourceFile(name, queries, tables, templatePath, generator, engine, projectName, config) {
2252
2365
  const templateSrc = readFileSync(templatePath, "utf-8");
2253
2366
  const template = Handlebars.compile(templateSrc);
2254
2367
  Handlebars.registerHelper("mapType", (column) => generator.mapType(column));
@@ -2261,6 +2374,7 @@ function generateSourceFile(name, queries, tables, templatePath, generator, engi
2261
2374
  queries: queries.map((q) => new SqlQueryHelper(q, generator, generator.getStatement(q))),
2262
2375
  tables: tableHelpers,
2263
2376
  className: generator.getClassName(name),
2377
+ projectName,
2264
2378
  config
2265
2379
  }, {
2266
2380
  allowProtoPropertiesByDefault: true,
@@ -2400,7 +2514,7 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
2400
2514
  await generator.beforeGenerate(projectDir, gen, queries, tables);
2401
2515
  const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
2402
2516
  const name = gen.name ?? basename(file, extname(file));
2403
- const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.config);
2517
+ const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.projectName ?? name, gen.config);
2404
2518
  if (writeToStdout) {
2405
2519
  process.stdout.write(sourceFile);
2406
2520
  if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
@@ -2527,7 +2641,11 @@ async function processProjectFromConfig(project, projectDir, writeToStdout = fal
2527
2641
  });
2528
2642
  }
2529
2643
  for (const gen of gens) {
2530
- const outputPath = await writeGeneratedFile(projectDir, gen, getGenerator(gen.generator), sqlFile, queries, tables, engine, writeToStdout);
2644
+ const generator = getGenerator(gen.generator);
2645
+ const outputPath = await writeGeneratedFile(projectDir, {
2646
+ ...gen,
2647
+ projectName: project.name
2648
+ }, generator, sqlFile, queries, tables, engine, writeToStdout);
2531
2649
  if (outputPath !== null) files.push(outputPath);
2532
2650
  }
2533
2651
  }
@@ -2550,7 +2668,7 @@ async function processProject(projectPath) {
2550
2668
  //#region src/mcp-server.ts
2551
2669
  const server = new Server({
2552
2670
  name: "sqg-mcp",
2553
- version: process.env.npm_package_version ?? "0.6.0"
2671
+ version: process.env.npm_package_version ?? "0.8.0"
2554
2672
  }, { capabilities: {
2555
2673
  tools: {},
2556
2674
  resources: {}
@@ -2879,7 +2997,7 @@ async function startMcpServer() {
2879
2997
 
2880
2998
  //#endregion
2881
2999
  //#region src/sqg.ts
2882
- const version = process.env.npm_package_version ?? "0.6.0";
3000
+ const version = process.env.npm_package_version ?? "0.8.0";
2883
3001
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
2884
3002
  consola.level = LogLevels.info;
2885
3003
  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}}
@@ -0,0 +1,141 @@
1
+ // {{generatedComment}}
2
+ import type { Client, InArgs } from '@libsql/client';
3
+
4
+ interface RunResult {
5
+ changes: number;
6
+ lastInsertRowid: bigint;
7
+ }
8
+
9
+ export class {{className}} {
10
+ constructor(private client: Client) {}
11
+
12
+ static getMigrations(): string[] {
13
+ return [
14
+ {{#each migrations}}
15
+ {{{quote sqlQuery}}},
16
+ {{/each}}
17
+ ];
18
+ }
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
+
60
+ static getQueryNames(): Map<string, keyof {{className}}> {
61
+ return new Map([
62
+ {{#each queries}} {{#unless skipGenerateFunction}}
63
+ ["{{id}}", "{{functionName}}"],{{/unless}}{{/each}}]
64
+ );
65
+ }
66
+
67
+ {{#each queries}}
68
+ {{#unless skipGenerateFunction}}
69
+ async {{functionName}}({{#each variables}}{{name}}: {{type}}{{#unless @last}}, {{/unless}}{{/each}}): Promise<{{> returnType }}> {
70
+ const result = await this.client.execute({
71
+ sql: {{{quote sqlQuery}}},
72
+ args: [{{> params}}] as InArgs,
73
+ });
74
+ {{> execute}}
75
+ }
76
+ {{/unless}}
77
+ {{/each}}
78
+ }
79
+
80
+ {{#*inline "params"}}{{#each parameterNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
81
+
82
+ {{#*inline "paramTypes"}}{{#each parameters}}{{type}} {{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
83
+
84
+ {{#*inline "columnTypes"}}{{#each columns}}{{name}}: {{mapType .}}{{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
85
+
86
+ {{#*inline "rowType"}}
87
+ {{#if isQuery~}}
88
+ {{#if isPluck~}}
89
+ {{#if columns.length~}} {{mapType (lookup columns 0)}} {{else}}unknown{{/if~}}
90
+ {{~else~}}
91
+ {{#if columns.length}}{ {{> columnTypes}} }{{else}}unknown{{/if~}}
92
+ {{/if~}}
93
+ {{~else~}}
94
+ unknown
95
+ {{~/if~}}
96
+ {{/inline~}}
97
+
98
+ {{#*inline "resultType"}}
99
+ {{#if isQuery~}}
100
+ {{> rowType}}
101
+ {{~else~}}
102
+ unknown
103
+ {{~/if~}}
104
+ {{/inline~}}
105
+
106
+
107
+ {{#*inline "returnType"}}
108
+ {{#if isQuery~}}
109
+ {{#if isOne~}}
110
+ {{> rowType}} | undefined
111
+ {{~else~}}
112
+ {{> rowType}}[]
113
+ {{/if~}}
114
+ {{~else~}}
115
+ RunResult
116
+ {{~/if~}}
117
+ {{/inline~}}
118
+
119
+ {{#*inline "execute"}}
120
+ {{#if isQuery}}
121
+ {{#if isOne}}
122
+ {{#if isPluck}}
123
+ const row = result.rows[0];
124
+ return row ? Object.values(row)[0] as {{> rowType}} : undefined;
125
+ {{else}}
126
+ return (result.rows as unknown[])[0] as {{> rowType}} | undefined;
127
+ {{/if}}
128
+ {{else}}
129
+ {{#if isPluck}}
130
+ return result.rows.map(row => Object.values(row)[0] as {{> rowType}});
131
+ {{else}}
132
+ return (result.rows as unknown[]) as {{> rowType}}[];
133
+ {{/if}}
134
+ {{/if}}
135
+ {{else}}
136
+ return {
137
+ changes: result.rowsAffected,
138
+ lastInsertRowid: result.lastInsertRowid ?? 0n,
139
+ };
140
+ {{/if}}
141
+ {{/inline}}
@@ -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}}
@@ -0,0 +1,147 @@
1
+ // {{generatedComment}}
2
+ import { connect, type Database } from '@tursodatabase/database';
3
+
4
+ interface RunResult {
5
+ changes: number;
6
+ lastInsertRowid: number;
7
+ }
8
+
9
+ // Statement type inferred from Database.prepare()
10
+ type Statement = Awaited<ReturnType<Database['prepare']>>;
11
+
12
+ export class {{className}} {
13
+ private statements = new Map<string, Statement>();
14
+
15
+ constructor(private db: Database) {}
16
+
17
+ private async prepare(id: string, query: string): Promise<Statement> {
18
+ let stmt = this.statements.get(id);
19
+ if (!stmt) {
20
+ stmt = await this.db.prepare(query);
21
+ this.statements.set(id, stmt);
22
+ }
23
+ return stmt;
24
+ }
25
+
26
+ static getMigrations(): string[] {
27
+ return [
28
+ {{#each migrations}}
29
+ {{{quote sqlQuery}}},
30
+ {{/each}}
31
+ ];
32
+ }
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
+
69
+ static getQueryNames(): Map<string, keyof {{className}}> {
70
+ return new Map([
71
+ {{#each queries}} {{#unless skipGenerateFunction}}
72
+ ["{{id}}", "{{functionName}}"],{{/unless}}{{/each}}]
73
+ );
74
+ }
75
+
76
+ {{#each queries}}
77
+ {{#unless skipGenerateFunction}}
78
+ async {{functionName}}({{#each variables}}{{name}}: {{type}}{{#unless @last}}, {{/unless}}{{/each}}): Promise<{{> returnType }}> {
79
+ const stmt = await this.prepare('{{id}}',
80
+ {{{quote sqlQuery}}});
81
+ {{> execute}}
82
+ }
83
+ {{/unless}}
84
+ {{/each}}
85
+ }
86
+
87
+ {{#*inline "params"}}{{#each parameterNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
88
+
89
+ {{#*inline "paramTypes"}}{{#each parameters}}{{type}} {{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
90
+
91
+ {{#*inline "columnTypes"}}{{#each columns}}{{name}}: {{mapType .}}{{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
92
+
93
+ {{#*inline "rowType"}}
94
+ {{#if isQuery~}}
95
+ {{#if isPluck~}}
96
+ {{#if columns.length~}} {{mapType (lookup columns 0)}} {{else}}unknown{{/if~}}
97
+ {{~else~}}
98
+ {{#if columns.length}}{ {{> columnTypes}} }{{else}}unknown{{/if~}}
99
+ {{/if~}}
100
+ {{~else~}}
101
+ unknown
102
+ {{~/if~}}
103
+ {{/inline~}}
104
+
105
+ {{#*inline "resultType"}}
106
+ {{#if isQuery~}}
107
+ {{> rowType}}
108
+ {{~else~}}
109
+ unknown
110
+ {{~/if~}}
111
+ {{/inline~}}
112
+
113
+
114
+ {{#*inline "returnType"}}
115
+ {{#if isQuery~}}
116
+ {{#if isOne~}}
117
+ {{> rowType}} | undefined
118
+ {{~else~}}
119
+ {{> rowType}}[]
120
+ {{/if~}}
121
+ {{~else~}}
122
+ RunResult
123
+ {{~/if~}}
124
+ {{/inline~}}
125
+
126
+ {{#*inline "execute"}}
127
+ {{#if isQuery}}
128
+ {{#if isOne}}
129
+ {{#if isPluck}}
130
+ const row = await stmt.get({{> params}}) as Record<string, {{> rowType}}> | undefined;
131
+ return row ? Object.values(row)[0] : undefined;
132
+ {{else}}
133
+ return await stmt.get({{> params}}) as {{> rowType}} | undefined;
134
+ {{/if}}
135
+ {{else}}
136
+ {{#if isPluck}}
137
+ const rows = await stmt.all({{> params}}) as Record<string, {{> rowType}}>[];
138
+ return rows.map(row => Object.values(row)[0]);
139
+ {{else}}
140
+ return await stmt.all({{> params}}) as {{> rowType}}[];
141
+ {{/if}}
142
+ {{/if}}
143
+ {{else}}
144
+ const result = await stmt.run({{> params}});
145
+ return result as RunResult;
146
+ {{/if}}
147
+ {{/inline}}
@@ -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.6.0",
3
+ "version": "0.8.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",
@@ -60,6 +61,8 @@
60
61
  "@modelcontextprotocol/sdk": "^1.0.4"
61
62
  },
62
63
  "devDependencies": {
64
+ "@libsql/client": "^0.17.0",
65
+ "@tursodatabase/database": "^0.4.3",
63
66
  "@types/better-sqlite3": "^7.6.13",
64
67
  "@types/node": "^25.0.3",
65
68
  "@types/pg": "^8.16.0",