From d73068df4bfe3e9a1f2c385bed3604bf59e50a95 Mon Sep 17 00:00:00 2001 From: Renatillas Date: Sat, 29 Aug 2026 15:55:54 +0200 Subject: [PATCH] Gleamy API --- src/cf/d1.gleam | 141 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 116 insertions(+), 25 deletions(-) diff --git a/src/cf/d1.gleam b/src/cf/d1.gleam index 141e23b..d0fdd25 100644 --- a/src/cf/d1.gleam +++ b/src/cf/d1.gleam @@ -5,16 +5,19 @@ //// operations as `Promise(Result(_, String))` values rather than throwing for //// ordinary query failures. //// -//// Values returned from D1 rows are represented as `gleam/dynamic.Dynamic`. -//// Decode them at the boundary of your application using `gleam/dynamic/decode`. +//// Use `returning` with a `gleam/dynamic/decode.Decoder` to turn row objects +//// into typed Gleam values. Raw D1 results remain available as `Dynamic`. //// //// The wrapper does not attempt to model SQL types or schemas. Applications own //// SQL construction, parameter ordering, row decoding, and domain-specific error //// conversion. import gleam/dynamic.{type Dynamic} +import gleam/dynamic/decode import gleam/javascript/array.{type Array} import gleam/javascript/promise.{type Promise} +import gleam/list +import gleam/result /// An opaque Cloudflare D1 database binding. /// @@ -22,24 +25,42 @@ import gleam/javascript/promise.{type Promise} /// Worker environment. The value corresponds to Cloudflare's `D1Database` object. pub type Database +type D1PreparedStatement + +pub type Error { + BatchError(String) + RunError(String) + DecodeErrors(List(decode.DecodeError)) + StatementResultMismatch +} + /// Prepare a SQL statement against a D1 database. /// /// The query may contain positional placeholders (`?`) for later binding with /// `bind`. This function mirrors `D1Database.prepare` and does not execute SQL. @external(javascript, "../cf_d1_ffi.mjs", "prepare") -pub fn prepare(db: Database, query: String) -> PreparedStatement +fn do_prepare(db: Database, query: String) -> D1PreparedStatement + +pub fn prepare(db: Database, query: String) -> PreparedStatement(RunResult) { + PreparedStatement(statement: do_prepare(db, query), decode_result: Ok) +} /// An opaque prepared D1 statement. /// -/// Prepared statements can be executed directly with `run`, `raw`, or `first`, -/// or grouped with `batch`. -pub type PreparedStatement +/// The type parameter is the value produced by `run`. Statements initially +/// return `RunResult`; `returning` changes this to a decoded list of rows. +pub opaque type PreparedStatement(output) { + PreparedStatement( + statement: D1PreparedStatement, + decode_result: fn(RunResult) -> Result(output, List(decode.DecodeError)), + ) +} @external(javascript, "../cf_d1_ffi.mjs", "bind") fn do_bind( - statement: PreparedStatement, + statement: D1PreparedStatement, values: Array(String), -) -> PreparedStatement +) -> D1PreparedStatement /// Bind string parameters to a prepared statement. /// @@ -48,10 +69,29 @@ fn do_bind( /// callers should convert numbers and other primitive values explicitly before /// binding. pub fn bind( - statement: PreparedStatement, + statement: PreparedStatement(output), values: List(String), -) -> PreparedStatement { - do_bind(statement, array.from_list(values)) +) -> PreparedStatement(output) { + PreparedStatement( + ..statement, + statement: do_bind(statement.statement, array.from_list(values)), + ) +} + +/// Decode each row returned by `run` with the supplied dynamic decoder. +/// +/// Decoder failures are returned as `Error(String)`, like D1 query failures. +/// The statement is still executed before its returned rows are decoded. +pub fn returning( + statement: PreparedStatement(previous_output), + decoder: decode.Decoder(row), +) -> PreparedStatement(List(row)) { + PreparedStatement(..statement, decode_result: fn(run_result: RunResult) { + run_result.results + |> array.to_list + |> dynamic.list + |> decode.run(decode.list(decoder)) + }) } /// Result metadata and rows from a D1 statement execution. @@ -70,48 +110,99 @@ pub type RunResult { /// Execute a prepared statement using D1's `run` method. /// -/// Use this for inserts, updates, deletes, or SQL that should return a D1 result -/// object. D1 errors are returned as `Error(String)`. +/// A statement from `prepare` returns the complete `RunResult`. A statement +/// configured with `returning` returns its decoded rows instead. D1 and decoder +/// errors are returned as `Error(String)`. @external(javascript, "../cf_d1_ffi.mjs", "run") -pub fn run(statement: PreparedStatement) -> Promise(Result(RunResult, String)) +fn do_run(statement: D1PreparedStatement) -> Promise(Result(RunResult, String)) + +pub fn run( + statement: PreparedStatement(output), +) -> Promise(Result(output, Error)) { + use run_result <- promise.map(do_run(statement.statement)) + case run_result { + Ok(run_result) -> + statement.decode_result(run_result) |> result.map_error(DecodeErrors) + Error(error) -> Error(RunError(error)) + } +} /// Execute a prepared statement and return rows as positional arrays. /// /// This mirrors D1's `raw` method. It is useful when selecting a known column /// order and decoding by index rather than by object field name. @external(javascript, "../cf_d1_ffi.mjs", "raw") -pub fn raw( - statement: PreparedStatement, +fn do_raw( + statement: D1PreparedStatement, ) -> Promise(Result(Array(Array(Dynamic)), String)) +pub fn raw( + statement: PreparedStatement(output), +) -> Promise(Result(Array(Array(Dynamic)), String)) { + do_raw(statement.statement) +} + /// Execute a prepared statement and return the first row. /// /// The returned dynamic value is the first row object from D1. If D1 returns no /// row, Cloudflare's binding decides the exact JavaScript value; decode it at /// the call site according to the query you issued. @external(javascript, "../cf_d1_ffi.mjs", "first") -pub fn first(statement: PreparedStatement) -> Promise(Result(Dynamic, String)) +fn do_first(statement: D1PreparedStatement) -> Promise(Result(Dynamic, String)) + +pub fn first( + statement: PreparedStatement(output), +) -> Promise(Result(Dynamic, String)) { + do_first(statement.statement) +} @external(javascript, "../cf_d1_ffi.mjs", "batch") fn do_batch( db: Database, - statements: Array(PreparedStatement), + statements: Array(D1PreparedStatement), ) -> Promise(Result(Array(RunResult), String)) /// Execute multiple prepared statements as one D1 batch. /// -/// D1 executes batch statements sequentially and returns one `RunResult` per -/// statement in the same order. Each result contains that statement's `success`, -/// `meta`, and `results` fields. +/// D1 executes batch statements sequentially and returns one decoded output per +/// statement in the same order. Statements from `prepare` yield `RunResult`; +/// statements configured with `returning` yield decoded row lists. /// /// Cloudflare D1 rejects empty batches. Callers should avoid calling `batch` /// with `[]`, or deliberately handle the returned error. pub fn batch( db: Database, - statements: List(PreparedStatement), -) -> Promise(Result(Array(RunResult), String)) { - use result <- promise.await(do_batch(db, array.from_list(statements))) - result |> promise.resolve() + statements: List(PreparedStatement(output)), +) -> Promise(Result(List(output), Error)) { + let native_statements = + statements + |> list.map(fn(statement) { statement.statement }) + |> array.from_list + + use batch_result <- promise.map(do_batch(db, native_statements)) + case batch_result { + Error(error) -> Error(BatchError(error)) + Ok(run_results) -> + decode_batch_results(statements, array.to_list(run_results), []) + } +} + +fn decode_batch_results( + statements: List(PreparedStatement(output)), + run_results: List(RunResult), + acc: List(output), +) -> Result(List(output), Error) { + case statements, run_results { + [], [] -> Ok(acc |> list.reverse()) + [statement, ..statements], [run_result, ..run_results] -> + case statement.decode_result(run_result) { + Ok(value) -> { + decode_batch_results(statements, run_results, [value, ..acc]) + } + Error(errors) -> Error(DecodeErrors(errors)) + } + _, _ -> Error(StatementResultMismatch) + } } /// Result from D1's `exec` API. -- 2.51.2