Skip to content

Commit 75e1edc

Browse files
committed
test(sql): lock outer ORDER BY/DISTINCT/OFFSET over FTS and spatial subqueries
Covers the non-vector post-processing paths (full-text bm25-ranked derived tables and spatial nearest-first derived tables), asserting outcomes independently of which plan variant the subquery body lowers to.
1 parent 5909e1d commit 75e1edc

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// SPDX-License-Identifier: BUSL-1.1
2+
3+
//! Post-processing (outer ORDER BY / OFFSET / DISTINCT) over NON-vector
4+
//! subquery bodies: full-text and spatial.
5+
//!
6+
//! The vector case (`sql_search_subquery_composition.rs`) needs a hit-specific
7+
//! flatten because a vector hit nests the document `body`. Full-text hits use
8+
//! the `{id, data}` document envelope and spatial rows are flat document maps,
9+
//! so both flow through the ordinary storage→relational flatten — these tests
10+
//! lock that the outer relational constraints actually apply over them, by
11+
//! asserting the OUTCOME (correct reorder / dedup) independent of which plan
12+
//! variant the body lowers to.
13+
14+
mod common;
15+
16+
use common::pgwire_harness::TestServer;
17+
18+
// ── Full-text subquery bodies ────────────────────────────────────────────────
19+
20+
async fn create_fts_collection(server: &TestServer) {
21+
server
22+
.exec("CREATE COLLECTION fts_sub WITH (engine='document_schemaless')")
23+
.await
24+
.unwrap();
25+
// Every row matches 'database' so the inner ranked search returns all three;
26+
// `tag` splits them for DISTINCT and `id` gives a deterministic outer order.
27+
for (id, tag, content) in [
28+
("t1", "keep", "database alpha"),
29+
("t2", "drop", "database beta"),
30+
("t3", "keep", "database gamma"),
31+
] {
32+
server
33+
.exec(&format!(
34+
"INSERT INTO fts_sub {{ id: '{id}', tag: '{tag}', content: '{content}' }}"
35+
))
36+
.await
37+
.unwrap();
38+
}
39+
}
40+
41+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
42+
async fn outer_order_by_reorders_fts_subquery() {
43+
let server = TestServer::start().await;
44+
create_fts_collection(&server).await;
45+
46+
// Inner: a bm25-ranked full-text search as a derived table. Outer: reorder
47+
// by a payload column (`id`) descending — the outer ORDER BY must reach the
48+
// post-processor, not be dropped.
49+
let rows = server
50+
.query_text(
51+
"SELECT id FROM \
52+
(SELECT id, tag, bm25_score(content, 'database') AS score \
53+
FROM fts_sub ORDER BY score DESC LIMIT 3) s \
54+
ORDER BY s.id DESC",
55+
)
56+
.await
57+
.unwrap();
58+
assert_eq!(
59+
rows,
60+
vec!["t3".to_string(), "t2".to_string(), "t1".to_string()],
61+
"outer ORDER BY must reorder the full-text subquery result, got: {rows:?}"
62+
);
63+
}
64+
65+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
66+
async fn outer_distinct_dedups_fts_subquery() {
67+
let server = TestServer::start().await;
68+
create_fts_collection(&server).await;
69+
70+
let mut rows = server
71+
.query_text(
72+
"SELECT DISTINCT tag FROM \
73+
(SELECT id, tag, bm25_score(content, 'database') AS score \
74+
FROM fts_sub ORDER BY score DESC LIMIT 3) s",
75+
)
76+
.await
77+
.unwrap();
78+
rows.sort();
79+
assert_eq!(
80+
rows,
81+
vec!["drop".to_string(), "keep".to_string()],
82+
"outer DISTINCT must dedup the full-text subquery's projected column, got: {rows:?}"
83+
);
84+
}
85+
86+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
87+
async fn outer_offset_skips_fts_subquery_rows() {
88+
let server = TestServer::start().await;
89+
create_fts_collection(&server).await;
90+
91+
// Inner orders by id ascending; OFFSET 1 over the three rows drops the
92+
// first (t1), leaving t2, t3.
93+
let rows = server
94+
.query_text(
95+
"SELECT id FROM \
96+
(SELECT id FROM fts_sub WHERE text_match(content, 'database') ORDER BY id) s \
97+
OFFSET 1",
98+
)
99+
.await
100+
.unwrap();
101+
assert_eq!(
102+
rows,
103+
vec!["t2".to_string(), "t3".to_string()],
104+
"outer OFFSET must skip leading rows of the full-text subquery, got: {rows:?}"
105+
);
106+
}
107+
108+
// ── Spatial subquery bodies ──────────────────────────────────────────────────
109+
110+
async fn create_spatial_collection(server: &TestServer) {
111+
server
112+
.exec(
113+
"CREATE COLLECTION geo_sub COLUMNS (id TEXT, location GEOMETRY, name TEXT) \
114+
WITH (engine='spatial')",
115+
)
116+
.await
117+
.unwrap();
118+
// Points at increasing distance from the origin; `name` gives a
119+
// deterministic outer order distinct from distance order.
120+
for (id, lon, lat, name) in [
121+
("g1", 0.0f64, 0.0f64, "charlie"),
122+
("g2", 0.1, 0.1, "bravo"),
123+
("g3", 0.2, 0.2, "alpha"),
124+
] {
125+
server
126+
.exec(&format!(
127+
"INSERT INTO geo_sub (id, location, name) \
128+
VALUES ('{id}', ST_Point({lon}, {lat}), '{name}')"
129+
))
130+
.await
131+
.unwrap();
132+
}
133+
}
134+
135+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
136+
async fn outer_order_by_reorders_spatial_subquery() {
137+
let server = TestServer::start().await;
138+
create_spatial_collection(&server).await;
139+
140+
// Inner: nearest-first spatial ordering as a derived table (distance order
141+
// g1, g2, g3). Outer: reorder by `name` ascending → alpha(g3), bravo(g2),
142+
// charlie(g1). Proves the outer ORDER BY reaches the post-processor and the
143+
// spatial payload column is materialized.
144+
let rows = server
145+
.query_text(
146+
"SELECT id FROM \
147+
(SELECT id, name FROM geo_sub \
148+
ORDER BY ST_Distance(location, ST_Point(0.0, 0.0)) LIMIT 3) s \
149+
ORDER BY s.name ASC",
150+
)
151+
.await
152+
.unwrap();
153+
assert_eq!(
154+
rows,
155+
vec!["g3".to_string(), "g2".to_string(), "g1".to_string()],
156+
"outer ORDER BY must reorder the spatial subquery by a payload column, got: {rows:?}"
157+
);
158+
}

0 commit comments

Comments
 (0)