@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.
- package/dist/sqg.mjs +133 -78
- 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
|
|
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
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
}
|
|
1141
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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 "${
|
|
1149
|
+
await this.dbInitial.query(`DROP DATABASE IF EXISTS "${tempDatabaseName}";`);
|
|
1158
1150
|
} catch (error) {}
|
|
1159
1151
|
try {
|
|
1160
|
-
await this.dbInitial.query(`CREATE DATABASE "${
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
1273
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|