Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions c_bridges/pg-bridge.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
#include <libpq-fe.h>
#include <stdlib.h>
#include <string.h>

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;
}
2 changes: 2 additions & 0 deletions docs/stdlib/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
147 changes: 147 additions & 0 deletions docs/stdlib/postgres.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading