feat: add PostgreSQL driver with Postgres effect

Implements full PostgreSQL support through the Postgres effect:
- connect(connStr): Connect to PostgreSQL database
- close(conn): Close connection
- execute(conn, sql): Execute INSERT/UPDATE/DELETE, return affected rows
- query(conn, sql): Execute SELECT, return all rows as records
- queryOne(conn, sql): Execute SELECT, return first row as Option
- beginTx(conn): Start transaction
- commit(conn): Commit transaction
- rollback(conn): Rollback transaction

Includes:
- Connection tracking with connection IDs
- Row mapping to Lux records with field access
- Transaction support
- Example: examples/postgres_demo.lux
- Documentation in docs/guide/11-databases.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 04:30:44 -05:00
parent 204950357f
commit 87c1fb1bbd
6 changed files with 1447 additions and 10 deletions

View File

@@ -5,6 +5,7 @@
use crate::ast::*;
use crate::diagnostics::{Diagnostic, ErrorCode, Severity};
use rand::Rng;
use postgres::{Client as PgClient, NoTls};
use rusqlite::Connection;
use std::cell::RefCell;
use std::collections::HashMap;
@@ -615,6 +616,10 @@ pub struct Interpreter {
sql_connections: RefCell<HashMap<i64, Connection>>,
/// Next SQL connection ID
next_sql_conn_id: RefCell<i64>,
/// PostgreSQL database connections (connection ID -> Client)
pg_connections: RefCell<HashMap<i64, PgClient>>,
/// Next PostgreSQL connection ID
next_pg_conn_id: RefCell<i64>,
}
/// Results from running tests
@@ -657,6 +662,8 @@ impl Interpreter {
test_results: RefCell::new(TestResults::default()),
sql_connections: RefCell::new(HashMap::new()),
next_sql_conn_id: RefCell::new(1),
pg_connections: RefCell::new(HashMap::new()),
next_pg_conn_id: RefCell::new(1),
}
}
@@ -4054,6 +4061,314 @@ impl Interpreter {
}
}
// ============================================================
// PostgreSQL Effect
// ============================================================
("Postgres", "connect") => {
let conn_str = match request.args.first() {
Some(Value::String(s)) => s.clone(),
_ => return Err(RuntimeError {
message: "Postgres.connect requires a connection string".to_string(),
span: None,
}),
};
match PgClient::connect(&conn_str, NoTls) {
Ok(client) => {
let id = *self.next_pg_conn_id.borrow();
*self.next_pg_conn_id.borrow_mut() += 1;
self.pg_connections.borrow_mut().insert(id, client);
Ok(Value::Int(id))
}
Err(e) => Err(RuntimeError {
message: format!("Postgres.connect failed: {}", e),
span: None,
}),
}
}
("Postgres", "close") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Postgres.close requires a connection ID".to_string(),
span: None,
}),
};
if self.pg_connections.borrow_mut().remove(&conn_id).is_some() {
Ok(Value::Unit)
} else {
Err(RuntimeError {
message: format!("Postgres.close: invalid connection ID {}", conn_id),
span: None,
})
}
}
("Postgres", "execute") => {
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
_ => return Err(RuntimeError {
message: "Postgres.execute requires connection ID and SQL string".to_string(),
span: None,
}),
};
let mut conns = self.pg_connections.borrow_mut();
let conn = match conns.get_mut(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Postgres.execute: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute(&sql, &[]) {
Ok(rows) => Ok(Value::Int(rows as i64)),
Err(e) => Err(RuntimeError {
message: format!("Postgres.execute failed: {}", e),
span: None,
}),
}
}
("Postgres", "query") => {
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
_ => return Err(RuntimeError {
message: "Postgres.query requires connection ID and SQL string".to_string(),
span: None,
}),
};
let mut conns = self.pg_connections.borrow_mut();
let conn = match conns.get_mut(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Postgres.query: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.query(&sql, &[]) {
Ok(rows) => {
let mut results = Vec::new();
for row in rows {
let mut record = HashMap::new();
for (i, col) in row.columns().iter().enumerate() {
let col_name = col.name().to_string();
let value: Value = match col.type_().name() {
"int4" | "int8" | "int2" => {
let v: Option<i64> = row.get(i);
match v {
Some(n) => Value::Int(n),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
"float4" | "float8" => {
let v: Option<f64> = row.get(i);
match v {
Some(n) => Value::Float(n),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
"bool" => {
let v: Option<bool> = row.get(i);
match v {
Some(b) => Value::Bool(b),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
"text" | "varchar" | "char" | "bpchar" | "name" => {
let v: Option<String> = row.get(i);
match v {
Some(s) => Value::String(s),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
_ => {
// Try to get as string for other types
let v: Option<String> = row.try_get(i).ok().flatten();
match v {
Some(s) => Value::String(s),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
};
record.insert(col_name, value);
}
results.push(Value::Record(record));
}
Ok(Value::List(results))
}
Err(e) => Err(RuntimeError {
message: format!("Postgres.query failed: {}", e),
span: None,
}),
}
}
("Postgres", "queryOne") => {
let (conn_id, sql) = match (request.args.get(0), request.args.get(1)) {
(Some(Value::Int(id)), Some(Value::String(s))) => (*id, s.clone()),
_ => return Err(RuntimeError {
message: "Postgres.queryOne requires connection ID and SQL string".to_string(),
span: None,
}),
};
let mut conns = self.pg_connections.borrow_mut();
let conn = match conns.get_mut(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Postgres.queryOne: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.query_opt(&sql, &[]) {
Ok(Some(row)) => {
let mut record = HashMap::new();
for (i, col) in row.columns().iter().enumerate() {
let col_name = col.name().to_string();
let value: Value = match col.type_().name() {
"int4" | "int8" | "int2" => {
let v: Option<i64> = row.get(i);
match v {
Some(n) => Value::Int(n),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
"float4" | "float8" => {
let v: Option<f64> = row.get(i);
match v {
Some(n) => Value::Float(n),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
"bool" => {
let v: Option<bool> = row.get(i);
match v {
Some(b) => Value::Bool(b),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
"text" | "varchar" | "char" | "bpchar" | "name" => {
let v: Option<String> = row.get(i);
match v {
Some(s) => Value::String(s),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
_ => {
let v: Option<String> = row.try_get(i).ok().flatten();
match v {
Some(s) => Value::String(s),
None => Value::Constructor { name: "None".to_string(), fields: vec![] },
}
}
};
record.insert(col_name, value);
}
Ok(Value::Constructor {
name: "Some".to_string(),
fields: vec![Value::Record(record)],
})
}
Ok(None) => Ok(Value::Constructor {
name: "None".to_string(),
fields: vec![],
}),
Err(e) => Err(RuntimeError {
message: format!("Postgres.queryOne failed: {}", e),
span: None,
}),
}
}
("Postgres", "beginTx") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Postgres.beginTx requires a connection ID".to_string(),
span: None,
}),
};
let mut conns = self.pg_connections.borrow_mut();
let conn = match conns.get_mut(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Postgres.beginTx: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute("BEGIN", &[]) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Postgres.beginTx failed: {}", e),
span: None,
}),
}
}
("Postgres", "commit") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Postgres.commit requires a connection ID".to_string(),
span: None,
}),
};
let mut conns = self.pg_connections.borrow_mut();
let conn = match conns.get_mut(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Postgres.commit: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute("COMMIT", &[]) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Postgres.commit failed: {}", e),
span: None,
}),
}
}
("Postgres", "rollback") => {
let conn_id = match request.args.first() {
Some(Value::Int(id)) => *id,
_ => return Err(RuntimeError {
message: "Postgres.rollback requires a connection ID".to_string(),
span: None,
}),
};
let mut conns = self.pg_connections.borrow_mut();
let conn = match conns.get_mut(&conn_id) {
Some(c) => c,
None => return Err(RuntimeError {
message: format!("Postgres.rollback: invalid connection ID {}", conn_id),
span: None,
}),
};
match conn.execute("ROLLBACK", &[]) {
Ok(_) => Ok(Value::Unit),
Err(e) => Err(RuntimeError {
message: format!("Postgres.rollback failed: {}", e),
span: None,
}),
}
}
_ => Err(RuntimeError {
message: format!(
"Unhandled effect operation: {}.{}",