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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
//! v7.39 (round 652) — `ADD CONSTRAINT … CHECK` never looked at the rows
//! that were already there.
//!
//! The F31 audit went looking for tests and comments that assert SPG's own
//! gaps as rules. The parser carried this one:
//!
//! > SPG validates inline at ADD CONSTRAINT time (no NOT VALID / VALIDATE
//! > separation), so VALIDATE is an accept-and-no-op for pg_dump round-trip.
//!
//! Measured against PG18, the premise was false in both directions. The
//! `NOT VALID` suffix was a syntax error, so the constraint pg_dump emits
//! for a table PG deliberately left unvalidated could not be restored at
//! all. And plain `ADD CONSTRAINT` did not scan either — it installed the
//! constraint over rows that violate it, silently, so a table ended up
//! holding data contradicting its own declared CHECK while every reader
//! (pg_dump included) believed it did not. PG refuses that ALTER.
//!
//! The comment is why nobody looked: it named a correctness property in
//! the present tense, and the property did not exist.
use spg_engine::{Engine, QueryResult};
fn one(e: &mut Engine, sql: &str) -> String {
match e.execute(sql).unwrap_or_else(|err| panic!("{sql}: {err}")) {
QueryResult::Rows { rows, .. } => rows
.iter()
.map(|r| {
r.values
.iter()
.map(spg_engine::eval::value_to_text)
.collect::<Vec<_>>()
.join("|")
})
.collect::<Vec<_>>()
.join(","),
other => panic!("{sql}: {other:?}"),
}
}
fn err(e: &mut Engine, sql: &str) -> String {
match e.execute(sql) {
Err(err) => format!("{err}"),
Ok(ok) => panic!("{sql}: expected an error, got {ok:?}"),
}
}
/// PG: `check constraint "nv_pos" of relation "nv" is violated by some row`,
/// and the constraint is not installed — the later INSERT of another
/// negative row succeeds, leaving three.
#[test]
fn round652_a_plain_add_scans_the_rows_already_there() {
let mut e = Engine::new();
e.execute("CREATE TABLE nv(a int)").unwrap();
e.execute("INSERT INTO nv VALUES (1),(-5)").unwrap();
let msg = err(&mut e, "ALTER TABLE nv ADD CONSTRAINT nv_pos CHECK (a > 0)");
assert!(
msg.contains("check constraint \"nv_pos\" of relation \"nv\" is violated by some row"),
"{msg}"
);
// Refused means not installed: the table is untouched and still takes
// negative rows. A half-applied ALTER would show up right here.
e.execute("INSERT INTO nv VALUES (-9)").unwrap();
assert_eq!(one(&mut e, "SELECT count(*) FROM nv"), "3");
assert_eq!(
one(
&mut e,
"SELECT count(*) FROM pg_constraint WHERE conname='nv_pos'"
),
"0"
);
}
/// An unnamed CHECK gets the synthesised name in the message, the same
/// `<table>_<col>_check` form pg_constraint would give it.
#[test]
fn round652_b_the_message_names_the_constraint_it_would_have_installed() {
let mut e = Engine::new();
e.execute("CREATE TABLE nv(a int)").unwrap();
e.execute("INSERT INTO nv VALUES (-5)").unwrap();
let msg = err(&mut e, "ALTER TABLE nv ADD CHECK (a > 0)");
assert!(msg.contains("\"nv_a_check\""), "{msg}");
}
/// NOT VALID installs it without the scan. New rows are still checked —
/// that is the half people get wrong; NOT VALID grandfathers the past, it
/// does not disable the constraint.
#[test]
fn round652_c_not_valid_grandfathers_the_past_only() {
let mut e = Engine::new();
e.execute("CREATE TABLE nv(a int)").unwrap();
e.execute("INSERT INTO nv VALUES (1),(-5)").unwrap();
e.execute("ALTER TABLE nv ADD CONSTRAINT nv_pos CHECK (a > 0) NOT VALID")
.unwrap();
let msg = err(&mut e, "INSERT INTO nv VALUES (-9)");
assert!(msg.contains("violates check constraint"), "{msg}");
assert_eq!(one(&mut e, "SELECT count(*) FROM nv"), "2");
assert_eq!(
one(
&mut e,
"SELECT convalidated FROM pg_constraint WHERE conname='nv_pos'"
),
"false"
);
// The suffix pg_dump reads back. Without it the restore would validate
// a constraint PG deliberately did not.
assert_eq!(
one(
&mut e,
"SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname='nv_pos'"
),
"CHECK ((a > 0)) NOT VALID"
);
}
/// VALIDATE runs the scan that NOT VALID skipped: it refuses while the
/// offending rows are there and succeeds once they are gone, flipping both
/// `convalidated` and the deparse.
#[test]
fn round652_d_validate_runs_the_deferred_scan() {
let mut e = Engine::new();
e.execute("CREATE TABLE nv(a int)").unwrap();
e.execute("INSERT INTO nv VALUES (1),(-5)").unwrap();
e.execute("ALTER TABLE nv ADD CONSTRAINT nv_pos CHECK (a > 0) NOT VALID")
.unwrap();
let msg = err(&mut e, "ALTER TABLE nv VALIDATE CONSTRAINT nv_pos");
assert!(msg.contains("is violated by some row"), "{msg}");
assert_eq!(
one(
&mut e,
"SELECT convalidated FROM pg_constraint WHERE conname='nv_pos'"
),
"false"
);
// The DELETE tombstones the row rather than removing it; the scan has
// to skip tombstones or this second VALIDATE would refuse too.
e.execute("DELETE FROM nv WHERE a < 0").unwrap();
e.execute("ALTER TABLE nv VALIDATE CONSTRAINT nv_pos")
.unwrap();
assert_eq!(
one(
&mut e,
"SELECT convalidated FROM pg_constraint WHERE conname='nv_pos'"
),
"true"
);
assert_eq!(
one(
&mut e,
"SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname='nv_pos'"
),
"CHECK ((a > 0))"
);
}
/// Validating an already-valid constraint is a no-op; naming one that does
/// not exist is PG's 42704.
#[test]
fn round652_e_validate_edges() {
let mut e = Engine::new();
e.execute("CREATE TABLE nv(a int)").unwrap();
e.execute("ALTER TABLE nv ADD CONSTRAINT nv_pos CHECK (a > 0)")
.unwrap();
e.execute("ALTER TABLE nv VALIDATE CONSTRAINT nv_pos")
.unwrap();
let msg = err(&mut e, "ALTER TABLE nv VALIDATE CONSTRAINT nope");
assert!(
msg.contains("constraint \"nope\" of relation \"nv\" does not exist"),
"{msg}"
);
}
/// A CHECK written inside CREATE TABLE cannot be NOT VALID — there are no
/// rows to grandfather, and PG rejects the suffix there.
#[test]
fn round652_f_not_valid_is_an_alter_only_suffix() {
let mut e = Engine::new();
assert!(
e.execute("CREATE TABLE nv(a int CHECK (a > 0) NOT VALID)")
.is_err()
);
assert!(
e.execute("CREATE TABLE nv2(a int, CONSTRAINT c CHECK (a > 0) NOT VALID)")
.is_err()
);
}
/// `oid = 'pg_class'::regclass` worked and `oid IN ('pg_class'::regclass, …)`
/// did not: the IN list ran a static type check before comparing anything,
/// and a reg cast describes as Text because SPG has no reg `DataType`. The
/// ANY form failed one layer further down — the array constructor classified
/// reg values by the catch-all arm and built a text array.
#[test]
fn round652_g_reg_casts_survive_in_lists_and_arrays() {
let mut e = Engine::new();
assert_eq!(
one(
&mut e,
"SELECT count(*) FROM pg_class WHERE oid = 'pg_class'::regclass"
),
"1"
);
assert_eq!(
one(
&mut e,
"SELECT count(*) FROM pg_class WHERE oid IN ('pg_class'::regclass)"
),
"1"
);
assert_eq!(
one(
&mut e,
"SELECT count(*) FROM pg_class \
WHERE oid IN ('pg_class'::regclass, 'pg_type'::regclass)"
),
"2"
);
assert_eq!(
one(
&mut e,
"SELECT count(*) FROM pg_class WHERE oid = ANY(ARRAY['pg_class'::regclass])"
),
"1"
);
assert_eq!(
one(
&mut e,
"SELECT count(*) FROM pg_type WHERE oid IN ('int4'::regtype, 'text'::regtype)"
),
"2"
);
}
/// The array built from reg values is a bigint array, not text: the
/// classification arm and the materialisation arm are two lists that have
/// to agree, and teaching only the first one panicked on the wire.
#[test]
fn round652_h_a_reg_array_is_an_oid_array() {
let mut e = Engine::new();
assert_eq!(
one(&mut e, "SELECT pg_typeof(ARRAY['pg_class'::regclass])"),
"bigint[]"
);
assert_eq!(
one(
&mut e,
"SELECT (ARRAY['int4'::regtype])[1] = 'int4'::regtype::oid"
),
"true"
);
}
/// v7.39 (round 652) — dropping a constraint must not move another one's
/// NOT VALID mark onto it. The mark rides on the constraint itself now, so
/// this is structurally safe — but it was an index-parallel vector for a
/// while (an attempt to dodge a perf effect that four broken instruments
/// had overstated), and under that shape a mis-shifted index would have
/// been invisible: nothing goes red, pg_dump just emits NOT VALID for the
/// wrong constraint. The pin stays as the regression guard for whatever
/// shape the mark takes next.
#[test]
fn round652_i_dropping_a_constraint_leaves_the_other_marks_alone() {
let mut e = Engine::new();
e.execute("CREATE TABLE nv(a int, b int)").unwrap();
e.execute("INSERT INTO nv VALUES (1, -5)").unwrap();
e.execute("ALTER TABLE nv ADD CONSTRAINT c_a CHECK (a > 0)")
.unwrap();
e.execute("ALTER TABLE nv ADD CONSTRAINT c_b CHECK (b > 0) NOT VALID")
.unwrap();
assert_eq!(
one(
&mut e,
"SELECT conname, convalidated FROM pg_constraint \
WHERE contype='c' ORDER BY conname"
),
"c_a|true,c_b|false"
);
// c_a occupies the slot before c_b; dropping it shifts c_b down one.
e.execute("ALTER TABLE nv DROP CONSTRAINT c_a").unwrap();
assert_eq!(
one(
&mut e,
"SELECT conname, convalidated FROM pg_constraint \
WHERE contype='c' ORDER BY conname"
),
"c_b|false",
"the mark has to stay on c_b"
);
assert_eq!(
one(
&mut e,
"SELECT pg_get_constraintdef(oid) FROM pg_constraint WHERE conname='c_b'"
),
"CHECK ((b > 0)) NOT VALID"
);
e.execute("DELETE FROM nv WHERE b < 0").unwrap();
e.execute("ALTER TABLE nv VALIDATE CONSTRAINT c_b").unwrap();
assert_eq!(
one(
&mut e,
"SELECT convalidated FROM pg_constraint WHERE conname='c_b'"
),
"true"
);
}