-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsequence_sql_test.go
More file actions
209 lines (183 loc) · 7.15 KB
/
Copy pathsequence_sql_test.go
File metadata and controls
209 lines (183 loc) · 7.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package main
import (
"context"
"errors"
"strings"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
func TestResetSequenceStatements_QuotesSchemaAndSequenceRegclass(t *testing.T) {
table := Table{PGName: "events"}
col := Column{PGName: "id", Extra: "auto_increment"}
stmts := resetSequenceStatements("order", table, col)
if len(stmts) != 3 {
t.Fatalf("statement count = %d, want 3", len(stmts))
}
if !strings.Contains(stmts[0], `CREATE SEQUENCE IF NOT EXISTS "order"."events_id_seq"`) {
t.Fatalf("create sequence statement = %q", stmts[0])
}
if !strings.Contains(stmts[1], `SELECT setval('"order"."events_id_seq"'::regclass`) {
t.Fatalf("setval statement = %q", stmts[1])
}
if !strings.Contains(stmts[2], `SET DEFAULT nextval('"order"."events_id_seq"'::regclass)`) {
t.Fatalf("nextval statement = %q", stmts[2])
}
}
func TestResetSequenceStatements_QuotesNonTrivialSequenceName(t *testing.T) {
table := Table{PGName: "audit"}
col := Column{PGName: "event-id", Extra: "auto_increment"}
stmts := resetSequenceStatements("app", table, col)
if !strings.Contains(stmts[0], `"app"."audit_event-id_seq"`) {
t.Fatalf("create sequence statement = %q", stmts[0])
}
if !strings.Contains(stmts[1], `'"app"."audit_event-id_seq"'::regclass`) {
t.Fatalf("setval statement = %q", stmts[1])
}
if !strings.Contains(stmts[2], `'"app"."audit_event-id_seq"'::regclass`) {
t.Fatalf("nextval statement = %q", stmts[2])
}
}
func TestResetSequenceStatements_QuotesReservedColumnName(t *testing.T) {
table := Table{PGName: "audit"}
col := Column{PGName: "collation", Extra: "auto_increment"}
stmts := resetSequenceStatements("app", table, col)
if !strings.Contains(stmts[1], `SELECT MAX("collation") FROM "app"."audit"`) {
t.Fatalf("setval statement = %q", stmts[1])
}
if !strings.Contains(stmts[2], `ALTER TABLE "app"."audit" ALTER COLUMN "collation" SET DEFAULT`) {
t.Fatalf("nextval statement = %q", stmts[2])
}
}
func TestDataOnlySequenceLookupSQL_ResolvesAttachedThenConventionName(t *testing.T) {
table := Table{PGName: "Orders"}
col := Column{PGName: "Id", Extra: "auto_increment"}
q := dataOnlySequenceLookupSQL("myapp", table, col)
if !strings.Contains(q, `pg_get_serial_sequence('"myapp"."Orders"', 'Id')`) {
t.Fatalf("lookup should resolve the attached sequence first, got: %q", q)
}
if !strings.Contains(q, `to_regclass('"myapp"."Orders_Id_seq"')`) {
t.Fatalf("lookup should fall back to the pgferry naming convention, got: %q", q)
}
if !strings.Contains(q, "COALESCE(") {
t.Fatalf("lookup should prefer the attached sequence via COALESCE, got: %q", q)
}
}
func TestDataOnlySequenceLookupSQL_ResolvesDefaultNextvalSequence(t *testing.T) {
table := Table{PGName: "Orders"}
col := Column{PGName: "Id", Extra: "auto_increment"}
q := dataOnlySequenceLookupSQL("myapp", table, col)
if !strings.Contains(q, "pg_attrdef") || !strings.Contains(q, "pg_depend") {
t.Fatalf("lookup should resolve sequences referenced only by the column DEFAULT (issue #250), got: %q", q)
}
first := strings.Index(q, "pg_get_serial_sequence")
def := strings.Index(q, "pg_attrdef")
last := strings.Index(q, `to_regclass('"myapp"."Orders_Id_seq"')`)
if first == -1 || def == -1 || last == -1 || !(first < def && def < last) {
t.Fatalf("lookup order should be attached, then DEFAULT expression, then convention name, got: %q", q)
}
}
func TestDataOnlySequenceLookupSQL_EscapesQuotes(t *testing.T) {
table := Table{PGName: "o'brien"}
col := Column{PGName: "it's_id", Extra: "auto_increment"}
q := dataOnlySequenceLookupSQL("app", table, col)
if !strings.Contains(q, `'"app"."o''brien"'`) {
t.Fatalf("table literal should escape single quotes, got: %q", q)
}
if !strings.Contains(q, `, 'it''s_id')`) {
t.Fatalf("column literal should escape single quotes, got: %q", q)
}
if !strings.Contains(q, `to_regclass('"app"."o''brien_it''s_id_seq"')`) {
t.Fatalf("fallback sequence literal should escape single quotes, got: %q", q)
}
}
// fakeSequenceRow implements pgx.Row for the sequence lookup query.
type fakeSequenceRow struct {
seq *string
err error
}
func (r fakeSequenceRow) Scan(dest ...any) error {
if r.err != nil {
return r.err
}
if len(dest) != 1 {
return errors.New("expected one destination")
}
out, ok := dest[0].(**string)
if !ok {
return errors.New("expected **string destination")
}
*out = r.seq
return nil
}
// fakeSequenceExecutor implements queryExecutor for resetAttachedSequence tests.
type fakeSequenceExecutor struct {
row fakeSequenceRow
querySQLs []string
execSQLs []string
execErr error
}
func (f *fakeSequenceExecutor) QueryRow(_ context.Context, sql string, _ ...any) pgx.Row {
f.querySQLs = append(f.querySQLs, sql)
return f.row
}
func (f *fakeSequenceExecutor) Exec(_ context.Context, sql string, _ ...any) (pgconn.CommandTag, error) {
f.execSQLs = append(f.execSQLs, sql)
if f.execErr != nil {
return pgconn.CommandTag{}, f.execErr
}
return pgconn.NewCommandTag("SELECT 1"), nil
}
func TestResetAttachedSequence_UsesResolvedSequenceName(t *testing.T) {
seq := `myapp."OrderIdSequence"`
exec := &fakeSequenceExecutor{row: fakeSequenceRow{seq: &seq}}
table := Table{PGName: "Orders"}
col := Column{PGName: "Id", Extra: "auto_increment"}
if err := resetAttachedSequence(context.Background(), exec, "myapp", table, col); err != nil {
t.Fatalf("resetAttachedSequence() error: %v", err)
}
if len(exec.execSQLs) != 1 {
t.Fatalf("exec calls = %v, want exactly one setval", exec.execSQLs)
}
if !strings.Contains(exec.execSQLs[0], `SELECT setval('myapp."OrderIdSequence"'::regclass`) {
t.Fatalf("setval should target the resolved sequence, got: %q", exec.execSQLs[0])
}
if !strings.Contains(exec.execSQLs[0], `COALESCE((SELECT MAX("Id") FROM "myapp"."Orders"), 0) + 1, false)`) {
t.Fatalf("setval should advance to max+1, got: %q", exec.execSQLs[0])
}
}
func TestResetAttachedSequence_NoSequenceFailsWithClearError(t *testing.T) {
exec := &fakeSequenceExecutor{row: fakeSequenceRow{seq: nil}}
table := Table{PGName: "Orders"}
col := Column{PGName: "Id", Extra: "auto_increment"}
err := resetAttachedSequence(context.Background(), exec, "myapp", table, col)
if err == nil {
t.Fatal("expected error when no sequence exists for the column")
}
if len(exec.execSQLs) != 0 {
t.Fatalf("no setval should run when the sequence is missing, got: %v", exec.execSQLs)
}
msg := err.Error()
if !strings.Contains(msg, `"myapp"."Orders"."Id"`) {
t.Fatalf("error should name the column, got: %q", msg)
}
if !strings.Contains(msg, "Orders_Id_seq") {
t.Fatalf("error should mention the convention name that was tried, got: %q", msg)
}
}
func TestResetAttachedSequence_LookupErrorIsWrapped(t *testing.T) {
lookupErr := errors.New("connection lost")
exec := &fakeSequenceExecutor{row: fakeSequenceRow{err: lookupErr}}
table := Table{PGName: "orders"}
col := Column{PGName: "id", Extra: "auto_increment"}
err := resetAttachedSequence(context.Background(), exec, "app", table, col)
if err == nil {
t.Fatal("expected error")
}
if !errors.Is(err, lookupErr) {
t.Fatalf("error should wrap the lookup failure, got: %v", err)
}
if len(exec.execSQLs) != 0 {
t.Fatalf("no setval should run when the lookup fails, got: %v", exec.execSQLs)
}
}