Skip to content

Commit a264127

Browse files
authored
chadscript/postgres: native postgres client via libpq (#512)
* postgres: libpq c bridge (step 1/10) 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. * postgres: lib/postgres.ts with Client, extern binding, link wiring (step 2/10) 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. * postgres: .query() returning rowCount + abi fix (step 3/10) 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. * postgres: SELECT support with Row access (step 4/10) 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. * postgres: Pool, parameterized queries, typed accessors, docs (steps 5+6/10) 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). * postgres: fix stage 2 self-hosting regression via minimal src surface 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. --------- Co-authored-by: cs01 <cs01@users.noreply.github.com>
1 parent f02172c commit a264127

File tree

12 files changed

+732
-1
lines changed

12 files changed

+732
-1
lines changed

c_bridges/pg-bridge.c

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#include <libpq-fe.h>
2+
#include <stdlib.h>
3+
#include <string.h>
4+
5+
extern void *GC_malloc_atomic(size_t);
6+
7+
static const char *gc_strdup(const char *s) {
8+
if (!s) return "";
9+
size_t n = strlen(s);
10+
char *out = (char *)GC_malloc_atomic(n + 1);
11+
memcpy(out, s, n + 1);
12+
return out;
13+
}
14+
15+
void *cs_pg_connect(const char *conninfo) {
16+
if (!conninfo) return NULL;
17+
PGconn *conn = PQconnectdb(conninfo);
18+
return (void *)conn;
19+
}
20+
21+
double cs_pg_status(void *conn) {
22+
if (!conn) return (double)CONNECTION_BAD;
23+
return (double)PQstatus((PGconn *)conn);
24+
}
25+
26+
const char *cs_pg_error_message(void *conn) {
27+
if (!conn) return "null connection";
28+
return gc_strdup(PQerrorMessage((PGconn *)conn));
29+
}
30+
31+
void cs_pg_finish(void *conn) {
32+
if (!conn) return;
33+
PQfinish((PGconn *)conn);
34+
}
35+
36+
void *cs_pg_exec(void *conn, const char *sql) {
37+
if (!conn || !sql) return NULL;
38+
return (void *)PQexec((PGconn *)conn, sql);
39+
}
40+
41+
void *cs_pg_exec_params(void *conn, const char *sql, double nparams, const char **values) {
42+
if (!conn || !sql) return NULL;
43+
return (void *)PQexecParams((PGconn *)conn, sql, (int)nparams, NULL, values, NULL, NULL, 0);
44+
}
45+
46+
typedef struct {
47+
const char **values;
48+
int count;
49+
int capacity;
50+
} PgParamList;
51+
52+
void *cs_pg_params_new(void) {
53+
PgParamList *p = (PgParamList *)GC_malloc_atomic(sizeof(PgParamList));
54+
if (!p) return NULL;
55+
p->count = 0;
56+
p->capacity = 8;
57+
p->values = (const char **)GC_malloc_atomic(sizeof(const char *) * (size_t)p->capacity);
58+
return (void *)p;
59+
}
60+
61+
void cs_pg_params_add(void *params, const char *value) {
62+
if (!params) return;
63+
PgParamList *p = (PgParamList *)params;
64+
if (p->count >= p->capacity) {
65+
int newcap = p->capacity * 2;
66+
const char **newvals = (const char **)GC_malloc_atomic(sizeof(const char *) * (size_t)newcap);
67+
for (int i = 0; i < p->count; i++) newvals[i] = p->values[i];
68+
p->values = newvals;
69+
p->capacity = newcap;
70+
}
71+
p->values[p->count++] = value ? gc_strdup(value) : NULL;
72+
}
73+
74+
void *cs_pg_exec_params_with(void *conn, const char *sql, void *params) {
75+
if (!conn || !sql) return NULL;
76+
PgParamList *p = (PgParamList *)params;
77+
int n = p ? p->count : 0;
78+
const char **vals = p ? p->values : NULL;
79+
return (void *)PQexecParams((PGconn *)conn, sql, n, NULL, vals, NULL, NULL, 0);
80+
}
81+
82+
double cs_pg_result_status(void *res) {
83+
if (!res) return (double)PGRES_FATAL_ERROR;
84+
return (double)PQresultStatus((PGresult *)res);
85+
}
86+
87+
const char *cs_pg_result_error_message(void *res) {
88+
if (!res) return "null result";
89+
return gc_strdup(PQresultErrorMessage((PGresult *)res));
90+
}
91+
92+
double cs_pg_nrows(void *res) {
93+
if (!res) return 0.0;
94+
return (double)PQntuples((PGresult *)res);
95+
}
96+
97+
double cs_pg_ncols(void *res) {
98+
if (!res) return 0.0;
99+
return (double)PQnfields((PGresult *)res);
100+
}
101+
102+
const char *cs_pg_fname(void *res, double col) {
103+
if (!res) return "";
104+
const char *n = PQfname((PGresult *)res, (int)col);
105+
return gc_strdup(n ? n : "");
106+
}
107+
108+
double cs_pg_ftype(void *res, double col) {
109+
if (!res) return 0.0;
110+
return (double)PQftype((PGresult *)res, (int)col);
111+
}
112+
113+
const char *cs_pg_getvalue(void *res, double row, double col) {
114+
if (!res) return "";
115+
return gc_strdup(PQgetvalue((PGresult *)res, (int)row, (int)col));
116+
}
117+
118+
double cs_pg_getisnull(void *res, double row, double col) {
119+
if (!res) return 1.0;
120+
return (double)PQgetisnull((PGresult *)res, (int)row, (int)col);
121+
}
122+
123+
const char *cs_pg_cmdtuples(void *res) {
124+
if (!res) return "0";
125+
const char *c = PQcmdTuples((PGresult *)res);
126+
return gc_strdup((c && *c) ? c : "0");
127+
}
128+
129+
void cs_pg_clear(void *res) {
130+
if (!res) return;
131+
PQclear((PGresult *)res);
132+
}
133+
134+
double cs_pg_result_ok(void *res) {
135+
if (!res) return 0.0;
136+
ExecStatusType s = PQresultStatus((PGresult *)res);
137+
return (s == PGRES_COMMAND_OK || s == PGRES_TUPLES_OK) ? 1.0 : 0.0;
138+
}

