vibesql-executor 0.2.0

Query execution engine for vibesql SQL database
Documentation
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! Spatial index optimization for WHERE clause filtering
//!
//! Implements automatic spatial index usage for spatial predicates in WHERE clauses.
//! Uses two-phase filtering: R-tree MBR query (fast, approximate) followed by
//! full spatial predicate evaluation (slower, exact).

use rstar::AABB;
use vibesql_ast::Expression;
use vibesql_storage::{Database, Row};
use vibesql_types::SqlValue;

use crate::{
    errors::ExecutorError,
    evaluator::functions::spatial::{constructors::parse_wkt, Geometry},
    schema::CombinedSchema,
};

/// Spatial index usage information
#[derive(Debug)]
pub struct SpatialIndexUsage {
    pub index_name: String,
    pub column_name: String,
    pub predicate: SpatialPredicate,
    pub query_mbr: AABB<[f64; 2]>,
    pub query_geometry: Geometry,
}

/// Supported spatial predicates for index optimization
#[derive(Debug, Clone)]
pub enum SpatialPredicate {
    Contains,                  // ST_Contains(indexed_col, literal_geom)
    Intersects,                // ST_Intersects(indexed_col, literal_geom)
    Within,                    // ST_Within(indexed_col, literal_geom)
    DWithin { distance: f64 }, // ST_DWithin(indexed_col, literal_geom, distance)
}

/// Try to use spatial index for WHERE clause optimization
/// Returns Some(rows) if optimization applied, None otherwise
pub(in crate::select::executor) fn try_spatial_index_optimization(
    database: &Database,
    where_expr: Option<&Expression>,
    all_rows: &[Row],
    schema: &CombinedSchema,
) -> Result<Option<Vec<Row>>, ExecutorError> {
    let where_expr = match where_expr {
        Some(expr) => expr,
        None => return Ok(None), // No WHERE clause
    };

    // Try to detect spatial predicate and find matching index
    let spatial_usage = match detect_spatial_index_usage(where_expr, database, schema)? {
        Some(usage) => usage,
        None => return Ok(None), // No spatial index can be used
    };

    // Apply two-phase filtering
    apply_two_phase_filtering(database, &spatial_usage, all_rows, schema)
}

/// Detect if WHERE expression can use a spatial index
fn detect_spatial_index_usage(
    expr: &Expression,
    database: &Database,
    schema: &CombinedSchema,
) -> Result<Option<SpatialIndexUsage>, ExecutorError> {
    // Match function calls like ST_Contains(column, geometry_literal)
    if let Expression::Function { name, args, .. } = expr {
        // ST_Contains(indexed_col, literal_geom)
        if name.eq_ignore_ascii_case("ST_Contains") && args.len() == 2 {
            return try_detect_spatial_predicate(
                &args[0],
                &args[1],
                SpatialPredicate::Contains,
                database,
                schema,
            );
        }

        // ST_Intersects(indexed_col, literal_geom)
        if name.eq_ignore_ascii_case("ST_Intersects") && args.len() == 2 {
            return try_detect_spatial_predicate(
                &args[0],
                &args[1],
                SpatialPredicate::Intersects,
                database,
                schema,
            );
        }

        // ST_Within(indexed_col, literal_geom)
        if name.eq_ignore_ascii_case("ST_Within") && args.len() == 2 {
            return try_detect_spatial_predicate(
                &args[0],
                &args[1],
                SpatialPredicate::Within,
                database,
                schema,
            );
        }

        // ST_DWithin(indexed_col, literal_geom, distance)
        if name.eq_ignore_ascii_case("ST_DWithin") && args.len() == 3 {
            // Extract distance from third argument
            let distance = match &args[2] {
                Expression::Literal(SqlValue::Integer(d)) => *d as f64,
                Expression::Literal(SqlValue::Float(d)) => *d as f64,
                _ => return Ok(None), // Distance must be a literal
            };

            return try_detect_spatial_predicate(
                &args[0],
                &args[1],
                SpatialPredicate::DWithin { distance },
                database,
                schema,
            );
        }
    }

    Ok(None)
}

