capabilities.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. }
  116. type Documentation struct {
  117. Compatibility string `json:"compatibility"`
  118. Isolation string `json:"isolation"`
  119. }
  120. // Current returns the capability document for this build.
  121. func Current() *Document {
  122. return &Document{
  123. SchemaVersion: SchemaVersion,
  124. Engine: "pizzasql",
  125. EngineVersion: version.String(),
  126. Commit: version.Commit,
  127. GeneratedAt: time.Now().UTC().Format(time.RFC3339),
  128. Protocol: Protocol{
  129. PostgresCompat: "14.0",
  130. SQLiteCompat: "3.35.0",
  131. SimpleQuery: true,
  132. ExtendedQuery: true,
  133. 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"},
  134. BinaryResults: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "all-text result formats only; per-column format lists are also rejected"},
  135. BoundedExecute: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "Execute maxRows must be zero; portals are one-shot, not resumable"},
  136. ProxyErrors: true,
  137. TLS: "terminated by the managed pgproxy, not the engine listener",
  138. },
  139. Notifications: Notifications{
  140. Listen: Feature{Supported: true, Status: "enforced"},
  141. Unlisten: Feature{Supported: true, Status: "enforced", Notes: "UNLISTEN * removes every subscription"},
  142. Notify: Feature{Supported: true, Status: "enforced", Notes: "duplicate (channel,payload) pairs collapse within one transaction"},
  143. PayloadLimit: 8000,
  144. ChannelLimit: 63,
  145. Transactional: true,
  146. DurableReplay: false,
  147. CommitLSNParam: "pizzasql.lsn",
  148. },
  149. Constraints: Constraints{
  150. NotNull: Feature{Supported: true, Status: "enforced", SQLState: "23502", Notes: "validated after defaults and assignments; explicit NULL does not select a default"},
  151. Unique: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "unique indexes, INLINE UNIQUE, and ON CONFLICT are enforced atomically"},
  152. PrimaryKey: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "inline and table-level single-column keys; INTEGER PRIMARY KEY remains the rowid alias"},
  153. 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)"},
  154. 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"},
  155. 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)"},
  156. 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"},
  157. },
  158. DDL: DDL{
  159. 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"},
  160. 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"},
  161. 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"},
  162. 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"},
  163. 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"},
  164. 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)"},
  165. CreateTableIfNotExists: true,
  166. AddColumnIfNotExists: true,
  167. },
  168. Transactions: Transactions{
  169. Model: "optimistic compare-batch commit; no MVCC or snapshots",
  170. Isolation: "serializable-equivalent for single-row compare-batch commits; write skew is possible across independent rows",
  171. SerializationCode: "40001",
  172. RowLocks: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "SELECT ... FOR UPDATE/SHARE and NOWAIT/SKIP LOCKED are rejected"},
  173. Savepoints: Feature{Supported: true, Status: "enforced"},
  174. RetryRecommended: true,
  175. },
  176. Types: Types{
  177. Integer: true,
  178. Bigint: true,
  179. Real: true,
  180. Text: true,
  181. Blob: true,
  182. Boolean: true,
  183. DateTime: true,
  184. JSONText: true,
  185. JSONB: Feature{Supported: false, Status: "type_name_alias", Notes: "JSONB is accepted as a column type name with no native semantics; values are stored as text and reported as TEXT over the wire"},
  186. Arrays: Feature{Supported: false, Status: "rejected", SQLState: "0A000"},
  187. 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"},
  188. },
  189. Parameters: Parameters{
  190. Positional: true,
  191. MaxOID: "int8/text/bool/bytea/float; arrays are rejected",
  192. Arrays: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "id = ANY($1) is rejected; expand to IN (...) with individual parameters"},
  193. BinaryNumeric: true,
  194. },
  195. JSON: JSON{
  196. Storage: "TEXT (JSON and JSONB type names are accepted without native semantics; insert JSON as text)",
  197. 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",
  198. ArrowOperators: Feature{Supported: true, Status: "supported", Notes: "-> returns JSON text for a member or element, ->> 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"},
  199. InvalidInput: "json_extract/json_set and ->/->> throw 'malformed JSON'; json_valid returns 0",
  200. },
  201. ErrorCodes: map[string]string{
  202. "23502": "not_null_violation",
  203. "23505": "unique_violation",
  204. "23503": "foreign_key_violation",
  205. "23514": "check_violation",
  206. "40001": "serialization_failure",
  207. "0A000": "feature_not_supported",
  208. "42501": "insufficient_privilege (managed proxy)",
  209. "42601": "syntax_error",
  210. "25P02": "in_failed_sql_transaction",
  211. "08P01": "protocol_violation (managed proxy: TLS required)",
  212. "53300": "too_many_connections (managed proxy: organization quota)",
  213. "XX000": "internal_error",
  214. },
  215. Documentation: Documentation{
  216. Compatibility: "https://docs.database.pizza/sql-reference/compatibility/",
  217. Isolation: "https://docs.database.pizza/engine/concurrency/",
  218. },
  219. }
  220. }