2
0

check_commit_test.go 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. package pgserver
  2. import (
  3. "testing"
  4. "github.com/danfragoso/pizzasql-next/pkg/storage"
  5. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  6. )
  7. func simpleQueryMessage(sql string) *Message {
  8. return &Message{Type: MsgQuery, Data: append([]byte(sql), 0)}
  9. }
  10. // A row constraint that only the commit can detect (a column and its CHECK were
  11. // published after the row was staged) must report its SQLSTATE, leave the wire
  12. // session idle, and let the client's cleanup ROLLBACK complete.
  13. func TestConstraintViolationAtCommitClearsTransactionState(t *testing.T) {
  14. tests := []struct {
  15. name string
  16. ddl string
  17. code string
  18. }{
  19. {
  20. name: "check",
  21. ddl: "ALTER TABLE t ADD COLUMN qty INTEGER DEFAULT -1 CHECK (qty >= 0)",
  22. code: ErrCodeCheckViolation,
  23. },
  24. {
  25. name: "not null",
  26. ddl: "ALTER TABLE t ADD COLUMN note TEXT NOT NULL",
  27. code: "23502",
  28. },
  29. }
  30. for _, tt := range tests {
  31. t.Run(tt.name, func(t *testing.T) {
  32. kv := testkv.New(t)
  33. pool := kv.Pool(8)
  34. t.Cleanup(func() { pool.Close() })
  35. schema := storage.NewSchemaManager(pool, "pg_commit_constraints")
  36. table := storage.NewTableManager(pool, schema, "pg_commit_constraints")
  37. writer, writerClient := newDataConnectionFor(t, schema, table)
  38. ddl, ddlClient := newDataConnectionFor(t, schema, table)
  39. runQuery(t, writer, writerClient, simpleQueryMessage("CREATE TABLE t (id INTEGER PRIMARY KEY)"))
  40. runQuery(t, writer, writerClient, simpleQueryMessage("BEGIN"))
  41. runQuery(t, writer, writerClient, simpleQueryMessage("INSERT INTO t (id) VALUES (1)"))
  42. // The row is only staged, so the new constraint sees an empty table
  43. // and the commit is where the staged row violates it.
  44. runQuery(t, ddl, ddlClient, simpleQueryMessage(tt.ddl))
  45. msgs := runQuery(t, writer, writerClient, simpleQueryMessage("COMMIT"))
  46. if code := errorCode(msgs[0]); code != tt.code {
  47. t.Fatalf("COMMIT SQLSTATE = %q, want %q", code, tt.code)
  48. }
  49. if writer.txStatus != TxStatusIdle {
  50. t.Fatalf("tx status = %v, want idle after a failed COMMIT", writer.txStatus)
  51. }
  52. // Storage already rolled the transaction back, so the client's
  53. // cleanup ROLLBACK is a no-op rather than an error.
  54. msgs = runQuery(t, writer, writerClient, simpleQueryMessage("ROLLBACK"))
  55. if len(msgs) == 0 || msgs[0].Type == MsgErrorResponse {
  56. t.Fatalf("ROLLBACK after failed COMMIT = %v, want a successful no-op", msgs)
  57. }
  58. if writer.txStatus != TxStatusIdle {
  59. t.Fatalf("tx status = %v, want idle after the no-op ROLLBACK", writer.txStatus)
  60. }
  61. // The session keeps working.
  62. msgs = runQuery(t, writer, writerClient, simpleQueryMessage("SELECT 1"))
  63. if len(msgs) < 2 || msgs[0].Type != MsgRowDescription {
  64. t.Fatalf("SELECT after failed COMMIT = %v, want a result", msgs)
  65. }
  66. })
  67. }
  68. }