/// Try to detect spatial predicate pattern and find matching index
fn try_detect_spatial_predicate(
    column_expr: &Expression,
    geometry_expr: &Expression,
    predicate: SpatialPredicate,
    database: &Database,
    schema: &CombinedSchema,
) -> Result<Option<SpatialIndexUsage>, ExecutorError> {
    // First argument should be a column reference
    let column_name = match column_expr {
        Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() => {
            col_id.column_canonical().to_string()
        }
        _ => return Ok(None), // Not a column reference
    };

    // Find which table this column belongs to
    let table_name = find_table_for_column(&column_name, schema)?;
    if table_name.is_none() {
        return Ok(None);
    }
    let table_name = table_name.unwrap();

    // Check if there's a spatial index on this table and column
    let spatial_indexes = database.get_spatial_indexes_for_table(&table_name);
    let matching_index =
        spatial_indexes.iter().find(|(metadata, _)| metadata.column_name == column_name);

    if matching_index.is_none() {
        return Ok(None); // No spatial index on this column
    }

    let (metadata, _) = matching_index.unwrap();

    // Extract geometry from second argument
    let query_geometry = match extract_geometry_from_expr(geometry_expr)? {
        Some(geom) => geom,
        None => return Ok(None), // Could not extract geometry
    };

    // Compute MBR from query geometry
    let query_mbr = compute_mbr(&query_geometry, &predicate)?;

    // Build a schema-qualified lookup name so the two-phase lookup targets the
    // exact spatial index that matched here, even when a same-named index
    // exists in another schema (spatial indexes are schema-aware — #5558).
    // `get_spatial_index` resolves an explicit `schema.index` form precisely.
    let index_name = format!("{}.{}", metadata.schema, metadata.index_name);

    Ok(Some(SpatialIndexUsage {
        index_name,
        column_name: column_name.clone(),
        predicate,
        query_mbr,
        query_geometry,
    }))
}

/// Find which table a column belongs to
fn find_table_for_column(
    column_name: &str,
    schema: &CombinedSchema,
) -> Result<Option<String>, ExecutorError> {
    for (table, (_start_idx, table_schema)) in &schema.table_schemas {
        if table_schema.get_column_index(column_name).is_some() {
            return Ok(Some(table.to_string()));
        }
    }
    Ok(None)
}

/// Extract geometry from expression (handles ST_GeomFromText and direct WKT)
fn extract_geometry_from_expr(expr: &Expression) -> Result<Option<Geometry>, ExecutorError> {
    match expr {
        // Case 1: ST_GeomFromText('POINT(10 20)')
        Expression::Function { name, args, .. }
            if name.eq_ignore_ascii_case("ST_GeomFromText") && !args.is_empty() =>
        {
            if let Expression::Literal(SqlValue::Varchar(wkt) | SqlValue::Character(wkt)) = &args[0]
            {
                return Ok(Some(parse_wkt(wkt)?));
            }
        }
        // Case 2: Direct string literal 'POINT(10 20)'
        Expression::Literal(SqlValue::Varchar(wkt) | SqlValue::Character(wkt)) => {
            // Try to parse as WKT
            if let Ok(geom) = parse_wkt(wkt) {
                return Ok(Some(geom));
            }
        }
        _ => {}
    }
    Ok(None)
}

/// Compute MBR (Minimum Bounding Rectangle) from geometry
fn compute_mbr(
    geometry: &Geometry,
    predicate: &SpatialPredicate,
) -> Result<AABB<[f64; 2]>, ExecutorError> {
    let base_mbr = match geometry {
        Geometry::Point { x, y } => {
            // Point MBR is just the point itself
            AABB::from_point([*x, *y])
        }
        Geometry::LineString { points } => {
            if points.is_empty() {
                return Err(ExecutorError::Other("Empty LineString".to_string()));
            }

            let xs: Vec<f64> = points.iter().map(|(x, _)| *x).collect();
            let ys: Vec<f64> = points.iter().map(|(_, y)| *y).collect();

            let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
            let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            let min_y = ys.iter().cloned().fold(f64::INFINITY, f64::min);
            let max_y = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

            AABB::from_corners([min_x, min_y], [max_x, max_y])
        }
        Geometry::Polygon { rings } => {
            if rings.is_empty() {
                return Err(ExecutorError::Other("Empty Polygon".to_string()));
            }

            // Compute MBR from exterior ring (rings[0])
            let exterior = &rings[0];
            if exterior.is_empty() {
                return Err(ExecutorError::Other("Empty Polygon exterior ring".to_string()));
            }

            let xs: Vec<f64> = exterior.iter().map(|(x, _)| *x).collect();
            let ys: Vec<f64> = exterior.iter().map(|(_, y)| *y).collect();

            let min_x = xs.iter().cloned().fold(f64::INFINITY, f64::min);
            let max_x = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
            let min_y = ys.iter().cloned().fold(f64::INFINITY, f64::min);
            let max_y = ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);

            AABB::from_corners([min_x, min_y], [max_x, max_y])
        }
        _ => {
            return Err(ExecutorError::UnsupportedFeature(format!(
                "MBR computation for {} not yet supported",
                geometry.geometry_type()
            )))
        }
    };

    // For ST_DWithin, expand MBR by distance
    if let SpatialPredicate::DWithin { distance } = predicate {
        let lower = base_mbr.lower();
        let upper = base_mbr.upper();
        return Ok(AABB::from_corners(
            [lower[0] - distance, lower[1] - distance],
            [upper[0] + distance, upper[1] + distance],
        ));
    }

    Ok(base_mbr)
}