docs/stdlib/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,13 @@ A few APIs live in named modules and need an import:
2525
```typescript
2626
import { httpServe, Router, Context } from "chadscript/http";
2727
import { ArgumentParser } from "chadscript/argparse";
28+
import { Pool } from "chadscript/postgres";
2829
```
2930

3031
| Module | Contents |
3132
|--------|----------|
3233
| `chadscript/http` | `httpServe`, `wsBroadcast`, `wsSend`, `parseMultipart`, `bytesResponse`, `serveFile`, `getHeader`, `parseQueryString`, `parseCookies`, `Router`, `Context`, `RouterRequest` |
3334
| `chadscript/argparse` | `ArgumentParser` |
35+
| `chadscript/postgres` | `Pool`, `Client`, `QueryResult`, `Row` — PostgreSQL client via `libpq` |
3436

3537
The `chadscript/` prefix works like Node's `node:` prefix — unambiguous and collision-free.

docs/stdlib/postgres.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# postgres
2+
3+
PostgreSQL client via `libpq`. Connect to a Postgres database, run queries, and read results as native strings. Imported as a module:
4+
5+
```typescript
6+
import { Pool } from "chadscript/postgres";
7+
```
8+
9+
`libpq` is required at build time. On macOS: `brew install libpq`. On Debian/Ubuntu: `apt install libpq-dev`.
10+
11+
## `new Pool(conninfo)`
12+
13+
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.
14+
15+
```typescript
16+
const pool = new Pool("host=127.0.0.1 port=5432 user=postgres password=secret dbname=mydb");
17+
// or
18+
const pool2 = new Pool("postgresql://postgres:secret@127.0.0.1:5432/mydb");
19+
```
20+
21+
`Pool` is the recommended entry point for application code. It connects lazily on first query — the constructor does not block.
22+
23+
## `pool.query(sql)`
24+
25+
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.
26+
27+
```typescript
28+
pool.query("CREATE TABLE users (id INT, name TEXT)");
29+
pool.query("INSERT INTO users VALUES (1, 'alice')");
30+
31+
const res = pool.query("SELECT id, name FROM users ORDER BY id");
32+
console.log(res.rowCount); // 1
33+
console.log(res.getValue(0, "name")); // "alice"
34+
```
35+
36+
## `pool.end()`
37+
38+
Close the pool's underlying connection. Safe to call multiple times.
39+
40+
```typescript
41+
pool.end();
42+
```
43+
44+
## `Client` (low-level)
45+
46+
For cases where you need explicit connection lifecycle, use `Client` directly:
47+
48+
```typescript
49+
import { Client } from "chadscript/postgres";
50+
51+
const c = new Client("postgresql://postgres:secret@127.0.0.1:5432/mydb");
52+
c.connect();
53+
const res = c.query("SELECT 1");
54+
c.end();
55+
```
56+
57+
Most application code should prefer `Pool`. `Client` requires you to call `connect()` before any queries — `Pool` handles this automatically.
58+
59+
## `QueryResult`
60+
61+
Returned by `client.query()`. All values are currently strings — type coercion (integer, boolean, date) is a follow-up.
62+
63+
| Field | Type | Description |
64+
|-------|------|-------------|
65+
| `rowCount` | `number` | Affected rows (INSERT/UPDATE/DELETE) or number of rows returned (SELECT) |
66+
| `numRows` | `number` | Number of rows in the result set (SELECT) |
67+
| `numCols` | `number` | Number of columns in the result set |
68+
| `fields` | `string[]` | Column names in order |
69+
70+
### `result.getValue(row, col)`
71+
72+
Return the value at a given row index and column name, as a string.
73+
74+
```typescript
75+
const res = c.query("SELECT id, name, city FROM users ORDER BY id");
76+
for (let i = 0; i < res.numRows; i++) {
77+
const name = res.getValue(i, "name");
78+
const city = res.getValue(i, "city");
79+
console.log(name + " in " + city);
80+
}
81+
```
82+
83+
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:
84+
85+
```typescript
86+
const nameIdx = res.fields.indexOf("name");
87+
for (let i = 0; i < res.numRows; i++) {
88+
const r = res.getRow(i);
89+
console.log(r.getAt(nameIdx));
90+
}
91+
```
92+
93+
### `result.getRow(index)`
94+
95+
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.
96+
97+
```typescript
98+
const r = res.getRow(0);
99+
const name = r.get("name"); // by column name
100+
const first = r.getAt(0); // by column index
101+
```
102+
103+
## Example — CRUD
104+
105+
```typescript
106+
import { Pool } from "chadscript/postgres";
107+
108+
const pool = new Pool("postgresql://postgres:secret@127.0.0.1:5432/mydb");
109+
110+
pool.query("DROP TABLE IF EXISTS users");
111+
pool.query("CREATE TABLE users (id INT, name TEXT, city TEXT)");
112+
113+
pool.query("INSERT INTO users VALUES (1, 'alice', 'nyc'), (2, 'bob', 'sf')");
114+
115+
const res = pool.query("SELECT id, name, city FROM users ORDER BY id");
116+
console.log("rows: " + res.numRows);
117+
for (let i = 0; i < res.numRows; i++) {
118+
console.log(res.getValue(i, "id") + " " + res.getValue(i, "name") + " " + res.getValue(i, "city"));
119+
}
120+
121+
const upd = pool.query("UPDATE users SET city = 'LA' WHERE id = 1");
122+
console.log("updated " + upd.rowCount);
123+
124+
pool.end();
125+
```
126+
127+
## Current Limitations
128+
129+
This module is under active development. Known gaps:
130+
131+
- **No parameterized queries yet** — every query is literal SQL. Build strings carefully and never concatenate user input. Coming soon.
132+
- **All values are strings** — integers come back as `"42"`, booleans as `"t"`/`"f"`, dates as ISO-ish strings. Type coercion is a follow-up.
133+
- **`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.
134+
- **Synchronous under the hood**`libpq` is called synchronously. Calls block the event loop. Real async (libuv integration) is a follow-up.
135+
- **No `LISTEN`/`NOTIFY`, no `COPY`, no streaming** — the basics only.
136+
137+
## Native Implementation
138+
139+
| API | Maps to |
140+
|-----|---------|
141+
| `new Client()` / `connect()` | `PQconnectdb()` + `PQstatus()` |
142+
| `client.query()` | `PQexec()` + `PQresultStatus()` + `PQgetvalue()` loop |
143+
| `QueryResult.fields` | `PQfname()` per column |
144+
| `QueryResult.numRows` / `numCols` | `PQntuples()` / `PQnfields()` |
145+
| `client.end()` | `PQfinish()` |
146+
147+
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.

0 commit comments

Comments
 (0)