@sqg/sqg 0.14.0 → 0.16.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
@@ -29,7 +29,6 @@ import { execSync } from "node:child_process";
29
29
  import typescriptPlugin from "prettier/parser-typescript";
30
30
  import estree from "prettier/plugins/estree";
31
31
  import yoctoSpinner from "yocto-spinner";
32
-
33
32
  //#region src/constants.ts
34
33
  /**
35
34
  * SQG Constants - Centralized definitions for supported generators
@@ -223,7 +222,7 @@ SQL Annotation Syntax:
223
222
  -- EXEC <name> Execute statement (INSERT/UPDATE/DELETE)
224
223
  -- MIGRATE <number> Schema migration (run in order)
225
224
  -- TESTDATA <name> Test data setup (not generated)
226
- -- TABLE <name> :appender Table for bulk insert appender (DuckDB only)
225
+ -- TABLE <name> :appender Table for bulk insert appender (DuckDB, PostgreSQL)
227
226
 
228
227
  @set <varName> = <value> Define a variable
229
228
  \${varName} Reference a variable in SQL
@@ -244,7 +243,6 @@ Example:
244
243
 
245
244
  -- TABLE users :appender
246
245
  `.trim();
247
-
248
246
  //#endregion
249
247
  //#region src/errors.ts
250
248
  /**
@@ -302,9 +300,9 @@ var ConfigError = class extends SqgError {
302
300
  * Error for invalid generator names
303
301
  */
