capabilities_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  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)", "jsonb (3802,",
  299. "1000, 1005, 1007, 1009, 1015, 1016, 1021, 1022, 1231", "0A000",
  300. "Describe and simple-query results stay text",
  301. } {
  302. if !strings.Contains(results.Notes, claim) {
  303. t.Errorf("binary_results notes do not mention %q", claim)
  304. }
  305. }
  306. }