Skip to main content

nu_command/database/query_plan/
mod.rs

1use crate::database::values::sqlite::SQLiteQueryBuilder;
2use nu_protocol::{PipelineData, ShellError, Span, Value};
3
4/// A lazy query plan that can be composed and executed by filter commands.
5///
6/// This is the deepened seam between filter commands (which want to push down
7/// operations like limit, select, count) and database backends (which implement
8/// the actual query execution). Each variant wraps a concrete query builder.
9///
10/// Callers interact with this enum through its methods instead of downcasting
11/// to a concrete type. Adding a new backend means adding a variant here;
12/// filter commands do not change.
13pub enum QueryPlan {
14    /// SQLite-based lazy query (via `SQLiteQueryBuilder`).
15    Sqlite(SQLiteQueryBuilder),
16}
17
18impl QueryPlan {
19    /// Try to extract a `QueryPlan` from a `&dyn Any` reference.
20    ///
21    /// This is the single place where `downcast_ref` happens. All filter
22    /// commands call this instead of importing and downcasting to a concrete
23    /// query builder type.
24    pub fn try_from_any(val: &dyn std::any::Any) -> Option<Self> {
25        val.downcast_ref::<SQLiteQueryBuilder>()
26            .map(|b| Self::Sqlite(b.clone()))
27    }
28
29    /// Apply a LIMIT to the query plan.
30    pub fn with_limit(self, limit: i64) -> Self {
31        match self {
32            Self::Sqlite(b) => Self::Sqlite(b.with_limit(limit)),
33        }
34    }
35
36    /// Apply an OFFSET to the query plan.
37    pub fn with_offset(self, offset: i64) -> Self {
38        match self {
39            Self::Sqlite(b) => Self::Sqlite(b.with_offset(offset)),
40        }
41    }
42
43    /// Apply a DISTINCT to the query plan.
44    pub fn with_distinct(self) -> Self {
45        match self {
46            Self::Sqlite(b) => Self::Sqlite(b.with_distinct()),
47        }
48    }
49
50    /// Apply an ORDER BY to the query plan.
51    pub fn with_order_by(self, order_by: String) -> Self {
52        match self {
53            Self::Sqlite(b) => Self::Sqlite(b.with_order_by(order_by)),
54        }
55    }
56
57    /// Project the output to a subset of columns.
58    ///
59    /// Returns `None` if the projection cannot be expressed (e.g. complex
60    /// column expressions), allowing callers to fall back to in-memory
61    /// processing.
62    pub fn project_output_columns(&self, columns: &[String]) -> Option<Self> {
63        match self {
64            Self::Sqlite(b) => b.project_output_columns(columns).map(Self::Sqlite),
65        }
66    }
67
68    /// Execute the query and return the result as `PipelineData`.
69    pub fn execute(&self, call_span: Span) -> Result<PipelineData, ShellError> {
70        match self {
71            Self::Sqlite(b) => b.execute(call_span),
72        }
73    }
74
75    /// Count the number of rows without fetching them.
76    pub fn count(&self, call_span: Span) -> Result<i64, ShellError> {
77        match self {
78            Self::Sqlite(b) => b.count(call_span),
79        }
80    }
81
82    /// Human-readable type name for error messages.
83    pub fn type_name(&self) -> &'static str {
84        match self {
85            Self::Sqlite(_) => "lazy query",
86        }
87    }
88
89    /// Convert back into a `Value::Custom` for pipeline propagation.
90    pub fn into_value(self, span: Span) -> Value {
91        match self {
92            Self::Sqlite(b) => Value::custom(Box::new(b), span),
93        }
94    }
95}
96
97#[cfg(test)]
98mod test {
99    use super::*;
100    use crate::database::values::sqlite::SQLiteDatabase;
101    use nu_protocol::Signals;
102    use std::path::Path;
103
104    fn sample_builder(table: &str) -> SQLiteQueryBuilder {
105        SQLiteQueryBuilder::new(
106            Path::new(":memory:").to_path_buf(),
107            table.to_string(),
108            Signals::empty(),
109        )
110    }
111
112    #[test]
113    fn try_from_any_accepts_sqlite_query_builder() {
114        let builder = sample_builder("test");
115        let plan = QueryPlan::try_from_any(&builder as &dyn std::any::Any);
116        assert!(matches!(plan, Some(QueryPlan::Sqlite(_))));
117    }
118
119    #[test]
120    fn try_from_any_rejects_other_custom_values() {
121        let db = SQLiteDatabase::new(Path::new(":memory:"), Signals::empty());
122        let plan = QueryPlan::try_from_any(&db as &dyn std::any::Any);
123        assert!(plan.is_none());
124    }
125
126    #[test]
127    fn try_from_any_rejects_non_custom_values() {
128        let val = 42i64;
129        let plan = QueryPlan::try_from_any(&val as &dyn std::any::Any);
130        assert!(plan.is_none());
131    }
132
133    #[test]
134    fn with_limit_delegates() {
135        let plan = QueryPlan::Sqlite(sample_builder("t"));
136        let limited = plan.with_limit(5);
137        assert!(matches!(limited, QueryPlan::Sqlite(_)));
138        // Verify the limit took effect by checking the generated SQL
139        let sql = match &limited {
140            QueryPlan::Sqlite(b) => b.build_sql(),
141        };
142        assert!(
143            sql.contains("LIMIT 5"),
144            "SQL should contain LIMIT 5, got: {sql}"
145        );
146    }
147
148    #[test]
149    fn with_offset_delegates() {
150        let plan = QueryPlan::Sqlite(sample_builder("t"));
151        let offset = plan.with_offset(10);
152        let sql = match &offset {
153            QueryPlan::Sqlite(b) => b.build_sql(),
154        };
155        assert!(
156            sql.contains("OFFSET 10"),
157            "SQL should contain OFFSET 10, got: {sql}"
158        );
159    }
160
161    #[test]
162    fn with_distinct_delegates() {
163        let plan = QueryPlan::Sqlite(sample_builder("t"));
164        let distinct = plan.with_distinct();
165        let sql = match &distinct {
166            QueryPlan::Sqlite(b) => b.build_sql(),
167        };
168        assert!(
169            sql.starts_with("SELECT DISTINCT"),
170            "SQL should start with SELECT DISTINCT, got: {sql}"
171        );
172    }
173
174    #[test]
175    fn with_order_by_delegates() {
176        let plan = QueryPlan::Sqlite(sample_builder("t"));
177        let ordered = plan.with_order_by("id DESC".to_string());
178        let sql = match &ordered {
179            QueryPlan::Sqlite(b) => b.build_sql(),
180        };
181        assert!(
182            sql.contains("ORDER BY id DESC"),
183            "SQL should contain ORDER BY id DESC, got: {sql}"
184        );
185    }
186
187    #[test]
188    fn type_name_returns_expected() {
189        let plan = QueryPlan::Sqlite(sample_builder("t"));
190        assert_eq!(plan.type_name(), "lazy query");
191    }
192
193    #[test]
194    fn into_value_round_trip_produces_sqlite_query_builder() {
195        let plan = QueryPlan::Sqlite(sample_builder("roundtrip"));
196        let span = Span::test_data();
197        let value = plan.into_value(span);
198
199        // It should be a Value::Custom
200        assert!(matches!(&value, Value::Custom { .. }));
201
202        // The inner CustomValue should be a SQLiteQueryBuilder
203        if let Value::Custom { val, .. } = &value {
204            let retrieved = val.as_any().downcast_ref::<SQLiteQueryBuilder>();
205            assert!(retrieved.is_some());
206            assert_eq!(retrieved.unwrap().table_name, "roundtrip");
207        }
208    }
209}