304
302
  var InvalidGeneratorError = class extends SqgError {
305
- constructor(generatorName, validGenerators$1, suggestion) {
303
+ constructor(generatorName, validGenerators, suggestion) {
306
304
  const similarMsg = suggestion ? ` Did you mean '${suggestion}'?` : "";
307
- super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators$1.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators$1.join(", ")}`, { generator: generatorName });
305
+ super(`Invalid generator '${generatorName}'.${similarMsg} Valid generators: ${validGenerators.join(", ")}`, "INVALID_GENERATOR", suggestion ? `Use '${suggestion}' instead` : `Choose from: ${validGenerators.join(", ")}`, { generator: generatorName });
308
306
  this.name = "InvalidGeneratorError";
309
307
  }
310
308
  };
@@ -386,7 +384,6 @@ function formatErrorForOutput(err) {
386
384
  }
387
385
  };
388
386
  }
389
-
390
387
  //#endregion
391
388
  //#region src/init.ts
392
389
  /**
@@ -714,7 +711,6 @@ async function initProject(options) {
714
711
  clack.log.message(` 3. Import the generated code from ${output}`);
715
712
  clack.outro("Documentation: https://sqg.dev");
716
713
  }
717
-
718
714
  //#endregion
719
715
  //#region src/parser/sql-parser.ts
720
716
  const parser = LRParser.deserialize({
@@ -735,7 +731,6 @@ const parser = LRParser.deserialize({
735
731
  topRules: { "File": [0, 1] },
736
732
  tokenPrec: 245
737
733
  });
738
-
739
734
  //#endregion
740
735
  //#region src/sql-query.ts
741
736
  var ListType = class {
@@ -896,8 +891,8 @@ function parseSQLQueries(filePath, extraVariables) {
896
891
  let to = -1;
897
892
  class SQLQueryBuilder {
898
893
  sqlParts = [];
899
- appendSql(sql$1) {
900
- this.sqlParts.push(sql$1);
894
+ appendSql(sql) {
895
+ this.sqlParts.push(sql);
901
896
  }
902
897
  appendVariable(varName, value) {
903
898
  this.sqlParts.push({
@@ -913,23 +908,23 @@ function parseSQLQueries(filePath, extraVariables) {
913
908
  return this.sqlParts.filter((part) => typeof part !== "string" && !part.name.startsWith("sources_"));
914
909
  }
915
910
  toSqlWithAnonymousPlaceholders() {
916
- let sql$1 = "";
911
+ let sql = "";
917
912
  const sqlParts = [];
918
913
  for (const part of this.sqlParts) if (typeof part === "string") {
919
- sql$1 += part;
914
+ sql += part;
920
915
  sqlParts.push(part);
921
916
  } else {
922
- if (sql$1.length > 0) {
923
- const last = sql$1[sql$1.length - 1];
924
- if (last !== " " && last !== "=" && last !== ">" && last !== "<") sql$1 += " ";
917
+ if (sql.length > 0) {
918
+ const last = sql[sql.length - 1];
919
+ if (last !== " " && last !== "=" && last !== ">" && last !== "<") sql += " ";
925
920
  }
926
- sql$1 += "?";
921
+ sql += "?";
927
922
  if (part.name.startsWith("sources_")) sqlParts.push(part);
928
923
  else sqlParts.push("?");
929
924
  }
930
925
  return {
931
926
  parameters: this.parameters(),
932
- sql: sql$1,
927
+ sql,
933
928
  sqlParts
934
929
  };
935
930
  }
@@ -999,7 +994,7 @@ function parseSQLQueries(filePath, extraVariables) {
999
994
  }
1000
995
  if (queryType === "TABLE") {
1001
996
  const hasAppender = modifiers.includes(":appender");
1002
- const tableName = sqlContentStr.trim() || name;
997
+ const tableName = sqlContentStr.split("\n").map((l) => l.trim()).find((l) => l.length > 0) || name;
1003
998
  const includeColumns = [];
1004
999
  for (const mod of modifiers) {
1005
1000
  const match = mod.match(/:appender\(([^)]+)\)/);
@@ -1036,7 +1031,6 @@ function parseSQLQueries(filePath, extraVariables) {
1036
1031
  tables
1037
1032
  };
1038
1033
  }
1039
-
1040
1034
  //#endregion
1041
1035
  //#region src/db/types.ts
1042
1036
  async function initializeDatabase(queries, execQueries, reporter) {
@@ -1058,7 +1052,6 @@ async function initializeDatabase(queries, execQueries, reporter) {
1058
1052
  if (migrationQueries.length + testdataQueries.length === 0) consola.warn("No migration or testdata queries found");
1059
1053
  reporter?.onDatabaseInitialized?.();
1060
1054
  }
1061
-
1062
1055
  //#endregion
1063
1056
  //#region src/db/duckdb.ts
1064
1057
  /** Cache of enum type names, keyed by stringified sorted values for lookup */
@@ -1207,7 +1200,6 @@ const duckdb = new class {
1207
1200
  enumNameCache = /* @__PURE__ */ new Map();
1208
1201
  }
1209
1202
  }();
1210
-
1211
1203
  //#endregion
1212
1204
  //#region src/db/postgres.ts
1213
1205
  const tempDatabaseName = "sqg-db-temp";
@@ -1445,13 +1437,14 @@ const postgres = new class {
1445
1437
  for (const table of tables) {
1446
1438
  reporter?.onTableStart?.(table.tableName);
1447
1439
  try {
1448
- table.columns = (await db.query(`SELECT column_name, data_type, is_nullable
1440
+ table.columns = (await db.query(`SELECT column_name, data_type, udt_name, is_nullable, column_default, is_identity
1449
1441
  FROM information_schema.columns
1450
1442
  WHERE table_name = $1
1451
1443
  ORDER BY ordinal_position`, [table.tableName])).rows.map((row) => ({
1452
1444
  name: row.column_name,
1453
- type: row.data_type.toUpperCase(),
1454
- nullable: row.is_nullable === "YES"
1445
+ type: row.udt_name.toUpperCase(),
1446
+ nullable: row.is_nullable === "YES",
1447
+ generated: row.is_identity === "YES" || (row.column_default?.startsWith("nextval") ?? false)
1455
1448
  }));
1456
1449
  reporter?.onTableComplete?.(table.tableName, table.columns.length);
1457
1450
  } catch (error) {
@@ -1466,7 +1459,6 @@ const postgres = new class {
1466
1459
  this.enumTypeCache = /* @__PURE__ */ new Map();
1467
1460
  }
1468
1461
  }();
1469
-
1470
1462
  //#endregion
1471
1463
  //#region src/db/sqlite.ts
1472
1464
  const sqlite = new class {
@@ -1548,7 +1540,6 @@ const sqlite = new class {
1548
1540
  }
1549
1541
  }
1550
1542
  }();
1551
-
1552
1543
  //#endregion
1553
1544
  //#region src/db/index.ts
1554
1545
  function getDatabaseEngine(engine) {
@@ -1559,7 +1550,6 @@ function getDatabaseEngine(engine) {
1559
1550
  default: throw new Error(`Unsupported database engine: ${engine}`);
1560
1551
  }
1561
1552
  }
1562
-
1563
1553
  //#endregion
1564
1554
  //#region src/type-mapping.ts
