@sqg/sqg 0.13.0 → 0.15.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/README.md CHANGED
@@ -12,8 +12,8 @@ queries with it and then generate the code from the same file.
12
12
  ## Features
13
13
 
14
14
  - **Type-safe by design** - Generates fully-typed code with accurate column types inferred from your database
15
- - **Multiple database engines** - Supports SQLite, DuckDB, and (soon) PostgreSQL
16
- - **Multiple language targets** - Generate TypeScript or Java code from the same SQL files
15
+ - **Multiple database engines** - Supports SQLite, DuckDB, and PostgreSQL
16
+ - **Multiple language targets** - Generate TypeScript, Java, or Python code from the same SQL files
17
17
  - **Arrow API support** - Can generate Apache Arrow API bindings for DuckDB (Java)
18
18
  - **DBeaver compatible** - Works seamlessly with DBeaver for database development and testing
19
19
  - **Complex type support** - DuckDB: Handles structs, lists, and maps
@@ -129,11 +129,13 @@ console.log(user?.name);
129
129
 
130
130
  | Language | Database | API | Generator | Status |
131
131
  |----------|----------|-----|-----------|--------|
132
- | TypeScript | SQLite | better-sqlite3 | `typescript/better-sqlite3` | Tested |
132
+ | TypeScript | SQLite | better-sqlite3 | `typescript/sqlite` | Tested |
133
133
  | TypeScript | DuckDB | @duckdb/node-api | `typescript/duckdb` | Tested |
134
- | Java | Any (JDBC) | JDBC | `java/jdbc` | Tested |
135
- | Java | DuckDB | Apache Arrow | `java/duckdb-arrow` | Tested |
136
- | TypeScript | PostgreSQL | pg (node-postgres) | `typescript/pg` | under development |
134
+ | Java | SQLite/DuckDB/PostgreSQL | JDBC | `java/sqlite`, `java/duckdb`, `java/postgres` | Tested |
135
+ | Java | DuckDB | Apache Arrow | `java/duckdb/arrow` | Tested |
136
+ | Python | SQLite | sqlite3 | `python/sqlite` | Tested |
137
+ | Python | DuckDB | duckdb | `python/duckdb` | Tested |
138
+ | Python | PostgreSQL | psycopg3 | `python/postgres` | Tested |
137
139
 
138
140
  ## CLI Commands
139
141
 
package/dist/sqg.mjs CHANGED
@@ -13,7 +13,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
14
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
15
15
  import YAML from "yaml";
16
- import { camelCase, pascalCase } from "es-toolkit/string";
16
+ import { camelCase, pascalCase, snakeCase } from "es-toolkit/string";
17
17
  import Handlebars from "handlebars";
18
18
  import { z } from "zod";
19
19
  import { DuckDBEnumType, DuckDBInstance, DuckDBListType, DuckDBMapType, DuckDBStructType } from "@duckdb/node-api";
@@ -25,6 +25,7 @@ import types from "pg-types";
25
25
  import BetterSqlite3 from "better-sqlite3";
26
26
  import prettier from "prettier/standalone";
27
27
  import prettierPluginJava from "prettier-plugin-java";
28
+ import { execSync } from "node:child_process";
28
29
  import typescriptPlugin from "prettier/parser-typescript";
29
30
  import estree from "prettier/plugins/estree";
30
31
  import yoctoSpinner from "yocto-spinner";
@@ -115,6 +116,30 @@ const GENERATORS = {
115
116
  description: "Java with JDBC for PostgreSQL",
116
117
  extension: ".java",
117
118
  template: "java-jdbc.hbs"
119
+ },
120
+ "python/sqlite/sqlite3": {
121
+ language: "python",
122
+ engine: "sqlite",
123
+ driver: "sqlite3",
124
+ description: "Python with sqlite3 standard library",
125
+ extension: ".py",
126
+ template: "python.hbs"
127
+ },
128
+ "python/duckdb/duckdb": {
129
+ language: "python",
130
+ engine: "duckdb",
131
+ driver: "duckdb",
132
+ description: "Python with duckdb driver",
133
+ extension: ".py",
134
+ template: "python.hbs"
135
+ },
136
+ "python/postgres/psycopg": {
137
+ language: "python",
138
+ engine: "postgres",
139
+ driver: "psycopg",
140
+ description: "Python with psycopg3 driver",
141
+ extension: ".py",
142
+ template: "python.hbs"
118
143
  }
119
144
  };
120
145
  /** Default drivers for language/engine combinations */
@@ -123,7 +148,10 @@ const DEFAULT_DRIVERS = {
123
148
  "typescript/duckdb": "node-api",
124
149
  "java/sqlite": "jdbc",
125
150
  "java/duckdb": "jdbc",
126
- "java/postgres": "jdbc"
151
+ "java/postgres": "jdbc",
152
+ "python/sqlite": "sqlite3",
153
+ "python/duckdb": "duckdb",
154
+ "python/postgres": "psycopg"
127
155
  };
