@sqg/sqg 0.11.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.
Files changed (2) hide show
  1. package/dist/sqg.mjs +133 -78
  2. package/package.json +6 -4
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,7 +1252,7 @@ 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);
@@ -1208,7 +1266,8 @@ const postgres = new class {
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
 
@@ -2743,7 +2794,7 @@ async function processProject(projectPath) {
2743
2794
  //#region src/mcp-server.ts
2744
2795
  const server = new Server({
2745
2796
  name: "sqg-mcp",
2746
- version: process.env.npm_package_version ?? "0.11.0"
2797
+ version: process.env.npm_package_version ?? "0.12.0"
2747
2798
  }, { capabilities: {
2748
2799
  tools: {},
2749
2800
  resources: {}
@@ -3072,7 +3123,11 @@ async function startMcpServer() {
3072
3123
 
3073
3124
  //#endregion
3074
3125
  //#region src/sqg.ts
3075
- const version = process.env.npm_package_version ?? "0.11.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}" });
3076
3131
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
3077
3132
  consola.level = LogLevels.info;
3078
3133
  const program = new Command().name("sqg").description(`${description}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.11.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",