/// Apply two-phase filtering: R-tree query + full predicate evaluation
fn apply_two_phase_filtering(
    database: &Database,
    spatial_usage: &SpatialIndexUsage,
    all_rows: &[Row],
    schema: &CombinedSchema,
) -> Result<Option<Vec<Row>>, ExecutorError> {
    // Phase 1: R-tree MBR query (fast, approximate)
    let spatial_index = database.get_spatial_index(&spatial_usage.index_name);
    if spatial_index.is_none() {
        return Ok(None); // Index disappeared?
    }
    let spatial_index = spatial_index.unwrap();

    let candidate_row_ids = spatial_index.locate_in_envelope(&spatial_usage.query_mbr);

    // Convert row IDs to actual rows
    let candidate_rows: Vec<Row> = candidate_row_ids
        .iter()
        .filter_map(
            |&row_id| {
                if row_id < all_rows.len() {
                    Some(all_rows[row_id].clone())
                } else {
                    None
                }
            },
        )
        .collect();

    // Phase 2: Full spatial predicate evaluation (exact)
    let filtered_rows = apply_full_spatial_predicate(candidate_rows, spatial_usage, schema)?;

    Ok(Some(filtered_rows))
}

/// Apply full spatial predicate to filter false positives from R-tree results
fn apply_full_spatial_predicate(
    candidate_rows: Vec<Row>,
    spatial_usage: &SpatialIndexUsage,
    schema: &CombinedSchema,
) -> Result<Vec<Row>, ExecutorError> {
    // Find column index for the spatial column
    let column_index = find_column_index(&spatial_usage.column_name, schema)?;
    if column_index.is_none() {
        return Err(ExecutorError::Other(format!(
            "Column '{}' not found in schema",
            spatial_usage.column_name
        )));
    }
    let column_index = column_index.unwrap();

    // Import spatial predicate functions
    use crate::evaluator::functions::spatial::predicates::{
        st_contains, st_dwithin, st_intersects, st_within,
    };

    // Filter candidates by applying full predicate
    let mut filtered_rows = Vec::new();

    for row in candidate_rows {
        // Get geometry value from row
        let row_geometry_value = &row.values[column_index];

        // Skip NULL geometries
        if matches!(row_geometry_value, SqlValue::Null) {
            continue;
        }

        // Convert query geometry to SqlValue for predicate functions
        let query_geom_wkt =
            SqlValue::Varchar(arcstr::ArcStr::from(spatial_usage.query_geometry.to_wkt()));

        // Apply the appropriate spatial predicate
        let predicate_result = match &spatial_usage.predicate {
            SpatialPredicate::Contains => {
                st_contains(&[row_geometry_value.clone(), query_geom_wkt])?
            }
            SpatialPredicate::Intersects => {
                st_intersects(&[row_geometry_value.clone(), query_geom_wkt])?
            }
            SpatialPredicate::Within => st_within(&[row_geometry_value.clone(), query_geom_wkt])?,
            SpatialPredicate::DWithin { distance } => st_dwithin(&[
                row_geometry_value.clone(),
                query_geom_wkt,
                SqlValue::Float(*distance as f32),
            ])?,
        };

        // Include row if predicate is true
        if let SqlValue::Boolean(true) = predicate_result {
            filtered_rows.push(row);
        }
    }

    Ok(filtered_rows)
}

/// Find column index in combined schema
fn find_column_index(
    column_name: &str,
    schema: &CombinedSchema,
) -> Result<Option<usize>, ExecutorError> {
    for (start_idx, table_schema) in schema.table_schemas.values() {
        if let Some(col_idx) = table_schema.get_column_index(column_name) {
            return Ok(Some(start_idx + col_idx));
        }
    }
    Ok(None)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_compute_mbr_point() {
        let geom = Geometry::Point { x: 10.0, y: 20.0 };
        let mbr = compute_mbr(&geom, &SpatialPredicate::Contains).unwrap();
        assert_eq!(mbr.lower(), [10.0, 20.0]);
        assert_eq!(mbr.upper(), [10.0, 20.0]);
    }

    #[test]
    fn test_compute_mbr_polygon() {
        let geom = Geometry::Polygon {
            rings: vec![vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]],
        };
        let mbr = compute_mbr(&geom, &SpatialPredicate::Contains).unwrap();
        assert_eq!(mbr.lower(), [0.0, 0.0]);
        assert_eq!(mbr.upper(), [10.0, 10.0]);
    }

    #[test]
    fn test_compute_mbr_dwithin_expansion() {
        let geom = Geometry::Point { x: 50.0, y: 50.0 };
        let mbr = compute_mbr(&geom, &SpatialPredicate::DWithin { distance: 10.0 }).unwrap();
        assert_eq!(mbr.lower(), [40.0, 40.0]);
        assert_eq!(mbr.upper(), [60.0, 60.0]);
    }

    #[test]
    fn test_extract_geometry_from_wkt_literal() {
        let expr = Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("POINT(5 10)")));
        let geom = extract_geometry_from_expr(&expr).unwrap();
        assert!(geom.is_some());
        if let Some(Geometry::Point { x, y }) = geom {
            assert_eq!(x, 5.0);
            assert_eq!(y, 10.0);
        } else {
            panic!("Expected Point geometry");
        }
    }
}