capabilities_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. package capabilities
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  7. "github.com/danfragoso/pizzasql-next/pkg/version"
  8. )
  9. func TestCurrentDocumentIsVersionedAndMarshalable(t *testing.T) {
  10. doc := Current()
  11. if doc.SchemaVersion != SchemaVersion {
  12. t.Fatalf("schema version = %d, want %d", doc.SchemaVersion, SchemaVersion)
  13. }
  14. if doc.Engine != "pizzasql" || doc.EngineVersion != version.String() {
  15. t.Fatalf("engine identity = %q/%q", doc.Engine, doc.EngineVersion)
  16. }
  17. body, err := json.Marshal(doc)
  18. if err != nil {
  19. t.Fatalf("marshal: %v", err)
  20. }
  21. var round Document
  22. if err := json.Unmarshal(body, &round); err != nil {
  23. t.Fatalf("unmarshal: %v", err)
  24. }
  25. if round.Constraints.Unique.SQLState != "23505" {
  26. t.Fatalf("unique SQLSTATE = %q, want 23505", round.Constraints.Unique.SQLState)
  27. }
  28. if round.Transactions.SerializationCode != "40001" {
  29. t.Fatalf("serialization SQLSTATE = %q, want 40001", round.Transactions.SerializationCode)
  30. }
  31. if round.ErrorCodes["0A000"] != "feature_not_supported" {
  32. t.Fatalf("0A000 mapping = %q", round.ErrorCodes["0A000"])
  33. }
  34. if !round.Notifications.Listen.Supported || !round.Notifications.Notify.Supported {
  35. t.Fatal("LISTEN/NOTIFY should be advertised as supported")
  36. }
  37. if round.Notifications.PayloadLimit != 8000 || round.Notifications.DurableReplay {
  38. t.Fatalf("unexpected notification limits: %+v", round.Notifications)
  39. }
  40. if !round.DDL.RenameColumnIndexes.Supported {
  41. t.Fatal("RENAME COLUMN must advertise index metadata rewriting")
  42. }
  43. }
  44. func TestJSONArrowOperatorsAreAdvertised(t *testing.T) {
  45. arrow := Current().JSON.ArrowOperators
  46. if !arrow.Supported {
  47. t.Fatal("-> and ->> should be advertised as supported")
  48. }
  49. if arrow.SQLState != "" {
  50. t.Fatalf("arrow operator SQLSTATE = %q, want none", arrow.SQLState)
  51. }
  52. if arrow.Status == "rejected" {
  53. t.Fatalf("arrow operator status = %q, want a supported status", arrow.Status)
  54. }
  55. }
  56. func TestTransactionalDDLIsAdvertised(t *testing.T) {
  57. ddl := Current().DDL
  58. if !ddl.Transactional.Supported {
  59. t.Fatalf("transactional DDL = %+v, want supported", ddl.Transactional)
  60. }
  61. if ddl.Transactional.Status != "staged_until_commit" {
  62. t.Fatalf("transactional DDL status = %q, want staged_until_commit", ddl.Transactional.Status)
  63. }
  64. }
  65. func TestAddColumnBackfillIsAdvertised(t *testing.T) {
  66. doc := Current()
  67. if !doc.DDL.AddColumnBackfill.Supported {
  68. t.Fatalf("add_column_backfill = %+v, want supported", doc.DDL.AddColumnBackfill)
  69. }
  70. if doc.DDL.AddColumnBackfill.Status != "rewrites_rows" {
  71. t.Fatalf("add_column_backfill status = %q, want rewrites_rows", doc.DDL.AddColumnBackfill.Status)
  72. }
  73. if doc.DDL.AddColumn.Status == "metadata_only" {
  74. t.Fatalf("add_column status = %q, but a DEFAULT rewrites rows", doc.DDL.AddColumn.Status)
  75. }
  76. }
  77. // The capability document must describe the current default semantics: constant
  78. // and whitelisted clock defaults are supported, the clock ones are evaluated on
  79. // write, and only the remaining non-constant forms are rejected.
  80. func TestNonConstantDefaultsAreAdvertisedAsEvaluatedAtWriteTime(t *testing.T) {
  81. defaults := Current().Constraints.DefaultValues
  82. if !defaults.Supported {
  83. t.Fatalf("default_values = %+v, want supported", defaults)
  84. }
  85. if defaults.Status != "evaluated_at_write_time" {
  86. t.Fatalf("default_values status = %q, want evaluated_at_write_time", defaults.Status)
  87. }
  88. if defaults.SQLState != "0A000" {
  89. t.Fatalf("default_values SQLSTATE = %q, want 0A000 for the remaining rejected forms", defaults.SQLState)
  90. }
  91. }
  92. func TestCheckConstraintIsAdvertised(t *testing.T) {
  93. doc := Current()
  94. check := doc.Constraints.Check
  95. if !check.Supported {
  96. t.Fatalf("check = %+v, want supported", check)
  97. }
  98. if check.Status != "enforced" {
  99. t.Fatalf("check status = %q, want enforced", check.Status)
  100. }
  101. if check.SQLState != pgerror.CodeCheckViolation {
  102. t.Fatalf("check SQLSTATE = %q, want %q", check.SQLState, pgerror.CodeCheckViolation)
  103. }
  104. if check.Notes == "" {
  105. t.Fatal("check notes must describe the enforcement surface")
  106. }
  107. if got := doc.ErrorCodes[pgerror.CodeCheckViolation]; got != "check_violation" {
  108. t.Fatalf("23514 mapping = %q, want check_violation", got)
  109. }
  110. }
  111. func TestDDLNotesDescribeCheckDependencies(t *testing.T) {
  112. doc := Current()
  113. if !strings.Contains(doc.DDL.DropColumn.Notes, "CHECK") || !strings.Contains(doc.DDL.DropColumn.Notes, "0A000") {
  114. t.Fatalf("drop_column notes = %q, want the persisted CHECK restriction", doc.DDL.DropColumn.Notes)
  115. }
  116. if !strings.Contains(doc.DDL.RenameColumn.Notes, "CHECK") || !strings.Contains(doc.DDL.RenameColumn.Notes, "0A000") {
  117. t.Fatalf("rename_column notes = %q, want the persisted CHECK restriction", doc.DDL.RenameColumn.Notes)
  118. }
  119. if !strings.Contains(doc.DDL.AddColumnBackfill.Notes, "23514") {
  120. t.Fatalf("add_column_backfill notes = %q, want the CHECK backfill failure", doc.DDL.AddColumnBackfill.Notes)
  121. }
  122. }
  123. // Composite primary keys are advertised as supported, with the SQLSTATE callers
  124. // should expect for a duplicate key.
  125. func TestCompositePrimaryKeyIsAdvertised(t *testing.T) {
  126. doc := Current()
  127. composite := doc.Constraints.CompositePK
  128. if !composite.Supported {
  129. t.Fatalf("composite_primary_key = %+v, want supported", composite)
  130. }
  131. if composite.Status == "rejected" {
  132. t.Fatalf("composite_primary_key status = %q, want a supported status", composite.Status)
  133. }
  134. if composite.SQLState != "23505" {
  135. t.Fatalf("composite_primary_key SQLSTATE = %q, want 23505", composite.SQLState)
  136. }
  137. if !doc.Constraints.PrimaryKey.Supported {
  138. t.Fatalf("primary_key = %+v, want supported", doc.Constraints.PrimaryKey)
  139. }
  140. body, err := json.Marshal(doc)
  141. if err != nil {
  142. t.Fatalf("marshal: %v", err)
  143. }
  144. if !strings.Contains(string(body), `"composite_primary_key":{"supported":true`) {
  145. t.Fatalf("document does not advertise composite_primary_key as supported: %s", body)
  146. }
  147. }
  148. // Foreign keys are advertised as enforced, with the SQLSTATE callers should
  149. // expect for a dangling reference, the referential actions that execute
  150. // immediately, and the explicit exclusions for deferred enforcement.
  151. func TestForeignKeyIsAdvertised(t *testing.T) {
  152. doc := Current()
  153. fk := doc.Constraints.ForeignKey
  154. if !fk.Supported {
  155. t.Fatalf("foreign_key = %+v, want supported", fk)
  156. }
  157. if fk.Status != "enforced" {
  158. t.Fatalf("foreign_key status = %q, want enforced", fk.Status)
  159. }
  160. if fk.SQLState != pgerror.CodeForeignKeyViolation {
  161. t.Fatalf("foreign_key SQLSTATE = %q, want %q", fk.SQLState, pgerror.CodeForeignKeyViolation)
  162. }
  163. for _, want := range []string{
  164. "UNIQUE", "COMMIT", "MATCH SIMPLE", "MATCH keyword", "self",
  165. "CASCADE", "SET NULL", "SET DEFAULT", "NOT NULL", "transitively",
  166. "autocommit", "no later than COMMIT", "statement savepoint", "hand-authored catalog",
  167. "O(FK component)", "DEFERRABLE", "0A000",
  168. "PRAGMA foreign_keys", "rejects OFF", "foreign_key_list", "database-wide foreign-key write gate",
  169. "DROP/RENAME COLUMN", "DROP TABLE", "pg_constraint",
  170. } {
  171. if !strings.Contains(fk.Notes, want) {
  172. t.Fatalf("foreign_key notes = %q, want it to mention %q", fk.Notes, want)
  173. }
  174. }
  175. if got := doc.ErrorCodes[pgerror.CodeForeignKeyViolation]; got != "foreign_key_violation" {
  176. t.Fatalf("23503 mapping = %q, want foreign_key_violation", got)
  177. }
  178. }
  179. func TestArraysAreAdvertised(t *testing.T) {
  180. doc := Current()
  181. types := doc.Types.Arrays
  182. if !types.Supported {
  183. t.Fatalf("types.arrays = %+v, want supported", types)
  184. }
  185. if types.Status != "one_dimensional" {
  186. t.Fatalf("types.arrays status = %q, want one_dimensional", types.Status)
  187. }
  188. for _, want := range []string{"TEXT[]", "INTEGER[]", "BOOLEAN[]", "ARRAY[", "array_length", "unnest", "0A000", "22P02"} {
  189. if !strings.Contains(types.Notes, want) {
  190. t.Errorf("types.arrays notes do not mention %q", want)
  191. }
  192. }
  193. params := doc.Parameters.Arrays
  194. if !params.Supported {
  195. t.Fatalf("parameters.arrays = %+v, want supported", params)
  196. }
  197. if params.Status != "text_and_binary" {
  198. t.Fatalf("parameters.arrays status = %q, want text_and_binary", params.Status)
  199. }
  200. for _, want := range []string{"1009", "1007", "1016", "1000", "1022", "1021", "1231", "1015", "0A000", "binary", "22P02"} {
  201. if !strings.Contains(params.Notes, want) {
  202. t.Errorf("parameters.arrays notes do not mention %q", want)
  203. }
  204. }
  205. if !strings.Contains(doc.Parameters.MaxOID, "text[]/varchar[]/int4[]") {
  206. t.Fatalf("parameters.max_oid = %q, want the array OIDs listed", doc.Parameters.MaxOID)
  207. }
  208. }
  209. // JSONB is advertised as a native type with precise canonicalization, operator,
  210. // function, ordering, parameter, and rejection claims.
  211. func TestJSONBIsAdvertised(t *testing.T) {
  212. doc := Current()
  213. types := doc.Types.JSONB
  214. if !types.Supported {
  215. t.Fatalf("types.jsonb = %+v, want supported", types)
  216. }
  217. if types.Status != "native" {
  218. t.Fatalf("types.jsonb status = %q, want native", types.Status)
  219. }
  220. for _, want := range []string{"canonical", "sorted", "last-wins", "idempotent", "OID 3802", "22P02", "0A000", "jsonpath", "GIN"} {
  221. if !strings.Contains(types.Notes, want) {
  222. t.Errorf("types.jsonb notes do not mention %q", want)
  223. }
  224. }
  225. if !doc.Types.JSONText {
  226. t.Fatal("JSON (text) remains supported and must stay advertised")
  227. }
  228. jsonb := doc.JSON.JSONB
  229. for _, want := range []string{"sorted", "last-wins", "1.50", "idempotent"} {
  230. if !strings.Contains(jsonb.Canonicalization, want) {
  231. t.Errorf("json.jsonb.canonicalization does not mention %q", want)
  232. }
  233. }
  234. for _, want := range []string{"exact canonical text bytes", "DISTINCT", "ORDER BY", "numeric"} {
  235. if !strings.Contains(jsonb.Ordering, want) {
  236. t.Errorf("json.jsonb.ordering does not mention %q", want)
  237. }
  238. }
  239. if !jsonb.Operators.Supported || jsonb.Operators.SQLState != pgerror.CodeDatatypeMismatch {
  240. t.Fatalf("json.jsonb.operators = %+v", jsonb.Operators)
  241. }
  242. for _, want := range []string{"@>", "<@", "?|", "?&", "#>", "#>>", "#-", "||", "scalar string never matches", "SQL NULL"} {
  243. if !strings.Contains(jsonb.Operators.Notes, want) {
  244. t.Errorf("json.jsonb.operators notes do not mention %q", want)
  245. }
  246. }
  247. if !jsonb.Functions.Supported || jsonb.Functions.SQLState != pgerror.CodeInvalidParameter {
  248. t.Fatalf("json.jsonb.functions = %+v", jsonb.Functions)
  249. }
  250. for _, want := range []string{"jsonb_typeof", "jsonb_array_length", "jsonb_strip_nulls", "jsonb_pretty", "jsonb_set", "jsonb_insert", "jsonb_build_object", "jsonb_build_array", "to_jsonb"} {
  251. if !strings.Contains(jsonb.Functions.Notes, want) {
  252. t.Errorf("json.jsonb.functions notes do not mention %q", want)
  253. }
  254. }
  255. if !jsonb.SetReturning.Supported || jsonb.SetReturning.Status != "single_from_item" {
  256. t.Fatalf("json.jsonb.set_returning = %+v", jsonb.SetReturning)
  257. }
  258. for _, want := range []string{"jsonb_each", "jsonb_array_elements", "jsonb_object_keys", "0A000"} {
  259. if !strings.Contains(jsonb.SetReturning.Notes, want) {
  260. t.Errorf("json.jsonb.set_returning notes do not mention %q", want)
  261. }
  262. }
  263. if !jsonb.Parameters.Supported || jsonb.Parameters.Status != "text_and_binary" {
  264. t.Fatalf("json.jsonb.parameters = %+v", jsonb.Parameters)
  265. }
  266. for _, want := range []string{"3802", "0x01", "pgx", "22P02"} {
  267. if !strings.Contains(jsonb.Parameters.Notes, want) {
  268. t.Fatalf("json.jsonb.parameters notes do not mention %q: %q", want, jsonb.Parameters.Notes)
  269. }
  270. }
  271. if jsonb.Rejected.Supported || jsonb.Rejected.SQLState != pgerror.CodeFeatureNotSupported {
  272. t.Fatalf("json.jsonb.rejected = %+v", jsonb.Rejected)
  273. }
  274. for _, want := range []string{"jsonpath", "GIN", "expression indexes", "wildcard"} {
  275. if !strings.Contains(jsonb.Rejected.Notes, want) {
  276. t.Errorf("json.jsonb.rejected notes do not mention %q", want)
  277. }
  278. }
  279. if !strings.Contains(doc.Parameters.MaxOID, "3802") {
  280. t.Fatalf("parameters.max_oid = %q, want OID 3802 listed", doc.Parameters.MaxOID)
  281. }
  282. if got := doc.ErrorCodes[pgerror.CodeInvalidParameter]; got != "invalid_parameter_value" {
  283. t.Fatalf("22023 mapping = %q, want invalid_parameter_value", got)
  284. }
  285. }
  286. // Binary result support is advertised with the encodable OIDs and the SQLSTATE
  287. // for a type that has no binary encoder. pkg/pgserver covers each claim.
  288. func TestBinaryResultsAreAdvertised(t *testing.T) {
  289. results := Current().Protocol.BinaryResults
  290. if !results.Supported {
  291. t.Fatalf("binary_results = %+v, want supported", results)
  292. }
  293. if results.Status != "supported" {
  294. t.Fatalf("binary_results status = %q, want supported", results.Status)
  295. }
  296. for _, claim := range []string{
  297. "bool (16)", "bytea (17)", "int8 (20)", "int2 (21)", "int4 (23)", "text (25)",
  298. "float4 (700)", "float8 (701)", "varchar (1043)", "numeric (1700) only as the element type of numeric[] (1231)", "jsonb (3802,",
  299. "1000, 1005, 1007, 1009, 1015, 1016, 1021, 1022, 1231", "0A000",
  300. "unrepresentable stored value", "22P02",
  301. "Describe and simple-query results stay text",
  302. } {
  303. if !strings.Contains(results.Notes, claim) {
  304. t.Errorf("binary_results notes do not mention %q", claim)
  305. }
  306. }
  307. }
  308. // Bounded Execute is advertised with its resume semantics, portal lifetime, and
  309. // limits. pkg/pgserver covers each claim with raw-protocol portal tests.
  310. func TestBoundedExecuteIsAdvertised(t *testing.T) {
  311. bounded := Current().Protocol.BoundedExecute
  312. if !bounded.Supported {
  313. t.Fatalf("bounded_execute = %+v, want supported", bounded)
  314. }
  315. if bounded.Status != "supported" {
  316. t.Fatalf("bounded_execute status = %q, want supported", bounded.Status)
  317. }
  318. if bounded.SQLState != "34000" {
  319. t.Fatalf("bounded_execute sqlstate = %q, want 34000", bounded.SQLState)
  320. }
  321. for _, claim := range []string{
  322. "PortalSuspended", "maxRows = 0", "materialized once", "no scroll",
  323. "WITH HOLD", "RowDescription is sent by the first Execute only",
  324. "Close('P')", "ROLLBACK TO SAVEPOINT", "42P03", "34000", "25P02", "08P01", "RETURNING",
  325. } {
  326. if !strings.Contains(bounded.Notes, claim) {
  327. t.Errorf("bounded_execute notes do not mention %q", claim)
  328. }
  329. }
  330. }
  331. // The catalog document advertises the read-only introspection surface an ORM
  332. // uses: the relation and function matrix, the deterministic OID scheme, and the
  333. // SET/SHOW compatibility surface. pkg/executor covers each relation.
  334. func TestCatalogIntrospectionIsAdvertised(t *testing.T) {
  335. doc := Current()
  336. catalog := doc.Catalog
  337. if !catalog.PgCatalog.Supported || !catalog.InformationSchema.Supported {
  338. t.Fatalf("catalog = %+v, want both schemas supported", catalog)
  339. }
  340. if catalog.PgCatalog.SQLState != pgerror.CodeFeatureNotSupported {
  341. t.Fatalf("pg_catalog SQLSTATE = %q, want 0A000 for writes", catalog.PgCatalog.SQLState)
  342. }
  343. if catalog.Writable {
  344. t.Fatal("catalog relations are read-only")
  345. }
  346. for _, want := range []string{
  347. "pg_catalog.pg_class", "pg_catalog.pg_namespace", "pg_catalog.pg_attribute",
  348. "pg_catalog.pg_type", "pg_catalog.pg_index", "pg_catalog.pg_constraint",
  349. "pg_catalog.pg_attrdef", "pg_catalog.pg_database", "pg_catalog.pg_tablespace",
  350. "pg_catalog.pg_proc", "pg_catalog.pg_am", "pg_catalog.pg_description",
  351. "information_schema.tables", "information_schema.columns",
  352. "information_schema.table_constraints", "information_schema.key_column_usage",
  353. "information_schema.referential_constraints",
  354. "information_schema.constraint_column_usage", "information_schema.schemata",
  355. "information_schema.sequences", "information_schema.views",
  356. } {
  357. found := false
  358. for _, relation := range catalog.Relations {
  359. if relation == want {
  360. found = true
  361. break
  362. }
  363. }
  364. if !found {
  365. t.Errorf("catalog.relations does not list %q", want)
  366. }
  367. }
  368. for _, want := range []string{
  369. "current_schema", "current_database", "format_type", "pg_table_is_visible",
  370. "pg_get_indexdef", "pg_get_constraintdef", "pg_get_expr", "pg_get_serial_sequence",
  371. "obj_description", "col_description", "has_table_privilege",
  372. } {
  373. found := false
  374. for _, function := range catalog.Functions {
  375. if function == want {
  376. found = true
  377. break
  378. }
  379. }
  380. if !found {
  381. t.Errorf("catalog.functions does not list %q", want)
  382. }
  383. }
  384. if !strings.Contains(catalog.OIDStrategy, "FNV-1a") || !strings.Contains(catalog.OIDStrategy, "deterministic") {
  385. t.Fatalf("oid_strategy = %q", catalog.OIDStrategy)
  386. }
  387. if !catalog.SetStatements.Supported || catalog.SetStatements.Status != "accepted_and_ignored" {
  388. t.Fatalf("set_statements = %+v", catalog.SetStatements)
  389. }
  390. for _, want := range []string{"search_path", "SHOW", "current_schema"} {
  391. if !strings.Contains(catalog.SetStatements.Notes, want) {
  392. t.Errorf("set_statements notes do not mention %q", want)
  393. }
  394. }
  395. body, err := json.Marshal(doc)
  396. if err != nil {
  397. t.Fatalf("marshal: %v", err)
  398. }
  399. if !strings.Contains(string(body), `"writable":false`) {
  400. t.Fatalf("document does not advertise the catalog as read-only: %s", body)
  401. }
  402. }