128
156
  /** List of all full generator names */
129
157
  const GENERATOR_NAMES = Object.keys(GENERATORS);
@@ -1033,6 +1061,11 @@ async function initializeDatabase(queries, execQueries, reporter) {
1033
1061
 
1034
1062
  //#endregion
1035
1063
  //#region src/db/duckdb.ts
1064
+ /** Cache of enum type names, keyed by stringified sorted values for lookup */
1065
+ let enumNameCache = /* @__PURE__ */ new Map();
1066
+ function enumCacheKey(values) {
1067
+ return values.join("\0");
1068
+ }
1036
1069
  function convertType(type) {
1037
1070
  if (type instanceof DuckDBListType) return new ListType(convertType(type.valueType));
1038
1071
  if (type instanceof DuckDBStructType) return new StructType(type.entryTypes.map((t, index) => ({
@@ -1049,7 +1082,10 @@ function convertType(type) {
1049
1082
  type: convertType(type.valueType),
1050
1083
  nullable: true
1051
1084
  });
1052
- if (type instanceof DuckDBEnumType) return new EnumType(type.values);
1085
+ if (type instanceof DuckDBEnumType) {
1086
+ const name = type.alias ?? enumNameCache.get(enumCacheKey(type.values));
1087
+ return new EnumType(type.values, name);
1088
+ }
1053
1089
  return type.toString();
1054
1090
  }
1055
1091
  const duckdb = new class {
@@ -1065,6 +1101,21 @@ const duckdb = new class {
1065
1101
  throw new SqlExecutionError(e.message, query.id, query.filename, query.rawQuery, e);
1066
1102
  }
1067
1103
  }, reporter);
1104
+ await this.loadEnumCache();
1105
+ }
1106
+ async loadEnumCache() {
1107
+ enumNameCache = /* @__PURE__ */ new Map();
1108
+ try {
1109
+ const result = await this.connection.runAndReadAll("SELECT type_name, labels FROM duckdb_types() WHERE logical_type = 'ENUM' AND internal = false");
1110
+ for (const row of result.getRows()) {
1111
+ const typeName = row[0];
1112
+ const labels = row[1];
1113
+ if (typeName && labels?.items) enumNameCache.set(enumCacheKey(labels.items), typeName);
1114
+ }
1115
+ consola.debug("DuckDB enum types:", Object.fromEntries(enumNameCache));
1116
+ } catch (e) {
1117
+ consola.debug("Failed to load DuckDB enum types:", e.message);
1118
+ }
1068
1119
  }
1069
1120
  async executeQueries(queries, reporter) {
1070
1121
  const connection = this.connection;
@@ -1099,7 +1150,11 @@ const duckdb = new class {
1099
1150
  query.parameterTypes = paramTypes;
1100
1151
  consola.debug("Parameter types:", Object.fromEntries(paramTypes));
1101
1152
  }
1102
- for (let i = 0; i < stmt.parameterCount; i++) stmt.bindValue(i + 1, statement.parameters[i].value);
1153
+ for (let i = 0; i < stmt.parameterCount; i++) {
1154
+ let value = statement.parameters[i].value;
1155
+ if (value.startsWith("'") && value.endsWith("'") || value.startsWith("\"") && value.endsWith("\"")) value = value.slice(1, -1);
1156
+ stmt.bindValue(i + 1, value);
1157
+ }
1103
1158
  if (query.isQuery) {
1104
1159
  const result = await stmt.stream();
1105
1160
  const columnNames = result.columnNames();
@@ -1149,6 +1204,7 @@ const duckdb = new class {
1149
1204
  }
1150
1205
  close() {
1151
1206
  this.connection.closeSync();
1207
+ enumNameCache = /* @__PURE__ */ new Map();
1152
1208
  }
1153
1209
  }();
1154
1210
 
@@ -1894,6 +1950,154 @@ var TypeScriptTypeMapper = class extends TypeMapper {
1894
1950
  return value;
1895
1951
  }
1896
1952
  };
1953
+ /**
1954
+ * Type mapper for generating Python types from SQL column types.
1955
+ * Maps SQL types to Python types (e.g., INTEGER -> int, VARCHAR -> str).
1956
+ * Generates frozen dataclasses for struct types and handles Python reserved keywords.
1957
+ */
1958
+ var PythonTypeMapper = class PythonTypeMapper extends TypeMapper {
1959
+ constructor() {
1960
+ super();
1961
+ }
1962
+ typeMap = {
1963
+ INTEGER: "int",
1964
+ INT: "int",
1965
+ INT2: "int",
1966
+ INT4: "int",
1967
+ TINYINT: "int",
1968
+ SMALLINT: "int",
1969
+ BIGINT: "int",
1970
+ INT8: "int",
1971
+ HUGEINT: "int",
1972
+ UHUGEINT: "int",
1973
+ UTINYINT: "int",
1974
+ USMALLINT: "int",
1975
+ UINTEGER: "int",
1976
+ UBIGINT: "int",
1977
+ SERIAL: "int",
1978
+ BIGSERIAL: "int",
1979
+ REAL: "float",
1980
+ DOUBLE: "float",
1981
+ FLOAT: "float",
1982
+ FLOAT4: "float",
1983
+ FLOAT8: "float",
1984
+ TEXT: "str",
1985
+ VARCHAR: "str",
1986
+ INTERVAL: "str",
1987
+ BIT: "str",
1988
+ UUID: "str",
1989
+ BOOLEAN: "bool",
1990
+ BOOL: "bool",
1991
+ BLOB: "bytes",
1992
+ BYTEA: "bytes",
1993
+ DATE: "datetime.date",
1994
+ TIMESTAMP: "datetime.datetime",
1995
+ DATETIME: "datetime.datetime",
1996
+ TIMESTAMPTZ: "datetime.datetime",
1997
+ "TIMESTAMP WITH TIME ZONE": "datetime.datetime",
1998
+ TIMESTAMP_S: "datetime.datetime",
1999
+ TIMESTAMP_MS: "datetime.datetime",
2000
+ TIMESTAMP_NS: "datetime.datetime",
2001
+ TIME: "datetime.time",
2002
+ "TIME WITH TIME ZONE": "datetime.time",
2003
+ NUMERIC: "Decimal",
2004
+ DECIMAL: "Decimal",
2005
+ BIGNUM: "Decimal",
2006
+ JSON: "Any",
2007
+ JSONB: "Any",
2008
+ NULL: "None",
2009
+ UNKNOWN: "Any"
2010
+ };
2011
+ static pythonReservedKeywords = new Set([
2012
+ "False",
2013
+ "None",
2014
+ "True",
2015
+ "and",
2016
+ "as",
2017
+ "assert",
2018
+ "async",
2019
+ "await",
2020
+ "break",
2021
+ "class",
2022
+ "continue",
2023
+ "def",
2024
+ "del",
2025
+ "elif",
2026
+ "else",
2027
+ "except",
2028
+ "finally",
2029
+ "for",
2030
+ "from",
2031
+ "global",
2032
+ "if",
2033
+ "import",
2034
+ "in",
2035
+ "is",
2036
+ "lambda",
2037
+ "nonlocal",
2038
+ "not",
2039
+ "or",
2040
+ "pass",
2041
+ "raise",
2042
+ "return",
2043
+ "try",
2044
+ "while",
2045
+ "with",
2046
+ "yield"
2047
+ ]);
2048
+ mapPrimitiveType(type, nullable) {
2049
+ const upperType = type.toString().toUpperCase();
2050
+ const mappedType = this.typeMap[upperType];
2051
+ if (mappedType) return nullable ? `${mappedType} | None` : mappedType;
2052
+ if (upperType.startsWith("DECIMAL(") || upperType.startsWith("NUMERIC(")) return nullable ? "Decimal | None" : "Decimal";
2053
+ if (upperType.startsWith("ENUM(")) return nullable ? "str | None" : "str";
2054
+ if (upperType.startsWith("UNION(")) return nullable ? "Any | None" : "Any";
2055
+ return nullable ? "Any | None" : "Any";
2056
+ }
2057
+ formatListType(elementType) {
2058
+ return `list[${elementType}]`;
2059
+ }
2060
+ formatStructTypeName(fieldName) {
2061
+ return `${pascalCase(fieldName)}Struct`;
2062
+ }
2063
+ formatMapTypeName(_fieldName) {
2064
+ return "dict";
2065
+ }
2066
+ generateStructDeclaration(column, path = "") {
2067
+ if (!(column.type instanceof StructType)) throw new Error(`Expected StructType ${column}`);
2068
+ const structName = this.formatStructTypeName(column.name);
2069
+ const newPath = `${path}${structName}.`;
2070
+ const children = column.type.fields.map((field) => {
2071
+ return this.getDeclarations({
2072
+ name: field.name,
2073
+ type: field.type,
2074
+ nullable: true
2075
+ }, newPath);
2076
+ }).filter(Boolean).join("\n\n");
2077
+ const fields = column.type.fields.map((field) => {
2078
+ const fieldType = this.getTypeName(field);
2079
+ return ` ${this.varName(field.name)}: ${fieldType}`;
2080
+ }).join("\n");
2081
+ let result = "";
2082
+ if (children) result += `${children}\n\n`;
2083
+ result += `@dataclass(frozen=True)\nclass ${structName}:\n${fields}`;
2084
+ return result;
2085
+ }
2086
+ varName(str) {
2087
+ const name = snakeCase(str);
2088
+ if (PythonTypeMapper.pythonReservedKeywords.has(name)) return `${name}_`;
2089
+ return name;
2090
+ }
2091
+ parseValue(column, value, _path) {
2092
+ if (column.type instanceof StructType) return `${this.formatStructTypeName(column.name)}(${column.type.fields.map((field) => {
2093
+ const pyName = this.varName(field.name);
2094
+ const dictAccess = `${value}["${field.name}"]`;
2095
+ if (field.type instanceof StructType) return `${pyName}=${this.parseValue(field, dictAccess, _path)}`;
2096
+ return `${pyName}=${dictAccess}`;
2097
+ }).join(", ")})`;
2098
+ return value;
2099
+ }
2100
+ };
1897
2101
 
1898
2102
  //#endregion
1899
2103
  //#region src/generators/base-generator.ts
@@ -2167,6 +2371,149 @@ var JavaDuckDBArrowGenerator = class extends BaseGenerator {
2167
2371
  }
2168
2372
  };
2169
2373
 
2374
+ //#endregion
2375
+ //#region src/generators/python-generator.ts
2376
+ var PythonGenerator = class extends BaseGenerator {
2377
+ engine;
2378
+ constructor(template, engine) {
2379
+ super(template, new PythonTypeMapper());
2380
+ this.engine = engine;
2381
+ }
2382
+ get isDuckDB() {
2383
+ return this.engine === "duckdb";
2384
+ }
2385
+ get isPostgres() {
2386
+ return this.engine === "postgres";
2387
+ }
2388
+ getFunctionName(id) {
2389
+ return snakeCase(id);
2390
+ }
2391
+ getFilename(sqlFileName) {
2392
+ return `${snakeCase(sqlFileName)}.py`;
2393
+ }
2394
+ getClassName(name) {
2395
+ return pascalCase(name);
2396
+ }
2397
+ rowType(query) {
2398
+ if (query.isPluck) return this.mapType({
2399
+ ...query.columns[0],
2400
+ name: query.id
2401
+ });
2402
+ return this.getClassName(`${query.id}_row`);
2403
+ }
2404
+ getStatement(q) {
2405
+ if (this.isDuckDB) return q.queryPositional;
2406
+ if (this.isPostgres) {
2407
+ const anon = q.queryAnonymous;
2408
+ return {
2409
+ ...anon,
2410
+ sql: anon.sql.replace(/\?/g, "%s")
2411
+ };
2412
+ }
2413
+ return q.queryAnonymous;
2414
+ }
2415
+ supportsAppenders(_engine) {
2416
+ return this.engine === "duckdb";
2417
+ }
2418
+ async beforeGenerate(_projectDir, _gen, _queries, _tables) {
2419
+ const pyMapper = this.typeMapper;
2420
+ Handlebars.registerHelper("isDuckDB", () => this.isDuckDB);
2421
+ Handlebars.registerHelper("isPostgres", () => this.isPostgres);
2422
+ Handlebars.registerHelper("connType", () => {
2423
+ if (this.isDuckDB) return "duckdb.DuckDBPyConnection";
2424
+ if (this.isPostgres) return "psycopg.Connection";
2425
+ return "sqlite3.Connection";
2426
+ });
2427
+ Handlebars.registerHelper("quote", (value) => {
2428
+ if (value.includes("\n") || value.includes("'") || value.includes("\"")) return `"""\\\n${value}"""`;
2429
+ return `"${value}"`;
2430
+ });
2431
+ Handlebars.registerHelper("pyType", (column) => {
2432
+ return this.getPyType(column);
2433
+ });
2434
+ Handlebars.registerHelper("declareTypes", (queryHelper) => {
2435
+ const query = queryHelper.query;
2436
+ if (queryHelper.isPluck) return "";
2437
+ const columns = queryHelper.columns;
2438
+ const rowTypeName = this.getClassName(`${query.id}_row`);
2439
+ const nestedDecls = [];
2440
+ for (const col of columns) {
2441
+ const decl = this.typeMapper.getDeclarations(col);
2442
+ if (decl) nestedDecls.push(decl);
2443
+ }
2444
+ const fields = columns.map((col) => {
2445
+ const pyType = this.getPyType(col);
2446
+ return ` ${pyMapper.varName(col.name)}: ${pyType}`;
2447
+ }).join("\n");
2448
+ let result = "";
2449
+ if (nestedDecls.length > 0) result += `${nestedDecls.join("\n\n")}\n\n`;
2450
+ result += `@dataclass(frozen=True)\nclass ${rowTypeName}:\n${fields}`;
2451
+ return new Handlebars.SafeString(result);
2452
+ });
2453
+ Handlebars.registerHelper("constructRow", (queryHelper) => {
2454
+ const query = queryHelper.query;
2455
+ const columns = queryHelper.columns;
2456
+ return `${this.getClassName(`${query.id}_row`)}(${columns.map((col, i) => {
2457
+ const pyName = pyMapper.varName(col.name);
2458
+ const value = `row[${i}]`;
2459
+ return `${pyName}=${this.typeMapper.parseValue(col, value, "")}`;
2460
+ }).join(", ")})`;
2461
+ });
2462
+ Handlebars.registerHelper("pyTypeOrNone", (column) => {
2463
+ const t = this.getPyType(column);
2464
+ if (t.endsWith(" | None")) return t;
2465
+ return `${t} | None`;
2466
+ });
2467
+ Handlebars.registerHelper("pyVarName", (name) => {
2468
+ return pyMapper.varName(name);
2469
+ });
2470
+ Handlebars.registerHelper("needsDatetime", (queries) => this.queryColumnsContainType(queries, "datetime."));
2471
+ Handlebars.registerHelper("needsDecimal", (queries) => this.queryColumnsContainType(queries, "Decimal"));
2472
+ }
2473
+ queryColumnsContainType(queries, substring) {
2474
+ for (const qh of queries) {
2475
+ if (!qh.isQuery || qh.skipGenerateFunction) continue;
2476
+ for (const col of qh.columns) if (this.getPyType(col).includes(substring)) return true;
2477
+ }
2478
+ return false;
2479
+ }
2480
+ getPyType(column) {
2481
+ const t = column.type;
2482
+ if (t instanceof ListType) {
2483
+ const base = `list[${this.getPyType({
2484
+ name: column.name,
2485
+ type: t.baseType,
2486
+ nullable: true
2487
+ })}]`;
2488
+ return column.nullable ? `${base} | None` : base;
2489
+ }
2490
+ if (t instanceof MapType) {
2491
+ const base = `dict[${this.getPyType({
2492
+ name: "key",
2493
+ type: t.keyType.type,
2494
+ nullable: false
2495
+ })}, ${this.getPyType({
2496
+ name: "value",
2497
+ type: t.valueType.type,
2498
+ nullable: true
2499
+ })}]`;
2500
+ return column.nullable ? `${base} | None` : base;
2501
+ }
2502
+ if (t instanceof StructType) {
2503
+ const structName = `${pascalCase(column.name)}Struct`;
2504
+ return column.nullable ? `${structName} | None` : structName;
2505
+ }
2506
+ return this.typeMapper.getTypeName(column);
2507
+ }
2508
+ async afterGenerate(outputPath) {
2509
+ try {
2510
+ execSync(`ruff format "${outputPath}"`, { stdio: "ignore" });
2511
+ } catch {
2512
+ consola.debug("ruff not available, skipping format for:", outputPath);
2513
+ }
2514
+ }
2515
+ };
2516
+
2170
2517
  //#endregion
2171
2518
  //#region src/generators/typescript-generator.ts
2172
2519
  /** Resolve a ColumnType to its DuckDB type constant name (e.g. "VARCHAR", "INTEGER") for use in generated code. */
@@ -2454,6 +2801,9 @@ function getGenerator(generator) {
2454
2801
  case "typescript/node-api": return new TsDuckDBGenerator(`templates/${info.template}`);
2455
2802
  case "java/jdbc": return new JavaGenerator(`templates/${info.template}`);
2456
2803
  case "java/arrow": return new JavaDuckDBArrowGenerator(`templates/${info.template}`);
2804
+ case "python/sqlite3": return new PythonGenerator(`templates/${info.template}`, "sqlite");
2805
+ case "python/duckdb": return new PythonGenerator(`templates/${info.template}`, "duckdb");
2806
+ case "python/psycopg": return new PythonGenerator(`templates/${info.template}`, "postgres");
2457
2807
  default: throw new Error(`No generator class for ${key}`);
2458
2808
  }
2459
2809
  } catch {
@@ -2971,7 +3321,7 @@ async function processProject(projectPath, ui) {
2971
3321
  //#region src/mcp-server.ts
2972
3322
  const server = new Server({
2973
3323
  name: "sqg-mcp",
2974
- version: process.env.npm_package_version ?? "0.13.0"
3324
+ version: process.env.npm_package_version ?? "0.15.0"
2975
3325
  }, { capabilities: {
2976
3326
  tools: {},
2977
3327
  resources: {}
@@ -3438,7 +3788,7 @@ function formatMs(ms) {
3438
3788
 
3439
3789
  //#endregion
3440
3790
  //#region src/sqg.ts
3441
- const version = process.env.npm_package_version ?? "0.13.0";
3791
+ const version = process.env.npm_package_version ?? "0.15.0";
3442
3792
  updateNotifier({ pkg: {
3443
3793
  name: "@sqg/sqg",
3444
3794
  version
@@ -0,0 +1,256 @@
1
+ # {{generatedComment}}
2
+ from __future__ import annotations
3
+
4
+ {{#if (isDuckDB)}}
5
+ import duckdb
6
+ {{else if (isPostgres)}}
7
+ import psycopg
8
+ {{else}}
9
+ import sqlite3
10
+ {{/if}}
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+ {{#if (needsDatetime queries)}}
14
+ import datetime
15
+ {{/if}}
16
+ {{#if (needsDecimal queries)}}
17
+ from decimal import Decimal
18
+ {{/if}}
19
+
20
+ {{#each queries}}
21
+ {{#unless skipGenerateFunction}}
22
+ {{#if isQuery}}
23
+ {{#unless isPluck}}
24
+ {{{declareTypes this}}}
25
+
26
+ {{/unless}}
27
+ {{/if}}
28
+ {{/unless}}
29
+ {{/each}}
30
+
31
+ class {{className}}:
32
+ def __init__(self, conn: {{{connType}}}) -> None:
33
+ self._conn = conn
34
+
35
+ @staticmethod
36
+ def get_migrations() -> list[str]:
37
+ return [
38
+ {{#each migrations}}
39
+ {{{quote sqlQuery}}},
40
+ {{/each}}
41
+ ]
42
+
43
+ {{#if config.migrations}}
44
+ @staticmethod
45
+ def apply_migrations(conn: {{{connType}}}, project_name: str = "{{projectName}}") -> None:
46
+ {{#if (isDuckDB)}}
47
+ conn.execute("""
48
+ CREATE TABLE IF NOT EXISTS _sqg_migrations (
49
+ project TEXT NOT NULL,
50
+ migration_id TEXT NOT NULL,
51
+ applied_at TIMESTAMP NOT NULL DEFAULT now(),
52
+ PRIMARY KEY (project, migration_id)
53
+ )""")
54
+ conn.begin()
55
+ try:
56
+ applied = set(
57
+ row[0]
58
+ for row in conn.execute(
59
+ "SELECT migration_id FROM _sqg_migrations WHERE project = $1",
60
+ [project_name],
61
+ ).fetchall()
62
+ )
63
+ migrations: list[tuple[str, str]] = [
64
+ {{#each migrations}}
65
+ ("{{{id}}}", {{{quote sqlQuery}}}),
66
+ {{/each}}
67
+ ]
68
+ for mid, sql in migrations:
69
+ if mid not in applied:
70
+ conn.execute(sql)
71
+ conn.execute(
72
+ "INSERT INTO _sqg_migrations (project, migration_id) VALUES ($1, $2)",
73
+ [project_name, mid],
74
+ )
75
+ conn.commit()
76
+ except Exception:
77
+ conn.rollback()
78
+ raise
79
+ {{else if (isPostgres)}}
80
+ with conn.cursor() as cur:
81
+ cur.execute("""
82
+ CREATE TABLE IF NOT EXISTS _sqg_migrations (
83
+ project TEXT NOT NULL,
84
+ migration_id TEXT NOT NULL,
85
+ applied_at TIMESTAMP NOT NULL DEFAULT now(),
86
+ PRIMARY KEY (project, migration_id)
87
+ )""")
88
+ conn.commit()
89
+ cur.execute(
90
+ "SELECT migration_id FROM _sqg_migrations WHERE project = %s",
91
+ (project_name,),
92
+ )
93
+ applied = set(row[0] for row in cur.fetchall())
94
+ migrations: list[tuple[str, str]] = [
95
+ {{#each migrations}}
96
+ ("{{{id}}}", {{{quote sqlQuery}}}),
97
+ {{/each}}
98
+ ]
99
+ for mid, sql in migrations:
100
+ if mid not in applied:
101
+ cur.execute(sql)
102
+ cur.execute(
103
+ "INSERT INTO _sqg_migrations (project, migration_id) VALUES (%s, %s)",
104
+ (project_name, mid),
105
+ )
106
+ conn.commit()
107
+ {{else}}
108
+ conn.execute("""
109
+ CREATE TABLE IF NOT EXISTS _sqg_migrations (
110
+ project TEXT NOT NULL,
111
+ migration_id TEXT NOT NULL,
112
+ applied_at TEXT NOT NULL DEFAULT (datetime('now')),
113
+ PRIMARY KEY (project, migration_id)
114
+ )""")
115
+ applied = set(
116
+ row[0]
117
+ for row in conn.execute(
118
+ "SELECT migration_id FROM _sqg_migrations WHERE project = ?",
119
+ (project_name,),
120
+ ).fetchall()
121
+ )
122
+ migrations: list[tuple[str, str]] = [
123
+ {{#each migrations}}
124
+ ("{{{id}}}", {{{quote sqlQuery}}}),
125
+ {{/each}}
126
+ ]
127
+ for mid, sql in migrations:
128
+ if mid not in applied:
129
+ conn.executescript(sql)
130
+ conn.execute(
131
+ "INSERT INTO _sqg_migrations (project, migration_id) VALUES (?, ?)",
132
+ (project_name, mid),
133
+ )
134
+ conn.commit()
135
+ {{/if}}
136
+ {{/if}}
137
+
138
+ {{#each queries}}
139
+ {{#unless skipGenerateFunction}}
140
+ {{#if isQuery}}
141
+ {{#if isPluck}}
142
+ {{#if isOne}}
143
+ def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> {{{pyTypeOrNone (lookup columns 0)}}}:
144
+ row = self._conn.execute(
145
+ {{{quote sqlQuery}}}, {{> params}}
146
+ ).fetchone()
147
+ if row is None:
148
+ return None
149
+ return row[0]
150
+
151
+ def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> tuple[Any, ...] | None:
152
+ return self._conn.execute(
153
+ {{{quote sqlQuery}}}, {{> params}}
154
+ ).fetchone()
155
+
156
+ {{else}}
157
+ def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[{{{pyType (lookup columns 0)}}}]:
158
+ rows = self._conn.execute(
159
+ {{{quote sqlQuery}}}, {{> params}}
160
+ ).fetchall()
161
+ return [r[0] for r in rows]
162
+
163
+ def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[tuple[Any, ...]]:
164
+ return self._conn.execute(
165
+ {{{quote sqlQuery}}}, {{> params}}
166
+ ).fetchall()
167
+
168
+ {{/if}}
169
+ {{else}}
170
+ {{#if isOne}}
171
+ def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> {{{rowType}}} | None:
172
+ row = self._conn.execute(
173
+ {{{quote sqlQuery}}}, {{> params}}
174
+ ).fetchone()
175
+ if row is None:
176
+ return None
177
+ return {{{constructRow this}}}
178
+
179
+ def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> tuple[Any, ...] | None:
180
+ return self._conn.execute(
181
+ {{{quote sqlQuery}}}, {{> params}}
182
+ ).fetchone()
183
+
184
+ {{else}}
185
+ def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[{{{rowType}}}]:
186
+ rows = self._conn.execute(
187
+ {{{quote sqlQuery}}}, {{> params}}
188
+ ).fetchall()
189
+ return [{{{constructRow this}}} for row in rows]
190
+
191
+ def {{functionName}}_raw(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> list[tuple[Any, ...]]:
192
+ return self._conn.execute(
193
+ {{{quote sqlQuery}}}, {{> params}}
194
+ ).fetchall()
195
+
196
+ {{/if}}
197
+ {{/if}}
198
+ {{else}}
199
+ def {{functionName}}(self{{#each variables}}, {{name}}: {{{type}}}{{/each}}) -> None:
200
+ self._conn.execute(
201
+ {{{quote sqlQuery}}}, {{> params}}
202
+ )
203
+
204
+ {{/if}}
205
+ {{/unless}}
206
+ {{/each}}
207
+ {{#if (isDuckDB)}}
208
+ {{#if tables.length}}
209
+ # ==================== Appenders ====================
210
+ {{#each tables}}
211
+
212
+ def {{functionName}}(self) -> {{className}}:
213
+ return {{className}}(self._conn.cursor())
214
+
215
+ {{/each}}
216
+ {{/if}}
217
+ {{/if}}
218
+
219
+ {{#if (isDuckDB)}}
220
+ {{#each tables}}
221
+
222
+ @dataclass(frozen=True)
223
+ class {{rowTypeName}}:
224
+ {{#each columns}}
225
+ {{pyVarName name}}: {{{pyType this}}}
226
+ {{/each}}
227
+
228
+
229
+ class {{className}}:
230
+ def __init__(self, cursor: duckdb.DuckDBPyConnection) -> None:
231
+ self._cursor = cursor
232
+
233
+ def append(self, row: {{rowTypeName}}) -> {{className}}:
234
+ self._cursor.execute(
235
+ "INSERT INTO {{tableName}} VALUES ({{> placeholders}})",
236
+ [{{#each columns}}row.{{pyVarName name}}{{#unless @last}}, {{/unless}}{{/each}}],
237
+ )
238
+ return self
239
+
240
+ def append_many(self, rows: list[{{rowTypeName}}]) -> {{className}}:
241
+ for row in rows:
242
+ self.append(row)
243
+ return self
244
+
245
+ def close(self) -> None:
246
+ self._cursor.close()
247
+
248
+ {{/each}}
249
+ {{/if}}
250
+
251
+ {{!-- ==================== Inline partials ==================== --}}
252
+
253
+ {{#*inline "params"}}{{#if (isDuckDB)}}[{{#each parameterNames}}{{this}}{{#unless @last}}, {{/unless}}{{/each}}]{{else}}({{#each parameterNames}}{{this}}, {{/each}}){{/if}}{{/inline}}
254
+
255
+
256
+ {{#*inline "placeholders"}}{{#each columns}}${{plusOne @index}}{{#unless @last}}, {{/unless}}{{/each}}{{/inline}}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@sqg/sqg",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "SQG - SQL Query Generator - Type-safe code generation from SQL (https://sqg.dev)",
5
5
  "type": "module",
6
6
  "bin": {
7
- "sqg": "./dist/sqg.mjs"
7
+ "sqg": "dist/sqg.mjs"
8
8
  },
9
9
  "files": [
10
10
  "dist"
@@ -20,6 +20,19 @@
20
20
  "bugs": {
21
21
  "url": "https://github.com/sqg-dev/sqg/issues"
22
22
  },
23
+ "scripts": {
24
+ "prepublishOnly": "pnpm run build",
25
+ "build": "tsc --noEmit && tsdown && cp -r src/templates dist",
26
+ "lezer-gen": "lezer-generator src/parser/sql.grammar -o src/parser/sql-parser.ts",
27
+ "test-grammar": "tsx src/test-grammar.ts",
28
+ "check": "biome check --write src/",
29
+ "check:errors": "pnpm biome check --write --diagnostic-level=error src/",
30
+ "test": "vitest",
31
+ "coverage": "vitest run --coverage",
32
+ "test:ui": "vitest --ui",
33
+ "test:run": "vitest run",
34
+ "sqg": "tsx src/sqg.ts"
35
+ },
23
36
  "keywords": [
24
37
  "sql",
25
38
  "codegen",
@@ -37,56 +50,45 @@
37
50
  ],
38
51
  "author": "Uwe Maurer",
39
52
  "license": "Apache-2.0",
53
+ "packageManager": "pnpm@10.26.0",
40
54
  "dependencies": {
41
- "@biomejs/biome": "2.3.3",
55
+ "@biomejs/biome": "catalog:",
42
56
  "@clack/prompts": "^1.1.0",
43
- "@duckdb/node-api": "1.4.3-r.2",
44
- "@duckdb/node-bindings-linux-x64": "1.4.3-r.2",
45
- "@lezer-unofficial/printer": "^1.0.1",
46
- "@lezer/common": "^1.5.0",
47
- "@lezer/generator": "^1.8.0",
48
- "@lezer/lr": "^1.4.6",
49
- "@modelcontextprotocol/sdk": "^1.0.4",
57
+ "@duckdb/node-api": "catalog:",
58
+ "@duckdb/node-bindings-linux-x64": "catalog:",
59
+ "@lezer-unofficial/printer": "catalog:",
60
+ "@lezer/common": "catalog:",
61
+ "@lezer/generator": "catalog:",
62
+ "@lezer/lr": "catalog:",
63
+ "@modelcontextprotocol/sdk": "catalog:",
50
64
  "@testcontainers/postgresql": "^10.21.0",
51
- "better-sqlite3": "^12.5.0",
52
- "commander": "^14.0.2",
53
- "consola": "^3.4.2",
54
- "dotenv": "^17.2.3",
55
- "es-toolkit": "^1.43.0",
56
- "handlebars": "^4.7.8",
57
- "pg": "^8.16.3",
58
- "pg-types": "^4.1.0",
65
+ "better-sqlite3": "catalog:",
66
+ "commander": "catalog:",
67
+ "consola": "catalog:",
68
+ "dotenv": "catalog:",
69
+ "es-toolkit": "catalog:",
70
+ "handlebars": "catalog:",
71
+ "pg": "catalog:",
72
+ "pg-types": "catalog:",
59
73
  "picocolors": "^1.1.1",
60
- "prettier": "^3.7.4",
61
- "prettier-plugin-java": "^2.7.7",
74
+ "prettier": "catalog:",
75
+ "prettier-plugin-java": "catalog:",
62
76
  "update-notifier": "^7.3.1",
63
- "yaml": "^2.8.2",
77
+ "yaml": "catalog:",
64
78
  "yocto-spinner": "^1.1.0",
65
- "zod": "^4.3.5"
79
+ "zod": "catalog:"
66
80
  },
67
81
  "devDependencies": {
68
82
  "@libsql/client": "^0.17.0",
69
83
  "@tursodatabase/database": "^0.4.3",
70
- "@types/better-sqlite3": "^7.6.13",
71
- "@types/node": "^25.0.3",
72
- "@types/pg": "^8.16.0",
84
+ "@types/better-sqlite3": "catalog:",
85
+ "@types/node": "catalog:",
86
+ "@types/pg": "catalog:",
73
87
  "@types/update-notifier": "^6.0.8",
74
- "@vitest/ui": "^4.0.16",
75
- "tsdown": "0.18.0",
76
- "tsx": "^4.21.0",
77
- "typescript": "^5.9.3",
78
- "vitest": "^4.0.16"
79
- },
80
- "scripts": {
81
- "build": "tsc --noEmit && tsdown && cp -r src/templates dist",
82
- "lezer-gen": "lezer-generator src/parser/sql.grammar -o src/parser/sql-parser.ts",
83
- "test-grammar": "tsx src/test-grammar.ts",
84
- "check": "biome check --write src/",
85
- "check:errors": "pnpm biome check --write --diagnostic-level=error src/",
86
- "test": "vitest",
87
- "coverage": "vitest run --coverage",
88
- "test:ui": "vitest --ui",
89
- "test:run": "vitest run",
90
- "sqg": "tsx src/sqg.ts"
88
+ "@vitest/ui": "catalog:",
89
+ "tsdown": "catalog:",
90
+ "tsx": "catalog:",
91
+ "typescript": "catalog:",
92
+ "vitest": "catalog:"
91
93
  }
92
- }
94
+ }