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:
@@ -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: {}.{}",
|
||||
|
||||
Reference in New Issue
Block a user