From 11237f98612856abc038c20e634d67886977f093 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 20 Oct 2026 13:57:53 +0100 Subject: [PATCH 1/4] db proposal as its own package --- deps/db/sql.wit | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ world.wit | 1 + 2 files changed, 119 insertions(+) create mode 100644 deps/db/sql.wit diff --git a/deps/db/sql.wit b/deps/db/sql.wit new file mode 100644 index 0000000..247b97d --- /dev/null +++ b/deps/db/sql.wit @@ -0,0 +1,118 @@ +package gcore:db@0.1.0; + +/// FastEdge SQL interface (synchronous variant). +/// +/// The contract is PostgreSQL: dialect, `$1`-style positional parameters and SQLSTATE error codes. +/// Applications name a binding (e.g. "main"); the host maps it to a backend. Backend, location and +/// credentials are invisible to the application, so a binding can move between backends without +/// code changes. +/// +/// Every call is a network round trip to a regional database. There is no session state between +/// calls: each `query` auto-commits and each `batch` is its own transaction. +/// +/// Calls are subject to host limits (timeout, response size, statements per batch). Exceeding +/// them returns `error::timeout` or `error::limit`. +interface sql { + /// A parameter or result value. + /// + /// Results use the narrowest variant that is lossless: + /// - `int2`, `int4` → `int32` + /// - `int8` → `int64` + /// - `float4`, `float8` → `float64` + /// - `numeric` → `numeric` (decimal text, e.g. "12345678901234567890.123") + /// - `bool` → `boolean` + /// - `json`, `jsonb` → `json` (JSON text) + /// - `bytea` → `bytes` + /// - everything else (text, varchar, uuid, date/time types, arrays, ...) → `text`, in PostgreSQL + /// text format. `timestamptz` is rendered in UTC. Arrays are PostgreSQL array literals + /// (e.g. "{1,2,3}"). Use `column.type-oid` to interpret them. + /// - SQL `NULL` → `null` + /// + /// Parameters are sent to the database as text; PostgreSQL casts them to the target type. + variant value { + null, + boolean(bool), + int32(s32), + int64(s64), + float64(f64), + numeric(string), + text(string), + bytes(list), + json(string), + } + + /// A result column. + record column { + /// Column name or alias. + name: string, + /// PostgreSQL type OID (e.g. 23 = int4, 1184 = timestamptz, 1007 = int4[]). + type-oid: u32, + } + + /// The result of one statement. + record query-result { + columns: list, + /// Rows in column order. Empty for statements that return no rows. + rows: list>, + /// Rows returned (SELECT) or affected (INSERT/UPDATE/DELETE). + row-count: u64, + } + + /// One statement in a batch. + record statement { + sql: string, + params: list, + } + + /// Error details reported by the database. + record db-error { + /// SQLSTATE code (e.g. "23505" unique_violation), when the database provides one. + code: option, + message: string, + detail: option, + hint: option, + /// Name of the violated constraint, for constraint errors. + constraint: option, + } + + /// FastEdge SQL database resource. + resource database { + /// Open the database bound to `name`. + /// + /// `error::no-such-database` will be raised if the `name` is not recognized. + open: static func(name: string) -> result; + + /// Run one statement with positional parameters (`$1`, `$2`, ...). Auto-commits. + /// + /// `sql` must contain exactly one statement. + query: func(sql: string, params: list) -> result; + + /// Run several statements in one transaction. Either all commit or none do. + /// + /// Returns one `query-result` per statement, in order. On failure nothing is committed + /// and the error of the failing statement is returned. + batch: func(statements: list) -> result, error>; + } + + /// The set of errors which may be raised by functions in this interface + variant error { + /// The host does not recognize the database binding requested. + no-such-database, + /// The requesting component does not have access to the specified database + /// (which may or may not exist). + access-denied, + /// The binding is misconfigured: bad credentials, unknown database name, invalid connection + /// settings. Not retryable without a configuration change. + config(db-error), + /// The database could not be reached (network, DNS, TLS, backend unavailable). + connection(string), + /// The call exceeded the host's time limit. The statement may or may not have been applied. + timeout, + /// A host or backend limit was exceeded (e.g. response size, statements per batch). + limit(string), + /// The database rejected the statement (syntax, constraint, permission, ...). + query(db-error), + /// Some unexpected internal error has occurred. + internal-error, + } +} diff --git a/world.wit b/world.wit index f319593..26c8ff1 100644 --- a/world.wit +++ b/world.wit @@ -8,6 +8,7 @@ world reactor { import key-value; import utils; import cache-sync; + import gcore:db/sql@0.1.0; export http-handler; } \ No newline at end of file From 6ddb92e650ea17448519b6fadd4beba808dc8280 Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 10:46:37 +0000 Subject: [PATCH 2/4] db: escape float64 case (keyword in older wit-bindgen) --- deps/db/sql.wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/db/sql.wit b/deps/db/sql.wit index 247b97d..4f0f86c 100644 --- a/deps/db/sql.wit +++ b/deps/db/sql.wit @@ -34,7 +34,7 @@ interface sql { boolean(bool), int32(s32), int64(s64), - float64(f64), + %float64(f64), numeric(string), text(string), bytes(list), From 6af3ae70606012a768ca058a1abd759b4e1ad92c Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 14:33:46 +0000 Subject: [PATCH 3/4] db: unescape float64 case again (sdk-js moved to wit-bindgen 0.52, which accepts it) Reverts 6ddb92e. --- deps/db/sql.wit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deps/db/sql.wit b/deps/db/sql.wit index 4f0f86c..247b97d 100644 --- a/deps/db/sql.wit +++ b/deps/db/sql.wit @@ -34,7 +34,7 @@ interface sql { boolean(bool), int32(s32), int64(s64), - %float64(f64), + float64(f64), numeric(string), text(string), bytes(list), From 83a7d0858d9d2138cb12d712891e7891006084cc Mon Sep 17 00:00:00 2001 From: Gordon Farquharson Date: Tue, 27 Oct 2026 15:24:00 +0000 Subject: [PATCH 4/4] db: split into sql-types + sql-sync (sync variant, like cache-sync); plain `sql` reserved for async --- deps/db/sql-sync.wit | 36 ++++++++++++++++++++++++++++++ deps/db/{sql.wit => sql-types.wit} | 36 +++--------------------------- world.wit | 2 +- 3 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 deps/db/sql-sync.wit rename deps/db/{sql.wit => sql-types.wit} (63%) diff --git a/deps/db/sql-sync.wit b/deps/db/sql-sync.wit new file mode 100644 index 0000000..35bdcff --- /dev/null +++ b/deps/db/sql-sync.wit @@ -0,0 +1,36 @@ +package gcore:db@0.1.0; + +/// FastEdge SQL interface (synchronous variant). +/// +/// The contract is PostgreSQL: dialect, `$1`-style positional parameters and SQLSTATE error codes. +/// Applications name a binding (e.g. "main"); the host maps it to a backend. Backend, location and +/// credentials are invisible to the application, so a binding can move between backends without +/// code changes. +/// +/// Every call is a network round trip to a regional database. There is no session state between +/// calls: each `query` auto-commits and each `batch` is its own transaction. +/// +/// Calls are subject to host limits (timeout, response size, statements per batch). Exceeding +/// them returns `error::timeout` or `error::limit`. +interface sql-sync { + use sql-types.{value, query-result, statement, error}; + + /// FastEdge SQL database resource. + resource database { + /// Open the database bound to `name`. + /// + /// `error::no-such-database` will be raised if the `name` is not recognized. + open: static func(name: string) -> result; + + /// Run one statement with positional parameters (`$1`, `$2`, ...). Auto-commits. + /// + /// `sql` must contain exactly one statement. + query: func(sql: string, params: list) -> result; + + /// Run several statements in one transaction. Either all commit or none do. + /// + /// Returns one `query-result` per statement, in order. On failure nothing is committed + /// and the error of the failing statement is returned. + batch: func(statements: list) -> result, error>; + } +} diff --git a/deps/db/sql.wit b/deps/db/sql-types.wit similarity index 63% rename from deps/db/sql.wit rename to deps/db/sql-types.wit index 247b97d..5e726de 100644 --- a/deps/db/sql.wit +++ b/deps/db/sql-types.wit @@ -1,18 +1,7 @@ package gcore:db@0.1.0; -/// FastEdge SQL interface (synchronous variant). -/// -/// The contract is PostgreSQL: dialect, `$1`-style positional parameters and SQLSTATE error codes. -/// Applications name a binding (e.g. "main"); the host maps it to a backend. Backend, location and -/// credentials are invisible to the application, so a binding can move between backends without -/// code changes. -/// -/// Every call is a network round trip to a regional database. There is no session state between -/// calls: each `query` auto-commits and each `batch` is its own transaction. -/// -/// Calls are subject to host limits (timeout, response size, statements per batch). Exceeding -/// them returns `error::timeout` or `error::limit`. -interface sql { +/// Shared types for the SQL interfaces. +interface sql-types { /// A parameter or result value. /// /// Results use the narrowest variant that is lossless: @@ -75,26 +64,7 @@ interface sql { constraint: option, } - /// FastEdge SQL database resource. - resource database { - /// Open the database bound to `name`. - /// - /// `error::no-such-database` will be raised if the `name` is not recognized. - open: static func(name: string) -> result; - - /// Run one statement with positional parameters (`$1`, `$2`, ...). Auto-commits. - /// - /// `sql` must contain exactly one statement. - query: func(sql: string, params: list) -> result; - - /// Run several statements in one transaction. Either all commit or none do. - /// - /// Returns one `query-result` per statement, in order. On failure nothing is committed - /// and the error of the failing statement is returned. - batch: func(statements: list) -> result, error>; - } - - /// The set of errors which may be raised by functions in this interface + /// The set of errors which may be raised by the SQL interfaces. variant error { /// The host does not recognize the database binding requested. no-such-database, diff --git a/world.wit b/world.wit index 26c8ff1..2f00602 100644 --- a/world.wit +++ b/world.wit @@ -8,7 +8,7 @@ world reactor { import key-value; import utils; import cache-sync; - import gcore:db/sql@0.1.0; + import gcore:db/sql-sync@0.1.0; export http-handler; } \ No newline at end of file