Skip to content

Commit 5909e1d

Browse files
authored
Merge pull request #225 from mkhairi/feat/search-vector-subquery-composition
feat(sql): accept SEARCH ... USING VECTOR in subquery position and with quoted collection names
2 parents d545ea4 + 79401bc commit 5909e1d

30 files changed

Lines changed: 1426 additions & 35 deletions

File tree

nodedb-cluster-tests/tests/transport_security.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ fn insecure_counter_guard() -> &'static Mutex<()> {
3838
LOCK.get_or_init(|| Mutex::new(()))
3939
}
4040

41+
use nodedb_cluster::topology::{ClusterTopology, NodeInfo, NodeState};
4142
use nodedb_cluster::transport::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
4243
use nodedb_cluster::{
4344
MacKey, NexarTransport, RaftRpcHandler, TlsCredentials, TransportCredentials,
4445
generate_node_credentials, insecure_transport_count, spki_pin_from_cert_der,
4546
};
46-
use nodedb_cluster::topology::{ClusterTopology, NodeInfo, NodeState};
4747
use nodedb_raft::message::{AppendEntriesRequest, AppendEntriesResponse, RequestVoteResponse};
4848
use nodedb_raft::transport::RaftTransport;
4949

nodedb-physical/src/physical_plan/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,8 @@ impl PhysicalPlan {
324324
| PhysicalPlan::Graph(GraphOp::WccSuperstep(_)) => None,
325325
// Exchange: recurse into the child plan to extract the collection.
326326
PhysicalPlan::Query(QueryOp::Exchange(op)) => op.child.collection(),
327+
// PostProcess: recurse into the materialized input plan.
328+
PhysicalPlan::Query(QueryOp::PostProcess { input, .. }) => input.collection(),
327329
// ProviderScan is a catalog/constant source — no user collection.
328330
PhysicalPlan::Query(QueryOp::ProviderScan { .. }) => None,
329331
// KV ops carry their own collection (sorted-index-only ops → None).

nodedb-physical/src/physical_plan/query.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,46 @@ pub enum QueryOp {
9292
distinct: bool,
9393
},
9494

95+
/// Relational post-processing over a materialized child plan.
96+
///
97+
/// Coordinator-resolved, exactly like [`QueryOp::Exchange`]: at resolve time
98+
/// the coordinator gathers `input`'s rows, flattens them to the relational
99+
/// row shape, and rewrites this node into a [`QueryOp::ProviderScan`]
100+
/// carrying the same relational parameters — which applies
101+
/// filter→offset→sort→distinct→project→limit on a single core. A Data-Plane
102+
/// core must NEVER see this node (defensive error if it does).
103+
///
104+
/// Lowered from `SqlPlan::Subquery`: an outer `ORDER BY` / `OFFSET` /
105+
/// `DISTINCT` / post-reorder `LIMIT` over a subquery/derived-table body
106+
/// whose leaf plan could not absorb those constraints. Constraints the leaf
107+
/// DID absorb (a `WHERE` pushed into a search engine, an unordered `LIMIT`
108+
/// folded into `top_k`) are applied by `input` and not repeated here.
109+
PostProcess {
110+
/// The subquery body to materialize. Wrapped in `Exchange{Gather}` by
111+
/// the converter when the body is a sharded source, so the gather runs
112+
/// exactly once over the full union before post-processing.
113+
input: Box<crate::physical_plan::PhysicalPlan>,
114+
/// Serialized `Vec<ScanFilter>` (MessagePack). Empty = no predicate.
115+
#[serde(default)]
116+
filters: Vec<u8>,
117+
/// Output column names to keep. Empty = emit all columns.
118+
#[serde(default)]
119+
projection: Vec<String>,
120+
/// Sort keys: `(column_name, ascending)`. Empty = unordered.
121+
#[serde(default)]
122+
sort_keys: Vec<(String, bool)>,
123+
/// Maximum rows to emit after offset/sort/distinct/projection.
124+
/// `None` = unlimited.
125+
#[serde(default)]
126+
limit: Option<usize>,
127+
/// Number of rows to skip before applying limit.
128+
#[serde(default)]
129+
offset: usize,
130+
/// SQL DISTINCT: deduplicate on the projected row.
131+
#[serde(default)]
132+
distinct: bool,
133+
},
134+
95135
/// Aggregate: GROUP BY + aggregate functions.
96136
Aggregate {
97137
collection: String,

nodedb-physical/src/physical_plan/routing.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,13 @@ pub fn plan_contains_cluster_partitioned_leaf(plan: &PhysicalPlan) -> bool {
7575
plan_contains_cluster_partitioned_leaf(child)
7676
}
7777

78+
// Recurse through PostProcess (its materialized input is the real
79+
// plan). Normally resolved to a `ProviderScan` before routing, but a
80+
// conservative recursion keeps routing correct if one survives.
81+
PhysicalPlan::Query(QueryOp::PostProcess { input, .. }) => {
82+
plan_contains_cluster_partitioned_leaf(input)
83+
}
84+
7885
// Recurse through lateral outer plans.
7986
PhysicalPlan::Query(QueryOp::LateralTopK { outer_plan, .. })
8087
| PhysicalPlan::Query(QueryOp::LateralLoop { outer_plan, .. }) => {

nodedb-sql/src/parser/preprocess/pipeline.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
use super::function_args::rewrite_object_literal_args;
1414
use super::lex::{first_sql_word, has_brace_outside_literals, has_operator_outside_literals};
1515
use super::object_literal_stmt::try_rewrite_object_literal;
16-
use super::search_vector::try_rewrite_search_using_vector;
16+
use super::search_vector::{
17+
try_rewrite_nested_search_using_vector, try_rewrite_search_using_vector,
18+
};
1719
use super::temporal::extract as extract_temporal;
1820
use super::vector_ops::{
1921
rewrite_arrow_distance, rewrite_cosine_distance, rewrite_neg_inner_product,
@@ -56,10 +58,16 @@ pub fn preprocess(sql: &str) -> Result<Option<PreprocessedSql>, SqlError> {
5658
// `SELECT * FROM <coll> ORDER BY vector_distance(...) LIMIT k` before any
5759
// first-word dispatch — the rewritten form re-enters the rest of the
5860
// pipeline as a plain SELECT.
61+
//
62+
// The same clause in subquery position — `FROM (SEARCH ...) s` — is
63+
// rewritten in place so a k-NN result composes with joins and filters.
5964
let (temporal_sql, search_vector_rewritten) =
6065
match try_rewrite_search_using_vector(&temporal_sql) {
6166
Some(rewritten) => (rewritten, true),
62-
None => (temporal_sql, false),
67+
None => match try_rewrite_nested_search_using_vector(&temporal_sql) {
68+
Some(rewritten) => (rewritten, true),
69+
None => (temporal_sql, false),
70+
},
6371
};
6472

6573
let first_word = first_sql_word(&temporal_sql)

0 commit comments

Comments
 (0)