@sqg/sqg 0.8.0 → 0.10.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), 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;
@@ -964,6 +966,25 @@ async function initializeDatabase(queries, execQueries) {
964
966
 
965
967
  //#endregion
966
968
  //#region src/db/duckdb.ts
969
+ function convertType(type) {
970
+ if (type instanceof DuckDBListType) return new ListType(convertType(type.valueType));
971
+ if (type instanceof DuckDBStructType) return new StructType(type.entryTypes.map((t, index) => ({
972
+ name: type.entryNames[index],
973
+ type: convertType(t),
974
+ nullable: true
975
+ })));
976
+ if (type instanceof DuckDBMapType) return new MapType({
977
+ name: "key",
978
+ type: convertType(type.keyType),
979
+ nullable: true
980
+ }, {
981
+ name: "value",
982
+ type: convertType(type.valueType),
983
+ nullable: true
984
+ });
985
+ if (type instanceof DuckDBEnumType) return new EnumType(type.values);
986
+ return type.toString();
987
+ }
967
988
  const duckdb = new class {
968
989
  db;
969
990
  connection;
@@ -1003,6 +1024,15 @@ const duckdb = new class {
1003
1024
  return ` ${part.value} `;
1004
1025
  }).join("");
1005
1026
  const stmt = await connection.prepare(sql);
1027
+ if (stmt.parameterCount > 0) {
1028
+ const paramTypes = /* @__PURE__ */ new Map();
1029
+ for (let i = 0; i < stmt.parameterCount; i++) {
1030
+ const paramType = stmt.parameterType(i + 1);
1031
+ paramTypes.set(statement.parameters[i].name, convertType(paramType));
1032
+ }
1033
+ query.parameterTypes = paramTypes;
1034
+ consola.debug("Parameter types:", Object.fromEntries(paramTypes));
1035
+ }
1006
1036
  for (let i = 0; i < stmt.parameterCount; i++) stmt.bindValue(i + 1, statement.parameters[i].value);
1007
1037
  if (query.isQuery) {
1008
1038
  const result = await stmt.stream();
@@ -1010,25 +1040,6 @@ const duckdb = new class {
1010
1040
  const columnTypes = result.columnTypes();
1011
1041
  consola.debug("Columns:", columnNames);
1012
1042
  consola.debug("Types:", columnTypes.map((t) => `${t.toString()} / ${t.constructor.name}`));
1013
- function convertType(type) {
1014
- if (type instanceof DuckDBListType) return new ListType(convertType(type.valueType));
1015
- if (type instanceof DuckDBStructType) return new StructType(type.entryTypes.map((t, index) => ({
1016
- name: type.entryNames[index],
1017
- type: convertType(t),
1018
- nullable: true
1019
- })));
1020
- if (type instanceof DuckDBMapType) return new MapType({
1021
- name: "key",
1022
- type: convertType(type.keyType),
1023
- nullable: true
1024
- }, {
1025
- name: "value",
1026
- type: convertType(type.valueType),
1027
- nullable: true
1028
- });
1029
- if (type instanceof DuckDBEnumType) return new EnumType(type.values);
1030
- return type.toString();
1031
- }
1032
1043
  query.columns = columnNames.map((name, index) => ({
1033
1044
  name,
1034
1045
  type: convertType(columnTypes[index]),
@@ -1051,33 +1062,17 @@ const duckdb = new class {
1051
1062
  for (const table of tables) {
1052
1063
  consola.info(`Introspecting table schema: ${table.tableName}`);
1053
1064
  try {
1054
- const rows = (await connection.runAndReadAll(`DESCRIBE ${table.tableName}`)).getRows();
1055
- function convertType(type) {
1056
- if (type instanceof DuckDBListType) return new ListType(convertType(type.valueType));
1057
- if (type instanceof DuckDBStructType) return new StructType(type.entryTypes.map((t, index) => ({
1058
- name: type.entryNames[index],
1059
- type: convertType(t),
1060
- nullable: true
1061
- })));
1062
- if (type instanceof DuckDBMapType) return new MapType({
1063
- name: "key",
1064
- type: convertType(type.keyType),
1065
- nullable: true
1066
- }, {
1067
- name: "value",
1068
- type: convertType(type.valueType),
1069
- nullable: true
1070
- });
1071
- if (type instanceof DuckDBEnumType) return new EnumType(type.values);
1072
- return type.toString();
1073
- }
1074
- table.columns = rows.map((row) => {
1075
- return {
1076
- name: row[0],
1077
- type: row[1],
1078
- nullable: row[2] !== "NO"
1079
- };
1080
- });
1065
+ const descRows = (await connection.runAndReadAll(`DESCRIBE ${table.tableName}`)).getRows();
1066
+ const nullabilityMap = /* @__PURE__ */ new Map();
1067
+ for (const row of descRows) nullabilityMap.set(row[0], row[2] !== "NO");
1068
+ const stream = await (await connection.prepare(`SELECT * FROM ${table.tableName} LIMIT 0`)).stream();
1069
+ const columnNames = stream.columnNames();
1070
+ const columnTypes = stream.columnTypes();
1071
+ table.columns = columnNames.map((name, index) => ({
1072
+ name,
1073
+ type: convertType(columnTypes[index]),
1074
+ nullable: nullabilityMap.get(name) ?? true
1075
+ }));
1081
1076
  consola.debug(`Table ${table.tableName} columns:`, table.columns);
1082
1077
  consola.success(`Introspected table: ${table.tableName} (${table.columns.length} columns)`);
1083
1078
  } catch (error) {
@@ -1205,6 +1200,23 @@ const postgres = new class {
1205
1200
  if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) return value.slice(1, -1);
1206
1201
  return value;
1207
1202
  });
1203
+ if (statement.parameters.length > 0) try {
1204
+ await db.query(`DEALLOCATE ALL`);
1205
+ await db.query(`PREPARE sqg_param_check AS ${statement.sql}`);
1206
+ 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`);
1208
+ if (paramTypeResult.rows.length === statement.parameters.length) {
1209
+ const paramTypes = /* @__PURE__ */ new Map();
1210
+ for (let i = 0; i < statement.parameters.length; i++) {
1211
+ const typeName = getTypeName(Number(paramTypeResult.rows[i].oid));
1212
+ paramTypes.set(statement.parameters[i].name, typeName);
1213
+ }
1214
+ query.parameterTypes = paramTypes;
1215
+ consola.debug("Parameter types:", Object.fromEntries(paramTypes));
1216
+ }
1217
+ } catch (e) {
1218
+ consola.debug(`Parameter type introspection failed for ${query.id}, using heuristic:`, e.message);
1219
+ }
1208
1220
  let result;
1209
1221
  try {
1210
1222
  await db.query("BEGIN");
@@ -1964,6 +1976,24 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
1964
1976
 
1965
1977
  //#endregion
1966
1978
  //#region src/generators/typescript-generator.ts
1979
+ /** Resolve a ColumnType to its DuckDB type constant name (e.g. "VARCHAR", "INTEGER") for use in generated code. */
1980
+ function resolveElementType(baseType) {
1981
+ if (baseType instanceof ListType) return `new DuckDBListType(${resolveElementType(baseType.baseType)})`;
1982
+ return {
1983
+ VARCHAR: "VARCHAR",
1984
+ TEXT: "VARCHAR",
1985
+ INTEGER: "INTEGER",
1986
+ INT: "INTEGER",
1987
+ BIGINT: "BIGINT",
1988
+ DOUBLE: "DOUBLE",
1989
+ FLOAT: "FLOAT",
1990
+ BOOLEAN: "BOOLEAN",
1991
+ DATE: "DATE",
1992
+ TIMESTAMP: "TIMESTAMP",
1993
+ SMALLINT: "SMALLINT",
1994
+ TINYINT: "TINYINT"
1995
+ }[baseType.toString().toUpperCase()] || "VARCHAR";
1996
+ }
1967
1997
  var TsGenerator = class extends BaseGenerator {
1968
1998
  constructor(template) {
1969
1999
  super(template, new TypeScriptTypeMapper());
@@ -1983,6 +2013,7 @@ var TsGenerator = class extends BaseGenerator {
1983
2013
  async beforeGenerate(_projectDir, _gen, _queries, _tables) {
1984
2014
  Handlebars.registerHelper("quote", (value) => this.quote(value));
1985
2015
  Handlebars.registerHelper("appendMethod", (column) => {
2016
+ if (column.type instanceof ListType) return "List";
1986
2017
  const typeStr = column.type?.toString().toUpperCase() || "";
1987
2018
  if (typeStr === "INTEGER" || typeStr === "INT" || typeStr === "INT4" || typeStr === "SIGNED") return "Integer";
1988
2019
  if (typeStr === "SMALLINT" || typeStr === "INT2" || typeStr === "SHORT") return "SmallInt";
@@ -2004,7 +2035,15 @@ var TsGenerator = class extends BaseGenerator {
2004
2035
  if (typeStr === "INTERVAL") return "Interval";
2005
2036
  return "Varchar";
2006
2037
  });
2038
+ Handlebars.registerHelper("appendListTypeArg", (column) => {
2039
+ if (!(column.type instanceof ListType)) return "";
2040
+ return `, new DuckDBListType(${resolveElementType(column.type.baseType)})`;
2041
+ });
2007
2042
  Handlebars.registerHelper("tsTypeForAppender", (column) => {
2043
+ if (column.type instanceof ListType) {
2044
+ const baseType$1 = "readonly DuckDBValue[]";
2045
+ return column.nullable ? `${baseType$1} | null` : baseType$1;
2046
+ }
2008
2047
  const typeStr = column.type?.toString().toUpperCase() || "";
2009
2048
  let baseType;
2010
2049
  if (typeStr === "INTEGER" || typeStr === "INT" || typeStr === "INT4" || typeStr === "SMALLINT" || typeStr === "INT2" || typeStr === "TINYINT" || typeStr === "INT1" || typeStr === "UINTEGER" || typeStr === "UINT4" || typeStr === "USMALLINT" || typeStr === "UINT2" || typeStr === "UTINYINT" || typeStr === "UINT1" || typeStr === "DOUBLE" || typeStr === "FLOAT8" || typeStr === "FLOAT" || typeStr === "FLOAT4" || typeStr === "REAL") baseType = "number";
@@ -2145,6 +2184,21 @@ var TsDuckDBGenerator = class extends TsGenerator {
2145
2184
  }
2146
2185
  async beforeGenerate(projectDir, gen, queries, tables) {
2147
2186
  await super.beforeGenerate(projectDir, gen, queries, tables);
2187
+ Handlebars.registerHelper("hasListParams", (queryHelper) => {
2188
+ const paramTypes = queryHelper.query.parameterTypes;
2189
+ if (!paramTypes) return false;
2190
+ for (const [, colType] of paramTypes) if (colType instanceof ListType) return true;
2191
+ return false;
2192
+ });
2193
+ Handlebars.registerHelper("bindStatements", (queryHelper) => {
2194
+ const paramNames = queryHelper.parameterNames;
2195
+ const paramTypes = queryHelper.query.parameterTypes;
2196
+ return paramNames.map((name, i) => {
2197
+ const colType = paramTypes?.get(name);
2198
+ if (colType instanceof ListType) return `stmt.bindList(${i + 1}, ${name}.items, new DuckDBListType(${resolveElementType(colType.baseType)}));`;
2199
+ return `stmt.bindValue(${i + 1}, ${name});`;
2200
+ }).join("\n ");
2201
+ });
2148
2202
  Handlebars.registerHelper("tsType", (column) => {
2149
2203
  const inlineType = (col) => {
2150
2204
  const t = col.type;
@@ -2304,7 +2358,7 @@ var SqlQueryHelper = class {
2304
2358
  get variables() {
2305
2359
  return Array.from(this.query.variables.entries()).map(([name, value]) => ({
2306
2360
  name,
2307
- type: this.generator.mapParameterType(detectParameterType(value), false)
2361
+ type: this.generator.mapParameterType(this.query.parameterTypes?.get(name) ?? detectParameterType(value), false)
2308
2362
  }));
2309
2363
  }
2310
2364
  get sqlQuery() {
@@ -2514,7 +2568,10 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
2514
2568
  await generator.beforeGenerate(projectDir, gen, queries, tables);
2515
2569
  const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
2516
2570
  const name = gen.name ?? basename(file, extname(file));
2517
- const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.projectName ?? name, gen.config);
2571
+ const sourceFile = generateSourceFile(name, queries, tables, templatePath, generator, engine, gen.projectName ?? name, {
2572
+ migrations: true,
2573
+ ...gen.config
2574
+ });
2518
2575
  if (writeToStdout) {
2519
2576
  process.stdout.write(sourceFile);
2520
2577
  if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
@@ -2668,7 +2725,7 @@ async function processProject(projectPath) {
2668
2725
  //#region src/mcp-server.ts
2669
2726
  const server = new Server({
2670
2727
  name: "sqg-mcp",
2671
- version: process.env.npm_package_version ?? "0.8.0"
2728
+ version: process.env.npm_package_version ?? "0.10.0"
2672
2729
  }, { capabilities: {
2673
2730
  tools: {},
2674
2731
  resources: {}
@@ -2997,7 +3054,7 @@ async function startMcpServer() {
2997
3054
 
2998
3055
  //#endregion
2999
3056
  //#region src/sqg.ts
3000
- const version = process.env.npm_package_version ?? "0.8.0";
3057
+ const version = process.env.npm_package_version ?? "0.10.0";
3001
3058
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
3002
3059
  consola.level = LogLevels.info;
3003
3060
  const program = new Command().name("sqg").description(`${description}
@@ -17,11 +17,17 @@ import java.time.OffsetDateTime;
17
17
  import java.time.OffsetTime;
18
18
  import java.util.ArrayList;
19
19
  import java.util.Arrays;
20
+ import java.util.Iterator;
20
21
  import java.util.List;
21
22
  import java.util.HashMap;
22
23
  import java.util.Collections;
24
+ import java.util.NoSuchElementException;
25
+ import java.util.Spliterator;
26
+ import java.util.Spliterators;
23
27
  import java.util.UUID;
24
28
  import java.util.function.Function;
29
+ import java.util.stream.Stream;
30
+ import java.util.stream.StreamSupport;
25
31
  {{#if tables.length}}
26
32
  import org.duckdb.DuckDBAppender;
27
33
  import org.duckdb.DuckDBConnection;
@@ -203,6 +209,36 @@ public class {{className}} {
203
209
  {{> execute}}
204
210
  }
205
211
  }
212
+ {{#if isQuery}}{{#unless isOne}}
213
+ public Stream<{{rowType}}> {{functionName}}Stream({{#each variables}}{{{type}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
214
+ var stmt = connection.prepareStatement({{{partsToString sqlQueryParts}}});
215
+ {{#each parameterNames}}stmt.setObject({{plusOne @index}}, {{this}});
216
+ {{/each}}
217
+ var rs = stmt.executeQuery();
218
+ var iter = new Iterator<{{rowType}}>() {
219
+ private Boolean hasNext = null;
220
+ public boolean hasNext() {
221
+ if (hasNext == null) {
222
+ try { hasNext = rs.next(); }
223
+ catch (SQLException e) { throw new RuntimeException(e); }
224
+ }
225
+ return hasNext;
226
+ }
227
+ public {{rowType}} next() {
228
+ if (!hasNext()) throw new NoSuchElementException();
229
+ hasNext = null;
230
+ try { return {{> readRow}}; }
231
+ catch (SQLException e) { throw new RuntimeException(e); }
232
+ }
233
+ };
234
+ return StreamSupport.stream(
235
+ Spliterators.spliteratorUnknownSize(iter, Spliterator.ORDERED), false
236
+ ).onClose(() -> {
237
+ try { rs.close(); stmt.close(); }
238
+ catch (SQLException e) { throw new RuntimeException(e); }
239
+ });
240
+ }
241
+ {{/unless}}{{/if}}
206
242
  {{/unless}}
207
243
  {{/each}}
208
244
 
@@ -220,7 +256,7 @@ public class {{className}} {
220
256
 
221
257
  {{#each tables}}
222
258
  /** Row type for {{tableName}} appender */
223
- public record {{rowTypeName}}({{#each columns}}{{mapType this}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
259
+ public record {{rowTypeName}}({{#each columns}}{{{mapType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
224
260
 
225
261
  /** Appender for bulk inserts into {{tableName}} */
226
262
  public static class {{className}} implements AutoCloseable {
@@ -246,7 +282,7 @@ public static class {{className}} implements AutoCloseable {
246
282
  }
247
283
 
248
284
  /** Append a single row with individual values */
249
- public {{className}} append({{#each columns}}{{mapType this}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
285
+ public {{className}} append({{#each columns}}{{{mapType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
250
286
  appender.beginRow();
251
287
  {{#each columns}}
252
288
  appender.append({{name}});
@@ -33,36 +33,32 @@ export class {{className}} {
33
33
 
34
34
  {{#if config.migrations}}
35
35
  static async applyMigrations(db: Database, projectName = '{{projectName}}'): Promise<void> {
36
- const createStmt = await db.prepare(`CREATE TABLE IF NOT EXISTS _sqg_migrations (
36
+ const createStmt = db.prepare(`CREATE TABLE IF NOT EXISTS _sqg_migrations (
37
37
  project TEXT NOT NULL,
38
38
  migration_id TEXT NOT NULL,
39
39
  applied_at TEXT NOT NULL DEFAULT (datetime('now')),
40
40
  PRIMARY KEY (project, migration_id)
41
41
  )`);
42
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
- ];
43
+ const selectStmt = db.prepare('SELECT migration_id FROM _sqg_migrations WHERE project = ?');
44
+ const rows = await selectStmt.all(projectName) as { migration_id: string }[];
45
+ const applied = new Set(rows.map(r => r.migration_id));
46
+ const migrations: [string, string][] = [
47
+ {{#each migrations}}
48
+ ['{{{id}}}', {{{quote sqlQuery}}}],
49
+ {{/each}}
50
+ ];
51
+ const applyFn = db.transaction(async () => {
53
52
  for (const [id, sql] of migrations) {
54
53
  if (!applied.has(id)) {
55
- const execStmt = await tx.prepare(sql);
54
+ const execStmt = db.prepare(sql);
56
55
  await execStmt.run();
57
- const insertStmt = await tx.prepare('INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)');
56
+ const insertStmt = db.prepare('INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)');
58
57
  await insertStmt.run(projectName, id);
59
58
  }
60
59
  }
61
- await tx.commit();
62
- } catch (e) {
63
- await tx.rollback();
64
- throw e;
65
- }
60
+ });
61
+ await applyFn();
66
62
  }
67
63
  {{/if}}
68
64
 
@@ -1,5 +1,5 @@
1
1
  // {{generatedComment}}
2
- import type { DuckDBConnection, DuckDBMaterializedResult, DuckDBAppender, DuckDBDateValue, DuckDBTimeValue, DuckDBTimestampValue, DuckDBBlobValue } from "@duckdb/node-api";
2
+ import { DuckDBListType, listValue, VARCHAR, INTEGER, BIGINT, DOUBLE, FLOAT, BOOLEAN, SMALLINT, TINYINT, type DuckDBConnection, type DuckDBMaterializedResult, type DuckDBAppender, type DuckDBDateValue, type DuckDBTimeValue, type DuckDBTimestampValue, type DuckDBBlobValue, type DuckDBValue } from "@duckdb/node-api";
3
3
 
4
4
  export class {{className}} {
5
5
 
@@ -60,6 +60,28 @@ export class {{className}} {
60
60
  {{#unless skipGenerateFunction}}
61
61
  async {{functionName}}({{#each variables}}{{name}}: {{type}}{{#unless @last}}, {{/unless}}{{/each}}): Promise<{{> returnType }}> {
62
62
  const sql = {{{quote sqlQuery}}};
63
+ {{#if (hasListParams this)}}
64
+ const stmt = await this.conn.prepare(sql);
65
+ {{{bindStatements this}}}
66
+ {{#if isQuery}}
67
+ const reader = await stmt.runAndReadAll();
68
+ {{#if isPluck}}
69
+ {{#if isOne}}
70
+ return reader.getRows()[0]?.[0] as {{> rowType}} | undefined;
71
+ {{else}}
72
+ return reader.getRows().map((row) => row[0] as {{> rowType}});
73
+ {{/if}}
74
+ {{else}}
75
+ {{#if isOne}}
76
+ return reader.getRowObjects()[0] as {{> rowType}} | undefined;
77
+ {{else}}
78
+ return reader.getRowObjects() as {{> rowType}}[];
79
+ {{/if}}
80
+ {{/if}}
81
+ {{else}}
82
+ return await stmt.run();
83
+ {{/if}}
84
+ {{else}}
63
85
  {{#if isQuery}}
64
86
  const reader = await this.conn.runAndReadAll(sql,[{{> params}}]);
65
87
  {{#if isPluck}}
@@ -78,6 +100,7 @@ export class {{className}} {
78
100
  {{else}}
79
101
  return await this.conn.run(sql,[{{> params}}]);
80
102
  {{/if}}
103
+ {{/if}}
81
104
  }
82
105
  {{/unless}}
83
106
 
@@ -113,10 +136,10 @@ export class {{className}} {
113
136
  if (row.{{name}} === null || row.{{name}} === undefined) {
114
137
  this.appender.appendNull();
115
138
  } else {
116
- this.appender.append{{appendMethod this}}(row.{{name}});
139
+ this.appender.append{{appendMethod this}}(row.{{name}}{{{appendListTypeArg this}}});
117
140
  }
118
141
  {{else}}
119
- this.appender.append{{appendMethod this}}(row.{{name}});
142
+ this.appender.append{{appendMethod this}}(row.{{name}}{{{appendListTypeArg this}}});
120
143
  {{/if}}
121
144
  {{/each}}
122
145
  this.appender.endRow();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
5
5
  "type": "module",
6
6
  "bin": {