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
// SPDX-License-Identifier: BUSL-1.1
//! Pgwire DROP COLLECTION / DROP TABLE lifecycle.
//!
//! Covers the `IF EXISTS` idempotency contract for both spellings:
//! a DROP against a present collection must succeed and remove it; a
//! DROP IF EXISTS against an absent collection must succeed silently;
//! a DROP against an absent collection without `IF EXISTS` must error
//! with `42P01`. The redb-layer counterpart lives in
//! `collection_hard_delete.rs`; this file pins the pgwire surface.
mod common;
use common::pgwire_harness::TestServer;
/// `DROP COLLECTION IF EXISTS` on a present collection must succeed
/// and put the collection into a state where queries against it
/// error with `42P01`. Soft-delete (the default) preserves the row
/// for `UNDROP` and reports a retention-window message; `PURGE`
/// reports the bare `does not exist` message. Either is acceptable —
/// the spec is "DROP succeeded, queries no longer return rows".
#[tokio::test]
async fn drop_collection_if_exists_on_existing_collection_succeeds() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION drop_present_coll")
.await
.unwrap();
srv.exec("DROP COLLECTION IF EXISTS drop_present_coll")
.await
.expect(
"DROP COLLECTION IF EXISTS on an existing collection must succeed; \
this is the documented idempotent path",
);
// 42P01 is the unifying SQLSTATE for "collection inaccessible" —
// both the soft-deleted "within retention window" message and the
// purged "does not exist" message use it. Asserting on the
// SQLSTATE rather than a specific phrase avoids tying the test to
// wording while still catching a "DROP silently no-ops" regression.
srv.expect_error("SELECT 1 FROM drop_present_coll", "42P01")
.await;
}
/// Same as above for the `DROP TABLE` spelling. The parser routes both
/// `DROP COLLECTION` and `DROP TABLE` to the same `DropCollection` AST
/// node, so this is a sibling-spelling regression guard, not a
/// duplicate.
#[tokio::test]
async fn drop_table_if_exists_on_existing_table_succeeds() {
let srv = TestServer::start().await;
srv.exec(
"CREATE TABLE drop_present_tbl (id TEXT, val INTEGER) \
WITH (engine='document_strict')",
)
.await
.unwrap();
srv.exec("DROP TABLE IF EXISTS drop_present_tbl")
.await
.expect(
"DROP TABLE IF EXISTS on an existing table must succeed; the \
`TABLE` spelling and `COLLECTION` spelling share one handler \
and must behave identically",
);
srv.expect_error("SELECT 1 FROM drop_present_tbl", "42P01")
.await;
}
/// `DROP COLLECTION IF EXISTS` on an absent name must succeed silently.
/// This is the documented `IF EXISTS` contract — the AST router has a
/// dedicated short-circuit at `ddl/router/ast/guards.rs` for exactly
/// this case.
#[tokio::test]
async fn drop_collection_if_exists_on_absent_collection_is_silent_success() {
let srv = TestServer::start().await;
srv.exec("DROP COLLECTION IF EXISTS never_created")
.await
.expect(
"DROP COLLECTION IF EXISTS on a name that was never created must \
succeed silently — this is the whole purpose of the IF EXISTS \
modifier",
);
}
/// Plain `DROP COLLECTION` (no `IF EXISTS`) against an absent name must
/// error with `42P01`. This is the negative pair to the IF EXISTS path
/// — proves we are not silently swallowing missing-collection errors on
/// the unmodified DROP.
#[tokio::test]
async fn drop_collection_without_if_exists_on_absent_collection_errors() {
let srv = TestServer::start().await;
srv.expect_error("DROP COLLECTION never_created_plain", "does not exist")
.await;
}
/// DROP (soft-delete) then CREATE the same name must yield a FRESH,
/// EMPTY collection — the old rows must not resurrect. Soft-delete keeps
/// old rows under the same `{db}:{tenant}:{name}:` storage prefix for the
/// retention window; re-creating the name before GC runs must not expose
/// those stale rows. Re-CREATE is an explicit request for a new
/// collection (distinct from `UNDROP` recovery), so the old data must be
/// purged synchronously before the new collection is registered.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_then_recreate_same_name_starts_empty() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION recycled").await.unwrap();
srv.exec("INSERT INTO recycled (id, data) VALUES ('a', 'stale')")
.await
.unwrap();
// Sanity: the row is there before the drop.
let before = srv.query_text("SELECT data FROM recycled").await.unwrap();
assert_eq!(
before.len(),
1,
"row must exist before DROP, got {before:?}"
);
srv.exec("DROP COLLECTION recycled").await.unwrap();
// Re-create the same name (before GC purges the soft-deleted data).
srv.exec("CREATE COLLECTION recycled").await.unwrap();
let after = srv.query_text("SELECT data FROM recycled").await.unwrap();
assert_eq!(
after.len(),
0,
"re-created collection must start empty; old rows resurrected: {after:?}"
);
}
/// The strict-engine twin of `drop_then_recreate_same_name_starts_empty`.
/// A `document_strict` collection stores its Binary Tuples under the same
/// name-derived `{db}:{tenant}:{name}:` prefix that a re-CREATE reuses —
/// collection identity is not per-creation. DROP must purge every stored
/// tuple (and its indexes) so a re-created same-name strict collection starts
/// empty; otherwise the old tuples are inherited by the new incarnation and
/// scan as all-NULL ghosts.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_then_recreate_document_strict_starts_empty() {
let srv = TestServer::start().await;
srv.exec(
"CREATE COLLECTION recycled_strict (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (engine='document_strict')",
)
.await
.unwrap();
for i in 0..7u32 {
srv.exec(&format!(
"INSERT INTO recycled_strict (id, title, body) VALUES ('smoke_{i}', 't{i}', 'b{i}')"
))
.await
.unwrap();
}
let before = srv
.query_rows("SELECT id FROM recycled_strict")
.await
.unwrap();
assert_eq!(
before.len(),
7,
"7 rows must exist before DROP, got {before:?}"
);
let cached_before = srv
.query_text("SELECT COUNT(*) FROM recycled_strict")
.await
.unwrap();
assert_eq!(
cached_before,
vec!["7".to_string()],
"COUNT(*) must reflect the predecessor before DROP, got {cached_before:?}"
);
srv.exec("DROP COLLECTION recycled_strict").await.unwrap();
srv.exec(
"CREATE COLLECTION recycled_strict (id TEXT PRIMARY KEY, title TEXT, body TEXT) \
WITH (engine='document_strict')",
)
.await
.unwrap();
let after = srv
.query_rows("SELECT id FROM recycled_strict")
.await
.unwrap();
assert_eq!(
after.len(),
0,
"re-created strict collection must start empty; old tuples resurrected: {after:?}"
);
let count = srv
.query_text("SELECT COUNT(*) FROM recycled_strict")
.await
.unwrap();
assert_eq!(
count,
vec!["0".to_string()],
"COUNT(*) on a freshly re-created strict collection must be 0, got {count:?}"
);
}
/// Grouped aggregates over a same-name re-CREATE must observe the new
/// collection incarnation. With no rows in the replacement, SQL aggregate
/// semantics require zero groups rather than groups cached from its predecessor.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn drop_then_recreate_discards_predecessor_grouped_aggregates() {
let srv = TestServer::start().await;
srv.exec("CREATE COLLECTION recycled_groups").await.unwrap();
srv.exec("INSERT INTO recycled_groups { id: 'a', bucket: 'old' }")
.await
.unwrap();
srv.exec("INSERT INTO recycled_groups { id: 'b', bucket: 'old' }")
.await
.unwrap();
let predecessor = srv
.query_rows("SELECT bucket, COUNT(*) AS n FROM recycled_groups GROUP BY bucket")
.await
.unwrap();
assert_eq!(
predecessor.len(),
1,
"predecessor must produce one cached group, got {predecessor:?}"
);
srv.exec("DROP COLLECTION recycled_groups").await.unwrap();
srv.exec("CREATE COLLECTION recycled_groups").await.unwrap();
let replacement = srv
.query_rows("SELECT bucket, COUNT(*) AS n FROM recycled_groups GROUP BY bucket")
.await
.unwrap();
assert!(
replacement.is_empty(),
"empty replacement must not inherit predecessor groups: {replacement:?}"
);
}