A wrapper around Postgres SQL database that can be used by Cxy applications
A PostgreSQL client library for Cxy that wraps libpq with a high-level, type-safe API.
Add to your Cxyfile.yaml:
dependencies:
- name: postgres
version: 0.1.0
Then install:
cxy package install
For compiling your Cxy application with the @postgres package:
brew install postgresqlapt-get install libpq-devapk add libpq-devFor running the compiled binary, you only need the PostgreSQL client libraries (not the -dev packages):
brew install libpq (or postgresql - same thing)apt-get install libpq5apk add libpqDocker Multi-Stage Build Example:
# Build stage
FROM alpine:latest AS builder
RUN apk add --no-cache libpq-dev build-base cxy
COPY . /app
WORKDIR /app
RUN cxy build --release
# Runtime stage - much smaller!
FROM alpine:latest
RUN apk add --no-cache libpq
COPY --from=builder /app/build/myapp /usr/local/bin/
CMD ["myapp"]
Note: The -dev packages include headers and static libraries needed for compilation. Runtime containers only need the shared libraries (.so files).
For local development and testing, use the included docker-compose.yml:
# Start PostgreSQL
docker-compose up -d
# Check status
docker-compose ps
# View logs
docker-compose logs -f
# Stop and remove
docker-compose down
# Stop and remove data volume
docker-compose down -v
The docker-compose setup provides:
testdbpostgres / Password: postgres54320 (mapped to avoid conflicts with local PostgreSQL)import { Database } from "@postgres"
func main() {
// Open database connection
var db = Database.open("host=localhost dbname=mydb user=postgres password=secret") catch {
println("Connection failed: ", ex!)
return
}
// Execute a query
var result = db.connection().exec("SELECT * FROM users WHERE id = $1", 1) catch {
println("Query failed: ", ex!)
return
}
// Read results
while result.next() {
var id = result.column[i64](0) catch 0
var name = result.column[String](1) catch "unknown"
println(f"User {id}: {name}")
}
}
import { Database } from "@postgres"
// URI format (recommended)
var db = Database.open("postgresql://user:pass@localhost/mydb")
var db = Database.open("postgresql://user:pass@localhost:54320/mydb")
var db = Database.open("postgres://user@localhost/mydb") // No password
// Key-value format
var db = Database.open("host=localhost dbname=mydb user=postgres password=secret")
var db = Database.open("host=192.168.1.100 port=5433 dbname=mydb user=admin")
// With all options
var db = Database.open(
"postgresql://user:pass@localhost/mydb",
"public", // schema name (default: "public")
false, // async mode (default: false)
5000, // timeout in ms (default: -1 = no timeout)
60000 // keep-alive in ms (default: -1 = no keep-alive)
) catch {
println("Failed to connect: ", ex!)
return
}
Connection String Formats:
postgresql://[user[:password]@][host][:port][/dbname][?param=value]host=localhost port=54320 dbname=mydb user=postgres password=secret// Simple query
var result = db.connection().exec("SELECT version()")
// Parameterized query
var result = db.connection().exec(
"SELECT * FROM users WHERE age > $1 AND city = $2",
18,
"New York"
)
// INSERT with auto-increment
var result = db.connection().exec(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
"Alice",
"alice@example.com"
)
var result = db.connection().exec("SELECT COUNT(*) FROM users") catch {
println("Query failed: ", ex!)
return
}
if result.next() {
var count = result.column[i64](0) catch 0
println(f"Total users: {count}")
}
var result = db.connection().exec("SELECT id, name, email FROM users") catch {
println("Query failed: ", ex!)
return
}
while result.next() {
var id = result.column[i64](0) catch 0
var name = result.column[String](1) catch "unknown"
var email = result.column[String](2) catch "no-email"
println(f"{id}: {name} <{email}>")
}
var result = db.connection().exec("SELECT id, name, active FROM users LIMIT 1") catch {
println("Query failed: ", ex!)
return
}
if result.next() {
var user = result.read[(i64, String, bool)]() catch {
println("Failed to read row: ", ex!)
return
}
println(f"User: {user.0}, {user.1}, {user.2}")
}
The library supports automatic mapping of query results to structs using reflection:
struct User {
id: i64
name: String
email: String
active: bool
created_at: String
}
var result = db.connection().exec("SELECT * FROM users WHERE id = $1", 1) catch {
println("Query failed: ", ex!)
return
}
if result.next() {
var user = result.read[User]() catch {
println("Failed to read user: ", ex!)
return
}
println(f"User: {user.name} ({user.email})")
}
import { Vector } from "stdlib/vector.cxy"
var result = db.connection().exec("SELECT * FROM users") catch {
println("Query failed: ", ex!)
return
}
var users = Vector[User]()
result >> users catch {
println("Failed to read users: ", ex!)
return
}
for user, _ in users {
println(f"{user.id}: {user.name}")
}
Use @sql attributes to customize field mapping (same as sqlite.cxy):
struct Product {
@sql(id: 0)
product_id: i64 // Maps to column 0
@sql(name: "product_name")
name: String // Maps to column named "product_name"
@sql(ignore)
cached_value: i32 // Ignored during mapping
price: f64 // Auto-mapped by field name
}
Create database tables automatically from struct definitions using createTable[T]():
struct User {
@sql(primaryKey, autoIncrement)
id: i64
@sql(notNull)
name: String
@sql(unique)
email: String
age: i32
}
var conn = db.connection()
conn.createTable[User]("users") catch {
println("Failed to create table: ", ex!)
return
}
// Creates: CREATE TABLE IF NOT EXISTS users (
// id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
// name text NOT NULL,
// email text UNIQUE,
// age integer
// )
Note: createTable can throw exceptions (e.g., permission errors, invalid schema). Always handle errors appropriately.
Use @sql attributes to control table schema generation:
struct Order {
@sql(primaryKey, autoIncrement)
id: i64
@sql(references: "customers(id)", notNull)
customer_id: i64
@sql(name: "order_total", notNull)
total: f64
@sql(ignore)
cachedData: String // Not created in database
}
Available attributes:
@sql(primaryKey) - PRIMARY KEY constraint@sql(autoIncrement) - GENERATED ALWAYS AS IDENTITY (integers only)@sql(unique) - UNIQUE constraint@sql(notNull) - NOT NULL constraint@sql(references: "table(column)") - Foreign key constraint@sql(name: "column_name") - Custom column name@sql(ignore) - Exclude from table definitionType mappings:
i16, u16 SMALLINTi32, u32 INTEGERi64, u64 BIGINTf32 REALf64 DOUBLE PRECISIONbool BOOLEANchar CHAR(1)import { PgTransaction } from "@postgres"
var conn = db.connection() catch {
println("Failed to get connection: ", ex!)
return
}
var tx = PgTransaction(&conn, true) // auto-commit = true
// Execute statements within transaction
tx.exec("INSERT INTO accounts (name, balance) VALUES ($1, $2)", "Alice", 1000.0) catch {
println("Transaction failed: ", ex!)
return
}
tx.exec("INSERT INTO accounts (name, balance) VALUES ($1, $2)", "Bob", 500.0) catch {
println("Transaction failed: ", ex!)
return
}
// Explicit commit (auto-commit on scope exit if not called)
tx.commit() catch {
println("Commit failed: ", ex!)
}
var tx = PgTransaction(&conn, false) // manual commit
tx.exec("UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice'")
tx.savepoint("sp1")
tx.exec("UPDATE accounts SET balance = balance + 100 WHERE name = 'Bob'")
// Rollback to savepoint
tx.rollback("sp1")
// Or release savepoint
tx.release("sp1")
tx.commit()
Enable async mode for non-blocking queries using Cxy's coroutine scheduler:
// Create database with async enabled
var db = Database.open(
"postgresql://user:pass@localhost/mydb",
"public",
true, // async = true
5000 // timeout = 5 seconds
)
// Queries automatically use async I/O
async {
var result = db.connection().exec("SELECT * FROM large_table")
while (result.next()) {
// Process results
}
}
The Database class automatically manages a connection pool:
var db = Database.open(
"postgresql://user:pass@localhost/mydb",
"public",
false,
-1,
60000 // keep-alive: 60 seconds
)
// Get connection from pool
var conn = db.connection()
// Use connection
var result = conn.exec("SELECT * FROM users")
// Connection is automatically returned to pool when `conn` goes out of scope
// Idle connections are cleaned up after keep-alive timeout
Check pool size:
println(f"Pool size: {db.poolSize()}")
i8, i16, i32, i64, u8, u16, u32, u64f32, f64string, String, __stringboolcharAll parameter types plus:
i32?, String?, etc. (maps NULL values)(i64, String, bool)import { PgError, PgConnectionError, PgQueryError } from "@postgres"
var db = Database.open("postgresql://localhost/mydb") catch {
match ex! {
PgConnectionError as err => {
println("Connection error: ", err.what())
return
}
PgError as err => {
println("General error: ", err.what())
return
}
... => {
println("Unknown error: ", ex!.what())
return
}
}
}
var result = db.connection().exec("SELECT * FROM users") catch {
if ex! is PgQueryError {
println("Query failed: ", ex!.what())
}
return
}
var conn = db.connection()
if conn.hasTable("users") {
println("Table 'users' exists")
}
if conn.hasTable("public", "users") {
println("Table 'public.users' exists")
}
var conn = db.connection()
// Prepare a statement (cached internally)
var stmt = conn.prepare("SELECT * FROM users WHERE age > $1")
// Execute multiple times with different parameters
var result1 = stmt.exec(18)
var result2 = stmt.exec(25)
var result3 = stmt.exec(30)
struct User {
id: i64
name: String
email: String? // Can be NULL
phone: String? // Can be NULL
}
var result = db.connection().exec("SELECT id, name, email, phone FROM users")
while result.next() {
var user = result.read[User]()
println(f"User: {user.name}")
if !!user.email {
println(f" Email: {*user.email}")
}
if !!user.phone {
println(f" Phone: {*user.phone}")
}
}
struct OrderSummary {
order_id: i64
customer_name: String
total_amount: f64
item_count: i32
order_date: String
}
var result = db.connection().exec("""
SELECT
o.id as order_id,
c.name as customer_name,
SUM(oi.price * oi.quantity) as total_amount,
COUNT(oi.id) as item_count,
o.created_at as order_date
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON oi.order_id = o.id
WHERE o.created_at > $1
GROUP BY o.id, c.name, o.created_at
ORDER BY total_amount DESC
""", "2024-01-01")
var summaries = Vector[OrderSummary]()
result >> summaries
for summary, _ in summaries {
println(f"Order {summary.order_id}: ${summary.total_amount}")
}
DatabaseStatic Methods:
open(connStr: string, dbname: string = "public", async: bool = false, timeout: i64 = -1, keepAlive: i64 = -1): !Database - Open database connection with optional schema name, async mode, timeout, and keep-alive settingsInstance Methods:
connection(): !PgConnection - Get a connection from the pooldbname(): String - Get database/schema namepoolSize(): u64 - Get current pool sizePgConnectionMethods:
exec(sql: string, ...args: auto): !PgResult - Execute query with variadic parameters (uses PostgreSQL $1, $2, etc. placeholders)prepare(sql: string): !PgStatement - Prepare and cache statement for reusecreateTable[T](name: string): !void - Create table from struct definition using reflectionhasTable(name: string): !bool - Check if table exists in current schemahasTable(schema: string, name: string): !bool - Check if table exists in specific schemaclose() - Close connection and return to poolisOpen(): bool - Check if connection is openPgResultMethods:
next(): bool - Advance to next row (returns false when no more rows)column[T](index: i32): !T - Read column by zero-based index with type conversionread[T](): !T - Read current row into type T (tuple or struct)>>[T](vec: &Vector[T]): !void - Read all remaining rows into vectorempty(): bool - Check if result set is emptysize(): i32 - Get total number of rows in resultreset() - Reset row cursor to beginningPgStatementMethods:
exec(...args: auto): !PgResult - Execute prepared statement with variadic parameterssql(): String - Get the SQL query stringisAsync(): bool - Check if async mode is enabled for this statementPgTransactionConstructor:
PgTransaction(conn: &PgConnection, autoCommit: bool) - Create transaction with auto-commit optionMethods:
begin(): !void - Explicitly begin transaction (called automatically)commit(): !void - Commit transactionrollback(savepoint: string = null): !void - Rollback entire transaction or to specific savepointsavepoint(name: string): !void - Create named savepointrelease(name: string): !void - Release savepoint (makes it inaccessible)exec(sql: string, ...args: auto): !void - Execute query within transaction contextisActive(): bool - Check if transaction is currently activePgError - Base exception for all PostgreSQL errorsPgConnectionError - Connection-related errors (extends PgError)PgQueryError - Query execution errors (extends PgError)prepare() to cache and reuse statementskeepAlive timeout to reuse connections$1, $2 placeholders instead of string concatenationdb.connection() safelyimport { Database } from "@postgres"
// Shared database instance (thread-safe)
var db = Database.open("postgresql://localhost/mydb", "public", false, -1, 60000)
// Spawn multiple threads
launch {
// Each thread gets its own connection
var conn = db.connection() catch {
println("Thread 1 failed to get connection")
return
}
var result = conn.exec("SELECT * FROM users WHERE id = $1", 1)
// Process result...
}
launch {
// Another thread with its own connection
var conn = db.connection() catch {
println("Thread 2 failed to get connection")
return
}
var result = conn.exec("SELECT * FROM products LIMIT 10")
// Process result...
}
Connection Management:
keepAlive timeout to balance connection reuse and resource cleanupdb.poolSize() to detect connection leaksQuery Optimization:
$1, $2) instead of string concatenationError Handling:
exec(), column[T](), and read[T]()PgConnectionError, PgQueryError) for targeted error handlingProduction Deployment:
keepAlive values (e.g., 60000ms)Security:
$1, $2 placeholders"postgresql://user@host/db?sslmode=require"Problem: PgConnectionError: could not connect to server
Solutions:
docker-compose ps or pg_isready -h localhost -p 54320pg_hba.conf allows connections from your hostpostgresql.confProblem: FATAL: database "mydb" does not exist
Solutions:
CREATE DATABASE mydb;\l in psql to list available databasesProblem: FATAL: role "username" does not exist
Solutions:
CREATE USER username WITH PASSWORD 'password';GRANT ALL PRIVILEGES ON DATABASE mydb TO username;Problem: PgQueryError: column "xyz" does not exist
Solutions:
\d tablename in psql to see table schemaProblem: PgQueryError: syntax error at or near "$1"
Solutions:
$1, $2 for parameters, not ? (SQLite style)Problem: Type conversion errors when reading results
Solutions:
column[T]()i64 for PostgreSQL bigint, String for text)column[i64?](0)Problem: Slow queries
Solutions:
EXPLAIN ANALYZE to identify slow operationsProblem: Too many connections / connection pool exhausted
Solutions:
keepAlive timeout to release idle connections fasterdb.poolSize()max_connections setting if neededProblem: undefined reference to PQconnectdb or similar libpq errors
Solutions:
- macOS: brew install postgresql
- Ubuntu/Debian: apt-get install libpq-dev
- Alpine: apk add libpq-dev
- macOS: brew install libpq
- Ubuntu/Debian: apt-get install libpq5
- Alpine: apk add libpq
libpq is in your library path@__cc:lib "pq" is present in the packageProblem: Type inference errors with struct mapping
Solutions:
init)@sql(ignore) for fields not in the database@sql(name: "column_name") for fields with different namesProblem: Segmentation fault or crash
Solutions:
PgConnection or PgResult between threadsProblem: Transaction automatically rolled back
Solutions:
commit() is called before transaction goes out of scope (if auto-commit is false)If you encounter issues not covered here:
println("Error: ", ex!.what())psql command-line tool firstMIT
Contributions are welcome! Please submit issues and pull requests on the repository.