capabilities.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. // Package capabilities describes the SQL surface an engine build supports.
  2. //
  3. // The document is machine-readable and versioned separately from the engine
  4. // build. Clients should compare SchemaVersion when parsing it and use
  5. // EngineVersion to pin behavior to a specific PizzaSQL release. Keep this file
  6. // in sync with the tests it advertises: every claim here must be covered by an
  7. // engine test in the same repository.
  8. package capabilities
  9. import (
  10. "time"
  11. "github.com/danfragoso/pizzasql-next/pkg/version"
  12. )
  13. // SchemaVersion changes only when the document's shape or semantics change in a
  14. // way clients must handle. Additive fields do not require a bump.
  15. const SchemaVersion = 1
  16. // Feature reports whether a capability is supported and, when it is not,
  17. // whether the engine rejects the syntax or accepts it with different semantics.
  18. type Feature struct {
  19. Supported bool `json:"supported"`
  20. Status string `json:"status"`
  21. SQLState string `json:"sqlstate,omitempty"`
  22. Notes string `json:"notes,omitempty"`
  23. }
  24. // Document is the machine-readable capability description served by
  25. // GET /capabilities.
  26. type Document struct {
  27. SchemaVersion int `json:"schema_version"`
  28. Engine string `json:"engine"`
  29. EngineVersion string `json:"engine_version"`
  30. Commit string `json:"commit"`
  31. GeneratedAt string `json:"generated_at,omitempty"`
  32. Protocol Protocol `json:"protocol"`
  33. Notifications Notifications `json:"notifications"`
  34. Constraints Constraints `json:"constraints"`
  35. DDL DDL `json:"ddl"`
  36. Transactions Transactions `json:"transactions"`
  37. Types Types `json:"types"`
  38. Parameters Parameters `json:"parameters"`
  39. JSON JSON `json:"json"`
  40. ErrorCodes map[string]string `json:"error_codes"`
  41. Documentation Documentation `json:"documentation"`
  42. }
  43. type Protocol struct {
  44. PostgresCompat string `json:"postgres_compat"`
  45. SQLiteCompat string `json:"sqlite_compat"`
  46. SimpleQuery bool `json:"simple_query"`
  47. ExtendedQuery bool `json:"extended_query"`
  48. Describe Feature `json:"describe"`
  49. BinaryResults Feature `json:"binary_results"`
  50. BoundedExecute Feature `json:"bounded_execute"`
  51. ProxyErrors bool `json:"proxy_error_relay"`
  52. TLS string `json:"tls"`
  53. }
  54. type Notifications struct {
  55. Listen Feature `json:"listen"`
  56. Unlisten Feature `json:"unlisten"`
  57. Notify Feature `json:"notify"`
  58. PayloadLimit int `json:"payload_limit_bytes"`
  59. ChannelLimit int `json:"channel_limit_bytes"`
  60. Transactional bool `json:"transactional"`
  61. DurableReplay bool `json:"durable_replay"`
  62. CommitLSNParam string `json:"commit_lsn_parameter"`
  63. }
  64. type Constraints struct {
  65. NotNull Feature `json:"not_null"`
  66. Unique Feature `json:"unique"`
  67. PrimaryKey Feature `json:"primary_key"`
  68. CompositePK Feature `json:"composite_primary_key"`
  69. ForeignKey Feature `json:"foreign_key"`
  70. Check Feature `json:"check"`
  71. DefaultValues Feature `json:"default_values"`
  72. }
  73. type DDL struct {
  74. Transactional Feature `json:"transactional"`
  75. AddColumn Feature `json:"add_column"`
  76. AddColumnBackfill Feature `json:"add_column_backfill"`
  77. DropColumn Feature `json:"drop_column"`
  78. RenameColumn Feature `json:"rename_column"`
  79. RenameColumnIndexes Feature `json:"rename_column_updates_indexes"`
  80. CreateTableIfNotExists bool `json:"create_table_if_not_exists"`
  81. AddColumnIfNotExists bool `json:"add_column_if_not_exists"`
  82. }
  83. type Transactions struct {
  84. Model string `json:"model"`
  85. Isolation string `json:"isolation"`
  86. SerializationCode string `json:"serialization_sqlstate"`
  87. RowLocks Feature `json:"row_locks"`
  88. Savepoints Feature `json:"savepoints"`
  89. RetryRecommended bool `json:"retry_recommended"`
  90. }
  91. type Types struct {
  92. Integer bool `json:"integer"`
  93. Bigint bool `json:"bigint"`
  94. Real bool `json:"real"`
  95. Text bool `json:"text"`
  96. Blob bool `json:"blob"`
  97. Boolean bool `json:"boolean"`
  98. DateTime bool `json:"datetime"`
  99. JSONText bool `json:"json_as_text"`
  100. JSONB Feature `json:"jsonb"`
  101. Arrays Feature `json:"arrays"`
  102. RowID Feature `json:"rowid"`
  103. }
  104. type Parameters struct {
  105. Positional bool `json:"positional"`
  106. MaxOID string `json:"max_oid"`
  107. Arrays Feature `json:"arrays"`
  108. BinaryNumeric bool `json:"binary_numeric"`
  109. }
  110. type JSON struct {
  111. Storage string `json:"storage"`
  112. Functions string `json:"functions"`
  113. ArrowOperators Feature `json:"arrow_operators"`
  114. InvalidInput string `json:"invalid_input"`
  115. JSONB JSONB `json:"jsonb"`
  116. }
  117. // JSONB describes the native jsonb type: canonical text storage, operators,
  118. // functions, ordering, parameters, and the shapes the engine rejects.
  119. type JSONB struct {
  120. Canonicalization string `json:"canonicalization"`
  121. Ordering string `json:"ordering"`
  122. Operators Feature `json:"operators"`
  123. Functions Feature `json:"functions"`
  124. SetReturning Feature `json:"set_returning"`
  125. Parameters Feature `json:"parameters"`
  126. Rejected Feature `json:"rejected"`
  127. }
  128. type Documentation struct {
  129. Compatibility string `json:"compatibility"`
  130. Isolation string `json:"isolation"`
  131. }
  132. // Current returns the capability document for this build.
  133. func Current() *Document {
  134. return &Document{
  135. SchemaVersion: SchemaVersion,
  136. Engine: "pizzasql",
  137. EngineVersion: version.String(),
  138. Commit: version.Commit,
  139. GeneratedAt: time.Now().UTC().Format(time.RFC3339),
  140. Protocol: Protocol{
  141. PostgresCompat: "14.0",
  142. SQLiteCompat: "3.35.0",
  143. SimpleQuery: true,
  144. ExtendedQuery: true,
  145. Describe: Feature{Supported: true, Status: "compatibility_no_data", Notes: "statement Describe returns ParameterDescription then NoData; portal Describe returns NoData; Execute still emits the real RowDescription"},
  146. BinaryResults: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "all-text result formats only; per-column format lists are also rejected"},
  147. BoundedExecute: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "Execute maxRows must be zero; portals are one-shot, not resumable"},
  148. ProxyErrors: true,
  149. TLS: "terminated by the managed pgproxy, not the engine listener",
  150. },
  151. Notifications: Notifications{
  152. Listen: Feature{Supported: true, Status: "enforced"},
  153. Unlisten: Feature{Supported: true, Status: "enforced", Notes: "UNLISTEN * removes every subscription"},
  154. Notify: Feature{Supported: true, Status: "enforced", Notes: "duplicate (channel,payload) pairs collapse within one transaction"},
  155. PayloadLimit: 8000,
  156. ChannelLimit: 63,
  157. Transactional: true,
  158. DurableReplay: false,
  159. CommitLSNParam: "pizzasql.lsn",
  160. },
  161. Constraints: Constraints{
  162. NotNull: Feature{Supported: true, Status: "enforced", SQLState: "23502", Notes: "validated after defaults and assignments; explicit NULL does not select a default"},
  163. Unique: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "unique indexes, INLINE UNIQUE, and ON CONFLICT are enforced atomically"},
  164. PrimaryKey: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "inline and table-level single-column keys; INTEGER PRIMARY KEY remains the rowid alias"},
  165. CompositePK: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "table-level PRIMARY KEY (a, b) with ordered columns; key columns are implicitly NOT NULL (23502) and a composite key is never a rowid alias; two inline PRIMARY KEY declarations or an inline/table-level mix are rejected with 0A000; the key is durable under a length-prefixed encoding and reported by PRAGMA table_info pk ordinals and sqlite_master table SQL, but no implicit index is created for it; a conflict target that names the key columns matches them in any order, and ON CONFLICT DO UPDATE cannot assign a key column (0A000)"},
  166. ForeignKey: Feature{Supported: true, Status: "enforced", SQLState: "23503", Notes: "inline REFERENCES and table-level FOREIGN KEY (named or unnamed, single or multi-column) are accepted in CREATE TABLE; the parent table must already exist and the referenced columns must be its PRIMARY KEY or exactly the column set of a UNIQUE index, otherwise DDL fails with 0A000 (foreign key mismatch); a dangling reference is 23503 on INSERT, multi-row INSERT, UPDATE, and parent DELETE/UPDATE, and COMMIT re-validates the transaction's final state so a staged child insert and a staged parent delete cannot both land; MATCH SIMPLE (a NULL component skips the check) and self-references are supported; the MATCH keyword is not parsed, so MATCH SIMPLE is the only behavior; ON DELETE and ON UPDATE accept NO ACTION, RESTRICT, CASCADE, SET NULL, and SET DEFAULT; cascading actions are applied with the statement in autocommit and no later than COMMIT inside an explicit transaction: CASCADE deletes referencing rows or rewrites their foreign-key columns to the new parent key (transitively, including multi-level and self-referential graphs; a mutual cycle can only exist in a hand-authored catalog because DDL rejects forward references), SET NULL writes NULL and fails with 23502 when a child column is NOT NULL, and SET DEFAULT writes the child column defaults and still enforces NOT NULL, CHECK, UNIQUE, and FOREIGN KEY on the result; inside an explicit transaction a NO ACTION/RESTRICT violation is detected at COMMIT and the statement itself succeeds, while a cascade constraint failure rolls back to the statement savepoint; cascading point DML is O(FK component), not O(1): each cascaded parent row scans the referencing child tables and COMMIT materializes every row of every FK-connected table; DEFERRABLE is rejected with 0A000; foreign keys are always enforced, so PRAGMA foreign_keys reports 1 and rejects OFF (SQLite defaults it to OFF) and PRAGMA foreign_key_list is not emulated; ALTER TABLE DROP/RENAME COLUMN and RENAME TABLE are rejected with 0A000 when a constraint depends on the column or names the table as its parent, and DROP TABLE of a referenced parent (including a self-reference) is rejected with 0A000; writes touching a participating table serialize on one database-wide foreign-key write gate; the clause is reported in sqlite_master/sqlite_schema table SQL, including declared ON DELETE/ON UPDATE actions, but pg_constraint and information_schema foreign keys are not emulated"},
  167. Check: Feature{Supported: true, Status: "enforced", SQLState: "23514", Notes: "inline and table-level CHECK are evaluated on INSERT, multi-row INSERT, UPDATE, and ON CONFLICT DO UPDATE; NULL/unknown results pass; predicates are restricted to deterministic, subquery-free expressions over unqualified columns; exposed in sqlite_master/sqlite_schema table SQL only (pg_constraint and information_schema.check_constraints are not emulated)"},
  168. DefaultValues: Feature{Supported: true, Status: "evaluated_at_write_time", SQLState: "0A000", Notes: "constant defaults are stored as their evaluated value; CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, datetime('now'), date('now'), and time('now') are stored as SQL text and evaluated once per statement on write (once per ALTER statement for an ADD COLUMN backfill), so they are never frozen at DDL time; any other non-constant default is rejected with 0A000"},
  169. },
  170. DDL: DDL{
  171. Transactional: Feature{Supported: true, Status: "staged_until_commit", Notes: "inside an explicit transaction CREATE TABLE, DROP TABLE, ALTER TABLE (add/drop/rename column, rename table), and CREATE/DROP INDEX are staged in a session-scoped catalog overlay: the issuing session reads its own DDL, other sessions keep reading the committed catalog, ROLLBACK discards the overlay and the transaction's rows together, and COMMIT publishes both in one atomic compare-batch write; validations use the staged catalog, and a staged catalog write that conflicts with a concurrently committed DDL fails with 40001; a failed DDL statement leaves the transaction's earlier schema intact; autocommit DDL keeps the same immediate behavior; because a transaction publishes one compare-batch, a DDL row rewrite (ALTER TABLE backfill, DROP TABLE, RENAME TABLE) is bounded by the storage batch ceiling (65535 operations or 64 MiB) and an oversized migration fails at COMMIT with an explicit limit error instead of being split, while the same statement outside a transaction pages its writes"},
  172. AddColumn: Feature{Supported: true, Status: "materializes_defaults", Notes: "adds one nullable column; a constant DEFAULT is written into every existing row, and a whitelisted clock default is evaluated once per ALTER statement, so the rewrite cost is O(rows) and holds the table's exclusive gate"},
  173. AddColumnBackfill: Feature{Supported: true, Status: "rewrites_rows", Notes: "a DEFAULT is materialized into existing rows during the rewrite: constants as their stored value, whitelisted clock defaults evaluated at one instant per ALTER statement; ADD COLUMN ... NOT NULL without a DEFAULT fails with 23502 on a non-empty table, ADD COLUMN ... UNIQUE conflicts with 23505 when backfilling a non-NULL default into more than one row (a constant default is predicted before the rewrite, a clock default is validated against the values the rewrite writes), ADD COLUMN ... CHECK fails with 23514 when an existing row or the backfilled default violates the predicate, and the column is published together with its CHECK constraints and UNIQUE index in one atomic schema write"},
  174. DropColumn: Feature{Supported: true, Status: "rewrites_rows", Notes: "rejected with 0A000 when a persisted CHECK constraint references the column or a FOREIGN KEY depends on it (child column or referenced parent column); the stored constraint text cannot be rewritten"},
  175. RenameColumn: Feature{Supported: true, Status: "rewrites_rows", Notes: "rejected with 0A000 when a persisted CHECK constraint, expression index, or FOREIGN KEY depends on the column (child column or referenced parent column); the stored constraint text cannot be rewritten"},
  176. RenameColumnIndexes: Feature{Supported: true, Status: "rewrites_index_metadata", Notes: "column references in index definitions are rewritten in place, including composite and unique indexes; index names are unchanged; RENAME COLUMN is rejected when an expression index references the column (drop that index, rename, then recreate it)"},
  177. CreateTableIfNotExists: true,
  178. AddColumnIfNotExists: true,
  179. },
  180. Transactions: Transactions{
  181. Model: "optimistic compare-batch commit; no MVCC or snapshots",
  182. Isolation: "serializable-equivalent for single-row compare-batch commits; write skew is possible across independent rows",
  183. SerializationCode: "40001",
  184. RowLocks: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "SELECT ... FOR UPDATE/SHARE and NOWAIT/SKIP LOCKED are rejected"},
  185. Savepoints: Feature{Supported: true, Status: "enforced"},
  186. RetryRecommended: true,
  187. },
  188. Types: Types{
  189. Integer: true,
  190. Bigint: true,
  191. Real: true,
  192. Text: true,
  193. Blob: true,
  194. Boolean: true,
  195. DateTime: true,
  196. JSONText: true,
  197. JSONB: Feature{Supported: true, Status: "native", SQLState: "0A000", Notes: "JSONB is a native column type stored as canonical JSON text: object keys sorted, duplicate keys last-wins, no insignificant whitespace, and numbers normalized to exact decimals (1.50 becomes 1.5, 1e2 becomes 100); the canonical form is idempotent and stable across reloads; malformed jsonb input reports 22P02, equality is canonical-text equality (key order and number spelling do not matter), and JSON/TEXT columns keep their existing text-backed JSON1 behavior; literals, ::jsonb and CAST(x AS JSONB), to_jsonb(any), and text-format jsonb parameters (OID 3802, canonicalized at Bind) produce jsonb; direct projections report OID 3802 while JSON stays TEXT; jsonpath, GIN and expression indexes over jsonb, binary jsonb parameters, and wildcard/slice/unbounded jsonb paths are rejected with 0A000; a jsonb value cannot be an array element type (0A000)"},
  198. Arrays: Feature{Supported: true, Status: "one_dimensional", SQLState: "0A000", Notes: "column types TEXT[], VARCHAR(n)[], INTEGER[], BIGINT[], SMALLINT[], BOOLEAN[], REAL[], DOUBLE PRECISION[], and NUMERIC[] in CREATE TABLE and ALTER TABLE ADD COLUMN; values are PostgreSQL array text ({a,b},NULL) stored canonically, with NULL and the empty array distinct; literals, ARRAY[...], ::type[] and CAST(x AS type[]), 1-based arr[n], array_length(arr, 1), and unnest(array) as the single FROM table function are supported; multidimensional arrays, arrays of other element types, and ANY/ALL over subqueries are rejected with 0A000; malformed array input reports 22P02 and a non-integer subscript or array_length dimension reports 42804; NUMERIC[] and DECIMAL[] elements are validated but stored as their exact text, so digits above 2^53 and trailing zeros survive, and NUMERIC comparisons are exact (1.10 = 1.1, and digits above 2^53 are not rounded); column types survive derived tables, CTEs, and a derived table or CTE joined on the right for subscripts and comparisons (CHECK constraints and generated columns evaluate through the stateless evaluator without a table schema, so array subscripts there keep the element text); UNIQUE indexes and primary keys still use the canonical raw text, so 1.10 and 1.1 are distinct index keys even though they compare equal"},
  199. RowID: Feature{Supported: true, Status: "auto_assigned", Notes: "rowid/oid/_rowid_ are BIGINT over the wire; monotonic when the engine assigns the rowid, stable across UPDATE, not monotonic for client-assigned descending INTEGER PRIMARY KEY values"},
  200. },
  201. Parameters: Parameters{
  202. Positional: true,
  203. MaxOID: "int8/text/bool/bytea/float plus OID 3802 jsonb and the array OIDs text[]/varchar[]/int4[]/int8[]/int2[]/bool[]/float4[]/float8[]/numeric[]",
  204. Arrays: Feature{Supported: true, Status: "text_format", SQLState: "0A000", Notes: "a text-format array parameter is bound as a typed array literal, so id = ANY($1) works with OIDs 1009 text[], 1015 varchar[], 1007 int4[], 1016 int8[], 1005 int2[], 1000 bool[], 1021 float4[], 1022 float8[], and 1231 numeric[]; elements are validated and canonicalized; binary-format array parameters are rejected with 0A000 instead of being misread"},
  205. BinaryNumeric: true,
  206. },
  207. JSON: JSON{
  208. Storage: "JSON is TEXT with the JSON1 surface; JSONB is a native type stored as canonical JSON text (see types.jsonb)",
  209. Functions: "json, json_extract, json_set, json_insert, json_replace, json_remove, json_valid, json_type, json_array, json_array_length, json_object, json_quote, json_group_array; PostgreSQL text compatibility: concat_ws, string_agg, pg_backend_pid",
  210. ArrowOperators: Feature{Supported: true, Status: "supported", Notes: "on TEXT/JSON, -> returns JSON text for a member or element and ->> returns the SQL value (text, number, 1/0 for booleans, NULL for JSON null); right operand is a label, an array index, or a $ path; missing paths and NULL operands return NULL; chaining is left-to-right; on jsonb, ->> returns PostgreSQL text rendering (true/false, exact decimal numbers) and a JSON null member is SQL NULL"},
  211. InvalidInput: "json_extract/json_set and ->/->> on text throw 'malformed JSON'; json_valid returns 0; jsonb input and casts report 22P02",
  212. JSONB: JSONB{
  213. Canonicalization: "canonical JSON text: object keys sorted byte-wise, duplicate object keys last-wins, no insignificant whitespace, numbers exact normalized decimals (1.50 becomes 1.5, 1e2 becomes 100; no exponent, no trailing fractional zeros, negative zero becomes 0); canonicalization is idempotent and every write path stores the canonical bytes, so a reload reads back the same text",
  214. Ordering: "jsonb equality is canonical-text equality, so key order and number spelling are irrelevant; DISTINCT and GROUP BY use the canonical stored value; ORDER BY follows the engine's existing value comparison on canonical text: when both values parse as numbers they compare numerically, and every other pair compares byte-wise",
  215. Operators: Feature{Supported: true, Status: "supported", SQLState: "42804", Notes: "-> and ->> extract a member/element ($ path also accepted); #> and #>> extract by a text[] path (or a $ path); @> and <@ test containment with PostgreSQL's object/array/scalar rules; ? tests an object key or top-level string array element; ?| and ?& test a text[] of keys; || merges objects, concatenates arrays, and wraps non-arrays as single-element arrays; - deletes an object key, array index, or text[] of keys/elements; #- deletes a path; NULL operands return NULL, missing keys return SQL NULL (a JSON null member is a jsonb 'null' for -> and SQL NULL for ->>), and @> sees a JSON null member as a value; a text-only @> or <@ is rejected with 42804; the HTTP /query API substitutes ? textually, so a statement using the ? operator cannot also use ? positional parameters there"},
  216. Functions: Feature{Supported: true, Status: "supported", SQLState: "22023", Notes: "jsonb_typeof, jsonb_array_length, jsonb_strip_nulls, jsonb_pretty, jsonb_build_object, jsonb_build_array, to_jsonb, jsonb_set, jsonb_insert; the JSON1 jsonb_* aliases (jsonb_set, jsonb_insert, jsonb_replace, jsonb_remove with $ paths) keep their previous behavior and return canonical jsonb; wrong document kinds report 22023 and malformed input 22P02; the JSON (text) and JSON1 function surface is unchanged"},
  217. SetReturning: Feature{Supported: true, Status: "single_from_item", SQLState: "0A000", Notes: "jsonb_each, jsonb_each_text, jsonb_array_elements, jsonb_array_elements_text, and jsonb_object_keys are supported only as the single FROM item (no joins); calling one in a projection is rejected with 0A000; a NULL document produces no rows"},
  218. Parameters: Feature{Supported: true, Status: "text_format", SQLState: "0A000", Notes: "text-format OID 3802 parameters are canonicalized at Bind, so an invalid document fails with 22P02 before execution and operators see a real jsonb value; binary-format jsonb parameters are rejected with 0A000; OID 114 (JSON) behavior is unchanged"},
  219. Rejected: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "jsonpath (jsonb_path_*), the @? and @@ jsonpath operators, GIN indexes, expression indexes whose result is jsonb, binary jsonb parameters, wildcard/slice/unbounded jsonb paths, and jsonb array element types"},
  220. },
  221. },
  222. ErrorCodes: map[string]string{
  223. "23502": "not_null_violation",
  224. "23505": "unique_violation",
  225. "23503": "foreign_key_violation",
  226. "23514": "check_violation",
  227. "40001": "serialization_failure",
  228. "0A000": "feature_not_supported",
  229. "22023": "invalid_parameter_value",
  230. "42501": "insufficient_privilege (managed proxy)",
  231. "42601": "syntax_error",
  232. "25P02": "in_failed_sql_transaction",
  233. "08P01": "protocol_violation (managed proxy: TLS required)",
  234. "53300": "too_many_connections (managed proxy: organization quota)",
  235. "XX000": "internal_error",
  236. },
  237. Documentation: Documentation{
  238. Compatibility: "https://docs.database.pizza/sql-reference/compatibility/",
  239. Isolation: "https://docs.database.pizza/engine/concurrency/",
  240. },
  241. }
  242. }