transactional_catalog_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. package pgserver
  2. import (
  3. "net"
  4. "testing"
  5. "github.com/danfragoso/pizzasql-next/pkg/storage"
  6. "github.com/danfragoso/pizzasql-next/pkg/testkv"
  7. )
  8. // catalogRows decodes every DataRow of a completed simple-query exchange.
  9. func catalogRows(t *testing.T, msgs []*Message) [][]string {
  10. t.Helper()
  11. var rows [][]string
  12. for _, msg := range msgs {
  13. if msg.Type == MsgDataRow {
  14. rows = append(rows, dataRowValues(t, msg))
  15. }
  16. }
  17. return rows
  18. }
  19. func newSharedDataConnections(t *testing.T) (*Connection, net.Conn, *Connection, net.Conn) {
  20. t.Helper()
  21. kv := testkv.New(t)
  22. pool := kv.Pool(8)
  23. t.Cleanup(func() { pool.Close() })
  24. schema := storage.NewSchemaManager(pool, "pg_tx_catalog")
  25. table := storage.NewTableManager(pool, schema, "pg_tx_catalog")
  26. writer, writerClient := newDataConnectionFor(t, schema, table)
  27. observer, observerClient := newDataConnectionFor(t, schema, table)
  28. return writer, writerClient, observer, observerClient
  29. }
  30. // The system-catalog emulation must read through the connection's session so it
  31. // sees DDL staged by an open transaction, and must keep showing the committed
  32. // catalog to other connections until that transaction commits.
  33. func TestCatalogEmulationSeesStagedDDLInTransaction(t *testing.T) {
  34. writer, writerClient, observer, observerClient := newSharedDataConnections(t)
  35. runQuery(t, writer, writerClient, simpleQueryMessage("CREATE TABLE base (id INTEGER PRIMARY KEY)"))
  36. runQuery(t, writer, writerClient, simpleQueryMessage("BEGIN"))
  37. runQuery(t, writer, writerClient, simpleQueryMessage("CREATE TABLE staged (id INTEGER PRIMARY KEY)"))
  38. runQuery(t, writer, writerClient, simpleQueryMessage("ALTER TABLE base ADD COLUMN note TEXT"))
  39. runQuery(t, writer, writerClient, simpleQueryMessage("CREATE INDEX idx_base_note ON base (note)"))
  40. hasTable := func(msgs []*Message, want string) bool {
  41. for _, row := range catalogRows(t, msgs) {
  42. if len(row) > 0 && row[0] == want {
  43. return true
  44. }
  45. }
  46. return false
  47. }
  48. // The transaction's own session sees its staged catalog.
  49. tables := runQuery(t, writer, writerClient, simpleQueryMessage("SELECT table_name FROM information_schema.tables"))
  50. if !hasTable(tables, "staged") {
  51. t.Fatal("transactional catalog emulation did not report a staged CREATE TABLE")
  52. }
  53. columns := runQuery(t, writer, writerClient, simpleQueryMessage("SELECT column_name FROM information_schema.columns"))
  54. if !hasTable(columns, "note") {
  55. t.Fatal("transactional catalog emulation did not report a staged ADD COLUMN")
  56. }
  57. indexes := runQuery(t, writer, writerClient, simpleQueryMessage("SELECT indexname FROM pg_indexes"))
  58. if !hasTable(indexes, "idx_base_note") {
  59. t.Fatal("transactional catalog emulation did not report a staged CREATE INDEX")
  60. }
  61. // Another connection keeps reading the committed catalog.
  62. observerTables := runQuery(t, observer, observerClient, simpleQueryMessage("SELECT table_name FROM information_schema.tables"))
  63. if hasTable(observerTables, "staged") {
  64. t.Fatal("observer saw a table created in another connection's transaction")
  65. }
  66. observerColumns := runQuery(t, observer, observerClient, simpleQueryMessage("SELECT column_name FROM information_schema.columns"))
  67. if hasTable(observerColumns, "note") {
  68. t.Fatal("observer saw a column added in another connection's transaction")
  69. }
  70. runQuery(t, writer, writerClient, simpleQueryMessage("COMMIT"))
  71. // After COMMIT the committed catalog reflects the migration for everyone.
  72. observerTables = runQuery(t, observer, observerClient, simpleQueryMessage("SELECT table_name FROM information_schema.tables"))
  73. if !hasTable(observerTables, "staged") {
  74. t.Fatal("observer did not see the committed CREATE TABLE")
  75. }
  76. observerColumns = runQuery(t, observer, observerClient, simpleQueryMessage("SELECT column_name FROM information_schema.columns"))
  77. if !hasTable(observerColumns, "note") {
  78. t.Fatal("observer did not see the committed ADD COLUMN")
  79. }
  80. observerIndexes := runQuery(t, observer, observerClient, simpleQueryMessage("SELECT indexname FROM pg_indexes"))
  81. if !hasTable(observerIndexes, "idx_base_note") {
  82. t.Fatal("observer did not see the committed CREATE INDEX")
  83. }
  84. // A ROLLBACK leaves the emulation on the committed catalog again.
  85. runQuery(t, writer, writerClient, simpleQueryMessage("BEGIN"))
  86. runQuery(t, writer, writerClient, simpleQueryMessage("CREATE TABLE rolled_back (id INTEGER PRIMARY KEY)"))
  87. if tables := runQuery(t, writer, writerClient, simpleQueryMessage("SELECT table_name FROM information_schema.tables")); !hasTable(tables, "rolled_back") {
  88. t.Fatal("transactional catalog emulation did not report a staged table before ROLLBACK")
  89. }
  90. runQuery(t, writer, writerClient, simpleQueryMessage("ROLLBACK"))
  91. if tables := runQuery(t, writer, writerClient, simpleQueryMessage("SELECT table_name FROM information_schema.tables")); hasTable(tables, "rolled_back") {
  92. t.Fatal("catalog emulation kept a rolled-back table")
  93. }
  94. }