capabilities.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. AddColumn Feature `json:"add_column"`
  75. AddColumnBackfill Feature `json:"add_column_backfill"`
  76. DropColumn Feature `json:"drop_column"`
  77. RenameColumn Feature `json:"rename_column"`
  78. RenameColumnIndexes Feature `json:"rename_column_updates_indexes"`
  79. CreateTableIfNotExists bool `json:"create_table_if_not_exists"`
  80. AddColumnIfNotExists bool `json:"add_column_if_not_exists"`
  81. }
  82. type Transactions struct {
  83. Model string `json:"model"`
  84. Isolation string `json:"isolation"`
  85. SerializationCode string `json:"serialization_sqlstate"`
  86. RowLocks Feature `json:"row_locks"`
  87. Savepoints Feature `json:"savepoints"`
  88. RetryRecommended bool `json:"retry_recommended"`
  89. }
  90. type Types struct {
  91. Integer bool `json:"integer"`
  92. Bigint bool `json:"bigint"`
  93. Real bool `json:"real"`
  94. Text bool `json:"text"`
  95. Blob bool `json:"blob"`
  96. Boolean bool `json:"boolean"`
  97. DateTime bool `json:"datetime"`
  98. JSONText bool `json:"json_as_text"`
  99. JSONB Feature `json:"jsonb"`
  100. Arrays Feature `json:"arrays"`
  101. RowID Feature `json:"rowid"`
  102. }
  103. type Parameters struct {
  104. Positional bool `json:"positional"`
  105. MaxOID string `json:"max_oid"`
  106. Arrays Feature `json:"arrays"`
  107. BinaryNumeric bool `json:"binary_numeric"`
  108. }
  109. type JSON struct {
  110. Storage string `json:"storage"`
  111. Functions string `json:"functions"`
  112. ArrowOperators Feature `json:"arrow_operators"`
  113. InvalidInput string `json:"invalid_input"`
  114. }
  115. type Documentation struct {
  116. Compatibility string `json:"compatibility"`
  117. Isolation string `json:"isolation"`
  118. }
  119. // Current returns the capability document for this build.
  120. func Current() *Document {
  121. return &Document{
  122. SchemaVersion: SchemaVersion,
  123. Engine: "pizzasql",
  124. EngineVersion: version.String(),
  125. Commit: version.Commit,
  126. GeneratedAt: time.Now().UTC().Format(time.RFC3339),
  127. Protocol: Protocol{
  128. PostgresCompat: "14.0",
  129. SQLiteCompat: "3.35.0",
  130. SimpleQuery: true,
  131. ExtendedQuery: true,
  132. 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"},
  133. BinaryResults: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "all-text result formats only; per-column format lists are also rejected"},
  134. BoundedExecute: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "Execute maxRows must be zero; portals are one-shot, not resumable"},
  135. ProxyErrors: true,
  136. TLS: "terminated by the managed pgproxy, not the engine listener",
  137. },
  138. Notifications: Notifications{
  139. Listen: Feature{Supported: true, Status: "enforced"},
  140. Unlisten: Feature{Supported: true, Status: "enforced", Notes: "UNLISTEN * removes every subscription"},
  141. Notify: Feature{Supported: true, Status: "enforced", Notes: "duplicate (channel,payload) pairs collapse within one transaction"},
  142. PayloadLimit: 8000,
  143. ChannelLimit: 63,
  144. Transactional: true,
  145. DurableReplay: false,
  146. CommitLSNParam: "pizzasql.lsn",
  147. },
  148. Constraints: Constraints{
  149. NotNull: Feature{Supported: true, Status: "enforced", SQLState: "23502", Notes: "validated after defaults and assignments; explicit NULL does not select a default"},
  150. Unique: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "unique indexes, INLINE UNIQUE, and ON CONFLICT are enforced atomically"},
  151. PrimaryKey: Feature{Supported: true, Status: "enforced", SQLState: "23505", Notes: "single-column only"},
  152. CompositePK: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "declare a single-column primary key"},
  153. ForeignKey: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "inline and table-level FOREIGN KEY are rejected; enforce referential integrity in the application, cascades are not implemented"},
  154. 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)"},
  155. 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"},
  156. },
  157. DDL: DDL{
  158. 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"},
  159. 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"},
  160. DropColumn: Feature{Supported: true, Status: "rewrites_rows", Notes: "rejected with 0A000 when a persisted CHECK constraint references the column; the stored predicate text cannot be rewritten"},
  161. RenameColumn: Feature{Supported: true, Status: "rewrites_rows", Notes: "rejected with 0A000 when a persisted CHECK constraint or expression index references the column; the stored expression text cannot be rewritten"},
  162. 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)"},
  163. CreateTableIfNotExists: true,
  164. AddColumnIfNotExists: true,
  165. },
  166. Transactions: Transactions{
  167. Model: "optimistic compare-batch commit; no MVCC or snapshots",
  168. Isolation: "serializable-equivalent for single-row compare-batch commits; write skew is possible across independent rows",
  169. SerializationCode: "40001",
  170. RowLocks: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "SELECT ... FOR UPDATE/SHARE and NOWAIT/SKIP LOCKED are rejected"},
  171. Savepoints: Feature{Supported: true, Status: "enforced"},
  172. RetryRecommended: true,
  173. },
  174. Types: Types{
  175. Integer: true,
  176. Bigint: true,
  177. Real: true,
  178. Text: true,
  179. Blob: true,
  180. Boolean: true,
  181. DateTime: true,
  182. JSONText: true,
  183. 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"},
  184. Arrays: Feature{Supported: false, Status: "rejected", SQLState: "0A000"},
  185. 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"},
  186. },
  187. Parameters: Parameters{
  188. Positional: true,
  189. MaxOID: "int8/text/bool/bytea/float; arrays are rejected",
  190. Arrays: Feature{Supported: false, Status: "rejected", SQLState: "0A000", Notes: "id = ANY($1) is rejected; expand to IN (...) with individual parameters"},
  191. BinaryNumeric: true,
  192. },
  193. JSON: JSON{
  194. Storage: "TEXT (JSON and JSONB type names are accepted without native semantics; insert JSON as text)",
  195. 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",
  196. 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"},
  197. InvalidInput: "json_extract/json_set and ->/->> throw 'malformed JSON'; json_valid returns 0",
  198. },
  199. ErrorCodes: map[string]string{
  200. "23502": "not_null_violation",
  201. "23505": "unique_violation",
  202. "23514": "check_violation",
  203. "40001": "serialization_failure",
  204. "0A000": "feature_not_supported",
  205. "42501": "insufficient_privilege (managed proxy)",
  206. "42601": "syntax_error",
  207. "25P02": "in_failed_sql_transaction",
  208. "08P01": "protocol_violation (managed proxy: TLS required)",
  209. "53300": "too_many_connections (managed proxy: organization quota)",
  210. "XX000": "internal_error",
  211. },
  212. Documentation: Documentation{
  213. Compatibility: "https://docs.database.pizza/sql-reference/compatibility/",
  214. Isolation: "https://docs.database.pizza/engine/concurrency/",
  215. },
  216. }
  217. }