From 9f2d837fd897b117122f92af99c4e744ee247478 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 12:14:40 +0000 Subject: [PATCH 01/13] poc(db): fastedge::db::sql (gcore:db/sql@0.1.0) + db_sql wstd example --- Cargo.lock | 1 + Cargo.toml | 3 +- context/CHANGELOG.md | 25 ++ examples/README.md | 1 + examples/http/wasi/db_sql/Cargo.lock | 644 +++++++++++++++++++++++++++ examples/http/wasi/db_sql/Cargo.toml | 17 + examples/http/wasi/db_sql/README.md | 50 +++ examples/http/wasi/db_sql/schema.sql | 14 + examples/http/wasi/db_sql/src/lib.rs | 61 +++ src/db.rs | 149 +++++++ src/lib.rs | 4 + wit | 2 +- 12 files changed, 969 insertions(+), 2 deletions(-) create mode 100644 examples/http/wasi/db_sql/Cargo.lock create mode 100644 examples/http/wasi/db_sql/Cargo.toml create mode 100644 examples/http/wasi/db_sql/README.md create mode 100644 examples/http/wasi/db_sql/schema.sql create mode 100644 examples/http/wasi/db_sql/src/lib.rs create mode 100644 src/db.rs diff --git a/Cargo.lock b/Cargo.lock index e29c1ce..cf6001d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,7 @@ dependencies = [ "fastedge-derive", "http", "mime", + "serde", "serde_json", "thiserror", "wit-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 34ecc7c..e1d5bcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ name = "fastedge" [features] default = ["proxywasm"] proxywasm = [] -json = ["serde_json"] +json = ["serde_json", "serde"] [dependencies] fastedge-derive = { path = "derive", version = "0.4" } @@ -45,6 +45,7 @@ wit-bindgen = "0.46" thiserror = "2.0" mime = "^0.3" serde_json = { version = "^1.0", optional = true } +serde = { version = "1", optional = true } [dev-dependencies] diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index a094ac1..0bd83c2 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -4,6 +4,31 @@ This file tracks agent decisions, architectural changes, and context for future --- +## [2026-10-27] — POC: `fastedge::db::sql` (gcore:db/sql@0.1.0) + +### Overview +Added `fastedge::db::sql`: SQL database (PostgreSQL, Neon in the POC) bound to the app by name; the host holds the credentials. + +### Decisions +- Module path `fastedge::db::sql` mirrors the WIT package `gcore:db/sql`, and keeps it apart from `fastedge::key_value` (Edge Storage). Upstash KV will follow as `fastedge::db::kv`. +- Re-export the generated types (`Database`, `Value`, `QueryResult`, `Error`, `DbError`, ...) as is, plus a thin layer: `params!` / `ToSql` for parameters, and `QueryAs::query_as::` / `rows_as` decoding rows by column name via serde_json. +- `query_as` is behind the existing `json` feature (now also enables optional `serde`), so the default build gains no deps. Alternatives: sqlx/diesel (own the connection, don't fit), a custom serde Deserializer (more code, same result). +- Calls are blocking (sync WIT). Async (futures / WASI p3) is an open decision; see coordinator `db-integration/context/async-calls.md`. + +### Changes +- `wit` submodule → FastEdge-wit `feat/db-interface` (`deps/db/sql.wit`, world imports `gcore:db/sql@0.1.0`) +- `src/lib.rs` — `with: { "gcore:db/sql@0.1.0": generate }` in `generate!`; `pub mod db` +- `src/db.rs` — new: `db::sql` re-exports, `ToSql`, `params!`, `QueryAs`, `rows_as`, `QueryAsError` +- `Cargo.toml` — optional `serde`, `json = ["serde_json", "serde"]` +- `examples/http/wasi/db_sql/` — new wstd example + `schema.sql`; `examples/README.md` entry + +### Follow-up +- `examples/http/wasi/db_sql/Cargo.toml` uses a path dependency until `fastedge::db` is published (same as `examples/cdn/cache`). +- The `wit` submodule points at a FastEdge-wit commit that is not pushed yet (`6ddb92e`). +- Not yet: fixtures, SDK_ARCHITECTURE/PROJECT_OVERVIEW updates, plugin manifest entries, docs generation. + +--- + ## [2026-09-14] — ProxyWasm cache module ### Overview diff --git a/examples/README.md b/examples/README.md index 6b07a2e..5a87eb5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,6 +26,7 @@ Examples are organized into three categories: | [headers](./http/wasi/headers/) | Echo request headers and add a custom header from an environment variable | | [variables_and_secrets](./http/wasi/variables_and_secrets/) | Read an environment variable and a secret | | [simple_fetch](./http/wasi/simple_fetch/) | Fetch from a configurable URL and return the response | +| [db_sql](./http/wasi/db_sql/) | Query a bound PostgreSQL database and decode rows into a struct (`fastedge::db::sql`) | ### cdn (proxy-wasm) diff --git a/examples/http/wasi/db_sql/Cargo.lock b/examples/http/wasi/db_sql/Cargo.lock new file mode 100644 index 0000000..d8b4497 --- /dev/null +++ b/examples/http/wasi/db_sql/Cargo.lock @@ -0,0 +1,644 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "db_sql" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "serde", + "serde_json", + "wstd", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "fastedge" +version = "0.4.5" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "serde", + "serde_json", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "fastedge-derive" +version = "0.4.5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wstd" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b52936db10a79bb724dadd1c2c3aac958e8229dcb1f1c7f2b7044ca9fc6a3a" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153db9b65508bf6c2efe26617169840c03671ba5719a35868db7682a5261f7d4" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/http/wasi/db_sql/Cargo.toml b/examples/http/wasi/db_sql/Cargo.toml new file mode 100644 index 0000000..84871e0 --- /dev/null +++ b/examples/http/wasi/db_sql/Cargo.toml @@ -0,0 +1,17 @@ +[workspace] + +[package] +name = "db_sql" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = { path = "../../../..", features = ["json"] } +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + diff --git a/examples/http/wasi/db_sql/README.md b/examples/http/wasi/db_sql/README.md new file mode 100644 index 0000000..141705e --- /dev/null +++ b/examples/http/wasi/db_sql/README.md @@ -0,0 +1,50 @@ +[← Back to examples](../../../README.md) + +# SQL database (WASI) + +Reads users from a PostgreSQL database bound to the app as `main`, decoding rows into a Rust struct +with `query_as`. + +## Setup + +1. Bind the database: add a secret `FASTEDGE_DB_SQL_main` with the Postgres connection string + (e.g. a Neon pooled URL). The app never sees it; the host resolves `Database::open("main")`. +2. Create the schema once, outside the app: + +```sh +psql "$DATABASE_URL" -f schema.sql +``` + +Schema changes (migrations) are run from your machine or CI with your usual tool, not by the app. + +## Usage + +``` +GET / → 200 [{"id":1,"email":"ann@example.com","nickname":null,"created":"…+00","meta":{"plan":"pro"}}, …] +GET /?id=2 → 200 {"id":2,"email":"bob@example.com","nickname":"bobby","created":"…+00","meta":{"plan":"free"}} +GET /?id=9 → 404 {"error":"not found"} +``` + +## Error responses + +| Condition | Status | Body | +|---|---|---| +| No database bound to `main` | 500 | `{"error":"NoSuchDatabase"}` | +| Schema not applied | 500 | `{"error":"relation \"example_users\" does not exist","code":"42P01"}` | +| Row doesn't match `User` | 500 | `{"error":"row decode: …"}` | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/db_sql.wasm +``` + +Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_sql.wasm --wasi-http true --secret "FASTEDGE_DB_SQL_main=postgresql://…"` + +## APIs used + +- `fastedge::db::sql::Database::open(name)` — open the database bound to `name` +- `db.query_as::(sql, params)` — run one statement and decode rows into `T` (feature `json`) +- `params![…]` — build `$1, $2, …` parameters from plain Rust values +- `Error::Query(DbError { code, message, … })` — Postgres error with SQLSTATE diff --git a/examples/http/wasi/db_sql/schema.sql b/examples/http/wasi/db_sql/schema.sql new file mode 100644 index 0000000..9e656ec --- /dev/null +++ b/examples/http/wasi/db_sql/schema.sql @@ -0,0 +1,14 @@ +-- Apply once, outside the app (psql, Neon SQL editor, or your migration tool): +-- psql "$DATABASE_URL" -f schema.sql +create table if not exists example_users ( + id int8 primary key, + email text not null unique, + nickname text, + created timestamptz not null default now(), + meta jsonb not null default '{}' +); + +insert into example_users (id, email, nickname, meta) values + (1, 'ann@example.com', null, '{"plan": "pro"}'), + (2, 'bob@example.com', 'bobby', '{"plan": "free"}') +on conflict (id) do nothing; diff --git a/examples/http/wasi/db_sql/src/lib.rs b/examples/http/wasi/db_sql/src/lib.rs new file mode 100644 index 0000000..165f80a --- /dev/null +++ b/examples/http/wasi/db_sql/src/lib.rs @@ -0,0 +1,61 @@ +use fastedge::db::sql::{params, Database, Error, QueryAs, QueryAsError}; +use serde::{Deserialize, Serialize}; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +#[derive(Debug, Deserialize, Serialize)] +struct User { + id: i64, + email: String, + nickname: Option, + created: String, // timestamptz, UTC text + meta: Meta, // jsonb, decoded into a struct +} + +#[derive(Debug, Deserialize, Serialize)] +struct Meta { + plan: String, +} + +const SELECT_USERS: &str = "select id, email, nickname, created, meta from example_users"; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let id = request + .uri() + .query() + .and_then(|q| q.split('&').find_map(|kv| kv.strip_prefix("id="))) + .map(str::parse::) + .transpose()?; + + // "main" is the binding name; the host resolves it to the app's database. + let db = match Database::open("main") { + Ok(db) => db, + Err(e) => return json(500, &serde_json::json!({ "error": format!("{e:?}") })), + }; + + let users = match id { + Some(id) => db.query_as::(&format!("{SELECT_USERS} where id = $1"), params![id]), + None => db.query_as::(&format!("{SELECT_USERS} order by id"), params![]), + }; + + match (id, users) { + (Some(_), Ok(mut users)) => match users.pop() { + Some(user) => json(200, &user), + None => json(404, &serde_json::json!({ "error": "not found" })), + }, + (None, Ok(users)) => json(200, &users), + // e.g. 42P01 (undefined_table): schema.sql not applied yet + (_, Err(QueryAsError::Db(Error::Query(e)))) => { + json(500, &serde_json::json!({ "error": e.message, "code": e.code })) + } + (_, Err(e)) => json(500, &serde_json::json!({ "error": e.to_string() })), + } +} + +fn json(status: u16, body: &impl Serialize) -> anyhow::Result> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(serde_json::to_string(body)?))?) +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..ed3ab86 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,149 @@ +//! Customer-owned databases bound to the app by name (`gcore:db`). +//! +//! The connection details stay with the host; the app only knows the binding name. + +/// SQL database (PostgreSQL), `gcore:db/sql`. +/// +/// Calls are blocking. Parameters use positional placeholders (`$1`, `$2`, ...). +/// +/// # Examples +/// +/// ```no_run +/// use fastedge::db::sql::{params, Database}; +/// +/// let db = Database::open("main")?; +/// let result = db.query("select id, email from users where id = $1", params![42i64])?; +/// println!("{} rows", result.row_count); +/// # Ok::<(), fastedge::db::sql::Error>(()) +/// ``` +/// +/// With the `json` feature, rows decode into any `serde::Deserialize` type by column name: +/// +/// ```ignore +/// use fastedge::db::sql::{params, Database, QueryAs}; +/// +/// #[derive(serde::Deserialize)] +/// struct User { id: i64, email: String, nickname: Option } +/// +/// let users: Vec = Database::open("main")? +/// .query_as("select id, email, nickname from users where id = $1", params![42i64])?; +/// ``` +pub mod sql { + #[doc(inline)] + pub use crate::gcore::db::sql::{ + Column, Database, DbError, Error, QueryResult, Statement, Value, + }; + + #[doc(inline)] + pub use crate::__db_sql_params as params; + + /// Converts a Rust value into a SQL parameter [`Value`]. + /// + /// Implemented for `bool`, `i32`, `i64`, `f64`, `&str`, `String`, `Vec` (bytea) and + /// `Option` (`None` is SQL `NULL`). + pub trait ToSql { + /// The parameter value. + fn to_sql(self) -> Value; + } + + macro_rules! to_sql { + ($($t:ty => $v:ident),*) => {$( + impl ToSql for $t { + fn to_sql(self) -> Value { + Value::$v(self.into()) + } + } + )*}; + } + to_sql!(bool => Boolean, i32 => Int32, i64 => Int64, f64 => Float64, String => Text, + &str => Text, Vec => Bytes); + + impl ToSql for Option { + fn to_sql(self) -> Value { + self.map_or(Value::Null, ToSql::to_sql) + } + } + + #[cfg(feature = "json")] + pub use typed::{rows_as, QueryAs, QueryAsError}; + + #[cfg(feature = "json")] + mod typed { + use super::{Database, Error, QueryResult, Value}; + use serde::de::DeserializeOwned; + use serde_json::{Map, Value as Json}; + + /// Error from [`QueryAs::query_as`]. + #[derive(Debug, thiserror::Error)] + pub enum QueryAsError { + /// The database or host reported an error. + #[error("{0}")] + Db(#[from] Error), + /// A row didn't match the target type (missing column, wrong type). + #[error("row decode: {0}")] + Decode(String), + } + + /// Typed queries: decode rows into any `serde::Deserialize` type by column name. + pub trait QueryAs { + /// Run one statement and decode every row into `T`. + fn query_as( + &self, + sql: &str, + params: &[Value], + ) -> Result, QueryAsError>; + } + + impl QueryAs for Database { + fn query_as( + &self, + sql: &str, + params: &[Value], + ) -> Result, QueryAsError> { + rows_as(self.query(sql, params)?) + } + } + + /// Decode every row of a result (e.g. one entry of a `batch` result) into `T`. + /// + /// `json`/`jsonb` columns are parsed, so a field can be a nested struct. `int8` is a + /// JSON number, `numeric` and text-format types (timestamps, uuid, ...) are strings, + /// `bytea` is a byte array, `NULL` is `None`. + pub fn rows_as(result: QueryResult) -> Result, QueryAsError> { + result + .rows + .into_iter() + .map(|row| { + let obj: Map = result + .columns + .iter() + .zip(row) + .map(|(c, v)| (c.name.clone(), to_json(v))) + .collect(); + serde_json::from_value(Json::Object(obj)) + .map_err(|e| QueryAsError::Decode(e.to_string())) + }) + .collect() + } + + fn to_json(v: Value) -> Json { + match v { + Value::Null => Json::Null, + Value::Boolean(b) => b.into(), + Value::Int32(i) => i.into(), + Value::Int64(i) => i.into(), + Value::Float64(f) => f.into(), + Value::Json(s) => serde_json::from_str(&s).unwrap_or(Json::String(s)), + Value::Numeric(s) | Value::Text(s) => s.into(), + Value::Bytes(b) => b.into(), + } + } + } +} + +/// Builds a `&[Value]` parameter list from plain Rust values; see [`sql::ToSql`]. +#[doc(hidden)] +#[macro_export] +macro_rules! __db_sql_params { + ($($p:expr),* $(,)?) => { &[$($crate::db::sql::ToSql::to_sql($p)),*] }; +} diff --git a/src/lib.rs b/src/lib.rs index 896c836..549b5e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -261,6 +261,7 @@ wit_bindgen::generate!({ world: "reactor", path: "wit", pub_export_macro: true, + with: { "gcore:db/sql@0.1.0": generate }, }); /// Fast read-only key-value dictionary for configuration. @@ -411,6 +412,9 @@ pub mod key_value { pub use crate::gcore::fastedge::key_value::Error; } +/// Customer-owned databases bound to the app by name (`gcore:db`). +pub mod db; + /// Synchronous cache API for FastEdge applications. /// /// This module exposes the cache interface generated from the Component Model diff --git a/wit b/wit index 68c713f..6ddb92e 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit 68c713f2f8aaa14c8964978a6e98d54571281065 +Subproject commit 6ddb92e650ea17448519b6fadd4beba808dc8280 From 5f248610e76c6eab758c2f60436f2da30ec9add4 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 12:24:37 +0000 Subject: [PATCH 02/13] example(db_sql): 'no such database' message, same as the JS example --- examples/http/wasi/db_sql/README.md | 2 +- examples/http/wasi/db_sql/src/lib.rs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/http/wasi/db_sql/README.md b/examples/http/wasi/db_sql/README.md index 141705e..da58bb1 100644 --- a/examples/http/wasi/db_sql/README.md +++ b/examples/http/wasi/db_sql/README.md @@ -29,7 +29,7 @@ GET /?id=9 → 404 {"error":"not found"} | Condition | Status | Body | |---|---|---| -| No database bound to `main` | 500 | `{"error":"NoSuchDatabase"}` | +| No database bound to `main` | 500 | `{"error":"no such database"}` | | Schema not applied | 500 | `{"error":"relation \"example_users\" does not exist","code":"42P01"}` | | Row doesn't match `User` | 500 | `{"error":"row decode: …"}` | diff --git a/examples/http/wasi/db_sql/src/lib.rs b/examples/http/wasi/db_sql/src/lib.rs index 165f80a..569b645 100644 --- a/examples/http/wasi/db_sql/src/lib.rs +++ b/examples/http/wasi/db_sql/src/lib.rs @@ -31,6 +31,9 @@ async fn main(request: Request) -> anyhow::Result> { // "main" is the binding name; the host resolves it to the app's database. let db = match Database::open("main") { Ok(db) => db, + Err(Error::NoSuchDatabase) => { + return json(500, &serde_json::json!({ "error": "no such database" })) + } Err(e) => return json(500, &serde_json::json!({ "error": format!("{e:?}") })), }; From a78e42727c0b013524218ef4ffedadf54dd44428 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 12:36:22 +0000 Subject: [PATCH 03/13] fix(db): non-finite floats decode as strings, not null --- src/db.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/db.rs b/src/db.rs index ed3ab86..45d2913 100644 --- a/src/db.rs +++ b/src/db.rs @@ -132,6 +132,9 @@ pub mod sql { Value::Boolean(b) => b.into(), Value::Int32(i) => i.into(), Value::Int64(i) => i.into(), + // JSON has no NaN/Infinity (serde_json would make them null): keep them as + // strings so an `f64` field fails to decode instead of reading NULL. + Value::Float64(f) if !f.is_finite() => Json::String(f.to_string()), Value::Float64(f) => f.into(), Value::Json(s) => serde_json::from_str(&s).unwrap_or(Json::String(s)), Value::Numeric(s) | Value::Text(s) => s.into(), From 659e3e6c8f1c17f288f109f667bb7281b3b1f99d Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 14:44:21 +0000 Subject: [PATCH 04/13] chore(wit): FastEdge-wit 6af3ae7 (float64 case unescaped again) --- wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wit b/wit index 6ddb92e..6af3ae7 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit 6ddb92e650ea17448519b6fadd4beba808dc8280 +Subproject commit 6af3ae70606012a768ca058a1abd759b4e1ad92c From f420841de0143f9be9bb768b7bc9d3c49201e873 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 15:32:33 +0000 Subject: [PATCH 05/13] feat(db): bind gcore:db/sql-types + sql-sync (WIT split; plain sql reserved for async) --- src/db.rs | 8 ++++---- src/lib.rs | 5 ++++- wit | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/db.rs b/src/db.rs index 45d2913..1a60e39 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,7 +2,7 @@ //! //! The connection details stay with the host; the app only knows the binding name. -/// SQL database (PostgreSQL), `gcore:db/sql`. +/// SQL database (PostgreSQL), `gcore:db/sql-sync`. /// /// Calls are blocking. Parameters use positional placeholders (`$1`, `$2`, ...). /// @@ -30,9 +30,9 @@ /// ``` pub mod sql { #[doc(inline)] - pub use crate::gcore::db::sql::{ - Column, Database, DbError, Error, QueryResult, Statement, Value, - }; + pub use crate::gcore::db::sql_sync::Database; + #[doc(inline)] + pub use crate::gcore::db::sql_types::{Column, DbError, Error, QueryResult, Statement, Value}; #[doc(inline)] pub use crate::__db_sql_params as params; diff --git a/src/lib.rs b/src/lib.rs index 549b5e5..0080c77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -261,7 +261,10 @@ wit_bindgen::generate!({ world: "reactor", path: "wit", pub_export_macro: true, - with: { "gcore:db/sql@0.1.0": generate }, + with: { + "gcore:db/sql-types@0.1.0": generate, + "gcore:db/sql-sync@0.1.0": generate, + }, }); /// Fast read-only key-value dictionary for configuration. diff --git a/wit b/wit index 6af3ae7..83a7d08 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit 6af3ae70606012a768ca058a1abd759b4e1ad92c +Subproject commit 83a7d0858d9d2138cb12d712891e7891006084cc From 0b5675206444d4d9ff1f515ae1ebf0fc5735280d Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 15:32:45 +0000 Subject: [PATCH 06/13] docs(context): db sql-sync WIT split --- context/CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index 0bd83c2..9355c20 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -4,6 +4,22 @@ This file tracks agent decisions, architectural changes, and context for future --- +## [2026-10-27] — db: bind `gcore:db/sql-sync` (WIT split) + +### Overview +FastEdge-wit split `gcore:db/sql` into `sql-types` + `sql-sync`, mirroring `cache-types` / `cache-sync`. + +### Decisions +- Rust stays on the sync interface by default; the plain `sql` name is reserved for an async (pollable) + variant intended for the JS SDK. Public API `fastedge::db::sql` is unchanged. + +### Changes +- `wit` submodule → FastEdge-wit `feat/db-interface` 83a7d08 +- `src/lib.rs` — `with:` generates `gcore:db/sql-types@0.1.0` and `gcore:db/sql-sync@0.1.0` +- `src/db.rs` — re-exports `Database` from `sql_sync`, data types from `sql_types` + +--- + ## [2026-10-27] — POC: `fastedge::db::sql` (gcore:db/sql@0.1.0) ### Overview From cf31945ec97d95df958b69818ffb204f5684404d Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 17:18:25 +0000 Subject: [PATCH 07/13] feat(db): fastedge::db::kv (gcore:db/kv-sync) + db_kv example --- context/CHANGELOG.md | 19 + examples/README.md | 1 + examples/http/wasi/db_kv/Cargo.lock | 640 ++++++++++++++++++++++++++++ examples/http/wasi/db_kv/Cargo.toml | 16 + examples/http/wasi/db_kv/README.md | 50 +++ examples/http/wasi/db_kv/src/lib.rs | 74 ++++ src/db.rs | 44 ++ src/lib.rs | 2 + wit | 2 +- 9 files changed, 847 insertions(+), 1 deletion(-) create mode 100644 examples/http/wasi/db_kv/Cargo.lock create mode 100644 examples/http/wasi/db_kv/Cargo.toml create mode 100644 examples/http/wasi/db_kv/README.md create mode 100644 examples/http/wasi/db_kv/src/lib.rs diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index 9355c20..3c4361f 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -4,6 +4,25 @@ This file tracks agent decisions, architectural changes, and context for future --- +## [2026-10-27] — POC: `fastedge::db::kv` (gcore:db/kv-sync@0.1.0) + +### Overview +Added `fastedge::db::kv`: Database KV (regional, durable, writable; Upstash Redis in the POC) bound +to the app by name; the host holds the credentials. + +### Decisions +- Sync interface `kv-sync`, like `sql-sync`; the plain `kv` name is reserved for an async variant + (JS SDK). +- Re-export the generated types as is (`Database`, `Op`, `OpResult`, `SetOptions`, ...), plus + `Default for SetOptions` (always write, no expiry). No other wrapper. +- Example mirrors the JS SDK `examples/db-kv` (same keys, limits, responses). + +### Changes +- `wit` submodule → FastEdge-wit `feat/db-interface-kv` ca89dc6 (`kv-types`, `kv-sync`) +- `src/lib.rs` — `with:` generates `gcore:db/kv-types@0.1.0` and `gcore:db/kv-sync@0.1.0` +- `src/db.rs` — `db::kv` module +- `examples/http/wasi/db_kv/` — new wstd example; `examples/README.md` entry + ## [2026-10-27] — db: bind `gcore:db/sql-sync` (WIT split) ### Overview diff --git a/examples/README.md b/examples/README.md index 5a87eb5..83e9e47 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,6 +27,7 @@ Examples are organized into three categories: | [variables_and_secrets](./http/wasi/variables_and_secrets/) | Read an environment variable and a secret | | [simple_fetch](./http/wasi/simple_fetch/) | Fetch from a configurable URL and return the response | | [db_sql](./http/wasi/db_sql/) | Query a bound PostgreSQL database and decode rows into a struct (`fastedge::db::sql`) | +| [db_kv](./http/wasi/db_kv/) | Per-user rate limit and visit counter in a bound Database KV, one atomic batch (`fastedge::db::kv`) | ### cdn (proxy-wasm) diff --git a/examples/http/wasi/db_kv/Cargo.lock b/examples/http/wasi/db_kv/Cargo.lock new file mode 100644 index 0000000..3f9f35d --- /dev/null +++ b/examples/http/wasi/db_kv/Cargo.lock @@ -0,0 +1,640 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "db_kv" +version = "0.1.0" +dependencies = [ + "anyhow", + "fastedge", + "serde_json", + "wstd", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "fastedge" +version = "0.4.5" +dependencies = [ + "bytes", + "fastedge-derive", + "http", + "mime", + "thiserror", + "wit-bindgen 0.46.0", +] + +[[package]] +name = "fastedge-derive" +version = "0.4.5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +dependencies = [ + "bitflags", + "futures", + "once_cell", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cabd629f94da277abc739c71353397046401518efb2c707669f805205f0b9890" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a4232e841089fa5f3c4fc732a92e1c74e1a3958db3b12f1de5934da2027f1f4" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.119", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0d4698c2913d8d9c2b220d116409c3f51a7aa8d7765151b886918367179ee9" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.119", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a866b19dba2c94d706ec58c92a4c62ab63e482b4c935d2a085ac94caecb136" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.239.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55c92c939d667b7bf0c6bf2d1f67196529758f99a2a45a3355cc56964fd5315d" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wstd" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29b52936db10a79bb724dadd1c2c3aac958e8229dcb1f1c7f2b7044ca9fc6a3a" +dependencies = [ + "anyhow", + "async-task", + "bytes", + "futures-lite", + "http", + "http-body", + "http-body-util", + "itoa", + "pin-project-lite", + "serde", + "serde_json", + "slab", + "wasip2", + "wstd-macro", +] + +[[package]] +name = "wstd-macro" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153db9b65508bf6c2efe26617169840c03671ba5719a35868db7682a5261f7d4" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/http/wasi/db_kv/Cargo.toml b/examples/http/wasi/db_kv/Cargo.toml new file mode 100644 index 0000000..34cc31d --- /dev/null +++ b/examples/http/wasi/db_kv/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] + +[package] +name = "db_kv" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wstd = "0.6" +fastedge = { path = "../../../.." } +anyhow = "1" +serde_json = "1" + diff --git a/examples/http/wasi/db_kv/README.md b/examples/http/wasi/db_kv/README.md new file mode 100644 index 0000000..140115d --- /dev/null +++ b/examples/http/wasi/db_kv/README.md @@ -0,0 +1,50 @@ +[← Back to examples](../../../README.md) + +# Database KV (WASI) + +Per-user rate limit and a visit counter in a Database KV bound to the app as `main`, updated in one +atomic `batch`. + +Database KV is regional, durable and writable from the app. It is not Edge Storage +(`fastedge::key_value`, read-only) or the PoP-local cache. + +## Setup + +Bind the database: add a secret `FASTEDGE_DB_KV_main` with the Upstash Redis REST URL and token as +`https://@` (e.g. `https://AXXX...@eu1-example-12345.upstash.io`). The app never sees +it; the host resolves `Database::open("main")`. + +## Usage + +``` +GET /?user=ann → 200 {"user":"ann","requests":1,"limit":5,"visits":17} +(6th request from ann within 60 s) +GET /?user=ann → 429 {"error":"rate limited","user":"ann","limit":5} +GET / → user "anonymous" +``` + +Each request runs one batch: `set` the user's window counter to `0` with a 60 s TTL if it doesn't +exist, `incr` it, `incr` the global `visits` counter. The batch is all-or-nothing. + +## Error responses + +| Condition | Status | Body | +|---|---|---| +| No database bound to `main` | 500 | `{"error":"no such database"}` | +| A key holds a non-counter value | 500 | `{"error":"…","code":"ERR"}` | + +## Build + +```sh +cargo build --release +# Output: target/wasm32-wasip2/release/db_kv.wasm +``` + +Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_kv.wasm --wasi-http true --secret "FASTEDGE_DB_KV_main=https://@"` + +## APIs used + +- `fastedge::db::kv::Database::open(name)` — open the database bound to `name` +- `db.batch(&[Op::Set(..), Op::Incr(..), …])` — run operations atomically +- `SetOptions { ttl_ms, condition: SetCondition::IfNotExists }` — expiry and conditional writes +- `Error::Operation(OpError { code, message, op_index })` — a failing operation diff --git a/examples/http/wasi/db_kv/src/lib.rs b/examples/http/wasi/db_kv/src/lib.rs new file mode 100644 index 0000000..ee34420 --- /dev/null +++ b/examples/http/wasi/db_kv/src/lib.rs @@ -0,0 +1,74 @@ +use fastedge::db::kv::{Database, Error, IncrOp, Op, OpResult, SetCondition, SetOp, SetOptions}; +use serde_json::json; +use wstd::http::body::Body; +use wstd::http::{Request, Response}; + +/// Requests one user may make per window. +const LIMIT: i64 = 5; +const WINDOW_MS: u64 = 60_000; + +#[wstd::http_server] +async fn main(request: Request) -> anyhow::Result> { + let user = request + .uri() + .query() + .and_then(|q| q.split('&').find_map(|kv| kv.strip_prefix("user="))) + .unwrap_or("anonymous") + .to_string(); + + // "main" is the binding name; the host resolves it to the app's database. + let db = match Database::open("main") { + Ok(db) => db, + Err(Error::NoSuchDatabase) => return json(500, json!({ "error": "no such database" })), + Err(e) => return json(500, json!({ "error": format!("{e:?}") })), + }; + + // One atomic batch: start the user's window counter if missing (it expires with the + // window), count this request, count the visit. + let window = format!("rl:{user}"); + let ops = [ + Op::Set(SetOp { + key: window.clone(), + value: b"0".to_vec(), + options: SetOptions { + ttl_ms: Some(WINDOW_MS), + condition: SetCondition::IfNotExists, + }, + }), + Op::Incr(IncrOp { + key: window, + delta: 1, + }), + Op::Incr(IncrOp { + key: "visits".into(), + delta: 1, + }), + ]; + let (requests, visits) = match db.batch(&ops).as_deref() { + Ok([_, OpResult::Incr(requests), OpResult::Incr(visits)]) => (*requests, *visits), + Ok(_) => return json(500, json!({ "error": "unexpected batch result" })), + // e.g. WRONGTYPE: "visits" holds something that isn't a counter + Err(Error::Operation(e)) => { + return json(500, json!({ "error": e.message, "code": e.code })) + } + Err(e) => return json(500, json!({ "error": format!("{e:?}") })), + }; + + if requests > LIMIT { + return json( + 429, + json!({ "error": "rate limited", "user": user, "limit": LIMIT }), + ); + } + json( + 200, + json!({ "user": user, "requests": requests, "limit": LIMIT, "visits": visits }), + ) +} + +fn json(status: u16, body: serde_json::Value) -> anyhow::Result> { + Ok(Response::builder() + .status(status) + .header("content-type", "application/json") + .body(Body::from(body.to_string()))?) +} diff --git a/src/db.rs b/src/db.rs index 1a60e39..40889fd 100644 --- a/src/db.rs +++ b/src/db.rs @@ -144,6 +144,50 @@ pub mod sql { } } +/// Database KV (regional, durable, writable key-value), `gcore:db/kv-sync`. +/// +/// Not Edge Storage ([`crate::key_value`], read-only, replicated to every edge) and not the +/// PoP-local cache. Calls are blocking. Values are bytes and must be valid UTF-8 for now. +/// +/// # Examples +/// +/// ```no_run +/// use fastedge::db::kv::{Database, Op, SetCondition, SetOp, SetOptions, IncrOp}; +/// +/// let db = Database::open("main")?; +/// db.set("greeting", b"hello", SetOptions::default())?; +/// let visits = db.incr("visits", 1)?; +/// +/// // Atomic: create a 60 s window counter if missing, then count this request. +/// let results = db.batch(&[ +/// Op::Set(SetOp { +/// key: "rl:ann".into(), +/// value: b"0".to_vec(), +/// options: SetOptions { ttl_ms: Some(60_000), condition: SetCondition::IfNotExists }, +/// }), +/// Op::Incr(IncrOp { key: "rl:ann".into(), delta: 1 }), +/// ])?; +/// # Ok::<(), fastedge::db::kv::Error>(()) +/// ``` +pub mod kv { + #[doc(inline)] + pub use crate::gcore::db::kv_sync::Database; + #[doc(inline)] + pub use crate::gcore::db::kv_types::{ + Error, ExpireOp, IncrOp, KvValue, Op, OpError, OpResult, SetCondition, SetOp, SetOptions, + }; + + /// Always write, no expiry. + impl Default for SetOptions { + fn default() -> Self { + Self { + ttl_ms: None, + condition: SetCondition::Always, + } + } + } +} + /// Builds a `&[Value]` parameter list from plain Rust values; see [`sql::ToSql`]. #[doc(hidden)] #[macro_export] diff --git a/src/lib.rs b/src/lib.rs index 0080c77..d88426d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -264,6 +264,8 @@ wit_bindgen::generate!({ with: { "gcore:db/sql-types@0.1.0": generate, "gcore:db/sql-sync@0.1.0": generate, + "gcore:db/kv-types@0.1.0": generate, + "gcore:db/kv-sync@0.1.0": generate, }, }); diff --git a/wit b/wit index 83a7d08..ca89dc6 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit 83a7d0858d9d2138cb12d712891e7891006084cc +Subproject commit ca89dc6fb5d9bf3ac1104bdc1c6811c6c492b9fe From 57ecb4d693e103b51abcd184e50aebfe36fca3ca Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 17:59:17 +0000 Subject: [PATCH 08/13] =?UTF-8?q?docs(db):=20kv=20consistency=20=E2=80=94?= =?UTF-8?q?=20read-your-writes=20within=20a=20request=20(FastEdge-wit=20ee?= =?UTF-8?q?d3ccd)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/db.rs | 3 +++ wit | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/db.rs b/src/db.rs index 40889fd..626a287 100644 --- a/src/db.rs +++ b/src/db.rs @@ -149,6 +149,9 @@ pub mod sql { /// Not Edge Storage ([`crate::key_value`], read-only, replicated to every edge) and not the /// PoP-local cache. Calls are blocking. Values are bytes and must be valid UTF-8 for now. /// +/// Within one request, reads see the request's own writes. Other requests may see a write only +/// after it has replicated (eventually, for a database with read replicas in other regions). +/// /// # Examples /// /// ```no_run diff --git a/wit b/wit index ca89dc6..eed3ccd 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit ca89dc6fb5d9bf3ac1104bdc1c6811c6c492b9fe +Subproject commit eed3ccd24e0ae1f4e35a9f68669440630a56c5d0 From 4f0784121d4b90dc0c6b612a34854d20fc158779 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Fri, 30 Oct 2026 09:25:16 +0000 Subject: [PATCH 09/13] fix(examples): db_sql decodes rows with the schema's default meta ({}) Meta.plan was required, so a row inserted with only id and email (meta defaults to '{}') made GET / fail with a decode error. plan is optional and still serialises as {}, matching the JS example's response. --- examples/http/wasi/db_sql/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/http/wasi/db_sql/src/lib.rs b/examples/http/wasi/db_sql/src/lib.rs index 569b645..c511a84 100644 --- a/examples/http/wasi/db_sql/src/lib.rs +++ b/examples/http/wasi/db_sql/src/lib.rs @@ -14,7 +14,9 @@ struct User { #[derive(Debug, Deserialize, Serialize)] struct Meta { - plan: String, + // Optional: the schema's default `meta` is `{}`. + #[serde(default, skip_serializing_if = "Option::is_none")] + plan: Option, } const SELECT_USERS: &str = "select id, email, nickname, created, meta from example_users"; From d75f8655a38366d9085ed39de31e5fdcdbe689b2 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Thu, 5 Nov 2026 09:21:53 +0000 Subject: [PATCH 10/13] db: fastedge::db::sql -> fastedge::db::postgres (gcore:db/postgres-sync) wit submodule -> FastEdge-wit e41118a. Same API; example db_sql renamed to db_postgres. --- context/CHANGELOG.md | 14 ++++++++++++ examples/README.md | 2 +- .../wasi/{db_sql => db_postgres}/Cargo.lock | 2 +- .../wasi/{db_sql => db_postgres}/Cargo.toml | 2 +- .../wasi/{db_sql => db_postgres}/README.md | 8 +++---- .../wasi/{db_sql => db_postgres}/schema.sql | 0 .../wasi/{db_sql => db_postgres}/src/lib.rs | 2 +- src/db.rs | 22 +++++++++---------- src/lib.rs | 4 ++-- wit | 2 +- 10 files changed, 36 insertions(+), 22 deletions(-) rename examples/http/wasi/{db_sql => db_postgres}/Cargo.lock (99%) rename examples/http/wasi/{db_sql => db_postgres}/Cargo.toml (92%) rename examples/http/wasi/{db_sql => db_postgres}/README.md (85%) rename examples/http/wasi/{db_sql => db_postgres}/schema.sql (100%) rename examples/http/wasi/{db_sql => db_postgres}/src/lib.rs (96%) diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index 3c4361f..36c2bfd 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -4,6 +4,20 @@ This file tracks agent decisions, architectural changes, and context for future --- +## [2026-11-05] — db: `fastedge::db::sql` renamed to `fastedge::db::postgres` + +### Overview +FastEdge-wit renamed the SQL interfaces after the engine they describe: `gcore:db/postgres-types` and +`gcore:db/postgres-sync` (async: `gcore:db/postgres`). The contract was already PostgreSQL ($1 +parameters, type OIDs, SQLSTATE); another dialect would get its own interface. Still `0.1.0` +(unreleased). + +### Changes +- `wit` submodule → FastEdge-wit `feat/db-interface-all` e41118a +- `src/lib.rs` — `with:` generates `gcore:db/postgres-types@0.1.0` and `gcore:db/postgres-sync@0.1.0` +- `src/db.rs` — `db::sql` → `db::postgres` (same API; `ToSql`, `params!`, `QueryAs` unchanged) +- `examples/http/wasi/db_sql/` → `examples/http/wasi/db_postgres/` + ## [2026-10-27] — POC: `fastedge::db::kv` (gcore:db/kv-sync@0.1.0) ### Overview diff --git a/examples/README.md b/examples/README.md index 83e9e47..6f15136 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,7 +26,7 @@ Examples are organized into three categories: | [headers](./http/wasi/headers/) | Echo request headers and add a custom header from an environment variable | | [variables_and_secrets](./http/wasi/variables_and_secrets/) | Read an environment variable and a secret | | [simple_fetch](./http/wasi/simple_fetch/) | Fetch from a configurable URL and return the response | -| [db_sql](./http/wasi/db_sql/) | Query a bound PostgreSQL database and decode rows into a struct (`fastedge::db::sql`) | +| [db_postgres](./http/wasi/db_postgres/) | Query a bound PostgreSQL database and decode rows into a struct (`fastedge::db::postgres`) | | [db_kv](./http/wasi/db_kv/) | Per-user rate limit and visit counter in a bound Database KV, one atomic batch (`fastedge::db::kv`) | ### cdn (proxy-wasm) diff --git a/examples/http/wasi/db_sql/Cargo.lock b/examples/http/wasi/db_postgres/Cargo.lock similarity index 99% rename from examples/http/wasi/db_sql/Cargo.lock rename to examples/http/wasi/db_postgres/Cargo.lock index d8b4497..cb54fa9 100644 --- a/examples/http/wasi/db_sql/Cargo.lock +++ b/examples/http/wasi/db_postgres/Cargo.lock @@ -33,7 +33,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] -name = "db_sql" +name = "db_postgres" version = "0.1.0" dependencies = [ "anyhow", diff --git a/examples/http/wasi/db_sql/Cargo.toml b/examples/http/wasi/db_postgres/Cargo.toml similarity index 92% rename from examples/http/wasi/db_sql/Cargo.toml rename to examples/http/wasi/db_postgres/Cargo.toml index 84871e0..53dcac3 100644 --- a/examples/http/wasi/db_sql/Cargo.toml +++ b/examples/http/wasi/db_postgres/Cargo.toml @@ -1,7 +1,7 @@ [workspace] [package] -name = "db_sql" +name = "db_postgres" version = "0.1.0" edition = "2021" diff --git a/examples/http/wasi/db_sql/README.md b/examples/http/wasi/db_postgres/README.md similarity index 85% rename from examples/http/wasi/db_sql/README.md rename to examples/http/wasi/db_postgres/README.md index da58bb1..33d799d 100644 --- a/examples/http/wasi/db_sql/README.md +++ b/examples/http/wasi/db_postgres/README.md @@ -1,6 +1,6 @@ [← Back to examples](../../../README.md) -# SQL database (WASI) +# PostgreSQL database (WASI) Reads users from a PostgreSQL database bound to the app as `main`, decoding rows into a Rust struct with `query_as`. @@ -37,14 +37,14 @@ GET /?id=9 → 404 {"error":"not found"} ```sh cargo build --release -# Output: target/wasm32-wasip2/release/db_sql.wasm +# Output: target/wasm32-wasip2/release/db_postgres.wasm ``` -Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_sql.wasm --wasi-http true --secret "FASTEDGE_DB_SQL_main=postgresql://…"` +Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_postgres.wasm --wasi-http true --secret "FASTEDGE_DB_SQL_main=postgresql://…"` ## APIs used -- `fastedge::db::sql::Database::open(name)` — open the database bound to `name` +- `fastedge::db::postgres::Database::open(name)` — open the database bound to `name` - `db.query_as::(sql, params)` — run one statement and decode rows into `T` (feature `json`) - `params![…]` — build `$1, $2, …` parameters from plain Rust values - `Error::Query(DbError { code, message, … })` — Postgres error with SQLSTATE diff --git a/examples/http/wasi/db_sql/schema.sql b/examples/http/wasi/db_postgres/schema.sql similarity index 100% rename from examples/http/wasi/db_sql/schema.sql rename to examples/http/wasi/db_postgres/schema.sql diff --git a/examples/http/wasi/db_sql/src/lib.rs b/examples/http/wasi/db_postgres/src/lib.rs similarity index 96% rename from examples/http/wasi/db_sql/src/lib.rs rename to examples/http/wasi/db_postgres/src/lib.rs index c511a84..d883fc5 100644 --- a/examples/http/wasi/db_sql/src/lib.rs +++ b/examples/http/wasi/db_postgres/src/lib.rs @@ -1,4 +1,4 @@ -use fastedge::db::sql::{params, Database, Error, QueryAs, QueryAsError}; +use fastedge::db::postgres::{params, Database, Error, QueryAs, QueryAsError}; use serde::{Deserialize, Serialize}; use wstd::http::body::Body; use wstd::http::{Request, Response}; diff --git a/src/db.rs b/src/db.rs index 626a287..e7e2af8 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,25 +2,25 @@ //! //! The connection details stay with the host; the app only knows the binding name. -/// SQL database (PostgreSQL), `gcore:db/sql-sync`. +/// PostgreSQL database, `gcore:db/postgres-sync`. /// /// Calls are blocking. Parameters use positional placeholders (`$1`, `$2`, ...). /// /// # Examples /// /// ```no_run -/// use fastedge::db::sql::{params, Database}; +/// use fastedge::db::postgres::{params, Database}; /// /// let db = Database::open("main")?; /// let result = db.query("select id, email from users where id = $1", params![42i64])?; /// println!("{} rows", result.row_count); -/// # Ok::<(), fastedge::db::sql::Error>(()) +/// # Ok::<(), fastedge::db::postgres::Error>(()) /// ``` /// /// With the `json` feature, rows decode into any `serde::Deserialize` type by column name: /// /// ```ignore -/// use fastedge::db::sql::{params, Database, QueryAs}; +/// use fastedge::db::postgres::{params, Database, QueryAs}; /// /// #[derive(serde::Deserialize)] /// struct User { id: i64, email: String, nickname: Option } @@ -28,14 +28,14 @@ /// let users: Vec = Database::open("main")? /// .query_as("select id, email, nickname from users where id = $1", params![42i64])?; /// ``` -pub mod sql { +pub mod postgres { #[doc(inline)] - pub use crate::gcore::db::sql_sync::Database; + pub use crate::gcore::db::postgres_sync::Database; #[doc(inline)] - pub use crate::gcore::db::sql_types::{Column, DbError, Error, QueryResult, Statement, Value}; + pub use crate::gcore::db::postgres_types::{Column, DbError, Error, QueryResult, Statement, Value}; #[doc(inline)] - pub use crate::__db_sql_params as params; + pub use crate::__db_postgres_params as params; /// Converts a Rust value into a SQL parameter [`Value`]. /// @@ -191,9 +191,9 @@ pub mod kv { } } -/// Builds a `&[Value]` parameter list from plain Rust values; see [`sql::ToSql`]. +/// Builds a `&[Value]` parameter list from plain Rust values; see [`postgres::ToSql`]. #[doc(hidden)] #[macro_export] -macro_rules! __db_sql_params { - ($($p:expr),* $(,)?) => { &[$($crate::db::sql::ToSql::to_sql($p)),*] }; +macro_rules! __db_postgres_params { + ($($p:expr),* $(,)?) => { &[$($crate::db::postgres::ToSql::to_sql($p)),*] }; } diff --git a/src/lib.rs b/src/lib.rs index d88426d..9cedc01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -262,8 +262,8 @@ wit_bindgen::generate!({ path: "wit", pub_export_macro: true, with: { - "gcore:db/sql-types@0.1.0": generate, - "gcore:db/sql-sync@0.1.0": generate, + "gcore:db/postgres-types@0.1.0": generate, + "gcore:db/postgres-sync@0.1.0": generate, "gcore:db/kv-types@0.1.0": generate, "gcore:db/kv-sync@0.1.0": generate, }, diff --git a/wit b/wit index eed3ccd..e41118a 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit eed3ccd24e0ae1f4e35a9f68669440630a56c5d0 +Subproject commit e41118a13a1df9149d57e15e231cc84d4874168f From df582532cc2d66af84664d9d92f3d89470269f17 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Thu, 5 Nov 2026 09:35:09 +0000 Subject: [PATCH 11/13] db: example README uses secret FASTEDGE_DB_POSTGRES_main --- context/CHANGELOG.md | 3 +++ examples/http/wasi/db_postgres/README.md | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index 36c2bfd..bf946c2 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -17,6 +17,9 @@ parameters, type OIDs, SQLSTATE); another dialect would get its own interface. S - `src/lib.rs` — `with:` generates `gcore:db/postgres-types@0.1.0` and `gcore:db/postgres-sync@0.1.0` - `src/db.rs` — `db::sql` → `db::postgres` (same API; `ToSql`, `params!`, `QueryAs` unchanged) - `examples/http/wasi/db_sql/` → `examples/http/wasi/db_postgres/` +- POC binding secret `FASTEDGE_DB_SQL_` → `FASTEDGE_DB_POSTGRES_` (engine), example README +- `ToSql` keeps its name: the usual Rust name for this trait (rust-postgres, rusqlite, sqlx), and it + is already namespaced under `db::postgres` ## [2026-10-27] — POC: `fastedge::db::kv` (gcore:db/kv-sync@0.1.0) diff --git a/examples/http/wasi/db_postgres/README.md b/examples/http/wasi/db_postgres/README.md index 33d799d..9d3a275 100644 --- a/examples/http/wasi/db_postgres/README.md +++ b/examples/http/wasi/db_postgres/README.md @@ -7,7 +7,7 @@ with `query_as`. ## Setup -1. Bind the database: add a secret `FASTEDGE_DB_SQL_main` with the Postgres connection string +1. Bind the database: add a secret `FASTEDGE_DB_POSTGRES_main` with the Postgres connection string (e.g. a Neon pooled URL). The app never sees it; the host resolves `Database::open("main")`. 2. Create the schema once, outside the app: @@ -40,7 +40,7 @@ cargo build --release # Output: target/wasm32-wasip2/release/db_postgres.wasm ``` -Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_postgres.wasm --wasi-http true --secret "FASTEDGE_DB_SQL_main=postgresql://…"` +Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_postgres.wasm --wasi-http true --secret "FASTEDGE_DB_POSTGRES_main=postgresql://…"` ## APIs used From 36c5013c192964e402c57dad6bb0ab98927c2516 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Thu, 5 Nov 2026 15:23:29 +0000 Subject: [PATCH 12/13] db: dialect-neutral fastedge::db::sql (gcore:db/sql-sync), following the WIT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wit → FastEdge-wit a23194c. Column.type_name replaces type_oid; example db_sql_postgres (named after its dialect). Secret FASTEDGE_DB_SQL_. --- context/CHANGELOG.md | 23 ++++++++++--------- examples/README.md | 2 +- .../Cargo.lock | 2 +- .../Cargo.toml | 2 +- .../README.md | 14 +++++------ .../schema.sql | 0 .../src/lib.rs | 2 +- src/db.rs | 23 ++++++++++--------- src/lib.rs | 4 ++-- wit | 2 +- 10 files changed, 38 insertions(+), 36 deletions(-) rename examples/http/wasi/{db_postgres => db_sql_postgres}/Cargo.lock (99%) rename examples/http/wasi/{db_postgres => db_sql_postgres}/Cargo.toml (91%) rename examples/http/wasi/{db_postgres => db_sql_postgres}/README.md (68%) rename examples/http/wasi/{db_postgres => db_sql_postgres}/schema.sql (100%) rename examples/http/wasi/{db_postgres => db_sql_postgres}/src/lib.rs (96%) diff --git a/context/CHANGELOG.md b/context/CHANGELOG.md index bf946c2..a3958b8 100644 --- a/context/CHANGELOG.md +++ b/context/CHANGELOG.md @@ -4,22 +4,23 @@ This file tracks agent decisions, architectural changes, and context for future --- -## [2026-11-05] — db: `fastedge::db::sql` renamed to `fastedge::db::postgres` +## [2026-11-05] — db: dialect-neutral SQL; `Column.type_name`; example `db_sql_postgres` ### Overview -FastEdge-wit renamed the SQL interfaces after the engine they describe: `gcore:db/postgres-types` and -`gcore:db/postgres-sync` (async: `gcore:db/postgres`). The contract was already PostgreSQL ($1 -parameters, type OIDs, SQLSTATE); another dialect would get its own interface. Still `0.1.0` +`gcore:db/sql` is documented as dialect-neutral: SQL text, placeholders and error codes are the bound +database's, and the host picks the backend from the binding's configuration. Still `0.1.0` (unreleased). +### Decisions +- No `dialect()` call: binding a database of the wrong dialect is the developer's mistake +- The example is named after its dialect (PostgreSQL schema, `$1` placeholders), so a SQLite example + can sit beside it later + ### Changes -- `wit` submodule → FastEdge-wit `feat/db-interface-all` e41118a -- `src/lib.rs` — `with:` generates `gcore:db/postgres-types@0.1.0` and `gcore:db/postgres-sync@0.1.0` -- `src/db.rs` — `db::sql` → `db::postgres` (same API; `ToSql`, `params!`, `QueryAs` unchanged) -- `examples/http/wasi/db_sql/` → `examples/http/wasi/db_postgres/` -- POC binding secret `FASTEDGE_DB_SQL_` → `FASTEDGE_DB_POSTGRES_` (engine), example README -- `ToSql` keeps its name: the usual Rust name for this trait (rust-postgres, rusqlite, sqlx), and it - is already namespaced under `db::postgres` +- `wit` submodule → FastEdge-wit `feat/db-interface-all` a23194c +- `Column.type_name` (the database's type name, e.g. `int4`) replaces the PostgreSQL `type_oid` +- `src/db.rs` — module docs describe the dialect as the bound database's +- `examples/http/wasi/db_sql/` → `examples/http/wasi/db_sql_postgres/` ## [2026-10-27] — POC: `fastedge::db::kv` (gcore:db/kv-sync@0.1.0) diff --git a/examples/README.md b/examples/README.md index 6f15136..aa31b8a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,7 +26,7 @@ Examples are organized into three categories: | [headers](./http/wasi/headers/) | Echo request headers and add a custom header from an environment variable | | [variables_and_secrets](./http/wasi/variables_and_secrets/) | Read an environment variable and a secret | | [simple_fetch](./http/wasi/simple_fetch/) | Fetch from a configurable URL and return the response | -| [db_postgres](./http/wasi/db_postgres/) | Query a bound PostgreSQL database and decode rows into a struct (`fastedge::db::postgres`) | +| [db_sql_postgres](./http/wasi/db_sql_postgres/) | Query a bound SQL database (PostgreSQL here) and decode rows into a struct (`fastedge::db::sql`) | | [db_kv](./http/wasi/db_kv/) | Per-user rate limit and visit counter in a bound Database KV, one atomic batch (`fastedge::db::kv`) | ### cdn (proxy-wasm) diff --git a/examples/http/wasi/db_postgres/Cargo.lock b/examples/http/wasi/db_sql_postgres/Cargo.lock similarity index 99% rename from examples/http/wasi/db_postgres/Cargo.lock rename to examples/http/wasi/db_sql_postgres/Cargo.lock index cb54fa9..a156c6e 100644 --- a/examples/http/wasi/db_postgres/Cargo.lock +++ b/examples/http/wasi/db_sql_postgres/Cargo.lock @@ -33,7 +33,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] -name = "db_postgres" +name = "db_sql_postgres" version = "0.1.0" dependencies = [ "anyhow", diff --git a/examples/http/wasi/db_postgres/Cargo.toml b/examples/http/wasi/db_sql_postgres/Cargo.toml similarity index 91% rename from examples/http/wasi/db_postgres/Cargo.toml rename to examples/http/wasi/db_sql_postgres/Cargo.toml index 53dcac3..d6f01b9 100644 --- a/examples/http/wasi/db_postgres/Cargo.toml +++ b/examples/http/wasi/db_sql_postgres/Cargo.toml @@ -1,7 +1,7 @@ [workspace] [package] -name = "db_postgres" +name = "db_sql_postgres" version = "0.1.0" edition = "2021" diff --git a/examples/http/wasi/db_postgres/README.md b/examples/http/wasi/db_sql_postgres/README.md similarity index 68% rename from examples/http/wasi/db_postgres/README.md rename to examples/http/wasi/db_sql_postgres/README.md index 9d3a275..4925897 100644 --- a/examples/http/wasi/db_postgres/README.md +++ b/examples/http/wasi/db_sql_postgres/README.md @@ -1,13 +1,13 @@ [← Back to examples](../../../README.md) -# PostgreSQL database (WASI) +# SQL database (WASI) -Reads users from a PostgreSQL database bound to the app as `main`, decoding rows into a Rust struct +Reads users from a SQL database (PostgreSQL in this example) bound to the app as `main`, decoding rows into a Rust struct with `query_as`. ## Setup -1. Bind the database: add a secret `FASTEDGE_DB_POSTGRES_main` with the Postgres connection string +1. Bind the database: add a secret `FASTEDGE_DB_SQL_main` with the Postgres connection string (e.g. a Neon pooled URL). The app never sees it; the host resolves `Database::open("main")`. 2. Create the schema once, outside the app: @@ -37,14 +37,14 @@ GET /?id=9 → 404 {"error":"not found"} ```sh cargo build --release -# Output: target/wasm32-wasip2/release/db_postgres.wasm +# Output: target/wasm32-wasip2/release/db_sql_postgres.wasm ``` -Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_postgres.wasm --wasi-http true --secret "FASTEDGE_DB_POSTGRES_main=postgresql://…"` +Local run: `fastedge-run http -w target/wasm32-wasip2/release/db_sql_postgres.wasm --wasi-http true --secret "FASTEDGE_DB_SQL_main=postgresql://…"` ## APIs used -- `fastedge::db::postgres::Database::open(name)` — open the database bound to `name` +- `fastedge::db::sql::Database::open(name)` — open the database bound to `name` - `db.query_as::(sql, params)` — run one statement and decode rows into `T` (feature `json`) - `params![…]` — build `$1, $2, …` parameters from plain Rust values -- `Error::Query(DbError { code, message, … })` — Postgres error with SQLSTATE +- `Error::Query(DbError { code, message, … })` — database error; for PostgreSQL `code` is the SQLSTATE diff --git a/examples/http/wasi/db_postgres/schema.sql b/examples/http/wasi/db_sql_postgres/schema.sql similarity index 100% rename from examples/http/wasi/db_postgres/schema.sql rename to examples/http/wasi/db_sql_postgres/schema.sql diff --git a/examples/http/wasi/db_postgres/src/lib.rs b/examples/http/wasi/db_sql_postgres/src/lib.rs similarity index 96% rename from examples/http/wasi/db_postgres/src/lib.rs rename to examples/http/wasi/db_sql_postgres/src/lib.rs index d883fc5..c511a84 100644 --- a/examples/http/wasi/db_postgres/src/lib.rs +++ b/examples/http/wasi/db_sql_postgres/src/lib.rs @@ -1,4 +1,4 @@ -use fastedge::db::postgres::{params, Database, Error, QueryAs, QueryAsError}; +use fastedge::db::sql::{params, Database, Error, QueryAs, QueryAsError}; use serde::{Deserialize, Serialize}; use wstd::http::body::Body; use wstd::http::{Request, Response}; diff --git a/src/db.rs b/src/db.rs index e7e2af8..3a8f3fc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,25 +2,26 @@ //! //! The connection details stay with the host; the app only knows the binding name. -/// PostgreSQL database, `gcore:db/postgres-sync`. +/// SQL database, `gcore:db/sql-sync`. The SQL dialect (placeholders, types, error codes) is that of +/// the database bound to the name; PostgreSQL on Neon in the POC. /// /// Calls are blocking. Parameters use positional placeholders (`$1`, `$2`, ...). /// /// # Examples /// /// ```no_run -/// use fastedge::db::postgres::{params, Database}; +/// use fastedge::db::sql::{params, Database}; /// /// let db = Database::open("main")?; /// let result = db.query("select id, email from users where id = $1", params![42i64])?; /// println!("{} rows", result.row_count); -/// # Ok::<(), fastedge::db::postgres::Error>(()) +/// # Ok::<(), fastedge::db::sql::Error>(()) /// ``` /// /// With the `json` feature, rows decode into any `serde::Deserialize` type by column name: /// /// ```ignore -/// use fastedge::db::postgres::{params, Database, QueryAs}; +/// use fastedge::db::sql::{params, Database, QueryAs}; /// /// #[derive(serde::Deserialize)] /// struct User { id: i64, email: String, nickname: Option } @@ -28,14 +29,14 @@ /// let users: Vec = Database::open("main")? /// .query_as("select id, email, nickname from users where id = $1", params![42i64])?; /// ``` -pub mod postgres { +pub mod sql { #[doc(inline)] - pub use crate::gcore::db::postgres_sync::Database; + pub use crate::gcore::db::sql_sync::Database; #[doc(inline)] - pub use crate::gcore::db::postgres_types::{Column, DbError, Error, QueryResult, Statement, Value}; + pub use crate::gcore::db::sql_types::{Column, DbError, Error, QueryResult, Statement, Value}; #[doc(inline)] - pub use crate::__db_postgres_params as params; + pub use crate::__db_sql_params as params; /// Converts a Rust value into a SQL parameter [`Value`]. /// @@ -191,9 +192,9 @@ pub mod kv { } } -/// Builds a `&[Value]` parameter list from plain Rust values; see [`postgres::ToSql`]. +/// Builds a `&[Value]` parameter list from plain Rust values; see [`sql::ToSql`]. #[doc(hidden)] #[macro_export] -macro_rules! __db_postgres_params { - ($($p:expr),* $(,)?) => { &[$($crate::db::postgres::ToSql::to_sql($p)),*] }; +macro_rules! __db_sql_params { + ($($p:expr),* $(,)?) => { &[$($crate::db::sql::ToSql::to_sql($p)),*] }; } diff --git a/src/lib.rs b/src/lib.rs index 9cedc01..d88426d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -262,8 +262,8 @@ wit_bindgen::generate!({ path: "wit", pub_export_macro: true, with: { - "gcore:db/postgres-types@0.1.0": generate, - "gcore:db/postgres-sync@0.1.0": generate, + "gcore:db/sql-types@0.1.0": generate, + "gcore:db/sql-sync@0.1.0": generate, "gcore:db/kv-types@0.1.0": generate, "gcore:db/kv-sync@0.1.0": generate, }, diff --git a/wit b/wit index e41118a..a23194c 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit e41118a13a1df9149d57e15e231cc84d4874168f +Subproject commit a23194c04982972c295310260a48de438a3bdf6e From 6b8695e67f370eb4feba9629b078091cdd9cfa05 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Thu, 5 Nov 2026 16:08:43 +0000 Subject: [PATCH 13/13] =?UTF-8?q?db:=20wit=20=E2=86=92=20FastEdge-wit=2029?= =?UTF-8?q?f389b=20(sql-sync=20portability=20wording)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wit b/wit index a23194c..29f389b 160000 --- a/wit +++ b/wit @@ -1 +1 @@ -Subproject commit a23194c04982972c295310260a48de438a3bdf6e +Subproject commit 29f389b40c8bcfc25b15cb942307b579267583e9