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

501
Cargo.lock generated
View File

@@ -2,6 +2,18 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "anyhow"
version = "1.0.101"
@@ -14,12 +26,29 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -32,12 +61,27 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bumpalo"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -107,6 +151,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -122,6 +175,27 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -170,6 +244,24 @@ version = "3.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
[[package]]
name = "fallible-iterator"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -236,6 +328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -270,6 +363,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
@@ -277,6 +371,16 @@ dependencies = [
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.17"
@@ -285,7 +389,19 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasip2",
]
[[package]]
@@ -320,6 +436,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -335,12 +460,30 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashlink"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.12"
@@ -575,6 +718,27 @@ version = "0.2.182"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
[[package]]
name = "libredox"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
dependencies = [
"bitflags 2.10.0",
"libc",
]
[[package]]
name = "libsqlite3-sys"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.11.0"
@@ -587,6 +751,15 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
@@ -625,8 +798,10 @@ version = "0.1.0"
dependencies = [
"lsp-server",
"lsp-types",
"rand",
"postgres",
"rand 0.8.5",
"reqwest",
"rusqlite",
"rustyline",
"serde",
"serde_json",
@@ -635,6 +810,16 @@ dependencies = [
"tiny_http",
]
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -654,7 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.61.2",
]
@@ -696,6 +881,24 @@ dependencies = [
"libc",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "objc2-system-configuration"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
dependencies = [
"objc2-core-foundation",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -746,12 +949,54 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_shared",
"serde",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -770,6 +1015,49 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "postgres"
version = "0.19.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c48ece1c6cda0db61b058c1721378da76855140e9214339fa1317decacb176"
dependencies = [
"bytes",
"fallible-iterator 0.2.0",
"futures-util",
"log",
"tokio",
"tokio-postgres",
]
[[package]]
name = "postgres-protocol"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee9dd5fe15055d2b6806f4736aa0c9637217074e224bbec46d4041b91bb9491"
dependencies = [
"base64 0.22.1",
"byteorder",
"bytes",
"fallible-iterator 0.2.0",
"hmac",
"md-5",
"memchr",
"rand 0.9.2",
"sha2",
"stringprep",
]
[[package]]
name = "postgres-types"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20"
dependencies = [
"bytes",
"fallible-iterator 0.2.0",
"postgres-protocol",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -839,8 +1127,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.5",
]
[[package]]
@@ -850,7 +1148,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core 0.9.5",
]
[[package]]
@@ -862,13 +1170,31 @@ dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.10.0",
]
[[package]]
name = "reqwest"
version = "0.11.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62"
dependencies = [
"base64",
"base64 0.21.7",
"bytes",
"encoding_rs",
"futures-core",
@@ -902,6 +1228,20 @@ dependencies = [
"winreg",
]
[[package]]
name = "rusqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
dependencies = [
"bitflags 2.10.0",
"fallible-iterator 0.3.0",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
name = "rustix"
version = "1.1.3"
@@ -921,7 +1261,7 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
dependencies = [
"base64",
"base64 0.21.7",
]
[[package]]
@@ -967,6 +1307,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.6.0"
@@ -1062,12 +1408,29 @@ dependencies = [
"serde",
]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "slab"
version = "0.4.12"
@@ -1106,6 +1469,23 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.115"
@@ -1210,6 +1590,21 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.49.0"
@@ -1234,6 +1629,32 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-postgres"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcea47c8f71744367793f16c2db1f11cb859d28f436bdb4ca9193eb1f787ee42"
dependencies = [
"async-trait",
"byteorder",
"bytes",
"fallible-iterator 0.2.0",
"futures-channel",
"futures-util",
"log",
"parking_lot",
"percent-encoding",
"phf",
"pin-project-lite",
"postgres-protocol",
"postgres-types",
"rand 0.9.2",
"socket2 0.6.2",
"tokio",
"tokio-util",
"whoami",
]
[[package]]
name = "tokio-util"
version = "0.7.18"
@@ -1278,12 +1699,39 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
[[package]]
name = "unicode-normalization"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@@ -1333,6 +1781,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "want"
version = "0.3.1"
@@ -1348,6 +1802,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
@@ -1366,6 +1829,15 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasite"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42"
dependencies = [
"wasi 0.14.7+wasi-0.2.4",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.108"
@@ -1469,6 +1941,19 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "whoami"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d"
dependencies = [
"libc",
"libredox",
"objc2-system-configuration",
"wasite",
"web-sys",
]
[[package]]
name = "windows-link"
version = "0.2.1"

View File

@@ -15,6 +15,8 @@ serde_json = "1"
rand = "0.8"
reqwest = { version = "0.11", features = ["blocking", "json"] }
tiny_http = "0.12"
rusqlite = { version = "0.31", features = ["bundled"] }
postgres = "0.19"
[dev-dependencies]

450
docs/guide/11-databases.md Normal file
View File

@@ -0,0 +1,450 @@
# Working with Databases
Lux includes built-in support for SQL databases through the `Sql` effect. This guide covers how to connect to databases, execute queries, handle transactions, and best practices for database operations.
## Quick Start
```lux
fn main(): Unit with {Console, Sql} = {
// Open an in-memory SQLite database
let db = Sql.openMemory()
// Create a table
Sql.execute(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
// Insert data
Sql.execute(db, "INSERT INTO users (name) VALUES ('Alice')")
Sql.execute(db, "INSERT INTO users (name) VALUES ('Bob')")
// Query data
let users = Sql.query(db, "SELECT * FROM users")
Console.print("Found users: " ++ toString(users))
// Clean up
Sql.close(db)
}
run main() with {}
```
## Connecting to Databases
### In-Memory Database
For testing and temporary data:
```lux
let db = Sql.openMemory()
// Database exists only in memory, lost when closed
```
### File-Based Database
For persistent storage:
```lux
let db = Sql.open("./data/app.db")
// Creates file if it doesn't exist
```
### Connection Lifecycle
Always close connections when done:
```lux
fn withDatabase<T>(path: String, f: fn(SqlConn): T with Sql): T with Sql = {
let db = Sql.open(path)
let result = f(db)
Sql.close(db)
result
}
```
## Executing Queries
### Non-Returning Queries (INSERT, UPDATE, DELETE, CREATE)
Use `Sql.execute` for statements that don't return rows:
```lux
// Create table
Sql.execute(db, "CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)")
// Insert
Sql.execute(db, "INSERT INTO posts (title, content) VALUES ('Hello', 'World')")
// Update
Sql.execute(db, "UPDATE posts SET title = 'Updated' WHERE id = 1")
// Delete
Sql.execute(db, "DELETE FROM posts WHERE id = 1")
```
### Queries that Return Rows (SELECT)
Use `Sql.query` to get all matching rows:
```lux
// Returns List<SqlRow>
let users = Sql.query(db, "SELECT * FROM users")
// Each row is a record-like structure
for user in users {
Console.print("User: " ++ user.name)
}
```
Use `Sql.queryOne` to get a single row (or None):
```lux
// Returns Option<SqlRow>
let maybeUser = Sql.queryOne(db, "SELECT * FROM users WHERE id = 1")
match maybeUser {
Some(user) => Console.print("Found: " ++ user.name),
None => Console.print("User not found")
}
```
## Transactions
Transactions ensure multiple operations succeed or fail together.
### Basic Transaction
```lux
// Start transaction
Sql.beginTx(db)
// Do operations
Sql.execute(db, "INSERT INTO accounts (name, balance) VALUES ('Alice', 1000)")
Sql.execute(db, "INSERT INTO accounts (name, balance) VALUES ('Bob', 1000)")
// Commit changes
Sql.commit(db)
```
### Rollback on Error
```lux
Sql.beginTx(db)
let result = transferFunds(db, fromId, toId, amount)
match result {
Ok(_) => Sql.commit(db),
Err(_) => Sql.rollback(db) // Undo all changes
}
```
### Transaction Helper
Here's a pattern for safe transactions:
```lux
fn transaction<T>(db: SqlConn, f: fn(): T with Sql): Result<T, String> with Sql = {
Sql.beginTx(db)
// Execute the function
// In a real implementation, you'd catch errors here
let result = f()
Sql.commit(db)
Ok(result)
}
```
## Working with Results
Query results are returned as `List<SqlRow>` where each row behaves like a record:
```lux
let rows = Sql.query(db, "SELECT id, name, age FROM users")
for row in rows {
// Access columns by name
let id = row.id // Int
let name = row.name // String
let age = row.age // Int or null
Console.print("{name} (age {age})")
}
```
### Handling NULL values
NULL values from the database are represented as options:
```lux
let row = Sql.queryOne(db, "SELECT email FROM users WHERE id = 1")
match row {
Some(r) => {
match r.email {
Some(email) => Console.print("Email: " ++ email),
None => Console.print("No email set")
}
},
None => Console.print("User not found")
}
```
## SQL Injection Prevention
**IMPORTANT**: The current API passes SQL strings directly. For production use, always:
1. Validate and sanitize user input
2. Use parameterized queries (when available)
3. Never concatenate user input into SQL strings
```lux
// DANGEROUS - Never do this!
let query = "SELECT * FROM users WHERE name = '" ++ userInput ++ "'"
// SAFER - Validate input first
fn safeUserLookup(db: SqlConn, userId: Int): Option<SqlRow> with Sql = {
// Integers are safe to interpolate
Sql.queryOne(db, "SELECT * FROM users WHERE id = " ++ toString(userId))
}
// For strings, escape quotes at minimum
fn escapeString(s: String): String = {
String.replace(s, "'", "''")
}
```
## Common Patterns
### Repository Pattern
Encapsulate database operations:
```lux
type User = { id: Int, name: String, email: Option<String> }
fn createUser(db: SqlConn, name: String): Int with Sql = {
Sql.execute(db, "INSERT INTO users (name) VALUES ('" ++ escapeString(name) ++ "')")
// Get last inserted ID
let row = Sql.queryOne(db, "SELECT last_insert_rowid() as id")
match row {
Some(r) => r.id,
None => -1
}
}
fn findUserById(db: SqlConn, id: Int): Option<User> with Sql = {
let row = Sql.queryOne(db, "SELECT * FROM users WHERE id = " ++ toString(id))
match row {
Some(r) => Some({ id: r.id, name: r.name, email: r.email }),
None => None
}
}
fn findAllUsers(db: SqlConn): List<User> with Sql = {
let rows = Sql.query(db, "SELECT * FROM users ORDER BY name")
List.map(rows, fn(r) => { id: r.id, name: r.name, email: r.email })
}
```
### Testing with In-Memory Database
```lux
fn testUserRepository(): Unit with {Test, Sql} = {
// Each test gets a fresh database
let db = Sql.openMemory()
// Set up schema
Sql.execute(db, "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
// Test
let id = createUser(db, "Test User")
Test.assertTrue(id > 0, "Should return valid ID")
let user = findUserById(db, id)
Test.assertTrue(Option.isSome(user), "Should find created user")
Sql.close(db)
}
```
### Handler for Testing (Mock Database)
The effect system lets you swap database implementations:
```lux
// Production handler uses real SQLite
handler realDatabase(): Sql {
// Uses actual rusqlite implementation
}
// Test handler uses in-memory storage
handler mockDatabase(): Sql {
let storage = ref []
fn execute(conn, sql) = {
// Parse and simulate SQL
}
fn query(conn, sql) = {
// Return mock data
[]
}
}
// Run with mock database in tests
run userService() with {
Sql -> mockDatabase()
}
```
## API Reference
### Types
```lux
type SqlConn // Opaque connection handle
type SqlRow // Row result with named column access
```
### Operations
| Function | Type | Description |
|----------|------|-------------|
| `Sql.open(path)` | `String -> SqlConn` | Open database file |
| `Sql.openMemory()` | `() -> SqlConn` | Open in-memory database |
| `Sql.close(conn)` | `SqlConn -> Unit` | Close connection |
| `Sql.execute(conn, sql)` | `(SqlConn, String) -> Int` | Execute statement, return affected rows |
| `Sql.query(conn, sql)` | `(SqlConn, String) -> List<SqlRow>` | Query, return all rows |
| `Sql.queryOne(conn, sql)` | `(SqlConn, String) -> Option<SqlRow>` | Query, return first row |
| `Sql.beginTx(conn)` | `SqlConn -> Unit` | Begin transaction |
| `Sql.commit(conn)` | `SqlConn -> Unit` | Commit transaction |
| `Sql.rollback(conn)` | `SqlConn -> Unit` | Rollback transaction |
## Error Handling
Database operations can fail. In the current implementation, errors cause runtime failures. Future versions will support returning `Result` types:
```lux
// Future API (not yet implemented)
fn safeQuery(db: SqlConn, sql: String): Result<List<SqlRow>, SqlError> with Sql = {
Sql.tryQuery(db, sql)
}
```
## Performance Tips
1. **Batch inserts in transactions** - Much faster than individual inserts:
```lux
Sql.beginTx(db)
for item in items {
Sql.execute(db, "INSERT INTO data (value) VALUES (" ++ toString(item) ++ ")")
}
Sql.commit(db)
```
2. **Use `queryOne` for single results** - More efficient than `query` when you only need one row
3. **Create indexes** for frequently queried columns:
```lux
Sql.execute(db, "CREATE INDEX idx_users_email ON users(email)")
```
4. **Close connections** when done to free resources
## PostgreSQL Support
Lux also provides native PostgreSQL support through the `Postgres` effect. This is ideal for production applications requiring a full-featured relational database.
### Connecting to PostgreSQL
```lux
fn main(): Unit with {Console, Postgres} = {
// Connect using a connection string
let connStr = "host=localhost user=myuser password=mypass dbname=mydb"
let conn = Postgres.connect(connStr)
Console.print("Connected to PostgreSQL!")
// ... use the connection ...
Postgres.close(conn)
}
```
### PostgreSQL Operations
The PostgreSQL API mirrors the SQLite API:
```lux
// Execute non-returning queries
Postgres.execute(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
Postgres.execute(conn, "INSERT INTO users (name) VALUES ('Alice')")
// Query multiple rows
let users = Postgres.query(conn, "SELECT * FROM users")
for user in users {
Console.print("User: " ++ user.name)
}
// Query single row
match Postgres.queryOne(conn, "SELECT * FROM users WHERE id = 1") {
Some(user) => Console.print("Found: " ++ user.name),
None => Console.print("Not found")
}
```
### PostgreSQL Transactions
```lux
// Start transaction
Postgres.beginTx(conn)
// Make changes
Postgres.execute(conn, "UPDATE accounts SET balance = balance - 100 WHERE id = 1")
Postgres.execute(conn, "UPDATE accounts SET balance = balance + 100 WHERE id = 2")
// Commit or rollback
Postgres.commit(conn)
// Or: Postgres.rollback(conn)
```
### PostgreSQL API Reference
| Function | Type | Description |
|----------|------|-------------|
| `Postgres.connect(connStr)` | `String -> Int` | Connect to PostgreSQL, returns connection ID |
| `Postgres.close(conn)` | `Int -> Unit` | Close connection |
| `Postgres.execute(conn, sql)` | `(Int, String) -> Int` | Execute statement, return affected rows |
| `Postgres.query(conn, sql)` | `(Int, String) -> List<Row>` | Query, return all rows |
| `Postgres.queryOne(conn, sql)` | `(Int, String) -> Option<Row>` | Query, return first row |
| `Postgres.beginTx(conn)` | `Int -> Unit` | Begin transaction |
| `Postgres.commit(conn)` | `Int -> Unit` | Commit transaction |
| `Postgres.rollback(conn)` | `Int -> Unit` | Rollback transaction |
### When to Use SQLite vs PostgreSQL
| Feature | SQLite | PostgreSQL |
|---------|--------|------------|
| Setup | No setup needed | Requires server |
| Deployment | Single file | Server process |
| Concurrency | Limited | Excellent |
| Scale | Small-medium | Large |
| Features | Basic | Full RDBMS |
| Use case | Embedded, testing | Production |
## Limitations
- No connection pooling yet
- No prepared statements / parameterized queries yet
- Limited type mapping (basic Int, String, Float)
## See Also
- [Effects Guide](./05-effects.md) - Understanding the effect system
- [Testing Guide](../testing.md) - Writing tests with mock handlers
- [Roadmap](../ROADMAP.md) - Planned database features

185
examples/postgres_demo.lux Normal file
View File

@@ -0,0 +1,185 @@
// PostgreSQL Database Example
//
// Demonstrates the Postgres effect for database operations.
//
// Prerequisites:
// - PostgreSQL server running locally
// - Database 'testdb' created
// - User 'testuser' with password 'testpass'
//
// To set up:
// createdb testdb
// psql testdb -c "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT, email TEXT);"
//
// Run with: lux examples/postgres_demo.lux
// ============================================================
// Helper Functions
// ============================================================
fn jsonStr(key: String, value: String): String =
"\"" + key + "\":\"" + value + "\""
fn jsonNum(key: String, value: Int): String =
"\"" + key + "\":" + toString(value)
fn jsonObj(content: String): String =
"{" + content + "}"
// ============================================================
// Database Operations
// ============================================================
// Insert a user
fn insertUser(connId: Int, name: String, email: String): Int with {Console, Postgres} = {
let sql = "INSERT INTO users (name, email) VALUES ('" + name + "', '" + email + "') RETURNING id"
Console.print("Inserting user: " + name)
match Postgres.queryOne(connId, sql) {
Some(row) => {
Console.print(" Inserted with ID: " + toString(row.id))
row.id
},
None => {
Console.print(" Insert failed")
-1
}
}
}
// Get all users
fn getUsers(connId: Int): Unit with {Console, Postgres} = {
Console.print("Fetching all users...")
let rows = Postgres.query(connId, "SELECT id, name, email FROM users ORDER BY id")
Console.print(" Found " + toString(List.length(rows)) + " users:")
List.forEach(rows, fn(row: { id: Int, name: String, email: String }): Unit with {Console} => {
Console.print(" - " + toString(row.id) + ": " + row.name + " <" + row.email + ">")
})
}
// Get user by ID
fn getUserById(connId: Int, id: Int): Unit with {Console, Postgres} = {
let sql = "SELECT id, name, email FROM users WHERE id = " + toString(id)
Console.print("Looking up user " + toString(id) + "...")
match Postgres.queryOne(connId, sql) {
Some(row) => Console.print(" Found: " + row.name + " <" + row.email + ">"),
None => Console.print(" User not found")
}
}
// Update user email
fn updateUserEmail(connId: Int, id: Int, newEmail: String): Unit with {Console, Postgres} = {
let sql = "UPDATE users SET email = '" + newEmail + "' WHERE id = " + toString(id)
Console.print("Updating user " + toString(id) + " email to " + newEmail)
let affected = Postgres.execute(connId, sql)
Console.print(" Rows affected: " + toString(affected))
}
// Delete user
fn deleteUser(connId: Int, id: Int): Unit with {Console, Postgres} = {
let sql = "DELETE FROM users WHERE id = " + toString(id)
Console.print("Deleting user " + toString(id))
let affected = Postgres.execute(connId, sql)
Console.print(" Rows affected: " + toString(affected))
}
// ============================================================
// Transaction Example
// ============================================================
fn transactionDemo(connId: Int): Unit with {Console, Postgres} = {
Console.print("")
Console.print("=== Transaction Demo ===")
// Start transaction
Console.print("Beginning transaction...")
Postgres.beginTx(connId)
// Make some changes
insertUser(connId, "TxUser1", "tx1@example.com")
insertUser(connId, "TxUser2", "tx2@example.com")
// Show users before commit
Console.print("Users before commit:")
getUsers(connId)
// Commit the transaction
Console.print("Committing transaction...")
Postgres.commit(connId)
Console.print("Transaction committed!")
}
// ============================================================
// Main
// ============================================================
fn main(): Unit with {Console, Postgres} = {
Console.print("========================================")
Console.print(" PostgreSQL Demo")
Console.print("========================================")
Console.print("")
// Connect to database
Console.print("Connecting to PostgreSQL...")
let connStr = "host=localhost user=testuser password=testpass dbname=testdb"
let connId = Postgres.connect(connStr)
Console.print("Connected! Connection ID: " + toString(connId))
Console.print("")
// Create table if not exists
Console.print("Creating users table...")
Postgres.execute(connId, "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL)")
Console.print("")
// Clear table for demo
Console.print("Clearing existing data...")
Postgres.execute(connId, "DELETE FROM users")
Console.print("")
// Insert some users
Console.print("=== Inserting Users ===")
let id1 = insertUser(connId, "Alice", "alice@example.com")
let id2 = insertUser(connId, "Bob", "bob@example.com")
let id3 = insertUser(connId, "Charlie", "charlie@example.com")
Console.print("")
// Query all users
Console.print("=== All Users ===")
getUsers(connId)
Console.print("")
// Query single user
Console.print("=== Single User Lookup ===")
getUserById(connId, id2)
Console.print("")
// Update user
Console.print("=== Update User ===")
updateUserEmail(connId, id2, "bob.new@example.com")
getUserById(connId, id2)
Console.print("")
// Delete user
Console.print("=== Delete User ===")
deleteUser(connId, id3)
getUsers(connId)
Console.print("")
// Transaction demo
transactionDemo(connId)
Console.print("")
// Final state
Console.print("=== Final State ===")
getUsers(connId)
Console.print("")
// Close connection
Console.print("Closing connection...")
Postgres.close(connId)
Console.print("Done!")
}
// Note: This will fail if PostgreSQL is not running
// To test the syntax only, you can comment out the last line
let output = run main() with {}

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: {}.{}",

View File

@@ -1229,7 +1229,7 @@ impl TypeChecker {
fn check_function(&mut self, func: &FunctionDecl) {
// Validate that all declared effects exist
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test", "Sql"];
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test", "Sql", "Postgres"];
for effect in &func.effects {
let is_builtin = builtin_effects.contains(&effect.name.as_str());
let is_defined = self.env.lookup_effect(&effect.name).is_some();
@@ -2010,7 +2010,7 @@ impl TypeChecker {
}
// Built-in effects are always available
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test", "Sql"];
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer", "Test", "Sql", "Postgres"];
let is_builtin = builtin_effects.contains(&effect.name.as_str());
// Track this effect for inference