1565
1555
  /**
@@ -1793,15 +1783,18 @@ var JavaTypeMapper = class JavaTypeMapper extends TypeMapper {
1793
1783
  if (column.type instanceof StructType) return `${path}${this.formatStructTypeName(column.name)}.fromAttributes(getAttr((Struct)${value}))`;
1794
1784
  const fieldType = this.getTypeName(column);
1795
1785
  const upperType = column.type?.toString().toUpperCase() ?? "";
1796
- if (upperType === "TIMESTAMP" || upperType === "DATETIME") return `toLocalDateTime((java.sql.Timestamp)${value})`;
1797
- if (upperType === "TIMESTAMPTZ") return `toOffsetDateTime((java.sql.Timestamp)${value})`;
1786
+ if (upperType === "TIMESTAMP" || upperType === "DATETIME") return `toLocalDateTime(${value})`;
1787
+ if (upperType === "TIMESTAMPTZ") return `toOffsetDateTime(${value})`;
1798
1788
  if (upperType === "TIMESTAMP WITH TIME ZONE") return `(OffsetDateTime)${value}`;
1799
- if (upperType === "DATE") return `toLocalDate((java.sql.Date)${value})`;
1800
- if (upperType === "TIME") return `toLocalTime((java.sql.Time)${value})`;
1789
+ if (upperType === "DATE") return `toLocalDate(${value})`;
1790
+ if (upperType === "TIME") return `toLocalTime(${value})`;
1801
1791
  if (upperType.startsWith("_")) {
1802
1792
  const baseType = upperType.substring(1);
1803
1793
  return `arrayToList((Array)${value}, ${this.typeMap[baseType] || "Object"}[].class)`;
1804
1794
  }
1795
+ if (fieldType === "Short") return `${value} != null ? ((Number)${value}).shortValue() : null`;
1796
+ if (fieldType === "Byte") return `${value} != null ? ((Number)${value}).byteValue() : null`;
1797
+ if (upperType === "JSON" || upperType === "JSONB") return `${value} != null ? ${value}.toString() : null`;
1805
1798
  return `(${fieldType})${value}`;
1806
1799
  }
1807
1800
  getInnermostType(type) {
@@ -2098,7 +2091,6 @@ var PythonTypeMapper = class PythonTypeMapper extends TypeMapper {
2098
2091
  return value;
2099
2092
  }
2100
2093
  };
2101
-
2102
2094
  //#endregion
2103
2095
  //#region src/generators/base-generator.ts
2104
2096
  var BaseGenerator = class {
@@ -2141,16 +2133,17 @@ var BaseGenerator = class {
2141
2133
  return q.queryAnonymous;
2142
2134
  }
2143
2135
  };
2144
-
2145
2136
  //#endregion
2146
2137
  //#region src/generators/java-generator.ts
2147
2138
  var JavaGenerator = class extends BaseGenerator {
2148
- constructor(template) {
2139
+ engine;
2140
+ constructor(template, engine = "duckdb") {
2149
2141
  super(template, new JavaTypeMapper());
2150
2142
  this.template = template;
2143
+ this.engine = engine;
2151
2144
  }
2152
2145
  supportsAppenders(engine) {
2153
- return engine === "duckdb";
2146
+ return engine === "duckdb" || engine === "postgres";
2154
2147
  }
2155
2148
  getFunctionName(id) {
2156
2149
  return camelCase(id);
@@ -2192,6 +2185,18 @@ var JavaGenerator = class extends BaseGenerator {
2192
2185
  return this.typeMapper.parseValue(column, `rs.getObject(${index + 1})`, path);
2193
2186
  }
2194
2187
  async beforeGenerate(_projectDir, _gen, _queries, _tables) {
2188
+ Handlebars.registerHelper("isDuckDB", () => this.engine === "duckdb");
2189
+ Handlebars.registerHelper("isPostgres", () => this.engine === "postgres");
2190
+ Handlebars.registerHelper("pgBulkType", (column) => {
2191
+ return pgBulkInsertType(column.type.toString().toUpperCase());
2192
+ });
2193
+ Handlebars.registerHelper("pgBulkAccessor", (column) => {
2194
+ return pgBulkInsertAccessor(column.type.toString().toUpperCase());
2195
+ });
2196
+ Handlebars.registerHelper("javaVarName", (name) => {
2197
+ const n = camelCase(name);
2198
+ return JavaTypeMapper.javaReservedKeywords.has(n) ? `${n}_` : n;
2199
+ });
2195
2200
  Handlebars.registerHelper("partsToString", (parts) => this.partsToString(parts));
2196
2201
  Handlebars.registerHelper("declareTypes", (queryHelper) => {
2197
2202
  const query = queryHelper.query;
@@ -2283,7 +2288,44 @@ var JavaGenerator = class extends BaseGenerator {
2283
2288
  }
2284
2289
  }
2285
2290
  };
2286
-
2291
+ const PG_BULK_TYPE_MAP = {
2292
+ SMALLINT: "INT2",
2293
+ INTEGER: "INT4",
2294
+ BIGINT: "INT8",
2295
+ REAL: "FLOAT4",
2296
+ "DOUBLE PRECISION": "FLOAT8",
2297
+ BOOLEAN: "BOOLEAN",
2298
+ TEXT: "TEXT",
2299
+ "CHARACTER VARYING": "TEXT",
2300
+ CHARACTER: "TEXT",
2301
+ NUMERIC: "NUMERIC",
2302
+ DECIMAL: "NUMERIC",
2303
+ DATE: "DATE",
2304
+ "TIME WITHOUT TIME ZONE": "TIME",
2305
+ "TIMESTAMP WITHOUT TIME ZONE": "TIMESTAMP",
2306
+ "TIMESTAMP WITH TIME ZONE": "TIMESTAMPTZ",
2307
+ UUID: "UUID",
2308
+ BYTEA: "BYTEA",
2309
+ JSONB: "JSONB",
2310
+ JSON: "JSONB",
2311
+ INT2: "INT2",
2312
+ INT4: "INT4",
2313
+ INT8: "INT8",
2314
+ FLOAT4: "FLOAT4",
2315
+ FLOAT8: "FLOAT8",
2316
+ BOOL: "BOOLEAN",
2317
+ VARCHAR: "TEXT",
2318
+ TIMESTAMP: "TIMESTAMP",
2319
+ TIMESTAMPTZ: "TIMESTAMPTZ"
2320
+ };
2321
+ function pgBulkInsertType(sqlType) {
2322
+ if (sqlType.startsWith("_")) return `array(PgBulkInsert.PostgresTypes.${PG_BULK_TYPE_MAP[sqlType.substring(1)] || "TEXT"})`;
2323
+ return PG_BULK_TYPE_MAP[sqlType] || "TEXT";
2324
+ }
2325
+ function pgBulkInsertAccessor(sqlType) {
2326
+ if (sqlType === "TIMESTAMPTZ") return "offsetDateTime";
2327
+ return "from";
2328
+ }
2287
2329
  //#endregion
2288
2330
  //#region src/generators/java-duckdb-arrow-generator.ts
2289
2331
  var JavaDuckDBArrowGenerator = class extends BaseGenerator {
@@ -2297,7 +2339,7 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
2297
2339
  return this.javaGenerator.getFunctionName(id);
2298
2340
  }
2299
2341
  async beforeGenerate(projectDir, gen, queries, tables) {
2300
- const q = queries.filter((q$1) => q$1.isQuery && q$1.isOne || q$1.isMigrate);
2342
+ const q = queries.filter((q) => q.isQuery && q.isOne || q.isMigrate);
2301
2343
  const name = `${gen.name}-jdbc`;
2302
2344
  writeGeneratedFile(projectDir, {
2303
2345
  name,
@@ -2370,7 +2412,6 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
2370
2412
  return this.getClassName(`${query.id}_Result`);
2371
2413
  }
2372
2414
  };
2373
-
2374
2415
  //#endregion
2375
2416
  //#region src/generators/python-generator.ts
2376
2417
  var PythonGenerator = class extends BaseGenerator {
@@ -2413,7 +2454,7 @@ var PythonGenerator = class extends BaseGenerator {
2413
2454
  return q.queryAnonymous;
2414
2455
  }
2415
2456
  supportsAppenders(_engine) {
2416
- return this.engine === "duckdb";
2457
+ return this.engine === "duckdb" || this.engine === "postgres";
2417
2458
  }
2418
2459
  async beforeGenerate(_projectDir, _gen, _queries, _tables) {
2419
2460
  const pyMapper = this.typeMapper;
@@ -2513,7 +2554,6 @@ var PythonGenerator = class extends BaseGenerator {
2513
2554
  }
2514
2555
  }
2515
2556
  };
2516
-
2517
2557
  //#endregion
2518
2558
  //#region src/generators/typescript-generator.ts
2519
2559
  /** Resolve a ColumnType to its DuckDB type constant name (e.g. "VARCHAR", "INTEGER") for use in generated code. */
