|
|
@@ -0,0 +1,196 @@
|
|
|
+package executor
|
|
|
+
|
|
|
+import (
|
|
|
+ "testing"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/danfragoso/pizzasql-next/pkg/storage"
|
|
|
+)
|
|
|
+
|
|
|
+func newNotifyExec(t *testing.T, hub *storage.Hub, schema *storage.SchemaManager, table *storage.TableManager, pid int32) *Executor {
|
|
|
+ t.Helper()
|
|
|
+ e := newExec(schema, table)
|
|
|
+ e.SetBackendPID(pid)
|
|
|
+ e.SetNotificationHub(hub)
|
|
|
+ t.Cleanup(func() { _ = e.Close() })
|
|
|
+ return e
|
|
|
+}
|
|
|
+
|
|
|
+func recvNotification(t *testing.T, e *Executor) storage.Notification {
|
|
|
+ t.Helper()
|
|
|
+ select {
|
|
|
+ case n := <-e.NotificationChannel():
|
|
|
+ return n
|
|
|
+ case <-time.After(2 * time.Second):
|
|
|
+ t.Fatal("timed out waiting for notification")
|
|
|
+ return storage.Notification{}
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func expectNoNotification(t *testing.T, e *Executor) {
|
|
|
+ t.Helper()
|
|
|
+ select {
|
|
|
+ case n := <-e.NotificationChannel():
|
|
|
+ t.Fatalf("unexpected notification: %+v", n)
|
|
|
+ case <-time.After(50 * time.Millisecond):
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestNotifyDeliversToListener(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN updates")
|
|
|
+ execMust(t, sender, "NOTIFY updates, 'hello'")
|
|
|
+
|
|
|
+ n := recvNotification(t, listener)
|
|
|
+ if n.Channel != "updates" || n.Payload != "hello" || n.PID != 202 {
|
|
|
+ t.Fatalf("unexpected notification: %+v", n)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestListenTakesEffectAtCommit(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "BEGIN")
|
|
|
+ execMust(t, listener, "LISTEN late")
|
|
|
+ execMust(t, sender, "NOTIFY late, 'before'")
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+
|
|
|
+ execMust(t, listener, "COMMIT")
|
|
|
+ execMust(t, sender, "NOTIFY late, 'after'")
|
|
|
+ n := recvNotification(t, listener)
|
|
|
+ if n.Payload != "after" {
|
|
|
+ t.Fatalf("payload = %q, want after", n.Payload)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestListenRollbackDiscardsSubscription(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "BEGIN")
|
|
|
+ execMust(t, listener, "LISTEN rolledback")
|
|
|
+ execMust(t, listener, "ROLLBACK")
|
|
|
+ execMust(t, sender, "NOTIFY rolledback, 'nope'")
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+}
|
|
|
+
|
|
|
+func TestNotifyOnlyDeliveredAfterCommit(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN events")
|
|
|
+ execMust(t, sender, "BEGIN")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'pending'")
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+ execMust(t, sender, "COMMIT")
|
|
|
+
|
|
|
+ n := recvNotification(t, listener)
|
|
|
+ if n.Payload != "pending" || n.LSN == 0 {
|
|
|
+ t.Fatalf("unexpected notification: %+v", n)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestNotifyRollbackDiscards(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN events")
|
|
|
+ execMust(t, sender, "BEGIN")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'gone'")
|
|
|
+ execMust(t, sender, "ROLLBACK")
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+}
|
|
|
+
|
|
|
+func TestNotifySavepointRollback(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN events")
|
|
|
+ execMust(t, sender, "BEGIN")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'kept'")
|
|
|
+ execMust(t, sender, "SAVEPOINT sp")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'discarded'")
|
|
|
+ execMust(t, sender, "ROLLBACK TO SAVEPOINT sp")
|
|
|
+ execMust(t, sender, "COMMIT")
|
|
|
+
|
|
|
+ n := recvNotification(t, listener)
|
|
|
+ if n.Payload != "kept" {
|
|
|
+ t.Fatalf("payload = %q, want kept", n.Payload)
|
|
|
+ }
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+}
|
|
|
+
|
|
|
+func TestNotifyDuplicatesCollapseInTransaction(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN events")
|
|
|
+ execMust(t, sender, "BEGIN")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'same'")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'same'")
|
|
|
+ execMust(t, sender, "NOTIFY events, 'different'")
|
|
|
+ execMust(t, sender, "COMMIT")
|
|
|
+
|
|
|
+ first := recvNotification(t, listener)
|
|
|
+ second := recvNotification(t, listener)
|
|
|
+ if first.Payload != "same" || second.Payload != "different" {
|
|
|
+ t.Fatalf("unexpected notifications: %+v %+v", first, second)
|
|
|
+ }
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+}
|
|
|
+
|
|
|
+func TestUnlistenAllAndDisconnectCleanup(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ hub := storage.NewHub()
|
|
|
+ listener := newNotifyExec(t, hub, schema, table, 101)
|
|
|
+ sender := newNotifyExec(t, hub, schema, table, 202)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN a")
|
|
|
+ execMust(t, listener, "LISTEN b")
|
|
|
+ execMust(t, listener, "UNLISTEN *")
|
|
|
+ execMust(t, sender, "NOTIFY a, 'x'")
|
|
|
+ execMust(t, sender, "NOTIFY b, 'y'")
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+
|
|
|
+ execMust(t, listener, "LISTEN c")
|
|
|
+ _ = listener.Close()
|
|
|
+ execMust(t, sender, "NOTIFY c, 'z'")
|
|
|
+ expectNoNotification(t, listener)
|
|
|
+}
|
|
|
+
|
|
|
+func TestCommitLSNIsMonotonic(t *testing.T) {
|
|
|
+ _, schema, table := newTestDB(t)
|
|
|
+ e := newExec(schema, table)
|
|
|
+ execMust(t, e, "CREATE TABLE lsn_test (id INTEGER PRIMARY KEY, v TEXT)")
|
|
|
+
|
|
|
+ execMust(t, e, "BEGIN")
|
|
|
+ execMust(t, e, "INSERT INTO lsn_test VALUES (1, 'a')")
|
|
|
+ first := execMust(t, e, "COMMIT")
|
|
|
+ if first.LSN == 0 {
|
|
|
+ t.Fatal("first commit LSN is zero")
|
|
|
+ }
|
|
|
+
|
|
|
+ execMust(t, e, "BEGIN")
|
|
|
+ execMust(t, e, "INSERT INTO lsn_test VALUES (2, 'b')")
|
|
|
+ second := execMust(t, e, "COMMIT")
|
|
|
+ if second.LSN <= first.LSN {
|
|
|
+ t.Fatalf("second commit LSN %d did not advance past %d", second.LSN, first.LSN)
|
|
|
+ }
|
|
|
+}
|