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-types.wit b/deps/db/sql-types.wit new file mode 100644 index 0000000..5e726de --- /dev/null +++ b/deps/db/sql-types.wit @@ -0,0 +1,88 @@ +package gcore:db@0.1.0; + +/// Shared types for the SQL interfaces. +interface sql-types { + /// 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, + } + + /// 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, + /// 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..2f00602 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-sync@0.1.0; export http-handler; } \ No newline at end of file