diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9a75de4..a86b53f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,7 +75,9 @@ jobs: run: nimble test -y - name: Install async_postgres - run: nimble install -y + run: | + rm -rf ~/.nimble/pkgs2/async_postgres-* + nimble install -y - name: Compile examples (asyncdispatch) run: for f in examples/*.nim; do nim c "$f"; done diff --git a/examples/large_object.nim b/examples/large_object.nim index ef0166d..4157139 100644 --- a/examples/large_object.nim +++ b/examples/large_object.nim @@ -37,7 +37,7 @@ proc main() {.async.} = let size = await lo.loSize() echo "Size: ", size, " bytes" - # Streaming read + # Streaming read, then clean up in the same transaction conn.withTransaction: conn.withLargeObject(lo, oid, INV_READ): echo "\nStreaming read:" @@ -45,8 +45,6 @@ proc main() {.async.} = echo " chunk (", data.len, " bytes): ", data.toString() await lo.loReadStream(cb, chunkSize = 10) - # Clean up - conn.withTransaction: await conn.loUnlink(oid) echo "\nDeleted Large Object: oid=", oid diff --git a/examples/pipeline.nim b/examples/pipeline.nim index 135c4cd..ecb7271 100644 --- a/examples/pipeline.nim +++ b/examples/pipeline.nim @@ -49,8 +49,8 @@ proc main() {.async.} = echo "Operation ", i, " (", r.queryResult.rowCount, " rows):" for row in r.queryResult.rows: var cols: seq[string] - for j in 0 ..< r.queryResult.fields.len: - cols.add(r.queryResult.fields[j].name & "=" & row.getStr(j)) + for field in r.queryResult.fields: + cols.add(field.name & "=" & row.getStr(field.name)) echo " ", cols.join(", ") waitFor main() diff --git a/examples/pool.nim b/examples/pool.nim index 10be154..1192e9e 100644 --- a/examples/pool.nim +++ b/examples/pool.nim @@ -9,16 +9,16 @@ import pkg/async_postgres proc main() {.async.} = - let connConfig = ConnConfig( - host: "127.0.0.1", - port: 15432, - user: "test", - password: "test", - database: "test", - sslMode: sslDisable, + let connConfig = initConnConfig( + host = "127.0.0.1", + port = 15432, + user = "test", + password = "test", + database = "test", + sslMode = sslDisable, ) - let pool = await newPool(PoolConfig(connConfig: connConfig, minSize: 2, maxSize: 5)) + let pool = await newPool(initPoolConfig(connConfig, minSize = 2, maxSize = 5)) defer: await pool.close() @@ -41,9 +41,10 @@ proc main() {.async.} = # Run concurrent queries using withConnection proc countExpensive(): Future[int64] {.async.} = + let minPrice = 150'i32 pool.withConnection(conn): return await conn.queryValue( - int64, "SELECT count(*) FROM products WHERE price > $1", @[toPgParam(150'i32)] + int64, sql"SELECT count(*) FROM products WHERE price > {minPrice}" ) proc cheapest(): Future[string] {.async.} = diff --git a/examples/pool_cluster.nim b/examples/pool_cluster.nim index 0e1b76f..00bfe00 100644 --- a/examples/pool_cluster.nim +++ b/examples/pool_cluster.nim @@ -10,17 +10,17 @@ import pkg/async_postgres proc main() {.async.} = - let connConfig = ConnConfig( - host: "127.0.0.1", - port: 15432, - user: "test", - password: "test", - database: "test", - sslMode: sslDisable, + let connConfig = initConnConfig( + host = "127.0.0.1", + port = 15432, + user = "test", + password = "test", + database = "test", + sslMode = sslDisable, ) - let primaryConfig = PoolConfig(connConfig: connConfig, minSize: 1, maxSize: 3) - let replicaConfig = PoolConfig(connConfig: connConfig, minSize: 1, maxSize: 3) + let primaryConfig = initPoolConfig(connConfig, minSize = 1, maxSize = 3) + let replicaConfig = initPoolConfig(connConfig, minSize = 1, maxSize = 3) # fallbackPrimary: if replica is unavailable, fall back to primary for reads let cluster = diff --git a/examples/query_direct.nim b/examples/query_direct.nim new file mode 100644 index 0000000..74977e9 --- /dev/null +++ b/examples/query_direct.nim @@ -0,0 +1,69 @@ +## Zero-allocation query macros. +## +## Demonstrates `queryDirect` and `execDirect`, which encode parameters into +## the connection's send buffer at compile time. They avoid the intermediate +## `seq[PgParam]` and per-parameter `seq[byte]` allocations that the regular +## `query` / `exec` path incurs, making them suitable for hot paths where +## SQL is a literal and parameters are scalars. +## +## Constraints (vs. regular `query` / `exec`): +## - SQL must be a string literal or compile-time constant. +## - Arguments are positional (`$1, $2, …`) — no `{expr}` sugar. +## - No `timeout` parameter. +## - Same per-connection statement cache as `query` / `exec`; the statement +## is parsed once server-side and rebound on subsequent calls. +## +## Usage: +## nim c -r examples/query_direct.nim + +import pkg/async_postgres + +const Dsn = "postgresql://test:test@127.0.0.1:15432/test?sslmode=disable" + +proc main() {.async.} = + let conn = await connect(Dsn) + defer: + await conn.close() + + discard await conn.exec( + """ + CREATE TEMP TABLE metrics ( + id serial PRIMARY KEY, + host text NOT NULL, + cpu float8 NOT NULL, + ts int8 NOT NULL + ) + """ + ) + + # execDirect: zero-alloc INSERT in a tight loop. The SQL literal is parsed + # once (server-side plan cached), and each call re-binds scalars directly + # into the send buffer without heap allocations for the parameter list. + let host = "worker-1" + for i in 0 ..< 5: + let cpu = 0.1 * float64(i) + let ts = int64(1_700_000_000 + i) + discard await conn.execDirect( + "INSERT INTO metrics (host, cpu, ts) VALUES ($1, $2, $3)", host, cpu, ts + ) + + # queryDirect: zero-alloc read. Returns the same QueryResult shape as `query`. + let threshold = 0.2'f64 + let qr = await conn.queryDirect( + "SELECT host, cpu, ts FROM metrics WHERE cpu >= $1 ORDER BY ts", threshold + ) + echo "Rows above ", threshold, ":" + for row in qr.rows: + echo " host=", + row.getStr("host"), " cpu=", row.getStr("cpu"), " ts=", row.getInt("ts") + + # queryDirect drives `queryValue` the same way: wrap it manually since + # `queryValue[T]` takes a regular `seq[PgParam]`. For a single scalar, + # `queryDirect` + `rows[0].get(0, T)` is zero-alloc end-to-end. + let targetHost = "worker-1" + let count = await conn.queryDirect( + "SELECT count(*)::int8 FROM metrics WHERE host = $1", targetHost + ) + echo "Rows for ", targetHost, ": ", count.rows[0].get(0, int64) + +waitFor main() diff --git a/examples/query_variants.nim b/examples/query_variants.nim new file mode 100644 index 0000000..7b67a82 --- /dev/null +++ b/examples/query_variants.nim @@ -0,0 +1,99 @@ +## Query variants example. +## +## Demonstrates query convenience procs beyond the basic `query` / `exec` / +## `queryValue` entry points: optional-result accessors, existence checks, +## row callbacks, and the simple-protocol variants for session-level commands +## or multi-statement batches. +## +## Usage: +## nim c -r examples/query_variants.nim + +import std/options + +import pkg/async_postgres + +const Dsn = "postgresql://test:test@127.0.0.1:15432/test?sslmode=disable" + +proc main() {.async.} = + let conn = await connect(Dsn) + defer: + await conn.close() + + # simpleExec: session-level command via the simple query protocol. + # Appropriate for SET / VACUUM / single-shot DDL where the extended + # protocol's Parse/Bind round trip and plan cache entry would be wasted. + discard await conn.simpleExec("SET TIME ZONE 'UTC'") + + discard await conn.exec( + """ + CREATE TEMP TABLE employees ( + id serial PRIMARY KEY, + name text NOT NULL, + team text, + salary int4 + ) + """ + ) + discard await conn.exec( + """ + INSERT INTO employees (name, team, salary) VALUES + ('Alice', 'platform', 800), + ('Bob', 'platform', 650), + ('Charlie', 'data', NULL), + ('Dave', NULL, 700) + """ + ) + + # queryExists: boolean "does any row match" without fetching data. + let dataTeam = "data" + let hasData = + await conn.queryExists(sql"SELECT 1 FROM employees WHERE team = {dataTeam}") + echo "Has data team? ", hasData + + # queryValueOrDefault: return a fallback when the row or value is NULL. + # Here Charlie has no salary, so the default kicks in. + let charlieName = "Charlie" + let charlieSalary = await conn.queryValueOrDefault( + int32, sql"SELECT salary FROM employees WHERE name = {charlieName}", default = 0'i32 + ) + echo "Charlie's salary (0 = unknown): ", charlieSalary + + # queryValueOpt: distinguish "no row / NULL" from a real value. + let daveName = "Dave" + let maybeTeam = + await conn.queryValueOpt(sql"SELECT team FROM employees WHERE name = {daveName}") + if maybeTeam.isSome: + echo "Dave's team: ", maybeTeam.get + else: + echo "Dave has no team" + + # queryRowOpt: single-row lookup that tolerates a missing row. + let nobody = "Nobody" + let missing = await conn.queryRowOpt( + sql"SELECT name, salary FROM employees WHERE name = {nobody}" + ) + echo "Missing lookup isSome: ", missing.isSome + + # queryColumn: collect one column across all rows as `seq[string]`. + let names = await conn.queryColumn("SELECT name FROM employees ORDER BY id") + echo "Names: ", names + + # queryEach: stream rows through a callback without building a QueryResult. + # The Row is only valid during the callback — copy out what you need. + var totalSalary = 0'i64 + let rowCb: RowCallback = proc(row: Row) = + if not row.isNull("salary"): + totalSalary += row.getInt("salary") + let processed = await conn.queryEach("SELECT salary FROM employees", callback = rowCb) + echo "Processed ", processed, " rows, total salary: ", totalSalary + + # simpleQuery: multiple `;`-separated statements in one round trip. + # Returns one QueryResult per statement. Handy for read-only introspection + # batches; not usable for parameterised queries. + let multi = await conn.simpleQuery( + "SELECT count(*) FROM employees; SELECT count(DISTINCT team) FROM employees" + ) + echo "Total employees: ", multi[0].rows[0].getStr(0) + echo "Distinct teams: ", multi[1].rows[0].getStr(0) + +waitFor main()