@sqg/sqg 0.10.0 → 0.12.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
@@ -2,6 +2,7 @@
2
2
  import { exit } from "node:process";
3
3
  import { Command } from "commander";
4
4
  import consola, { LogLevels } from "consola";
5
+ import updateNotifier from "update-notifier";
5
6
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { homedir, tmpdir } from "node:os";
@@ -17,6 +18,7 @@ import { LRParser } from "@lezer/lr";
17
18
  import { camelCase, isNotNil, pascalCase, sortBy } from "es-toolkit";
18
19
  import { Client } from "pg";
19
20
  import types from "pg-types";
21
+ import { PostgreSqlContainer } from "@testcontainers/postgresql";
20
22
  import BetterSqlite3 from "better-sqlite3";
21
23
  import { camelCase as camelCase$1, pascalCase as pascalCase$1 } from "es-toolkit/string";
22
24
  import prettier from "prettier/standalone";
@@ -1088,84 +1090,141 @@ const duckdb = new class {
1088
1090
 
1089
1091
  //#endregion
1090
1092
  //#region src/db/postgres.ts
1091
- const databaseName = "sqg-db-temp";
1092
- let containerInstance = null;
1093
- async function startTestContainer() {
1094
- if (containerInstance) return containerInstance.getConnectionUri();
1095
- consola.info("Starting PostgreSQL container via testcontainers...");
1096
- const { PostgreSqlContainer } = await import("@testcontainers/postgresql");
1097
- containerInstance = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
1098
- const connectionUri = containerInstance.getConnectionUri();
1099
- consola.success(`PostgreSQL container started at: ${connectionUri}`);
1100
- return connectionUri;
1101
- }
1102
- async function stopTestContainer() {
1103
- if (containerInstance) {
1104
- consola.info("Stopping PostgreSQL container...");
1105
- await containerInstance.stop();
1106
- containerInstance = null;
1107
- }
1108
- }
1109
- async function getConnectionString() {
1110
- if (process.env.SQG_POSTGRES_URL) return process.env.SQG_POSTGRES_URL;
1111
- return await startTestContainer();
1112
- }
1113
- function getTempConnectionString(baseUrl) {
1114
- return baseUrl.replace(/\/[^/]+$/, `/${databaseName}`);
1115
- }
1093
+ const tempDatabaseName = "sqg-db-temp";
1116
1094
  const typeIdToName = /* @__PURE__ */ new Map();
1117
1095
  for (const [name, id] of Object.entries(types.builtins)) typeIdToName.set(Number(id), name);
1118
- let dynamicTypeCache = /* @__PURE__ */ new Map();
1119
- async function loadTypeCache(db) {
1120
- const result = await db.query(`
1121
- SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype
1122
- FROM pg_type t
1123
- LEFT JOIN pg_type et ON t.typelem = et.oid
1124
- WHERE t.typtype IN ('b', 'e', 'r', 'c') -- base, enum, range, composite
1125
- OR t.typelem != 0 -- array types
1126
- `);
1127
- dynamicTypeCache = /* @__PURE__ */ new Map();
1128
- for (const row of result.rows) {
1129
- const oid = row.oid;
1130
- let typeName = row.typname;
1131
- if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
1132
- else typeName = typeName.toUpperCase();
1133
- dynamicTypeCache.set(oid, typeName);
1096
+ /**
1097
+ * External database mode: connects directly to the user's database.
1098
+ * Uses a single transaction for the entire session and rolls back on close,
1099
+ * so the database is never modified. Individual queries use savepoints.
1100
+ */
1101
+ var ExternalDbMode = class {
1102
+ constructor(connectionString) {
1103
+ this.connectionString = connectionString;
1134
1104
  }
1135
- }
1136
- function getTypeName(dataTypeID) {
1137
- const cached = dynamicTypeCache.get(dataTypeID);
1138
- if (cached) return cached;
1139
- return typeIdToName.get(dataTypeID) || `type_${dataTypeID}`;
1140
- }
1141
- const postgres = new class {
1105
+ async connect() {
1106
+ const db = new Client({ connectionString: this.connectionString });
1107
+ try {
1108
+ await db.connect();
1109
+ } catch (e) {
1110
+ throw new DatabaseError(`Failed to connect to PostgreSQL: ${e.message}`, "postgres", `Check that PostgreSQL is running and accessible at ${this.connectionString}.`);
1111
+ }
1112
+ await db.query("BEGIN");
1113
+ return db;
1114
+ }
1115
+ async wrapQuery(db, fn) {
1116
+ try {
1117
+ await db.query("SAVEPOINT sqg_query");
1118
+ return await fn();
1119
+ } finally {
1120
+ await db.query("ROLLBACK TO SAVEPOINT sqg_query");
1121
+ }
1122
+ }
1123
+ async close(db) {
1124
+ await db.query("ROLLBACK");
1125
+ await db.end();
1126
+ }
1127
+ };
1128
+ /**
1129
+ * Temp database mode: creates a temporary database for SQG to work in.
1130
+ * Connects to the provided server first (dbInitial) to CREATE the temp DB,
1131
+ * then connects to the temp DB for all operations.
1132
+ * On close, drops the temp DB and optionally stops the testcontainer.
1133
+ */
1134
+ var TempDbMode = class {
1142
1135
  dbInitial;
1143
- db;
1144
- usingTestContainer = false;
1145
- async initializeDatabase(queries) {
1146
- const connectionString = await getConnectionString();
1147
- const connectionStringTemp = getTempConnectionString(connectionString);
1148
- this.usingTestContainer = containerInstance !== null;
1149
- this.dbInitial = new Client({ connectionString });
1150
- this.db = new Client({ connectionString: connectionStringTemp });
1136
+ container = null;
1137
+ constructor(connectionString, container) {
1138
+ this.connectionString = connectionString;
1139
+ this.container = container;
1140
+ }
1141
+ async connect() {
1142
+ this.dbInitial = new Client({ connectionString: this.connectionString });
1151
1143
  try {
1152
1144
  await this.dbInitial.connect();
1153
1145
  } catch (e) {
1154
- 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.`);
1146
+ throw new DatabaseError(`Failed to connect to PostgreSQL: ${e.message}`, "postgres", `Check that PostgreSQL is running and accessible at ${this.connectionString}. Set SQG_POSTGRES_URL environment variable to use a different connection string.`);
1155
1147
  }
1156
1148
  try {
1157
- await this.dbInitial.query(`DROP DATABASE IF EXISTS "${databaseName}";`);
1149
+ await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}";`);
1158
1150
  } catch (error) {}
1159
1151
  try {
1160
- await this.dbInitial.query(`CREATE DATABASE "${databaseName}";`);
1152
+ await this.dbInitial.query(`CREATE DATABASE "${tempDatabaseName}";`);
1161
1153
  } catch (error) {
1162
1154
  throw new DatabaseError(`Failed to create temporary database: ${error.message}`, "postgres", "Check PostgreSQL user permissions to create databases");
1163
1155
  }
1156
+ const db = new Client({ connectionString: this.connectionString.replace(/\/[^/]+$/, `/${tempDatabaseName}`) });
1164
1157
  try {
1165
- await this.db.connect();
1158
+ await db.connect();
1166
1159
  } catch (e) {
1167
1160
  throw new DatabaseError(`Failed to connect to temporary database: ${e.message}`, "postgres");
1168
1161
  }
1162
+ return db;
1163
+ }
1164
+ async wrapQuery(db, fn) {
1165
+ try {
1166
+ await db.query("BEGIN");
1167
+ return await fn();
1168
+ } finally {
1169
+ await db.query("ROLLBACK");
1170
+ }
1171
+ }
1172
+ async close(db) {
1173
+ await db.end();
1174
+ await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}"`);
1175
+ await this.dbInitial.end();
1176
+ if (this.container) {
1177
+ consola.info("Stopping PostgreSQL container...");
1178
+ await this.container.stop();
1179
+ this.container = null;
1180
+ }
1181
+ }
1182
+ };
1183
+ const postgres = new class {
1184
+ db;
1185
+ mode;
1186
+ dynamicTypeCache = /* @__PURE__ */ new Map();
1187
+ async startContainer() {
1188
+ consola.info("Starting PostgreSQL container via testcontainers...");
1189
+ const container = await new PostgreSqlContainer("postgres:16-alpine").withDatabase("sqg-db").withUsername("sqg").withPassword("secret").start();
1190
+ const connectionUri = container.getConnectionUri();
1191
+ consola.success(`PostgreSQL container started at: ${connectionUri}`);
1192
+ return {
1193
+ connectionUri,
1194
+ container
1195
+ };
1196
+ }
1197
+ async loadTypeCache(db) {
1198
+ const result = await db.query(`
1199
+ SELECT t.oid, t.typname, t.typtype, t.typelem, et.typname AS elemtype
1200
+ FROM pg_type t
1201
+ LEFT JOIN pg_type et ON t.typelem = et.oid
1202
+ WHERE t.typtype IN ('b', 'e', 'r', 'c') -- base, enum, range, composite
1203
+ OR t.typelem != 0 -- array types
1204
+ `);
1205
+ this.dynamicTypeCache = /* @__PURE__ */ new Map();
1206
+ for (const row of result.rows) {
1207
+ const oid = row.oid;
1208
+ let typeName = row.typname;
1209
+ if (typeName.startsWith("_") && row.elemtype) typeName = `_${row.elemtype.toUpperCase()}`;
1210
+ else typeName = typeName.toUpperCase();
1211
+ this.dynamicTypeCache.set(oid, typeName);
1212
+ }
1213
+ }
1214
+ getTypeName(dataTypeID) {
1215
+ const cached = this.dynamicTypeCache.get(dataTypeID);
1216
+ if (cached) return cached;
1217
+ return typeIdToName.get(dataTypeID) || `type_${dataTypeID}`;
1218
+ }
1219
+ async initializeDatabase(queries) {
1220
+ const externalUrl = process.env.SQG_POSTGRES_URL;
1221
+ this.dynamicTypeCache = /* @__PURE__ */ new Map();
1222
+ if (externalUrl) this.mode = new ExternalDbMode(externalUrl);
1223
+ else {
1224
+ const { connectionUri, container } = await this.startContainer();
1225
+ this.mode = new TempDbMode(connectionUri, container);
1226
+ }
1227
+ this.db = await this.mode.connect();
1169
1228
  await initializeDatabase(queries, async (query) => {
1170
1229
  try {
1171
1230
  await this.db.query(query.rawQuery);
@@ -1173,7 +1232,7 @@ const postgres = new class {
1173
1232
  throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
1174
1233
  }
1175
1234
  });
1176
- await loadTypeCache(this.db);
1235
+ await this.loadTypeCache(this.db);
1177
1236
  }
1178
1237
  async executeQueries(queries) {
1179
1238
  const db = this.db;
@@ -1183,7 +1242,6 @@ const postgres = new class {
1183
1242
  for (const query of executableQueries) {
1184
1243
  consola.debug(`Executing query: ${query.id}`);
1185
1244
  await this.executeQuery(db, query);
1186
- if (query.isQuery) {}
1187
1245
  consola.success(`Query ${query.id} executed successfully`);
1188
1246
  }
1189
1247
  } catch (error) {
@@ -1194,21 +1252,22 @@ const postgres = new class {
1194
1252
  async executeQuery(db, query) {
1195
1253
  const statement = query.queryPositional;
1196
1254
  try {
1197
- consola.info("Query:", statement.sql);
1255
+ consola.debug("Query:", statement.sql);
1198
1256
  const parameterValues = statement.parameters.map((p) => {
1199
1257
  const value = p.value;
1200
1258
  if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
1201
1259
  return value;
1202
1260
  });
1203
1261
  if (statement.parameters.length > 0) try {
1204
- await db.query(`DEALLOCATE ALL`);
1262
+ await db.query("DEALLOCATE ALL");
1205
1263
  await db.query(`PREPARE sqg_param_check AS ${statement.sql}`);
1206
1264
  const paramTypeResult = await db.query(`SELECT unnest(parameter_types)::oid AS oid FROM pg_prepared_statements WHERE name = 'sqg_param_check'`);
1207
- await db.query(`DEALLOCATE sqg_param_check`);
1265
+ await db.query("DEALLOCATE sqg_param_check");
1208
1266
  if (paramTypeResult.rows.length === statement.parameters.length) {
1209
1267
  const paramTypes = /* @__PURE__ */ new Map();
1210
1268
  for (let i = 0; i < statement.parameters.length; i++) {
1211
- const typeName = getTypeName(Number(paramTypeResult.rows[i].oid));
1269
+ const oid = Number(paramTypeResult.rows[i].oid);
1270
+ const typeName = this.getTypeName(oid);
1212
1271
  paramTypes.set(statement.parameters[i].name, typeName);
1213
1272
  }
1214
1273
  query.parameterTypes = paramTypes;
@@ -1217,17 +1276,11 @@ const postgres = new class {
1217
1276
  } catch (e) {
1218
1277
  consola.debug(`Parameter type introspection failed for ${query.id}, using heuristic:`, e.message);
1219
1278
  }
1220
- let result;
1221
- try {
1222
- await db.query("BEGIN");
1223
- result = await db.query(statement.sql, parameterValues);
1224
- } finally {
1225
- await db.query("ROLLBACK");
1226
- }
1279
+ const result = await this.mode.wrapQuery(db, () => db.query(statement.sql, parameterValues));
1227
1280
  if (query.isQuery) {
1228
1281
  const columnNames = result.fields.map((field) => field.name);
1229
1282
  const columnTypes = result.fields.map((field) => {
1230
- return getTypeName(field.dataTypeID);
1283
+ return this.getTypeName(field.dataTypeID);
1231
1284
  });
1232
1285
  consola.debug("Columns:", columnNames);
1233
1286
  consola.debug("Types:", columnTypes);
@@ -1269,10 +1322,8 @@ const postgres = new class {
1269
1322
  }
1270
1323
  }
1271
1324
  async close() {
1272
- await this.db.end();
1273
- await this.dbInitial.query(`DROP DATABASE IF EXISTS "${databaseName}"`);
1274
- await this.dbInitial.end();
1275
- if (this.usingTestContainer) await stopTestContainer();
1325
+ await this.mode.close(this.db);
1326
+ this.dynamicTypeCache = /* @__PURE__ */ new Map();
1276
1327
  }
1277
1328
  }();
1278
1329
 
@@ -1459,7 +1510,7 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
1459
1510
  TIMESTAMP_S: "Instant",
1460
1511
  TIMESTAMP_MS: "Instant",
1461
1512
  TIMESTAMP_NS: "Instant",
1462
- "TIMESTAMP WITH TIME ZONE": "Instant",
1513
+ "TIMESTAMP WITH TIME ZONE": "OffsetDateTime",
1463
1514
  UUID: "UUID",
1464
1515
  INTERVAL: "String",
1465
1516
  BIT: "String",
@@ -1600,6 +1651,7 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
1600
1651
  const upperType = column.type?.toString().toUpperCase() ?? "";
1601
1652
  if (upperType === "TIMESTAMP" || upperType === "DATETIME") return `toLocalDateTime((java.sql.Timestamp)${value})`;
1602
1653
  if (upperType === "TIMESTAMPTZ") return `toOffsetDateTime((java.sql.Timestamp)${value})`;
1654
+ if (upperType === "TIMESTAMP WITH TIME ZONE") return `(OffsetDateTime)${value}`;
1603
1655
  if (upperType === "DATE") return `toLocalDate((java.sql.Date)${value})`;
1604
1656
  if (upperType === "TIME") return `toLocalTime((java.sql.Time)${value})`;
1605
1657
  if (upperType.startsWith("_")) {
@@ -1857,6 +1909,9 @@ var JavaGenerator = class extends BaseGenerator {
1857
1909
  }, " ");
1858
1910
  return queryHelper.typeMapper.getDeclarations(query.allColumns);
1859
1911
  });
1912
+ Handlebars.registerHelper("appenderType", (column) => {
1913
+ return this.mapType(column);
1914
+ });
1860
1915
  Handlebars.registerHelper("readColumns", (queryHelper) => {
1861
1916
  const query = queryHelper.query;
1862
1917
  if (queryHelper.isPluck) return this.readColumn({
@@ -2346,10 +2401,24 @@ var SqlQueryHelper = class {
2346
2401
  }
2347
2402
  get parameters() {
2348
2403
  const vars = new Map(this.variables.map((param) => [param.name, param.type]));
2349
- return this.statement.parameters.map((param) => ({
2350
- name: param.name,
2351
- type: vars.get(param.name)
2352
- }));
2404
+ return this.statement.parameters.map((param) => {
2405
+ const rawType = this.query.parameterTypes?.get(param.name);
2406
+ let isArray = false;
2407
+ let arrayBaseType = null;
2408
+ if (rawType instanceof ListType) {
2409
+ isArray = true;
2410
+ arrayBaseType = rawType.baseType.toString();
2411
+ } else if (typeof rawType === "string" && rawType.startsWith("_")) {
2412
+ isArray = true;
2413
+ arrayBaseType = rawType.substring(1);
2414
+ }
2415
+ return {
2416
+ name: param.name,
2417
+ type: vars.get(param.name),
2418
+ isArray,
2419
+ arrayBaseType
2420
+ };
2421
+ });
2353
2422
  }
2354
2423
  get columns() {
2355
2424
  if (!(this.query.allColumns.type instanceof StructType)) throw new Error(`Expected StructType ${this.query.allColumns.type}`);
@@ -2725,7 +2794,7 @@ async function processProject(projectPath) {
2725
2794
  //#region src/mcp-server.ts
2726
2795
  const server = new Server({
2727
2796
  name: "sqg-mcp",
2728
- version: process.env.npm_package_version ?? "0.10.0"
2797
+ version: process.env.npm_package_version ?? "0.12.0"
2729
2798
  }, { capabilities: {
2730
2799
  tools: {},
2731
2800
  resources: {}
@@ -3054,7 +3123,11 @@ async function startMcpServer() {
3054
3123
 
3055
3124
  //#endregion
3056
3125
  //#region src/sqg.ts
3057
- const version = process.env.npm_package_version ?? "0.10.0";
3126
+ const version = process.env.npm_package_version ?? "0.12.0";
3127
+ updateNotifier({ pkg: {
3128
+ name: "@sqg/sqg",
3129
+ version
3130
+ } }).notify({ message: "Update available {currentVersion} → {latestVersion}" });
3058
3131
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
3059
3132
  consola.level = LogLevels.info;
3060
3133
  const program = new Command().name("sqg").description(`${description}
@@ -212,8 +212,9 @@ public class {{className}} {
212
212
  {{#if isQuery}}{{#unless isOne}}
213
213
  public Stream<{{rowType}}> {{functionName}}Stream({{#each variables}}{{{type}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
214
214
  var stmt = connection.prepareStatement({{{partsToString sqlQueryParts}}});
215
- {{#each parameterNames}}stmt.setObject({{plusOne @index}}, {{this}});
216
- {{/each}}
215
+ {{#each parameters}}{{#if isArray}}stmt.setArray({{plusOne @index}}, connection.createArrayOf("{{arrayBaseType}}", {{name}}.toArray()));
216
+ {{else}}stmt.setObject({{plusOne @index}}, {{name}});
217
+ {{/if}}{{/each}}
217
218
  var rs = stmt.executeQuery();
218
219
  var iter = new Iterator<{{rowType}}>() {
219
220
  private Boolean hasNext = null;
@@ -256,7 +257,7 @@ public class {{className}} {
256
257
 
257
258
  {{#each tables}}
258
259
  /** Row type for {{tableName}} appender */
259
- public record {{rowTypeName}}({{#each columns}}{{{mapType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
260
+ public record {{rowTypeName}}({{#each columns}}{{{appenderType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
260
261
 
261
262
  /** Appender for bulk inserts into {{tableName}} */
262
263
  public static class {{className}} implements AutoCloseable {
@@ -282,7 +283,7 @@ public static class {{className}} implements AutoCloseable {
282
283
  }
283
284
 
284
285
  /** Append a single row with individual values */
285
- public {{className}} append({{#each columns}}{{{mapType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
286
+ public {{className}} append({{#each columns}}{{{appenderType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
286
287
  appender.beginRow();
287
288
  {{#each columns}}
288
289
  appender.append({{name}});
@@ -327,8 +328,9 @@ int
327
328
  {{/inline~}}
328
329
 
329
330
  {{#*inline "execute"}}
330
- {{#each parameterNames}}stmt.setObject({{plusOne @index}}, {{this}});
331
- {{/each}}
331
+ {{#each parameters}}{{#if isArray}}stmt.setArray({{plusOne @index}}, connection.createArrayOf("{{arrayBaseType}}", {{name}}.toArray()));
332
+ {{else}}stmt.setObject({{plusOne @index}}, {{name}});
333
+ {{/if}}{{/each}}
332
334
  {{#if isQuery}}
333
335
  try(var rs = stmt.executeQuery()) {
334
336
  {{#if isOne}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -45,6 +45,8 @@
45
45
  "@lezer/common": "^1.5.0",
46
46
  "@lezer/generator": "^1.8.0",
47
47
  "@lezer/lr": "^1.4.6",
48
+ "@modelcontextprotocol/sdk": "^1.0.4",
49
+ "@testcontainers/postgresql": "^10.21.0",
48
50
  "better-sqlite3": "^12.5.0",
49
51
  "commander": "^14.0.2",
50
52
  "consola": "^3.4.2",
@@ -53,12 +55,11 @@
53
55
  "handlebars": "^4.7.8",
54
56
  "pg": "^8.16.3",
55
57
  "pg-types": "^4.1.0",
56
- "@testcontainers/postgresql": "^10.21.0",
57
58
  "prettier": "^3.7.4",
58
59
  "prettier-plugin-java": "^2.7.7",
60
+ "update-notifier": "^7.3.1",
59
61
  "yaml": "^2.8.2",
60
- "zod": "^4.3.5",
61
- "@modelcontextprotocol/sdk": "^1.0.4"
62
+ "zod": "^4.3.5"
62
63
  },
63
64
  "devDependencies": {
64
65
  "@libsql/client": "^0.17.0",
@@ -66,6 +67,7 @@
66
67
  "@types/better-sqlite3": "^7.6.13",
67
68
  "@types/node": "^25.0.3",
68
69
  "@types/pg": "^8.16.0",
70
+ "@types/update-notifier": "^6.0.8",
69
71
  "@vitest/ui": "^4.0.16",
70
72
  "tsdown": "0.18.0",
71
73
  "tsx": "^4.21.0",