@@ -2581,8 +2621,8 @@ var TsGenerator = class extends BaseGenerator {
2581
2621
  });
2582
2622
  Handlebars.registerHelper("tsTypeForAppender", (column) => {
2583
2623
  if (column.type instanceof ListType) {
2584
- const baseType$1 = "readonly DuckDBValue[]";
2585
- return column.nullable ? `${baseType$1} | null` : baseType$1;
2624
+ const baseType = "readonly DuckDBValue[]";
2625
+ return column.nullable ? `${baseType} | null` : baseType;
2586
2626
  }
2587
2627
  const typeStr = column.type?.toString().toUpperCase() || "";
2588
2628
  let baseType;
@@ -2705,7 +2745,6 @@ var TsGenerator = class extends BaseGenerator {
2705
2745
  return value.includes("\n") || value.includes("'") ? `\`${value}\`` : `'${value}'`;
2706
2746
  }
2707
2747
  };
2708
-
2709
2748
  //#endregion
2710
2749
  //#region src/generators/typescript-duckdb-generator.ts
2711
2750
  /**
@@ -2781,7 +2820,6 @@ var TsDuckDBGenerator = class extends TsGenerator {
2781
2820
  });
2782
2821
  }
2783
2822
  };
2784
-
2785
2823
  //#endregion
2786
2824
  //#region src/generators/index.ts
2787
2825
  /**
@@ -2799,7 +2837,7 @@ function getGenerator(generator) {
2799
2837
  case "typescript/libsql":
2800
2838
  case "typescript/turso": return new TsGenerator(`templates/${info.template}`);
2801
2839
  case "typescript/node-api": return new TsDuckDBGenerator(`templates/${info.template}`);
2802
- case "java/jdbc": return new JavaGenerator(`templates/${info.template}`);
2840
+ case "java/jdbc": return new JavaGenerator(`templates/${info.template}`, info.engine);
2803
2841
  case "java/arrow": return new JavaDuckDBArrowGenerator(`templates/${info.template}`);
2804
2842
  case "python/sqlite3": return new PythonGenerator(`templates/${info.template}`, "sqlite");
2805
2843
  case "python/duckdb": return new PythonGenerator(`templates/${info.template}`, "duckdb");
@@ -2811,7 +2849,6 @@ function getGenerator(generator) {
2811
2849
  throw new InvalidGeneratorError(fullGenerator, [...SHORT_GENERATOR_NAMES, ...GENERATOR_NAMES], similar.length > 0 ? similar[0] : void 0);
2812
2850
  }
2813
2851
  }
2814
-
2815
2852
  //#endregion
2816
2853
  //#region src/sqltool.ts
2817
2854
  const GENERATED_FILE_COMMENT = "This file is generated by SQG (https://sqg.dev). Do not edit manually.";
@@ -2833,22 +2870,22 @@ var Config = class Config {
2833
2870
  const result = configSchema.safeParse(configObj);
2834
2871
  if (!result.success) throw new Error(`Error parsing config for query ${name} in ${filePath}: \n${configStr}\n ${result.error}`);
2835
2872
  const columnMap = /* @__PURE__ */ new Map();
2836
- for (const [name$1, info] of Object.entries(result.data.result ?? {})) {
2873
+ for (const [name, info] of Object.entries(result.data.result ?? {})) {
2837
2874
  const parts = info.trim().split(" ").map((part) => part.trim());
2838
2875
  let type;
2839
2876
  let nullable = true;
2840
2877
  if (parts.length === 1) type = parts[0];
2841
2878
  else if (parts.length === 2) {
2842
2879
  type = parts[0];
2843
- if (parts[1].toLocaleLowerCase() !== "null") throw new Error(`Invalid config for column ${name$1} in ${filePath}: \n${configStr}\n ${info}`);
2880
+ if (parts[1].toLocaleLowerCase() !== "null") throw new Error(`Invalid config for column ${name} in ${filePath}: \n${configStr}\n ${info}`);
2844
2881
  nullable = true;
2845
2882
  } else if (parts.length === 3) {
2846
2883
  type = parts[0];
2847
- if (parts[1].toLocaleLowerCase() !== "not" || parts[2].toLocaleLowerCase() !== "null") throw new Error(`Invalid config for column ${name$1} in ${filePath}: \n${configStr}\n ${info}`);
2884
+ if (parts[1].toLocaleLowerCase() !== "not" || parts[2].toLocaleLowerCase() !== "null") throw new Error(`Invalid config for column ${name} in ${filePath}: \n${configStr}\n ${info}`);
2848
2885
  nullable = false;
2849
- } else throw new Error(`Invalid config for column ${name$1} in ${filePath}: \n${configStr}\n ${info}`);
2850
- columnMap.set(name$1, {
2851
- name: name$1,
2886
+ } else throw new Error(`Invalid config for column ${name} in ${filePath}: \n${configStr}\n ${info}`);
2887
+ columnMap.set(name, {
2888
+ name,
2852
2889
  type,
2853
2890
  nullable
2854
2891
  });
@@ -2960,8 +2997,9 @@ var TableHelper = class {
2960
2997
  return this.table.tableName;
2961
2998
  }
2962
2999
  get columns() {
2963
- if (this.table.includeColumns.length > 0) return this.table.columns.filter((c) => this.table.includeColumns.includes(c.name));
2964
- return this.table.columns;
3000
+ let cols = this.table.columns;
3001
+ if (this.table.includeColumns.length > 0) cols = cols.filter((c) => this.table.includeColumns.includes(c.name));
3002
+ return cols.filter((c) => !c.generated);
2965
3003
  }
2966
3004
  get skipGenerateFunction() {
2967
3005
  return this.table.skipGenerateFunction;
@@ -2975,6 +3013,9 @@ var TableHelper = class {
2975
3013
  get rowTypeName() {
2976
3014
  return this.generator.getClassName(`${this.table.id}_row`);
2977
3015
  }
3016
+ get constantName() {
3017
+ return this.table.id.toUpperCase();
3018
+ }
2978
3019
  get typeMapper() {
2979
3020
  return this.generator.typeMapper;
2980
3021
  }
@@ -3316,12 +3357,11 @@ async function processProject(projectPath, ui) {
3316
3357
  const projectDir = resolve(dirname(projectPath));
3317
3358
  return await processProjectFromConfig(parseProjectConfig(projectPath), projectDir, false, ui);
3318
3359
  }
3319
-
3320
3360
  //#endregion
3321
3361
  //#region src/mcp-server.ts
3322
3362
  const server = new Server({
3323
3363
  name: "sqg-mcp",
3324
- version: process.env.npm_package_version ?? "0.14.0"
3364
+ version: process.env.npm_package_version ?? "0.16.0"
3325
3365
  }, { capabilities: {
3326
3366
  tools: {},
3327
3367
  resources: {}
@@ -3647,7 +3687,6 @@ async function startMcpServer() {
3647
3687
  await server.connect(transport);
3648
3688
  console.error("SQG MCP server running on stdio");
3649
3689
  }
3650
-
3651
3690
  //#endregion
3652
3691
  //#region src/ui.ts
3653
3692
  /**
@@ -3785,10 +3824,9 @@ function formatMs(ms) {
3785
3824
  if (ms < 1e3) return `${Math.round(ms)}ms`;
3786
3825
  return `${(ms / 1e3).toFixed(1)}s`;
3787
3826
  }
3788
-
3789
3827
  //#endregion
3790
3828
  //#region src/sqg.ts
3791
- const version = process.env.npm_package_version ?? "0.14.0";
3829
+ const version = process.env.npm_package_version ?? "0.16.0";
3792
3830
  updateNotifier({ pkg: {
3793
3831
  name: "@sqg/sqg",
3794
3832
  version
@@ -3908,6 +3946,13 @@ program.command("init").description("Initialize a new SQG project with example c
3908
3946
  program.command("syntax").description("Show SQL annotation syntax reference").action(() => {
3909
3947
  console.log(SQL_SYNTAX_REFERENCE);
3910
3948
  });
3949
+ program.command("ide").description("Start the SQG IDE — interactive SQL development environment").argument("[project]", "Path to sqg.yaml project config").option("-p, --port <port>", "Server port", "3000").action(async (project, options) => {
3950
+ const { startIde } = await import("./ide-BieFvmwk.mjs");
3951
+ await startIde({
3952
+ project,
3953
+ port: parseInt(options.port, 10)
3954
+ });
3955
+ });
3911
3956
  program.command("mcp").description("Start MCP (Model Context Protocol) server for AI assistants").action(async () => {
3912
3957
  try {
3913
3958
  await startMcpServer();
@@ -3926,6 +3971,5 @@ try {
3926
3971
  consola.error(err);
3927
3972
  exit(1);
3928
3973
  }
3929
-
3930
3974
  //#endregion
3931
- export { };
3975
+ export {};
@@ -29,8 +29,14 @@ import java.util.function.Function;
29
29
  import java.util.stream.Stream;
30
30
  import java.util.stream.StreamSupport;
31
31
  {{#if tables.length}}
32
+ {{#if (isDuckDB)}}
32
33
  import org.duckdb.DuckDBAppender;
33
34
  import org.duckdb.DuckDBConnection;
35
+ {{else if (isPostgres)}}
36
+ // Requires dependency: de.bytefish:pgbulkinsert:9.0.0
37
+ import de.bytefish.pgbulkinsert.PgBulkInsert;
38
+ import java.io.IOException;
39
+ {{/if}}
34
40
  {{/if}}
35
41
 
36
42
  public class {{className}} {
@@ -61,20 +67,28 @@ public class {{className}} {
61
67
  }
62
68
  }
63
69
 
64
- private static LocalDateTime toLocalDateTime(java.sql.Timestamp ts) {
65
- return ts != null ? ts.toLocalDateTime() : null;
70
+ private static LocalDateTime toLocalDateTime(Object ts) {
71
+ if (ts == null) return null;
72
+ if (ts instanceof LocalDateTime ldt) return ldt;
73
+ return ((java.sql.Timestamp) ts).toLocalDateTime();
66
74
  }
67
75
 
68
- private static LocalDate toLocalDate(java.sql.Date d) {
69
- return d != null ? d.toLocalDate() : null;
76
+ private static LocalDate toLocalDate(Object d) {
77
+ if (d == null) return null;
78
+ if (d instanceof LocalDate ld) return ld;
79
+ return ((java.sql.Date) d).toLocalDate();
70
80
  }
71
81
 
72
- private static LocalTime toLocalTime(java.sql.Time t) {
73
- return t != null ? t.toLocalTime() : null;
82
+ private static LocalTime toLocalTime(Object t) {
83
+ if (t == null) return null;
84
+ if (t instanceof LocalTime lt) return lt;
85
+ return ((java.sql.Time) t).toLocalTime();
74
86
  }
75
87
 
76
- private static OffsetDateTime toOffsetDateTime(java.sql.Timestamp ts) {
77
- return ts != null ? ts.toInstant().atOffset(java.time.ZoneOffset.UTC) : null;
88
+ private static OffsetDateTime toOffsetDateTime(Object ts) {
89
+ if (ts == null) return null;
90
+ if (ts instanceof OffsetDateTime odt) return odt;
91
+ return ((java.sql.Timestamp) ts).toInstant().atOffset(java.time.ZoneOffset.UTC);
78
92
  }
79
93
 
80
94
  private static <K> List<K> arrayToList(
@@ -250,18 +264,30 @@ public class {{className}} {
250
264
  // ==================== Appenders ====================
251
265
  {{#each tables}}
252
266
 
267
+ {{#if (isDuckDB)}}
253
268
  /** Create an appender for bulk inserts into {{tableName}} */
254
269
  public {{className}} {{functionName}}() throws SQLException {
255
270
  return new {{className}}(((DuckDBConnection) connection).createAppender(DuckDBConnection.DEFAULT_SCHEMA, "{{tableName}}"));
256
271
  }
272
+ {{else if (isPostgres)}}
273
+ /** Bulk insert rows into {{tableName}} using PostgreSQL COPY BINARY (via PgBulkInsert). */
274
+ public void bulkInsert{{rowTypeName}}(Iterable<{{rowTypeName}}> rows) throws SQLException, IOException {
275
+ {{constantName}}_WRITER.saveAll(connection, "{{tableName}}", rows);
276
+ }
277
+ {{/if}}
257
278
  {{/each}}
258
279
  {{/if}}
259
280
 
260
281
 
261
282
  {{#each tables}}
262
283
  /** Row type for {{tableName}} appender */
263
- public record {{rowTypeName}}({{#each columns}}{{{appenderType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
284
+ {{#if (isPostgres)}}
285
+ public record {{rowTypeName}}({{#each columns}}{{{appenderType this}}} {{javaVarName name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
286
+ {{else}}
287
+ public record {{rowTypeName}}({{#each columns}}{{{appenderType this}}} {{javaVarName name}}{{#unless @last}}, {{/unless}}{{/each}}) {}
288
+ {{/if}}
264
289
 
290
+ {{#if (isDuckDB)}}
265
291
  /** Appender for bulk inserts into {{tableName}} */
266
292
  public static class {{className}} implements AutoCloseable {
267
293
  private final DuckDBAppender appender;
@@ -279,17 +305,17 @@ public static class {{className}} implements AutoCloseable {
279
305
  public {{className}} append({{rowTypeName}} row) throws SQLException {
280
306
  appender.beginRow();
281
307
  {{#each columns}}
282
- appender.append(row.{{name}}());
308
+ appender.append(row.{{javaVarName name}}());
283
309
  {{/each}}
284
310
  appender.endRow();
285
311
  return this;
286
312
  }
287
313
 
288
314
  /** Append a single row with individual values */
289
- public {{className}} append({{#each columns}}{{{appenderType this}}} {{name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
315
+ public {{className}} append({{#each columns}}{{{appenderType this}}} {{javaVarName name}}{{#unless @last}}, {{/unless}}{{/each}}) throws SQLException {
290
316
  appender.beginRow();
291
317
  {{#each columns}}
292
- appender.append({{name}});
318
+ appender.append({{javaVarName name}});
293
319
  {{/each}}
294
320
  appender.endRow();
295
321
  return this;
@@ -309,6 +335,16 @@ public static class {{className}} implements AutoCloseable {
309
335
  appender.close();
310
336
  }
311
337
  }
338
+ {{else if (isPostgres)}}
339
+ private static final PgBulkInsert.PgMapper<{{rowTypeName}}> {{constantName}}_MAPPER =
340
+ PgBulkInsert.PgMapper.forClass({{rowTypeName}}.class)
341
+ {{#each columns}}
342
+ .map("{{name}}", PgBulkInsert.PostgresTypes.{{{pgBulkType this}}}.{{{pgBulkAccessor this}}}({{../rowTypeName}}::{{javaVarName name}}))
343
+ {{/each}};
344
+
345
+ private static final PgBulkInsert.PgBulkWriter<{{rowTypeName}}> {{constantName}}_WRITER =
346
+ new PgBulkInsert.PgBulkWriter<>({{constantName}}_MAPPER);
347
+ {{/if}}
312
348
  {{/each}}
313
349
  }
314
350
 
@@ -204,19 +204,21 @@ class {{className}}:
204
204
  {{/if}}
205
205
  {{/unless}}
206
206
  {{/each}}
207
- {{#if (isDuckDB)}}
208
207
  {{#if tables.length}}
209
208
  # ==================== Appenders ====================
210
209
  {{#each tables}}
211
210
 
211
+ {{#if (isDuckDB)}}
212
212
  def {{functionName}}(self) -> {{className}}:
213
213
  return {{className}}(self._conn.cursor())
214
+ {{else if (isPostgres)}}
215
+ def {{functionName}}(self) -> {{className}}:
216
+ return {{className}}(self._conn)
217
+ {{/if}}
214
218
 
215
219
  {{/each}}
216
220
  {{/if}}
217
- {{/if}}
218
221
 
219
- {{#if (isDuckDB)}}
220
222
  {{#each tables}}
221
223
 
222
224
  @dataclass(frozen=True)
@@ -226,6 +228,7 @@ class {{rowTypeName}}:
226
228
  {{/each}}
227
229
 
228
230
 
231
+ {{#if (isDuckDB)}}
229
232
  class {{className}}:
230
233
  def __init__(self, cursor: duckdb.DuckDBPyConnection) -> None:
231
234
  self._cursor = cursor
@@ -244,10 +247,39 @@ class {{className}}:
244
247
 
245
248
  def close(self) -> None:
246
249
  self._cursor.close()
250
+ {{else if (isPostgres)}}
251
+ class {{className}}:
252
+ """COPY appender for high-performance bulk inserts into {{tableName}}."""
247
253
 
248
- {{/each}}
254
+ def __init__(self, conn: psycopg.Connection) -> None:
255
+ self._conn = conn
256
+ self._copy: psycopg.Copy | None = None
257
+
258
+ def __enter__(self) -> {{className}}:
259
+ cur = self._conn.cursor()
260
+ self._copy = cur.copy("COPY {{tableName}} ({{#each columns}}{{name}}{{#unless @last}}, {{/unless}}{{/each}}) FROM STDIN")
261
+ self._copy.__enter__()
262
+ return self
263
+
264
+ def __exit__(self, *args: object) -> None:
265
+ if self._copy is not None:
266
+ self._copy.__exit__(*args)
267
+ self._copy = None
268
+
269
+ def append(self, row: {{rowTypeName}}) -> {{className}}:
270
+ if self._copy is None:
271
+ raise RuntimeError("Use as context manager: with appender:")
272
+ self._copy.write_row(({{#each columns}}row.{{pyVarName name}}, {{/each}}))
273
+ return self
274
+
275
+ def append_many(self, rows: list[{{rowTypeName}}]) -> {{className}}:
276
+ for row in rows:
277
+ self.append(row)
278
+ return self
249
279
  {{/if}}
250
280
 
281
+ {{/each}}
282
+
251
283
  {{!-- ==================== Inline partials ==================== --}}
252
284
 
253
285
  {{#*inline "params"}}{{#if (isDuckDB)}}[{{#each parameterNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}]{{else}}({{#each parameterNames}}{{this}}, {{/each}}){{/if}}{{/inline}}