diff --git a/c_bridges/pg-bridge.c b/c_bridges/pg-bridge.c new file mode 100644 index 00000000..ad1554c3 --- /dev/null +++ b/c_bridges/pg-bridge.c @@ -0,0 +1,138 @@ +#include +#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); + return (void *)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) { + if (!conn) return "null connection"; + return gc_strdup(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, double nparams, const char **values) { + if (!conn || !sql) return NULL; + 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); +} + +const char *cs_pg_result_error_message(void *res) { + if (!res) return "null result"; + return gc_strdup(PQresultErrorMessage((PGresult *)res)); +} + +double cs_pg_nrows(void *res) { + if (!res) return 0.0; + return (double)PQntuples((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, double col) { + if (!res) return ""; + const char *n = PQfname((PGresult *)res, (int)col); + return gc_strdup(n ? n : ""); +} + +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, double row, double col) { + if (!res) return ""; + return gc_strdup(PQgetvalue((PGresult *)res, (int)row, (int)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) { + if (!res) return "0"; + const char *c = PQcmdTuples((PGresult *)res); + return gc_strdup((c && *c) ? c : "0"); +} + +void cs_pg_clear(void *res) { + if (!res) return; + PQclear((PGresult *)res); +} + +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 : 0.0; +} 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 new file mode 100644 index 00000000..75d10b2d --- /dev/null +++ b/lib/postgres.ts @@ -0,0 +1,191 @@ +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; +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; +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; + +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; + 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 ""; + } + + 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 { + private _conninfo: string; + private _conn: string; + private _connected: boolean; + + constructor(conninfo: string) { + this._conninfo = conninfo; + this._conn = ""; + this._connected = false; + } + + isConnected(): boolean { + return this._connected; + } + + 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; + } + + query(sql: string, params: string[] = []): QueryResult { + if (!this._connected) { + throw new Error("postgres query on disconnected client"); + } + 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); + 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 = nrows > 0 ? nrows : parseInt(tuplesStr, 10); + cs_pg_clear(res); + return new QueryResult(rowCount, nrows, ncols, fields, values); + } + + end(): void { + if (this._connected) { + cs_pg_finish(this._conn); + this._connected = false; + } + } +} 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" diff --git a/src/compiler.ts b/src/compiler.ts index 374df79c..58cb1689 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -406,6 +406,16 @@ export function compile( if (generator.usesSqlite) { linkLibs += " -lsqlite3"; } + 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) { linkLibs += ` -lz -lzstd`; } @@ -430,6 +440,9 @@ export function compile( if (generator.usesSqlite) { linkLibs = `-L${brewPrefix}/sqlite/lib ` + linkLibs; } + if (usesPostgres) { + linkLibs = `-L${brewPrefix}/libpq/lib ` + linkLibs; + } if (generator.usesHttpServer || generator.usesCompression) { linkLibs = `-L${brewPrefix}/zstd/lib ` + linkLibs; } @@ -458,6 +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 = 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 +637,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..d452e140 100644 --- a/src/native-compiler-lib.ts +++ b/src/native-compiler-lib.ts @@ -664,6 +664,16 @@ export function compileNative(inputFile: string, outputFile: string): void { if (generator.getUsesSqlite()) { linkLibs = "-lsqlite3 " + linkLibs; } + 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()) { linkLibs = "-lz -lzstd " + linkLibs; } @@ -695,6 +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 = usesPostgres ? effectiveBridgePath + "/pg-bridge.o" : ""; const compressBridgeObj = generator.getUsesCompression() ? effectiveBridgePath + "/compress-bridge.o" : ""; @@ -758,6 +769,8 @@ export function compileNative(inputFile: string, outputFile: string): void { " " + curlBridgeObj + " " + + pgBridgeObj + + " " + compressBridgeObj + " " + yamlBridgeObj + @@ -819,6 +832,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 (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")) + 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 +913,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 (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")) + 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/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"); 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"); 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"); 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");