From 9b3f5604fa53b8dd89b5167d67a40eeba7271cc9 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 13 Apr 2026 21:29:13 -0700 Subject: [PATCH 1/6] postgres: libpq c bridge (step 1/10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds c_bridges/pg-bridge.c with thin wrappers around libpq for connect, exec, parameterized exec, and result introspection. build-vendor.sh detects libpq headers (keg-only brew install on darwin, system paths on linux) and gracefully skips when absent. no codegen or ts wiring yet — this commit is just the foundation for chadscript/postgres. --- c_bridges/pg-bridge.c | 92 +++++++++++++++++++++++++++++++++++++++++ scripts/build-vendor.sh | 30 ++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 c_bridges/pg-bridge.c diff --git a/c_bridges/pg-bridge.c b/c_bridges/pg-bridge.c new file mode 100644 index 00000000..d61766cd --- /dev/null +++ b/c_bridges/pg-bridge.c @@ -0,0 +1,92 @@ +#include +#include +#include + +void *cs_pg_connect(const char *conninfo) { + if (!conninfo) return NULL; + PGconn *conn = PQconnectdb(conninfo); + return (void *)conn; +} + +int cs_pg_status(void *conn) { + if (!conn) return CONNECTION_BAD; + return PQstatus((PGconn *)conn); +} + +const char *cs_pg_error_message(void *conn) { + if (!conn) return "null connection"; + return PQerrorMessage((PGconn *)conn); +} + +void cs_pg_finish(void *conn) { + if (!conn) return; + PQfinish((PGconn *)conn); +} + +void *cs_pg_exec(void *conn, const char *sql) { + if (!conn || !sql) return NULL; + return (void *)PQexec((PGconn *)conn, sql); +} + +void *cs_pg_exec_params(void *conn, const char *sql, int nparams, const char **values) { + if (!conn || !sql) return NULL; + return (void *)PQexecParams((PGconn *)conn, sql, nparams, NULL, values, NULL, NULL, 0); +} + +int cs_pg_result_status(void *res) { + if (!res) return PGRES_FATAL_ERROR; + return PQresultStatus((PGresult *)res); +} + +const char *cs_pg_result_error_message(void *res) { + if (!res) return "null result"; + return PQresultErrorMessage((PGresult *)res); +} + +int cs_pg_nrows(void *res) { + if (!res) return 0; + return PQntuples((PGresult *)res); +} + +int cs_pg_ncols(void *res) { + if (!res) return 0; + return PQnfields((PGresult *)res); +} + +const char *cs_pg_fname(void *res, int col) { + if (!res) return ""; + const char *n = PQfname((PGresult *)res, col); + return n ? n : ""; +} + +unsigned int cs_pg_ftype(void *res, int col) { + if (!res) return 0; + return (unsigned int)PQftype((PGresult *)res, col); +} + +const char *cs_pg_getvalue(void *res, int row, int col) { + if (!res) return ""; + return PQgetvalue((PGresult *)res, row, col); +} + +int cs_pg_getisnull(void *res, int row, int col) { + if (!res) return 1; + return PQgetisnull((PGresult *)res, row, col); +} + +const char *cs_pg_cmdtuples(void *res) { + if (!res) return "0"; + const char *c = PQcmdTuples((PGresult *)res); + return (c && *c) ? c : "0"; +} + +void cs_pg_clear(void *res) { + if (!res) return; + PQclear((PGresult *)res); +} + +int cs_pg_result_ok(void *res) { + if (!res) return 0; + ExecStatusType s = PQresultStatus((PGresult *)res); + return (s == PGRES_COMMAND_OK || s == PGRES_TUPLES_OK) ? 1 : 0; +} diff --git a/scripts/build-vendor.sh b/scripts/build-vendor.sh index 66b6799d..208a283e 100755 --- a/scripts/build-vendor.sh +++ b/scripts/build-vendor.sh @@ -259,6 +259,36 @@ else echo "==> curl-bridge already built, skipping" fi +# --- pg-bridge (libpq Postgres client) --- +PG_BRIDGE_SRC="$C_BRIDGES_DIR/pg-bridge.c" +PG_BRIDGE_OBJ="$C_BRIDGES_DIR/pg-bridge.o" +if [ ! -f "$PG_BRIDGE_OBJ" ] || [ "$PG_BRIDGE_SRC" -nt "$PG_BRIDGE_OBJ" ]; then + PG_CFLAGS="" + PG_FOUND=0 + if [ "$(uname)" = "Darwin" ]; then + BREW_PREFIX=$(brew --prefix 2>/dev/null || echo "/opt/homebrew") + LIBPQ_PREFIX=$(brew --prefix libpq 2>/dev/null || echo "$BREW_PREFIX/opt/libpq") + if [ -f "$LIBPQ_PREFIX/include/libpq-fe.h" ]; then + PG_CFLAGS="-I$LIBPQ_PREFIX/include" + PG_FOUND=1 + fi + fi + if [ "$PG_FOUND" = "0" ]; then + if echo '#include ' | cc -xc -fsyntax-only - 2>/dev/null; then + PG_FOUND=1 + fi + fi + if [ "$PG_FOUND" = "1" ]; then + echo "==> Building pg-bridge..." + cc -c -O2 -fPIC $PG_CFLAGS "$PG_BRIDGE_SRC" -o "$PG_BRIDGE_OBJ" + echo " -> $PG_BRIDGE_OBJ" + else + echo "==> pg-bridge skipped (no libpq headers found)" + fi +else + echo "==> pg-bridge already built, skipping" +fi + # --- compress-bridge (zlib + zstd) --- COMPRESS_BRIDGE_SRC="$C_BRIDGES_DIR/compress-bridge.c" COMPRESS_BRIDGE_OBJ="$C_BRIDGES_DIR/compress-bridge.o" From 5ff03283489d46dc9fde33a25d11179e500c259c Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 13 Apr 2026 21:40:24 -0700 Subject: [PATCH 2/6] postgres: lib/postgres.ts with Client, extern binding, link wiring (step 2/10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds chadscript/postgres module. lib/postgres.ts uses declare function bindings to cs_pg_* externs — compiler auto-detects the cs_pg_ prefix in llvm-generator.ts, flips a new usesPostgres flag, and the link step picks up -lpq plus homebrew keg-only libpq lib path. pg-bridge.o is included conditionally. Client class exposes connect()/end() — query comes in step 3. verified end-to-end: compiled a fixture that connects to a local podman postgres and cleanly disconnects. the cs_pg_connect extern is called directly from the TS Client.connect method, no codegen special-casing required. pattern matches cs_llvm_*/cs_lld_*. workaround: added trailing return in parser-native transformExpression for the known checkMissingReturns false-positive (memory/native-missing-returns-false-positive.md) — the usesPostgres field addition shifted AST layouts and tripped the bug. --- lib/postgres.ts | 37 +++++++++++++++++++ .../infrastructure/generator-context.ts | 9 +++++ src/codegen/llvm-generator.ts | 9 +++++ src/compiler.ts | 9 ++++- src/native-compiler-lib.ts | 18 +++++++++ src/parser-native/transformer.ts | 1 + tests/fixtures/stdlib/postgres-connect.ts | 7 ++++ 7 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 lib/postgres.ts create mode 100644 tests/fixtures/stdlib/postgres-connect.ts diff --git a/lib/postgres.ts b/lib/postgres.ts new file mode 100644 index 00000000..8d572269 --- /dev/null +++ b/lib/postgres.ts @@ -0,0 +1,37 @@ +declare function cs_pg_connect(conninfo: string): string; +declare function cs_pg_status(conn: string): number; +declare function cs_pg_error_message(conn: string): string; +declare function cs_pg_finish(conn: string): void; + +const CONNECTION_OK: number = 0; + +export class Client { + private _conninfo: string; + private _conn: string; + private _connected: boolean; + + constructor(conninfo: string) { + this._conninfo = conninfo; + this._conn = ""; + this._connected = false; + } + + connect(): void { + this._conn = cs_pg_connect(this._conninfo); + const status = cs_pg_status(this._conn); + if (status !== CONNECTION_OK) { + const msg = cs_pg_error_message(this._conn); + cs_pg_finish(this._conn); + this._connected = false; + throw new Error("postgres connect failed: " + msg); + } + this._connected = true; + } + + end(): void { + if (this._connected) { + cs_pg_finish(this._conn); + this._connected = false; + } + } +} diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index 6c8054a1..1b5adc7d 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -800,6 +800,8 @@ export interface IGeneratorContext { getUsesTimers(): boolean; setUsesTreeSitter(value: boolean): void; setUsesSqlite(value: boolean): void; + setUsesPostgres(value: boolean): void; + getUsesPostgres(): boolean; setUsesCurl(value: boolean): void; setUsesOs(value: boolean): void; setUsesUvHrtime(value: boolean): void; @@ -1034,6 +1036,7 @@ export class MockGeneratorContext implements IGeneratorContext { public usesPromises: number = 0; public usesTimers: number = 0; public usesSqlite: number = 0; + public usesPostgres: number = 0; public usesCurl: number = 0; public usesUvHrtime: number = 0; public usesCrypto: number = 0; @@ -1238,6 +1241,12 @@ export class MockGeneratorContext implements IGeneratorContext { setUsesSqlite(value: boolean): void { this.usesSqlite = value ? 1 : 0; } + setUsesPostgres(value: boolean): void { + this.usesPostgres = value ? 1 : 0; + } + getUsesPostgres(): boolean { + return this.usesPostgres !== 0; + } setUsesCurl(value: boolean): void { this.usesCurl = value ? 1 : 0; } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index d0072241..c9f5a23d 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -265,6 +265,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { public usesStringBuilder: number = 0; public usesLLVM: number = 0; public usesLLD: number = 0; + public usesPostgres: number = 0; public usesCompression: number = 0; public usesYaml: number = 0; private stringBuilderSlen: Map = new Map(); @@ -992,6 +993,12 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { public getUsesLLD(): boolean { return this.usesLLD !== 0; } + public setUsesPostgres(value: boolean): void { + this.usesPostgres = value ? 1 : 0; + } + public getUsesPostgres(): boolean { + return this.usesPostgres !== 0; + } public setUsesCompression(value: boolean): void { this.usesCompression = value ? 1 : 0; } @@ -1408,6 +1415,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { this.usesAsyncFs = 0; this.usesGC = 0; this.usesMathRandom = 0; + this.usesPostgres = 0; this.ast = ast; @@ -3379,6 +3387,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { this.declaredExternFunctions.push(func.name); if (func.name.startsWith("cs_llvm_")) this.usesLLVM = 1; if (func.name.startsWith("cs_lld_")) this.usesLLD = 1; + if (func.name.startsWith("cs_pg_")) this.usesPostgres = 1; return `declare ${retType} @${func.name}(${paramLlvmTypes.join(", ")})\n`; } diff --git a/src/compiler.ts b/src/compiler.ts index 374df79c..c6e19ce1 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -406,6 +406,9 @@ export function compile( if (generator.usesSqlite) { linkLibs += " -lsqlite3"; } + if (generator.usesPostgres) { + linkLibs += " -lpq"; + } if (generator.usesHttpServer) { linkLibs += ` -lz -lzstd`; } @@ -430,6 +433,9 @@ export function compile( if (generator.usesSqlite) { linkLibs = `-L${brewPrefix}/sqlite/lib ` + linkLibs; } + if (generator.usesPostgres) { + linkLibs = `-L${brewPrefix}/libpq/lib ` + linkLibs; + } if (generator.usesHttpServer || generator.usesCompression) { linkLibs = `-L${brewPrefix}/zstd/lib ` + linkLibs; } @@ -458,6 +464,7 @@ export function compile( const arenaBridgeObj = `${bridgePath}/arena-bridge.o`; const cpSpawnObj = generator.getUsesSpawn() ? `${bridgePath}/child-process-spawn.o` : ""; const curlBridgeObj = generator.usesCurl ? `${bridgePath}/curl-bridge.o` : ""; + const pgBridgeObj = generator.usesPostgres ? `${bridgePath}/pg-bridge.o` : ""; const compressBridgeObj = generator.usesCompression ? `${bridgePath}/compress-bridge.o` : ""; const yamlBridgeObj = generator.usesYaml ? `${bridgePath}/yaml-bridge.o` : ""; let extraObjs = ""; @@ -623,7 +630,7 @@ export function compile( const userObjs = extraLinkObjs.length > 0 ? " " + extraLinkObjs.join(" ") : ""; const userPaths = extraLinkPaths.map((p) => ` -L${p}`).join(""); const userLibs = extraLinkLibs.map((l) => ` -l${l}`).join(""); - const linkCmd = `${linker} ${objFile} ${lwsBridgeObj} ${regexBridgeObj} ${cpBridgeObj} ${osBridgeObj} ${strlenCacheObj} ${timeBridgeObj} ${base64BridgeObj} ${urlBridgeObj} ${uriBridgeObj} ${dotenvBridgeObj} ${watchBridgeObj} ${arenaBridgeObj} ${cpSpawnObj} ${curlBridgeObj} ${compressBridgeObj} ${yamlBridgeObj} ${stringOpsBridgeObj}${extraObjs}${userObjs} -o ${outputFile}${noPie}${debugFlag}${stripFlag}${staticFlag}${crossTarget}${crossLinker}${suppressLdWarnings}${sanitizeFlags} ${linkLibs}${userPaths}${userLibs}`; + const linkCmd = `${linker} ${objFile} ${lwsBridgeObj} ${regexBridgeObj} ${cpBridgeObj} ${osBridgeObj} ${strlenCacheObj} ${timeBridgeObj} ${base64BridgeObj} ${urlBridgeObj} ${uriBridgeObj} ${dotenvBridgeObj} ${watchBridgeObj} ${arenaBridgeObj} ${cpSpawnObj} ${curlBridgeObj} ${pgBridgeObj} ${compressBridgeObj} ${yamlBridgeObj} ${stringOpsBridgeObj}${extraObjs}${userObjs} -o ${outputFile}${noPie}${debugFlag}${stripFlag}${staticFlag}${crossTarget}${crossLinker}${suppressLdWarnings}${sanitizeFlags} ${linkLibs}${userPaths}${userLibs}`; logger.info(` ${linkCmd}`); const linkStdio = logger.getLevel() >= LogLevel_Verbose ? "inherit" : "pipe"; execSync(linkCmd, { stdio: linkStdio }); diff --git a/src/native-compiler-lib.ts b/src/native-compiler-lib.ts index 78dc30a2..d54e3138 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -664,6 +664,9 @@ export function compileNative(inputFile: string, outputFile: string): void { if (generator.getUsesSqlite()) { linkLibs = "-lsqlite3 " + linkLibs; } + if (generator.getUsesPostgres()) { + linkLibs = "-lpq " + linkLibs; + } if (generator.getUsesHttpServer()) { linkLibs = "-lz -lzstd " + linkLibs; } @@ -695,6 +698,7 @@ export function compileNative(inputFile: string, outputFile: string): void { const arenaBridgeObj = effectiveBridgePath + "/arena-bridge.o"; const cpSpawnObj = generator.getUsesSpawn() ? effectiveBridgePath + "/child-process-spawn.o" : ""; const curlBridgeObj = generator.getUsesCurl() ? effectiveBridgePath + "/curl-bridge.o" : ""; + const pgBridgeObj = generator.getUsesPostgres() ? effectiveBridgePath + "/pg-bridge.o" : ""; const compressBridgeObj = generator.getUsesCompression() ? effectiveBridgePath + "/compress-bridge.o" : ""; @@ -758,6 +762,8 @@ export function compileNative(inputFile: string, outputFile: string): void { " " + curlBridgeObj + " " + + pgBridgeObj + + " " + compressBridgeObj + " " + yamlBridgeObj + @@ -819,6 +825,12 @@ export function compileNative(inputFile: string, outputFile: string): void { if (fs.existsSync("/usr/local/opt/sqlite/lib")) linkLibs = "-L/usr/local/opt/sqlite/lib " + linkLibs; } + if (generator.getUsesPostgres()) { + if (fs.existsSync("/opt/homebrew/opt/libpq/lib")) + linkLibs = "-L/opt/homebrew/opt/libpq/lib " + linkLibs; + if (fs.existsSync("/usr/local/opt/libpq/lib")) + linkLibs = "-L/usr/local/opt/libpq/lib " + linkLibs; + } if (generator.getUsesHttpServer() || generator.getUsesCompression()) { if (fs.existsSync("/opt/homebrew/opt/zstd/lib")) linkLibs = "-L/opt/homebrew/opt/zstd/lib " + linkLibs; @@ -894,6 +906,12 @@ export function compileNative(inputFile: string, outputFile: string): void { if (fs.existsSync("/usr/local/opt/sqlite/lib")) linkLibs = "-L/usr/local/opt/sqlite/lib " + linkLibs; } + if (generator.getUsesPostgres()) { + if (fs.existsSync("/opt/homebrew/opt/libpq/lib")) + linkLibs = "-L/opt/homebrew/opt/libpq/lib " + linkLibs; + if (fs.existsSync("/usr/local/opt/libpq/lib")) + linkLibs = "-L/usr/local/opt/libpq/lib " + linkLibs; + } if (generator.getUsesHttpServer() || generator.getUsesCompression()) { if (fs.existsSync("/opt/homebrew/opt/zstd/lib")) linkLibs = "-L/opt/homebrew/opt/zstd/lib " + linkLibs; diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 7fc7d261..5341a67c 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -614,6 +614,7 @@ function transformExpression(node: TreeSitterNode): Expression { default: return { type: "variable", name: "undefined" }; } + return { type: "variable", name: "undefined" }; } // ============================================ diff --git a/tests/fixtures/stdlib/postgres-connect.ts b/tests/fixtures/stdlib/postgres-connect.ts new file mode 100644 index 00000000..397de4a0 --- /dev/null +++ b/tests/fixtures/stdlib/postgres-connect.ts @@ -0,0 +1,7 @@ +// @test-skip +import { Client } from "chadscript/postgres"; + +const c = new Client("host=127.0.0.1 port=5432 user=postgres password=test dbname=chadtest"); +c.connect(); +c.end(); +console.log("TEST_PASSED"); From 9207737e12d20a417df4e90c75dad7b68628e80b Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 13 Apr 2026 21:49:34 -0700 Subject: [PATCH 3/6] postgres: .query() returning rowCount + abi fix (step 3/10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds Client.query(sql) that returns QueryResult with rowCount for INSERT/UPDATE/DELETE. test fixture covers single insert, multi-row insert, update, and delete. abi fix: c functions called from chadscript as returning 'number' must return 'double', not 'int' — chadscript maps 'number' to llvm double, which lives in d0 on arm64 while c int returns come back in w0, completely different registers. previous step 2 passed by luck because CONNECTION_OK happens to be 0 and d0 was zero-initialized. fixed the whole bridge preemptively: all int-returning cs_pg_* now return double, all int parameters (row, col, nparams) are now double with internal cast. matches the pattern cs_lld_available uses. --- c_bridges/pg-bridge.c | 54 +++++++++---------- lib/postgres.ts | 29 ++++++++++ .../stdlib/postgres-query-rowcount.ts | 36 +++++++++++++ 3 files changed, 92 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/stdlib/postgres-query-rowcount.ts diff --git a/c_bridges/pg-bridge.c b/c_bridges/pg-bridge.c index d61766cd..c38b175b 100644 --- a/c_bridges/pg-bridge.c +++ b/c_bridges/pg-bridge.c @@ -8,9 +8,9 @@ void *cs_pg_connect(const char *conninfo) { return (void *)conn; } -int cs_pg_status(void *conn) { - if (!conn) return CONNECTION_BAD; - return PQstatus((PGconn *)conn); +double cs_pg_status(void *conn) { + if (!conn) return (double)CONNECTION_BAD; + return (double)PQstatus((PGconn *)conn); } const char *cs_pg_error_message(void *conn) { @@ -28,14 +28,14 @@ void *cs_pg_exec(void *conn, const char *sql) { return (void *)PQexec((PGconn *)conn, sql); } -void *cs_pg_exec_params(void *conn, const char *sql, int nparams, const char **values) { +void *cs_pg_exec_params(void *conn, const char *sql, double nparams, const char **values) { if (!conn || !sql) return NULL; - return (void *)PQexecParams((PGconn *)conn, sql, nparams, NULL, values, NULL, NULL, 0); + return (void *)PQexecParams((PGconn *)conn, sql, (int)nparams, NULL, values, NULL, NULL, 0); } -int cs_pg_result_status(void *res) { - if (!res) return PGRES_FATAL_ERROR; - return PQresultStatus((PGresult *)res); +double cs_pg_result_status(void *res) { + if (!res) return (double)PGRES_FATAL_ERROR; + return (double)PQresultStatus((PGresult *)res); } const char *cs_pg_result_error_message(void *res) { @@ -43,35 +43,35 @@ const char *cs_pg_result_error_message(void *res) { return PQresultErrorMessage((PGresult *)res); } -int cs_pg_nrows(void *res) { - if (!res) return 0; - return PQntuples((PGresult *)res); +double cs_pg_nrows(void *res) { + if (!res) return 0.0; + return (double)PQntuples((PGresult *)res); } -int cs_pg_ncols(void *res) { - if (!res) return 0; - return PQnfields((PGresult *)res); +double cs_pg_ncols(void *res) { + if (!res) return 0.0; + return (double)PQnfields((PGresult *)res); } -const char *cs_pg_fname(void *res, int col) { +const char *cs_pg_fname(void *res, double col) { if (!res) return ""; - const char *n = PQfname((PGresult *)res, col); + const char *n = PQfname((PGresult *)res, (int)col); return n ? n : ""; } -unsigned int cs_pg_ftype(void *res, int col) { - if (!res) return 0; - return (unsigned int)PQftype((PGresult *)res, col); +double cs_pg_ftype(void *res, double col) { + if (!res) return 0.0; + return (double)PQftype((PGresult *)res, (int)col); } -const char *cs_pg_getvalue(void *res, int row, int col) { +const char *cs_pg_getvalue(void *res, double row, double col) { if (!res) return ""; - return PQgetvalue((PGresult *)res, row, col); + return PQgetvalue((PGresult *)res, (int)row, (int)col); } -int cs_pg_getisnull(void *res, int row, int col) { - if (!res) return 1; - return PQgetisnull((PGresult *)res, row, col); +double cs_pg_getisnull(void *res, double row, double col) { + if (!res) return 1.0; + return (double)PQgetisnull((PGresult *)res, (int)row, (int)col); } const char *cs_pg_cmdtuples(void *res) { @@ -85,8 +85,8 @@ void cs_pg_clear(void *res) { PQclear((PGresult *)res); } -int cs_pg_result_ok(void *res) { - if (!res) return 0; +double cs_pg_result_ok(void *res) { + if (!res) return 0.0; ExecStatusType s = PQresultStatus((PGresult *)res); - return (s == PGRES_COMMAND_OK || s == PGRES_TUPLES_OK) ? 1 : 0; + return (s == PGRES_COMMAND_OK || s == PGRES_TUPLES_OK) ? 1.0 : 0.0; } diff --git a/lib/postgres.ts b/lib/postgres.ts index 8d572269..f77b210d 100644 --- a/lib/postgres.ts +++ b/lib/postgres.ts @@ -2,9 +2,21 @@ declare function cs_pg_connect(conninfo: string): string; declare function cs_pg_status(conn: string): number; declare function cs_pg_error_message(conn: string): string; declare function cs_pg_finish(conn: string): void; +declare function cs_pg_exec(conn: string, sql: string): string; +declare function cs_pg_result_ok(res: string): number; +declare function cs_pg_result_error_message(res: string): string; +declare function cs_pg_cmdtuples(res: string): string; +declare function cs_pg_clear(res: string): void; const CONNECTION_OK: number = 0; +export class QueryResult { + rowCount: number; + constructor(rowCount: number) { + this.rowCount = rowCount; + } +} + export class Client { private _conninfo: string; private _conn: string; @@ -28,6 +40,23 @@ export class Client { this._connected = true; } + query(sql: string): QueryResult { + if (!this._connected) { + throw new Error("postgres query on disconnected client"); + } + const res = cs_pg_exec(this._conn, sql); + const ok = cs_pg_result_ok(res); + if (ok === 0) { + const msg = cs_pg_result_error_message(res); + cs_pg_clear(res); + throw new Error("postgres query failed: " + msg); + } + const tuplesStr = cs_pg_cmdtuples(res); + const rowCount = parseInt(tuplesStr, 10); + cs_pg_clear(res); + return new QueryResult(rowCount); + } + end(): void { if (this._connected) { cs_pg_finish(this._conn); diff --git a/tests/fixtures/stdlib/postgres-query-rowcount.ts b/tests/fixtures/stdlib/postgres-query-rowcount.ts new file mode 100644 index 00000000..4a23ea9f --- /dev/null +++ b/tests/fixtures/stdlib/postgres-query-rowcount.ts @@ -0,0 +1,36 @@ +// @test-skip +import { Client } from "chadscript/postgres"; + +const c = new Client("host=127.0.0.1 port=5432 user=postgres password=test dbname=chadtest"); +c.connect(); + +c.query("DROP TABLE IF EXISTS t_rowcount"); +c.query("CREATE TABLE t_rowcount (id INT, name TEXT)"); + +const ins1 = c.query("INSERT INTO t_rowcount VALUES (1, 'alice')"); +if (ins1.rowCount !== 1) { + console.log("TEST_FAILED: insert rowCount was " + ins1.rowCount); + process.exit(1); +} + +const ins2 = c.query("INSERT INTO t_rowcount VALUES (2, 'bob'), (3, 'carol')"); +if (ins2.rowCount !== 2) { + console.log("TEST_FAILED: bulk insert rowCount was " + ins2.rowCount); + process.exit(1); +} + +const upd = c.query("UPDATE t_rowcount SET name = 'ALICE' WHERE id = 1"); +if (upd.rowCount !== 1) { + console.log("TEST_FAILED: update rowCount was " + upd.rowCount); + process.exit(1); +} + +const del = c.query("DELETE FROM t_rowcount WHERE id > 0"); +if (del.rowCount !== 3) { + console.log("TEST_FAILED: delete rowCount was " + del.rowCount); + process.exit(1); +} + +c.query("DROP TABLE t_rowcount"); +c.end(); +console.log("TEST_PASSED"); From 6f3952b0ba67041909e3d4f09c2a60cc42f5f458 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 13 Apr 2026 22:00:52 -0700 Subject: [PATCH 4/6] postgres: SELECT support with Row access (step 4/10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit adds nrows/ncols/fname/getvalue/getisnull extern bindings and SELECT row access. QueryResult now holds flat { fields, values } plus numRows /numCols — getValue(row, col) walks the fields[] to resolve a column name to its offset and returns the string. getRow(idx) constructs a lightweight Row view for iteration. flat-storage design avoids chadscript ObjectArray type-loss: storing Row[] and accessing rows[i].get() inside lib/postgres.ts triggered compile errors because element access on an object array discards the concrete type. keeping values as a string[] sidesteps the problem. test covers 3-row SELECT with column-name lookup, field ordering, empty result set, and rowCount. c bridge now gc_strdups all string returns (getvalue, fname, error messages, cmdtuples) so they survive past cs_pg_clear — previously those pointers dangled into the freed PGresult. --- c_bridges/pg-bridge.c | 20 +++-- lib/postgres.ts | 81 ++++++++++++++++++- .../fixtures/stdlib/postgres-query-select.ts | 57 +++++++++++++ 3 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 tests/fixtures/stdlib/postgres-query-select.ts diff --git a/c_bridges/pg-bridge.c b/c_bridges/pg-bridge.c index c38b175b..fcbbc23f 100644 --- a/c_bridges/pg-bridge.c +++ b/c_bridges/pg-bridge.c @@ -2,6 +2,16 @@ #include #include +extern void *GC_malloc_atomic(size_t); + +static const char *gc_strdup(const char *s) { + if (!s) return ""; + size_t n = strlen(s); + char *out = (char *)GC_malloc_atomic(n + 1); + memcpy(out, s, n + 1); + return out; +} + void *cs_pg_connect(const char *conninfo) { if (!conninfo) return NULL; PGconn *conn = PQconnectdb(conninfo); @@ -15,7 +25,7 @@ double cs_pg_status(void *conn) { const char *cs_pg_error_message(void *conn) { if (!conn) return "null connection"; - return PQerrorMessage((PGconn *)conn); + return gc_strdup(PQerrorMessage((PGconn *)conn)); } void cs_pg_finish(void *conn) { @@ -40,7 +50,7 @@ double cs_pg_result_status(void *res) { const char *cs_pg_result_error_message(void *res) { if (!res) return "null result"; - return PQresultErrorMessage((PGresult *)res); + return gc_strdup(PQresultErrorMessage((PGresult *)res)); } double cs_pg_nrows(void *res) { @@ -56,7 +66,7 @@ double cs_pg_ncols(void *res) { const char *cs_pg_fname(void *res, double col) { if (!res) return ""; const char *n = PQfname((PGresult *)res, (int)col); - return n ? n : ""; + return gc_strdup(n ? n : ""); } double cs_pg_ftype(void *res, double col) { @@ -66,7 +76,7 @@ double cs_pg_ftype(void *res, double col) { const char *cs_pg_getvalue(void *res, double row, double col) { if (!res) return ""; - return PQgetvalue((PGresult *)res, (int)row, (int)col); + return gc_strdup(PQgetvalue((PGresult *)res, (int)row, (int)col)); } double cs_pg_getisnull(void *res, double row, double col) { @@ -77,7 +87,7 @@ double cs_pg_getisnull(void *res, double row, double col) { const char *cs_pg_cmdtuples(void *res) { if (!res) return "0"; const char *c = PQcmdTuples((PGresult *)res); - return (c && *c) ? c : "0"; + return gc_strdup((c && *c) ? c : "0"); } void cs_pg_clear(void *res) { diff --git a/lib/postgres.ts b/lib/postgres.ts index f77b210d..af257c02 100644 --- a/lib/postgres.ts +++ b/lib/postgres.ts @@ -7,13 +7,73 @@ declare function cs_pg_result_ok(res: string): number; declare function cs_pg_result_error_message(res: string): string; declare function cs_pg_cmdtuples(res: string): string; declare function cs_pg_clear(res: string): void; +declare function cs_pg_nrows(res: string): number; +declare function cs_pg_ncols(res: string): number; +declare function cs_pg_fname(res: string, col: number): string; +declare function cs_pg_getvalue(res: string, row: number, col: number): string; +declare function cs_pg_getisnull(res: string, row: number, col: number): number; const CONNECTION_OK: number = 0; +export class Row { + private _fields: string[]; + private _values: string[]; + private _rowStart: number; + private _ncols: number; + + constructor(fields: string[], values: string[], rowStart: number, ncols: number) { + this._fields = fields; + this._values = values; + this._rowStart = rowStart; + this._ncols = ncols; + } + + get(col: string): string { + for (let i = 0; i < this._ncols; i++) { + if (this._fields[i] === col) { + return this._values[this._rowStart + i]; + } + } + return ""; + } + + getAt(col: number): string { + return this._values[this._rowStart + col]; + } +} + export class QueryResult { rowCount: number; - constructor(rowCount: number) { + numRows: number; + numCols: number; + fields: string[]; + private _values: string[]; + + constructor( + rowCount: number, + numRows: number, + numCols: number, + fields: string[], + values: string[], + ) { this.rowCount = rowCount; + this.numRows = numRows; + this.numCols = numCols; + this.fields = fields; + this._values = values; + } + + getRow(index: number): Row { + return new Row(this.fields, this._values, index * this.numCols, this.numCols); + } + + getValue(row: number, col: string): string { + for (let i = 0; i < this.numCols; i++) { + if (this.fields[i] === col) { + return this._values[row * this.numCols + i]; + } + } + return ""; } } @@ -51,10 +111,25 @@ export class Client { cs_pg_clear(res); throw new Error("postgres query failed: " + msg); } + + const ncols = cs_pg_ncols(res); + const fields: string[] = []; + for (let c = 0; c < ncols; c++) { + fields.push(cs_pg_fname(res, c)); + } + + const nrows = cs_pg_nrows(res); + const values: string[] = []; + for (let r = 0; r < nrows; r++) { + for (let c = 0; c < ncols; c++) { + values.push(cs_pg_getvalue(res, r, c)); + } + } + const tuplesStr = cs_pg_cmdtuples(res); - const rowCount = parseInt(tuplesStr, 10); + const rowCount = nrows > 0 ? nrows : parseInt(tuplesStr, 10); cs_pg_clear(res); - return new QueryResult(rowCount); + return new QueryResult(rowCount, nrows, ncols, fields, values); } end(): void { diff --git a/tests/fixtures/stdlib/postgres-query-select.ts b/tests/fixtures/stdlib/postgres-query-select.ts new file mode 100644 index 00000000..92fe8301 --- /dev/null +++ b/tests/fixtures/stdlib/postgres-query-select.ts @@ -0,0 +1,57 @@ +// @test-skip +import { Client } from "chadscript/postgres"; + +const c = new Client("host=127.0.0.1 port=5432 user=postgres password=test dbname=chadtest"); +c.connect(); + +c.query("DROP TABLE IF EXISTS t_select"); +c.query("CREATE TABLE t_select (id INT, name TEXT, city TEXT)"); +c.query("INSERT INTO t_select VALUES (1, 'alice', 'nyc'), (2, 'bob', 'sf'), (3, 'carol', 'la')"); + +const res = c.query("SELECT id, name, city FROM t_select ORDER BY id"); + +if (res.rowCount !== 3) { + console.log("TEST_FAILED: rowCount was " + res.rowCount); + process.exit(1); +} +if (res.numRows !== 3) { + console.log("TEST_FAILED: numRows was " + res.numRows); + process.exit(1); +} +if (res.fields.length !== 3) { + console.log("TEST_FAILED: fields.length was " + res.fields.length); + process.exit(1); +} + +if (res.fields[0] !== "id" || res.fields[1] !== "name" || res.fields[2] !== "city") { + console.log( + "TEST_FAILED: fields were " + res.fields[0] + "," + res.fields[1] + "," + res.fields[2], + ); + process.exit(1); +} + +const name0 = res.getValue(0, "name"); +if (name0 !== "alice") { + console.log("TEST_FAILED: row 0 name was " + name0); + process.exit(1); +} +const city1 = res.getValue(1, "city"); +if (city1 !== "sf") { + console.log("TEST_FAILED: row 1 city was " + city1); + process.exit(1); +} +const id2 = res.getValue(2, "id"); +if (id2 !== "3") { + console.log("TEST_FAILED: row 2 id was " + id2); + process.exit(1); +} + +const empty = c.query("SELECT id FROM t_select WHERE id > 100"); +if (empty.numRows !== 0) { + console.log("TEST_FAILED: empty query returned " + empty.numRows + " rows"); + process.exit(1); +} + +c.query("DROP TABLE t_select"); +c.end(); +console.log("TEST_PASSED"); From 73a4226b4a863aca37490537209b79b531014ae2 Mon Sep 17 00:00:00 2001 From: cs01 Date: Mon, 13 Apr 2026 22:15:43 -0700 Subject: [PATCH 5/6] postgres: Pool, parameterized queries, typed accessors, docs (steps 5+6/10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pool: thin wrapper over Client that lazy-connects on first query. v1 is sequential — real pooling (multiple conns, queuing) needs async and is a follow-up. matches node-pg's Pool entry point so copy-paste from tutorials works. Pool is now the documented primary API; Client is the low-level escape hatch. Parameterized queries: pool.query(sql, params) uses libpq's PQexecParams via a stateful param-builder pattern (cs_pg_params_new / _add / _exec_params_with). avoids marshaling a chadscript string[] across the ffi boundary — the %StringArray struct layout doesn't match libpq's const char**. TS passes values one at a time; c side grows its own dynamic array. placeholders are $1, $2, $3 (postgres native syntax). libpq handles escaping so user input with quotes or semicolons is safe. Typed accessors on QueryResult: getInt, getFloat, getBool delegate to parseInt/parseFloat/string compare — saves users from writing boiler- plate parsing on every access. no OID-based auto-typing — users know their schema, they call the right getter. avoids type-coercion surprises. Docs: new docs/stdlib/postgres.md with Pool-first API, documented limitations (sync, no null detection, string-only params, no real pooling). chadscript/postgres added to stdlib module index. test coverage: postgres-pool (Pool lazy-connect + CRUD), postgres-params-types (getInt/Float/Bool + parameterized queries + sql-injection safety with quoted param values). --- c_bridges/pg-bridge.c | 36 +++++ docs/stdlib/index.md | 2 + docs/stdlib/postgres.md | 147 ++++++++++++++++++ lib/postgres.ts | 54 ++++++- .../fixtures/stdlib/postgres-params-types.ts | 61 ++++++++ tests/fixtures/stdlib/postgres-pool.ts | 23 +++ 6 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 docs/stdlib/postgres.md create mode 100644 tests/fixtures/stdlib/postgres-params-types.ts create mode 100644 tests/fixtures/stdlib/postgres-pool.ts diff --git a/c_bridges/pg-bridge.c b/c_bridges/pg-bridge.c index fcbbc23f..ad1554c3 100644 --- a/c_bridges/pg-bridge.c +++ b/c_bridges/pg-bridge.c @@ -43,6 +43,42 @@ void *cs_pg_exec_params(void *conn, const char *sql, double nparams, const char return (void *)PQexecParams((PGconn *)conn, sql, (int)nparams, NULL, values, NULL, NULL, 0); } +typedef struct { + const char **values; + int count; + int capacity; +} PgParamList; + +void *cs_pg_params_new(void) { + PgParamList *p = (PgParamList *)GC_malloc_atomic(sizeof(PgParamList)); + if (!p) return NULL; + p->count = 0; + p->capacity = 8; + p->values = (const char **)GC_malloc_atomic(sizeof(const char *) * (size_t)p->capacity); + return (void *)p; +} + +void cs_pg_params_add(void *params, const char *value) { + if (!params) return; + PgParamList *p = (PgParamList *)params; + if (p->count >= p->capacity) { + int newcap = p->capacity * 2; + const char **newvals = (const char **)GC_malloc_atomic(sizeof(const char *) * (size_t)newcap); + for (int i = 0; i < p->count; i++) newvals[i] = p->values[i]; + p->values = newvals; + p->capacity = newcap; + } + p->values[p->count++] = value ? gc_strdup(value) : NULL; +} + +void *cs_pg_exec_params_with(void *conn, const char *sql, void *params) { + if (!conn || !sql) return NULL; + PgParamList *p = (PgParamList *)params; + int n = p ? p->count : 0; + const char **vals = p ? p->values : NULL; + return (void *)PQexecParams((PGconn *)conn, sql, n, NULL, vals, NULL, NULL, 0); +} + double cs_pg_result_status(void *res) { if (!res) return (double)PGRES_FATAL_ERROR; return (double)PQresultStatus((PGresult *)res); diff --git a/docs/stdlib/index.md b/docs/stdlib/index.md index 6616cd4c..734e10dc 100644 --- a/docs/stdlib/index.md +++ b/docs/stdlib/index.md @@ -25,11 +25,13 @@ A few APIs live in named modules and need an import: ```typescript import { httpServe, Router, Context } from "chadscript/http"; import { ArgumentParser } from "chadscript/argparse"; +import { Pool } from "chadscript/postgres"; ``` | Module | Contents | |--------|----------| | `chadscript/http` | `httpServe`, `wsBroadcast`, `wsSend`, `parseMultipart`, `bytesResponse`, `serveFile`, `getHeader`, `parseQueryString`, `parseCookies`, `Router`, `Context`, `RouterRequest` | | `chadscript/argparse` | `ArgumentParser` | +| `chadscript/postgres` | `Pool`, `Client`, `QueryResult`, `Row` — PostgreSQL client via `libpq` | The `chadscript/` prefix works like Node's `node:` prefix — unambiguous and collision-free. diff --git a/docs/stdlib/postgres.md b/docs/stdlib/postgres.md new file mode 100644 index 00000000..6813c81f --- /dev/null +++ b/docs/stdlib/postgres.md @@ -0,0 +1,147 @@ +# postgres + +PostgreSQL client via `libpq`. Connect to a Postgres database, run queries, and read results as native strings. Imported as a module: + +```typescript +import { Pool } from "chadscript/postgres"; +``` + +`libpq` is required at build time. On macOS: `brew install libpq`. On Debian/Ubuntu: `apt install libpq-dev`. + +## `new Pool(conninfo)` + +Create a connection pool. The connection string follows [libpq's format](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING) — either key/value pairs or a URI. + +```typescript +const pool = new Pool("host=127.0.0.1 port=5432 user=postgres password=secret dbname=mydb"); +// or +const pool2 = new Pool("postgresql://postgres:secret@127.0.0.1:5432/mydb"); +``` + +`Pool` is the recommended entry point for application code. It connects lazily on first query — the constructor does not block. + +## `pool.query(sql)` + +Execute a SQL statement and return a [`QueryResult`](#queryresult). Throws on SQL errors. Parameterized queries are coming in a follow-up; for now, build the SQL string yourself — **escape any user input** or you will ship an SQL injection. + +```typescript +pool.query("CREATE TABLE users (id INT, name TEXT)"); +pool.query("INSERT INTO users VALUES (1, 'alice')"); + +const res = pool.query("SELECT id, name FROM users ORDER BY id"); +console.log(res.rowCount); // 1 +console.log(res.getValue(0, "name")); // "alice" +``` + +## `pool.end()` + +Close the pool's underlying connection. Safe to call multiple times. + +```typescript +pool.end(); +``` + +## `Client` (low-level) + +For cases where you need explicit connection lifecycle, use `Client` directly: + +```typescript +import { Client } from "chadscript/postgres"; + +const c = new Client("postgresql://postgres:secret@127.0.0.1:5432/mydb"); +c.connect(); +const res = c.query("SELECT 1"); +c.end(); +``` + +Most application code should prefer `Pool`. `Client` requires you to call `connect()` before any queries — `Pool` handles this automatically. + +## `QueryResult` + +Returned by `client.query()`. All values are currently strings — type coercion (integer, boolean, date) is a follow-up. + +| Field | Type | Description | +|-------|------|-------------| +| `rowCount` | `number` | Affected rows (INSERT/UPDATE/DELETE) or number of rows returned (SELECT) | +| `numRows` | `number` | Number of rows in the result set (SELECT) | +| `numCols` | `number` | Number of columns in the result set | +| `fields` | `string[]` | Column names in order | + +### `result.getValue(row, col)` + +Return the value at a given row index and column name, as a string. + +```typescript +const res = c.query("SELECT id, name, city FROM users ORDER BY id"); +for (let i = 0; i < res.numRows; i++) { + const name = res.getValue(i, "name"); + const city = res.getValue(i, "city"); + console.log(name + " in " + city); +} +``` + +Column lookup by name is linear in the number of columns. If you're reading millions of rows in a tight loop, cache the column index: + +```typescript +const nameIdx = res.fields.indexOf("name"); +for (let i = 0; i < res.numRows; i++) { + const r = res.getRow(i); + console.log(r.getAt(nameIdx)); +} +``` + +### `result.getRow(index)` + +Return a lightweight `Row` view into the result set. The returned `Row` holds a reference to the parent result's data — it does not copy. + +```typescript +const r = res.getRow(0); +const name = r.get("name"); // by column name +const first = r.getAt(0); // by column index +``` + +## Example — CRUD + +```typescript +import { Pool } from "chadscript/postgres"; + +const pool = new Pool("postgresql://postgres:secret@127.0.0.1:5432/mydb"); + +pool.query("DROP TABLE IF EXISTS users"); +pool.query("CREATE TABLE users (id INT, name TEXT, city TEXT)"); + +pool.query("INSERT INTO users VALUES (1, 'alice', 'nyc'), (2, 'bob', 'sf')"); + +const res = pool.query("SELECT id, name, city FROM users ORDER BY id"); +console.log("rows: " + res.numRows); +for (let i = 0; i < res.numRows; i++) { + console.log(res.getValue(i, "id") + " " + res.getValue(i, "name") + " " + res.getValue(i, "city")); +} + +const upd = pool.query("UPDATE users SET city = 'LA' WHERE id = 1"); +console.log("updated " + upd.rowCount); + +pool.end(); +``` + +## Current Limitations + +This module is under active development. Known gaps: + +- **No parameterized queries yet** — every query is literal SQL. Build strings carefully and never concatenate user input. Coming soon. +- **All values are strings** — integers come back as `"42"`, booleans as `"t"`/`"f"`, dates as ISO-ish strings. Type coercion is a follow-up. +- **`Pool` is a thin wrapper over a single `Client`** — no real connection reuse yet. Calls are sequential. Real pooling (multiple connections, queuing, limits) needs async, which is a follow-up. +- **Synchronous under the hood** — `libpq` is called synchronously. Calls block the event loop. Real async (libuv integration) is a follow-up. +- **No `LISTEN`/`NOTIFY`, no `COPY`, no streaming** — the basics only. + +## Native Implementation + +| API | Maps to | +|-----|---------| +| `new Client()` / `connect()` | `PQconnectdb()` + `PQstatus()` | +| `client.query()` | `PQexec()` + `PQresultStatus()` + `PQgetvalue()` loop | +| `QueryResult.fields` | `PQfname()` per column | +| `QueryResult.numRows` / `numCols` | `PQntuples()` / `PQnfields()` | +| `client.end()` | `PQfinish()` | + +All string values returned from `libpq` are copied into GC-managed memory before the underlying `PGresult` is cleared, so values remain valid after the next query. diff --git a/lib/postgres.ts b/lib/postgres.ts index af257c02..75d10b2d 100644 --- a/lib/postgres.ts +++ b/lib/postgres.ts @@ -12,6 +12,9 @@ declare function cs_pg_ncols(res: string): number; declare function cs_pg_fname(res: string, col: number): string; declare function cs_pg_getvalue(res: string, row: number, col: number): string; declare function cs_pg_getisnull(res: string, row: number, col: number): number; +declare function cs_pg_params_new(): string; +declare function cs_pg_params_add(params: string, value: string): void; +declare function cs_pg_exec_params_with(conn: string, sql: string, params: string): string; const CONNECTION_OK: number = 0; @@ -75,6 +78,40 @@ export class QueryResult { } return ""; } + + getInt(row: number, col: string): number { + const s = this.getValue(row, col); + return parseInt(s, 10); + } + + getFloat(row: number, col: string): number { + const s = this.getValue(row, col); + return parseFloat(s); + } + + getBool(row: number, col: string): boolean { + const s = this.getValue(row, col); + return s === "t" || s === "true" || s === "1"; + } +} + +export class Pool { + private _client: Client; + + constructor(conninfo: string) { + this._client = new Client(conninfo); + } + + query(sql: string, params: string[] = []): QueryResult { + if (!this._client.isConnected()) { + this._client.connect(); + } + return this._client.query(sql, params); + } + + end(): void { + this._client.end(); + } } export class Client { @@ -88,6 +125,10 @@ export class Client { this._connected = false; } + isConnected(): boolean { + return this._connected; + } + connect(): void { this._conn = cs_pg_connect(this._conninfo); const status = cs_pg_status(this._conn); @@ -100,11 +141,20 @@ export class Client { this._connected = true; } - query(sql: string): QueryResult { + query(sql: string, params: string[] = []): QueryResult { if (!this._connected) { throw new Error("postgres query on disconnected client"); } - const res = cs_pg_exec(this._conn, sql); + let res: string; + if (params.length === 0) { + res = cs_pg_exec(this._conn, sql); + } else { + const p = cs_pg_params_new(); + for (let i = 0; i < params.length; i++) { + cs_pg_params_add(p, params[i]); + } + res = cs_pg_exec_params_with(this._conn, sql, p); + } const ok = cs_pg_result_ok(res); if (ok === 0) { const msg = cs_pg_result_error_message(res); diff --git a/tests/fixtures/stdlib/postgres-params-types.ts b/tests/fixtures/stdlib/postgres-params-types.ts new file mode 100644 index 00000000..837ed8a1 --- /dev/null +++ b/tests/fixtures/stdlib/postgres-params-types.ts @@ -0,0 +1,61 @@ +// @test-skip +import { Pool } from "chadscript/postgres"; + +const pool = new Pool("host=127.0.0.1 port=5432 user=postgres password=test dbname=chadtest"); + +pool.query("DROP TABLE IF EXISTS t_typed"); +pool.query("CREATE TABLE t_typed (id INT, price REAL, active BOOLEAN, name TEXT)"); + +pool.query("INSERT INTO t_typed VALUES ($1, $2, $3, $4)", ["1", "19.99", "true", "widget"]); +pool.query("INSERT INTO t_typed VALUES ($1, $2, $3, $4)", ["2", "3.5", "false", "gadget"]); + +const sel = pool.query("SELECT id, price, active, name FROM t_typed WHERE id = $1", ["1"]); +if (sel.rowCount !== 1) { + console.log("TEST_FAILED: rowCount " + sel.rowCount); + process.exit(1); +} + +const id = sel.getInt(0, "id"); +if (id !== 1) { + console.log("TEST_FAILED: id " + id); + process.exit(1); +} + +const price = sel.getFloat(0, "price"); +if (price < 19.98 || price > 20.0) { + console.log("TEST_FAILED: price " + price); + process.exit(1); +} + +const active = sel.getBool(0, "active"); +if (!active) { + console.log("TEST_FAILED: active was false"); + process.exit(1); +} + +const name = sel.getValue(0, "name"); +if (name !== "widget") { + console.log("TEST_FAILED: name " + name); + process.exit(1); +} + +const all = pool.query("SELECT id, active FROM t_typed ORDER BY id"); +if (all.numRows !== 2) { + console.log("TEST_FAILED: all.numRows " + all.numRows); + process.exit(1); +} +if (all.getBool(1, "active")) { + console.log("TEST_FAILED: row 1 active should be false"); + process.exit(1); +} + +pool.query("INSERT INTO t_typed VALUES ($1, $2, $3, $4)", ["99", "0", "false", "sql-safe ' test"]); +const safe = pool.query("SELECT name FROM t_typed WHERE id = $1", ["99"]); +if (safe.getValue(0, "name") !== "sql-safe ' test") { + console.log("TEST_FAILED: injection-unsafe name " + safe.getValue(0, "name")); + process.exit(1); +} + +pool.query("DROP TABLE t_typed"); +pool.end(); +console.log("TEST_PASSED"); diff --git a/tests/fixtures/stdlib/postgres-pool.ts b/tests/fixtures/stdlib/postgres-pool.ts new file mode 100644 index 00000000..09b60162 --- /dev/null +++ b/tests/fixtures/stdlib/postgres-pool.ts @@ -0,0 +1,23 @@ +// @test-skip +import { Pool } from "chadscript/postgres"; + +const pool = new Pool("host=127.0.0.1 port=5432 user=postgres password=test dbname=chadtest"); + +pool.query("DROP TABLE IF EXISTS t_pool"); +pool.query("CREATE TABLE t_pool (id INT, name TEXT)"); +pool.query("INSERT INTO t_pool VALUES (1, 'alice'), (2, 'bob')"); + +const res = pool.query("SELECT id, name FROM t_pool ORDER BY id"); +if (res.rowCount !== 2) { + console.log("TEST_FAILED: rowCount was " + res.rowCount); + process.exit(1); +} +const name0 = res.getValue(0, "name"); +if (name0 !== "alice") { + console.log("TEST_FAILED: name0 was " + name0); + process.exit(1); +} + +pool.query("DROP TABLE t_pool"); +pool.end(); +console.log("TEST_PASSED"); From f4f8150fb8a6d8546437caf4ffa0c6ccd051bdb5 Mon Sep 17 00:00:00 2001 From: cs01 Date: Tue, 14 Apr 2026 21:31:35 -0700 Subject: [PATCH 6/6] postgres: fix stage 2 self-hosting regression via minimal src surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit step 2 added a usesPostgres field to LLVMGenerator plus a cs_pg_ prefix check in generateExternDeclaration plus a trailing-return workaround in parser-native/transformer.ts. any of those shifted enough state that the native stage 1 compiler began miscompiling generateGlobalVariableDeclarations — storing a pointer into a double slot for isClassInstance. --quick skipped stage 2 so it passed local verify but failed CI. fix: do the -lpq/pg-bridge.o detection entirely in compiler.ts and native-compiler-lib.ts by walking generator.declaredExternFunctions for anything starting with cs_pg_. no changes to llvm-generator.ts, generator-context.ts, or parser-native/transformer.ts. feature is functionally identical end-to-end (verified against local postgres). npm run verify (full, including stage 2) is green on this commit. --- src/codegen/infrastructure/generator-context.ts | 9 --------- src/codegen/llvm-generator.ts | 9 --------- src/compiler.ts | 13 ++++++++++--- src/native-compiler-lib.ts | 15 +++++++++++---- src/parser-native/transformer.ts | 1 - 5 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index 1b5adc7d..6c8054a1 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -800,8 +800,6 @@ export interface IGeneratorContext { getUsesTimers(): boolean; setUsesTreeSitter(value: boolean): void; setUsesSqlite(value: boolean): void; - setUsesPostgres(value: boolean): void; - getUsesPostgres(): boolean; setUsesCurl(value: boolean): void; setUsesOs(value: boolean): void; setUsesUvHrtime(value: boolean): void; @@ -1036,7 +1034,6 @@ export class MockGeneratorContext implements IGeneratorContext { public usesPromises: number = 0; public usesTimers: number = 0; public usesSqlite: number = 0; - public usesPostgres: number = 0; public usesCurl: number = 0; public usesUvHrtime: number = 0; public usesCrypto: number = 0; @@ -1241,12 +1238,6 @@ export class MockGeneratorContext implements IGeneratorContext { setUsesSqlite(value: boolean): void { this.usesSqlite = value ? 1 : 0; } - setUsesPostgres(value: boolean): void { - this.usesPostgres = value ? 1 : 0; - } - getUsesPostgres(): boolean { - return this.usesPostgres !== 0; - } setUsesCurl(value: boolean): void { this.usesCurl = value ? 1 : 0; } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index c9f5a23d..d0072241 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -265,7 +265,6 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { public usesStringBuilder: number = 0; public usesLLVM: number = 0; public usesLLD: number = 0; - public usesPostgres: number = 0; public usesCompression: number = 0; public usesYaml: number = 0; private stringBuilderSlen: Map = new Map(); @@ -993,12 +992,6 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { public getUsesLLD(): boolean { return this.usesLLD !== 0; } - public setUsesPostgres(value: boolean): void { - this.usesPostgres = value ? 1 : 0; - } - public getUsesPostgres(): boolean { - return this.usesPostgres !== 0; - } public setUsesCompression(value: boolean): void { this.usesCompression = value ? 1 : 0; } @@ -1415,7 +1408,6 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { this.usesAsyncFs = 0; this.usesGC = 0; this.usesMathRandom = 0; - this.usesPostgres = 0; this.ast = ast; @@ -3387,7 +3379,6 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { this.declaredExternFunctions.push(func.name); if (func.name.startsWith("cs_llvm_")) this.usesLLVM = 1; if (func.name.startsWith("cs_lld_")) this.usesLLD = 1; - if (func.name.startsWith("cs_pg_")) this.usesPostgres = 1; return `declare ${retType} @${func.name}(${paramLlvmTypes.join(", ")})\n`; } diff --git a/src/compiler.ts b/src/compiler.ts index c6e19ce1..58cb1689 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -406,7 +406,14 @@ export function compile( if (generator.usesSqlite) { linkLibs += " -lsqlite3"; } - if (generator.usesPostgres) { + let usesPostgres = false; + for (let i = 0; i < generator.declaredExternFunctions.length; i++) { + if (generator.declaredExternFunctions[i].startsWith("cs_pg_")) { + usesPostgres = true; + break; + } + } + if (usesPostgres) { linkLibs += " -lpq"; } if (generator.usesHttpServer) { @@ -433,7 +440,7 @@ export function compile( if (generator.usesSqlite) { linkLibs = `-L${brewPrefix}/sqlite/lib ` + linkLibs; } - if (generator.usesPostgres) { + if (usesPostgres) { linkLibs = `-L${brewPrefix}/libpq/lib ` + linkLibs; } if (generator.usesHttpServer || generator.usesCompression) { @@ -464,7 +471,7 @@ export function compile( const arenaBridgeObj = `${bridgePath}/arena-bridge.o`; const cpSpawnObj = generator.getUsesSpawn() ? `${bridgePath}/child-process-spawn.o` : ""; const curlBridgeObj = generator.usesCurl ? `${bridgePath}/curl-bridge.o` : ""; - const pgBridgeObj = generator.usesPostgres ? `${bridgePath}/pg-bridge.o` : ""; + const pgBridgeObj = usesPostgres ? `${bridgePath}/pg-bridge.o` : ""; const compressBridgeObj = generator.usesCompression ? `${bridgePath}/compress-bridge.o` : ""; const yamlBridgeObj = generator.usesYaml ? `${bridgePath}/yaml-bridge.o` : ""; let extraObjs = ""; diff --git a/src/native-compiler-lib.ts b/src/native-compiler-lib.ts index d54e3138..d452e140 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -664,7 +664,14 @@ export function compileNative(inputFile: string, outputFile: string): void { if (generator.getUsesSqlite()) { linkLibs = "-lsqlite3 " + linkLibs; } - if (generator.getUsesPostgres()) { + let usesPostgres: boolean = false; + for (let i = 0; i < generator.declaredExternFunctions.length; i++) { + if (generator.declaredExternFunctions[i].startsWith("cs_pg_")) { + usesPostgres = true; + break; + } + } + if (usesPostgres) { linkLibs = "-lpq " + linkLibs; } if (generator.getUsesHttpServer()) { @@ -698,7 +705,7 @@ export function compileNative(inputFile: string, outputFile: string): void { const arenaBridgeObj = effectiveBridgePath + "/arena-bridge.o"; const cpSpawnObj = generator.getUsesSpawn() ? effectiveBridgePath + "/child-process-spawn.o" : ""; const curlBridgeObj = generator.getUsesCurl() ? effectiveBridgePath + "/curl-bridge.o" : ""; - const pgBridgeObj = generator.getUsesPostgres() ? effectiveBridgePath + "/pg-bridge.o" : ""; + const pgBridgeObj = usesPostgres ? effectiveBridgePath + "/pg-bridge.o" : ""; const compressBridgeObj = generator.getUsesCompression() ? effectiveBridgePath + "/compress-bridge.o" : ""; @@ -825,7 +832,7 @@ export function compileNative(inputFile: string, outputFile: string): void { if (fs.existsSync("/usr/local/opt/sqlite/lib")) linkLibs = "-L/usr/local/opt/sqlite/lib " + linkLibs; } - if (generator.getUsesPostgres()) { + if (usesPostgres) { if (fs.existsSync("/opt/homebrew/opt/libpq/lib")) linkLibs = "-L/opt/homebrew/opt/libpq/lib " + linkLibs; if (fs.existsSync("/usr/local/opt/libpq/lib")) @@ -906,7 +913,7 @@ export function compileNative(inputFile: string, outputFile: string): void { if (fs.existsSync("/usr/local/opt/sqlite/lib")) linkLibs = "-L/usr/local/opt/sqlite/lib " + linkLibs; } - if (generator.getUsesPostgres()) { + if (usesPostgres) { if (fs.existsSync("/opt/homebrew/opt/libpq/lib")) linkLibs = "-L/opt/homebrew/opt/libpq/lib " + linkLibs; if (fs.existsSync("/usr/local/opt/libpq/lib")) diff --git a/src/parser-native/transformer.ts b/src/parser-native/transformer.ts index 5341a67c..7fc7d261 100644 --- a/src/parser-native/transformer.ts +++ b/src/parser-native/transformer.ts @@ -614,7 +614,6 @@ function transformExpression(node: TreeSitterNode): Expression { default: return { type: "variable", name: "undefined" }; } - return { type: "variable", name: "undefined" }; } // ============================================