@sqg/sqg 0.9.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,7 +680,7 @@ var EnumType = class {
680
680
  var SQLQuery = class {
681
681
  columns;
682
682
  allColumns;
683
- /** Database-reported parameter types (variable name → SQL type name), set by database adapters */
683
+ /** Database-reported parameter types (variable name → SQL type), set by database adapters */
684
684
  parameterTypes;
685
685
  constructor(filename, id, rawQuery, queryAnonymous, queryNamed, queryPositional, type, isOne, isPluck, variables, config) {
686
686
  this.filename = filename;
@@ -966,6 +966,25 @@ async function initializeDatabase(queries, execQueries) {
966
966
 
967
967
  //#endregion
968
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
+ }
969
988
  const duckdb = new class {
970
989
  db;
971
990
  connection;
@@ -1005,6 +1024,15 @@ const duckdb = new class {
1005
1024
  return ` ${part.value} `;
1006
1025
  }).join("");
1007
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
+ }
1008
1036
  for (let i = 0; i < stmt.parameterCount; i++) stmt.bindValue(i + 1, statement.parameters[i].value);
1009
1037
  if (query.isQuery) {
1010
1038
  const result = await stmt.stream();
@@ -1012,25 +1040,6 @@ const duckdb = new class {
1012
1040
  const columnTypes = result.columnTypes();
1013
1041
  consola.debug("Columns:", columnNames);
1014
1042
  consola.debug("Types:", columnTypes.map((t) => `${t.toString()} / ${t.constructor.name}`));
1015
- function convertType(type) {
1016
- if (type instanceof DuckDBListType) return new ListType(convertType(type.valueType));
1017
- if (type instanceof DuckDBStructType) return new StructType(type.entryTypes.map((t, index) => ({
1018
- name: type.entryNames[index],
1019
- type: convertType(t),
1020
- nullable: true
1021
- })));
1022
- if (type instanceof DuckDBMapType) return new MapType({
1023
- name: "key",
1024
- type: convertType(type.keyType),
1025
- nullable: true
1026
- }, {
1027
- name: "value",
1028
- type: convertType(type.valueType),
1029
- nullable: true
1030
- });
1031
- if (type instanceof DuckDBEnumType) return new EnumType(type.values);
1032
- return type.toString();
1033
- }
1034
1043
  query.columns = columnNames.map((name, index) => ({
1035
1044
  name,
1036
1045
  type: convertType(columnTypes[index]),
@@ -1053,33 +1062,17 @@ const duckdb = new class {
1053
1062
  for (const table of tables) {
1054
1063
  consola.info(`Introspecting table schema: ${table.tableName}`);
1055
1064
  try {
1056
- const rows = (await connection.runAndReadAll(`DESCRIBE ${table.tableName}`)).getRows();
1057
- function convertType(type) {
1058
- if (type instanceof DuckDBListType) return new ListType(convertType(type.valueType));
1059
- if (type instanceof DuckDBStructType) return new StructType(type.entryTypes.map((t, index) => ({
1060
- name: type.entryNames[index],
1061
- type: convertType(t),
1062
- nullable: true
1063
- })));
1064
- if (type instanceof DuckDBMapType) return new MapType({
1065
- name: "key",
1066
- type: convertType(type.keyType),
1067
- nullable: true
1068
- }, {
1069
- name: "value",
1070
- type: convertType(type.valueType),
1071
- nullable: true
1072
- });
1073
- if (type instanceof DuckDBEnumType) return new EnumType(type.values);
1074
- return type.toString();
1075
- }
1076
- table.columns = rows.map((row) => {
1077
- return {
1078
- name: row[0],
1079
- type: row[1],
1080
- nullable: row[2] !== "NO"
1081
- };
1082
- });
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
+ }));
1083
1076
  consola.debug(`Table ${table.tableName} columns:`, table.columns);
1084
1077
  consola.success(`Introspected table: ${table.tableName} (${table.columns.length} columns)`);
1085
1078
  } catch (error) {
@@ -1983,6 +1976,24 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
1983
1976
 
1984
1977
  //#endregion
1985
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
+ }
1986
1997
  var TsGenerator = class extends BaseGenerator {
1987
1998
  constructor(template) {
1988
1999
  super(template, new TypeScriptTypeMapper());
@@ -2002,6 +2013,7 @@ var TsGenerator = class extends BaseGenerator {
2002
2013
  async beforeGenerate(_projectDir, _gen, _queries, _tables) {
2003
2014
  Handlebars.registerHelper("quote", (value) => this.quote(value));
2004
2015
  Handlebars.registerHelper("appendMethod", (column) => {
2016
+ if (column.type instanceof ListType) return "List";
2005
2017
  const typeStr = column.type?.toString().toUpperCase() || "";
2006
2018
  if (typeStr === "INTEGER" || typeStr === "INT" || typeStr === "INT4" || typeStr === "SIGNED") return "Integer";
2007
2019
  if (typeStr === "SMALLINT" || typeStr === "INT2" || typeStr === "SHORT") return "SmallInt";
@@ -2023,7 +2035,15 @@ var TsGenerator = class extends BaseGenerator {
2023
2035
  if (typeStr === "INTERVAL") return "Interval";
2024
2036
  return "Varchar";
2025
2037
  });
2038
+ Handlebars.registerHelper("appendListTypeArg", (column) => {
2039
+ if (!(column.type instanceof ListType)) return "";
2040
+ return `, new DuckDBListType(${resolveElementType(column.type.baseType)})`;
2041
+ });
2026
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
+ }
2027
2047
  const typeStr = column.type?.toString().toUpperCase() || "";
2028
2048
  let baseType;
2029
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";
@@ -2164,6 +2184,21 @@ var TsDuckDBGenerator = class extends TsGenerator {
2164
2184
  }
2165
2185
  async beforeGenerate(projectDir, gen, queries, tables) {
2166
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
+ });
2167
2202
  Handlebars.registerHelper("tsType", (column) => {
2168
2203
  const inlineType = (col) => {
2169
2204
  const t = col.type;
@@ -2533,7 +2568,10 @@ async function writeGeneratedFile(projectDir, gen, generator, file, queries, tab
2533
2568
  await generator.beforeGenerate(projectDir, gen, queries, tables);
2534
2569
  const templatePath = join(dirname(new URL(import.meta.url).pathname), gen.template ?? generator.template);
2535
2570
  const name = gen.name ?? basename(file, extname(file));
2536
- 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
+ });
2537
2575
  if (writeToStdout) {
2538
2576
  process.stdout.write(sourceFile);
2539
2577
  if (!sourceFile.endsWith("\n")) process.stdout.write("\n");
@@ -2687,7 +2725,7 @@ async function processProject(projectPath) {
2687
2725
  //#region src/mcp-server.ts
2688
2726
  const server = new Server({
2689
2727
  name: "sqg-mcp",
2690
- version: process.env.npm_package_version ?? "0.9.0"
2728
+ version: process.env.npm_package_version ?? "0.10.0"
2691
2729
  }, { capabilities: {
2692
2730
  tools: {},
2693
2731
  resources: {}
@@ -3016,7 +3054,7 @@ async function startMcpServer() {
3016
3054
 
3017
3055
  //#endregion
3018
3056
  //#region src/sqg.ts
3019
- const version = process.env.npm_package_version ?? "0.9.0";
3057
+ const version = process.env.npm_package_version ?? "0.10.0";
3020
3058
  const description = process.env.npm_package_description ?? "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)";
3021
3059
  consola.level = LogLevels.info;
3022
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.9.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": {