Skip to main content

krishiv_sql/
grammar.rs

1#![forbid(unsafe_code)]
2//! SQL grammar and feature matrix for Krishiv.
3//!
4//! Provides a machine-readable inventory of which SQL dialect features are
5//! supported, partially supported, or planned.  Callers can query the matrix
6//! to build documentation, surface feature gaps, or validate queries before
7//! submission.
8
9/// Support status for a single SQL feature.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FeatureStatus {
12    /// Fully supported in the current release.
13    Supported,
14    /// Partially supported; the `note` field explains the gap.
15    Partial,
16    /// Planned for a future release.
17    Planned,
18    /// Not applicable to this engine.
19    NotApplicable,
20}
21
22impl FeatureStatus {
23    pub fn as_str(self) -> &'static str {
24        match self {
25            Self::Supported => "supported",
26            Self::Partial => "partial",
27            Self::Planned => "planned",
28            Self::NotApplicable => "n/a",
29        }
30    }
31}
32
33impl std::fmt::Display for FeatureStatus {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        f.write_str(self.as_str())
36    }
37}
38
39/// A single entry in the Krishiv SQL feature matrix.
40#[derive(Debug, Clone)]
41pub struct FeatureEntry {
42    /// Stable identifier (e.g. `"select.distinct"`).
43    pub id: &'static str,
44    /// Broad feature category (e.g. `"SELECT"`, `"JOIN"`, `"DML"`).
45    pub category: &'static str,
46    /// Human-readable description.
47    pub description: &'static str,
48    /// Support status.
49    pub status: FeatureStatus,
50    /// Optional clarifying note (gap description, limitations, workarounds).
51    pub note: Option<&'static str>,
52}
53
54impl FeatureEntry {
55    const fn new(
56        id: &'static str,
57        category: &'static str,
58        description: &'static str,
59        status: FeatureStatus,
60    ) -> Self {
61        Self {
62            id,
63            category,
64            description,
65            status,
66            note: None,
67        }
68    }
69
70    const fn with_note(mut self, note: &'static str) -> Self {
71        self.note = Some(note);
72        self
73    }
74}
75
76impl std::fmt::Display for FeatureEntry {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "[{}] {} — {}", self.status, self.id, self.description)?;
79        if let Some(note) = self.note {
80            write!(f, " ({})", note)?;
81        }
82        Ok(())
83    }
84}
85
86// ── Feature matrix ────────────────────────────────────────────────────────────
87
88/// Return the complete Krishiv SQL feature matrix.
89pub fn feature_matrix() -> &'static [FeatureEntry] {
90    FEATURES
91}
92
93/// Return only entries matching `category` (case-insensitive prefix match).
94pub fn features_for_category(category: &str) -> Vec<&'static FeatureEntry> {
95    let cat_upper = category.to_uppercase();
96    FEATURES
97        .iter()
98        .filter(|e| e.category.to_uppercase().starts_with(&cat_upper))
99        .collect()
100}
101
102/// Return only entries with the given `status`.
103pub fn features_by_status(status: FeatureStatus) -> Vec<&'static FeatureEntry> {
104    FEATURES.iter().filter(|e| e.status == status).collect()
105}
106
107const S: FeatureStatus = FeatureStatus::Supported;
108const P: FeatureStatus = FeatureStatus::Partial;
109
110static FEATURES: &[FeatureEntry] = &[
111    // ── SELECT ────────────────────────────────────────────────────────────────
112    FeatureEntry::new(
113        "select.projection",
114        "SELECT",
115        "Column projection and aliases",
116        S,
117    ),
118    FeatureEntry::new("select.star", "SELECT", "SELECT * expansion", S),
119    FeatureEntry::new(
120        "select.distinct",
121        "SELECT",
122        "SELECT DISTINCT deduplication",
123        S,
124    ),
125    FeatureEntry::new("select.where", "SELECT", "WHERE predicate filtering", S),
126    FeatureEntry::new(
127        "select.order_by",
128        "SELECT",
129        "ORDER BY with ASC/DESC and NULLS FIRST/LAST",
130        S,
131    ),
132    FeatureEntry::new(
133        "select.limit_offset",
134        "SELECT",
135        "LIMIT / OFFSET pagination",
136        S,
137    ),
138    FeatureEntry::new(
139        "select.having",
140        "SELECT",
141        "HAVING post-aggregation filter",
142        S,
143    ),
144    FeatureEntry::new(
145        "select.case",
146        "SELECT",
147        "CASE WHEN … THEN … ELSE … END expressions",
148        S,
149    ),
150    FeatureEntry::new(
151        "select.cast",
152        "SELECT",
153        "CAST(expr AS type) and TRY_CAST",
154        S,
155    ),
156    FeatureEntry::new(
157        "select.subquery_scalar",
158        "SELECT",
159        "Scalar subqueries in projection/predicate",
160        S,
161    ),
162    FeatureEntry::new(
163        "select.subquery_exists",
164        "SELECT",
165        "EXISTS / NOT EXISTS correlated subqueries",
166        S,
167    ),
168    FeatureEntry::new("select.subquery_in", "SELECT", "IN / NOT IN subqueries", S),
169    FeatureEntry::new(
170        "select.values",
171        "SELECT",
172        "VALUES clause for inline data",
173        S,
174    ),
175    // ── GROUP BY ─────────────────────────────────────────────────────────────
176    FeatureEntry::new("groupby.basic", "GROUP BY", "Basic GROUP BY column list", S),
177    FeatureEntry::new("groupby.rollup", "GROUP BY", "ROLLUP grouping sets", S),
178    FeatureEntry::new("groupby.cube", "GROUP BY", "CUBE grouping sets", S),
179    FeatureEntry::new(
180        "groupby.grouping_sets",
181        "GROUP BY",
182        "Explicit GROUPING SETS",
183        S,
184    ),
185    FeatureEntry::new(
186        "groupby.grouping_function",
187        "GROUP BY",
188        "GROUPING() function for NULL disambiguation",
189        S,
190    ),
191    // ── JOIN ─────────────────────────────────────────────────────────────────
192    FeatureEntry::new("join.inner", "JOIN", "INNER JOIN (equi and non-equi)", S),
193    FeatureEntry::new("join.left_outer", "JOIN", "LEFT OUTER JOIN", S),
194    FeatureEntry::new("join.right_outer", "JOIN", "RIGHT OUTER JOIN", S),
195    FeatureEntry::new("join.full_outer", "JOIN", "FULL OUTER JOIN", S),
196    FeatureEntry::new("join.cross", "JOIN", "CROSS JOIN", S),
197    FeatureEntry::new(
198        "join.natural",
199        "JOIN",
200        "NATURAL JOIN (column-name matching)",
201        S,
202    ),
203    FeatureEntry::new("join.using", "JOIN", "JOIN … USING (column_list)", S),
204    FeatureEntry::new(
205        "join.lateral",
206        "JOIN",
207        "LATERAL JOIN / CROSS JOIN LATERAL",
208        S,
209    ),
210    FeatureEntry::new(
211        "join.interval",
212        "JOIN",
213        "Streaming interval join on event-time bounds",
214        S,
215    ),
216    FeatureEntry::new(
217        "join.temporal_as_of",
218        "JOIN",
219        "Temporal AS OF point-in-time join",
220        S,
221    ),
222    FeatureEntry::new(
223        "join.broadcast_hint",
224        "JOIN",
225        "/*+ BROADCAST(t) */ optimizer hint",
226        P,
227    )
228    .with_note("hint parsed; broadcast decision is cost-based, not forced"),
229    // ── WINDOW FUNCTIONS ─────────────────────────────────────────────────────
230    FeatureEntry::new(
231        "window.over",
232        "WINDOW",
233        "OVER () window function clauses",
234        S,
235    ),
236    FeatureEntry::new(
237        "window.partition_by",
238        "WINDOW",
239        "PARTITION BY inside OVER",
240        S,
241    ),
242    FeatureEntry::new("window.order_by", "WINDOW", "ORDER BY inside OVER", S),
243    FeatureEntry::new(
244        "window.rows_range",
245        "WINDOW",
246        "ROWS / RANGE frame specification",
247        S,
248    ),
249    FeatureEntry::new(
250        "window.rank_dense_rank",
251        "WINDOW",
252        "RANK(), DENSE_RANK(), ROW_NUMBER()",
253        S,
254    ),
255    FeatureEntry::new("window.lead_lag", "WINDOW", "LEAD() and LAG()", S),
256    FeatureEntry::new(
257        "window.first_last_value",
258        "WINDOW",
259        "FIRST_VALUE() and LAST_VALUE()",
260        S,
261    ),
262    FeatureEntry::new("window.nth_value", "WINDOW", "NTH_VALUE()", S),
263    FeatureEntry::new("window.ntile", "WINDOW", "NTILE(n)", S),
264    FeatureEntry::new(
265        "window.cume_dist_percent",
266        "WINDOW",
267        "CUME_DIST() and PERCENT_RANK()",
268        S,
269    ),
270    FeatureEntry::new(
271        "window.tumble",
272        "WINDOW",
273        "TUMBLE(col, interval) streaming window",
274        S,
275    ),
276    FeatureEntry::new(
277        "window.hop",
278        "WINDOW",
279        "HOP(col, slide, size) sliding window",
280        S,
281    ),
282    FeatureEntry::new(
283        "window.session",
284        "WINDOW",
285        "Session window on inactivity gap",
286        S,
287    ),
288    // ── CTE ──────────────────────────────────────────────────────────────────
289    FeatureEntry::new(
290        "cte.non_recursive",
291        "CTE",
292        "WITH … AS (…) non-recursive CTEs",
293        S,
294    ),
295    FeatureEntry::new(
296        "cte.recursive",
297        "CTE",
298        "WITH RECURSIVE … (UNION ALL base + recursive)",
299        S,
300    ),
301    FeatureEntry::new("cte.multiple", "CTE", "Multiple CTEs in one WITH clause", S),
302    // ── SET OPERATIONS ────────────────────────────────────────────────────────
303    FeatureEntry::new("set.union_all", "SET", "UNION ALL", S),
304    FeatureEntry::new("set.union_distinct", "SET", "UNION (DISTINCT)", S),
305    FeatureEntry::new("set.intersect", "SET", "INTERSECT", S),
306    FeatureEntry::new("set.except", "SET", "EXCEPT", S),
307    // ── LATERAL / UNNEST ─────────────────────────────────────────────────────
308    FeatureEntry::new(
309        "lateral.unnest",
310        "LATERAL",
311        "UNNEST(array_col) in FROM clause",
312        S,
313    ),
314    FeatureEntry::new(
315        "lateral.generate_series",
316        "LATERAL",
317        "generate_series() table function",
318        S,
319    ),
320    FeatureEntry::new(
321        "lateral.cross_join_unnest",
322        "LATERAL",
323        "CROSS JOIN UNNEST(…) AS t(col)",
324        S,
325    ),
326    // ── PIVOT / UNPIVOT ───────────────────────────────────────────────────────
327    FeatureEntry::new(
328        "pivot.pivot",
329        "PIVOT",
330        "PIVOT(agg FOR col IN (v1, v2, …))",
331        S,
332    ),
333    FeatureEntry::new(
334        "pivot.unpivot",
335        "PIVOT",
336        "UNPIVOT(value FOR col IN (c1, c2, …))",
337        S,
338    ),
339    // ── DML ──────────────────────────────────────────────────────────────────
340    FeatureEntry::new("dml.copy_to", "DML", "COPY (query) TO 'path' (FORMAT …)", S).with_note(
341        "inherited from DataFusion's native parser/planner; no Krishiv-side code involved",
342    ),
343    FeatureEntry::new("dml.insert_into", "DML", "INSERT INTO table SELECT …", S),
344    FeatureEntry::new(
345        "dml.insert_overwrite",
346        "DML",
347        "INSERT OVERWRITE (full partition replace)",
348        S,
349    ),
350    FeatureEntry::new("dml.delete", "DML", "DELETE FROM table WHERE …", P)
351        .with_note("supported on Iceberg tables; in-memory and Parquet tables require rewrite"),
352    FeatureEntry::new("dml.update", "DML", "UPDATE table SET col = … WHERE …", P)
353        .with_note("supported on Iceberg tables via MERGE rewrite"),
354    FeatureEntry::new(
355        "dml.merge",
356        "DML",
357        "MERGE INTO target USING source ON … WHEN MATCHED …",
358        S,
359    ),
360    FeatureEntry::new(
361        "dml.iceberg_merge",
362        "DML",
363        "Atomic Iceberg MERGE with row-level deletes",
364        S,
365    ),
366    // ── DDL ──────────────────────────────────────────────────────────────────
367    FeatureEntry::new(
368        "ddl.create_external_table",
369        "DDL",
370        "CREATE EXTERNAL TABLE … STORED AS …",
371        S,
372    ),
373    FeatureEntry::new("ddl.create_view", "DDL", "CREATE VIEW name AS SELECT …", S),
374    FeatureEntry::new(
375        "ddl.create_function",
376        "DDL",
377        "CREATE FUNCTION … LANGUAGE SQL|PYTHON",
378        S,
379    ),
380    FeatureEntry::new("ddl.drop_table", "DDL", "DROP TABLE [IF EXISTS]", S),
381    FeatureEntry::new("ddl.drop_view", "DDL", "DROP VIEW [IF EXISTS]", S),
382    FeatureEntry::new(
383        "ddl.create_table_as",
384        "DDL",
385        "CREATE TABLE … AS SELECT (CTAS)",
386        S,
387    )
388    .with_note("durable Iceberg landing when the target resolves to a registered Iceberg catalog; session table otherwise"),
389    FeatureEntry::new(
390        "ddl.partitioned_by",
391        "DDL",
392        "CREATE TABLE … PARTITIONED BY (col | bucket(n, col) | truncate(w, col) | year/month/day/hour(col)) AS SELECT",
393        S,
394    )
395    .with_note("Iceberg catalog tables only; transforms follow the Iceberg partition spec"),
396    FeatureEntry::new(
397        "ddl.alter_table",
398        "DDL",
399        "ALTER TABLE ADD/DROP COLUMN, RENAME",
400        P,
401    )
402    .with_note("Iceberg schema evolution via ALTER TABLE is supported"),
403    FeatureEntry::new("ddl.create_schema", "DDL", "CREATE SCHEMA name", S)
404        .with_note("inherited from DataFusion's native catalog; no Krishiv-side code involved"),
405    // ── TEMPORAL ─────────────────────────────────────────────────────────────
406    FeatureEntry::new(
407        "temporal.as_of",
408        "TEMPORAL",
409        "AS OF TIMESTAMP point-in-time queries",
410        S,
411    ),
412    FeatureEntry::new(
413        "temporal.match_recognize",
414        "TEMPORAL",
415        "MATCH_RECOGNIZE pattern matching over ordered rows",
416        P,
417    )
418    .with_note(
419        "streaming CEP subset only: PARTITION BY / ORDER BY / PATTERN (…) / WITHIN <duration>; \
420         no DEFINE (pattern-variable predicates) or MEASURES (computed output) clauses, unlike \
421         Oracle/Flink's full MATCH_RECOGNIZE grammar",
422    ),
423    FeatureEntry::new(
424        "temporal.system_time",
425        "TEMPORAL",
426        "FOR SYSTEM_TIME AS OF (Iceberg time-travel)",
427        P,
428    )
429    .with_note("alias for AS OF on Iceberg tables"),
430    // ── PREPARED STATEMENTS ───────────────────────────────────────────────────
431    FeatureEntry::new(
432        "prepared.create",
433        "PREPARED",
434        "CREATE PREPARED STATEMENT via Flight SQL action",
435        S,
436    ),
437    FeatureEntry::new(
438        "prepared.execute",
439        "PREPARED",
440        "Execute prepared statement by handle",
441        S,
442    ),
443    FeatureEntry::new(
444        "prepared.close",
445        "PREPARED",
446        "CLOSE PREPARED STATEMENT to release server memory",
447        S,
448    ),
449    FeatureEntry::new(
450        "prepared.parameters",
451        "PREPARED",
452        "Positional parameter binding ($1, $2, …)",
453        S,
454    )
455    .with_note("local PreparedStatement::bind and Flight SQL DoPut parameter batches"),
456    FeatureEntry::new(
457        "prepared.sql_text",
458        "PREPARED",
459        "PREPARE name AS …; EXECUTE name(…); DEALLOCATE name",
460        S,
461    )
462    .with_note(
463        "inherited from DataFusion's native parser/planner (session-scoped named plans); \
464         distinct from the Flight SQL protocol-level prepared statement actions above",
465    ),
466    // ── OPERATION CONTROL ────────────────────────────────────────────────────
467    FeatureEntry::new(
468        "operation.id",
469        "OPERATION",
470        "Operation IDs for query tracking",
471        S,
472    ),
473    FeatureEntry::new(
474        "operation.cancel",
475        "OPERATION",
476        "Cancel a running operation by ID",
477        S,
478    ),
479    FeatureEntry::new(
480        "operation.timeout",
481        "OPERATION",
482        "Per-query execution timeout",
483        S,
484    ),
485    FeatureEntry::new(
486        "operation.progress",
487        "OPERATION",
488        "Query progress reporting via QueryHandle",
489        S,
490    ),
491    // ── ERROR HANDLING ────────────────────────────────────────────────────────
492    FeatureEntry::new(
493        "error.sqlstate",
494        "ERROR",
495        "SQLSTATE codes on error responses",
496        S,
497    ),
498    FeatureEntry::new(
499        "error.error_position",
500        "ERROR",
501        "Source line/column in error messages",
502        P,
503    )
504    .with_note("DataFusion provides message but not structured position"),
505    // ── FLIGHT SQL ────────────────────────────────────────────────────────────
506    FeatureEntry::new(
507        "flight.get_flight_info",
508        "FLIGHT SQL",
509        "GetFlightInfo for statement execution",
510        S,
511    ),
512    FeatureEntry::new(
513        "flight.do_get",
514        "FLIGHT SQL",
515        "DoGet streaming result delivery",
516        S,
517    ),
518    FeatureEntry::new(
519        "flight.prepared_statements",
520        "FLIGHT SQL",
521        "Prepared statement create/execute/close",
522        S,
523    ),
524    FeatureEntry::new(
525        "flight.do_action",
526        "FLIGHT SQL",
527        "DoAction for custom Krishiv operations",
528        S,
529    ),
530    FeatureEntry::new(
531        "flight.get_sql_info",
532        "FLIGHT SQL",
533        "GetSqlInfo capability introspection",
534        S,
535    ),
536    FeatureEntry::new(
537        "flight.auth",
538        "FLIGHT SQL",
539        "Bearer token authentication",
540        S,
541    ),
542    FeatureEntry::new(
543        "flight.policy",
544        "FLIGHT SQL",
545        "Table-level access policy enforcement",
546        S,
547    ),
548    FeatureEntry::new(
549        "flight.transactions",
550        "FLIGHT SQL",
551        "BEGIN/COMMIT/ROLLBACK transactions",
552        P,
553    )
554    .with_note("Flight SQL BeginTransaction/EndTransaction actions; SQL BEGIN/COMMIT not routed"),
555    FeatureEntry::new(
556        "flight.schemas",
557        "FLIGHT SQL",
558        "GetDbSchemas / GetTables catalog introspection",
559        P,
560    )
561    .with_note("tables listed via Krishiv catalog; schema introspection via get_sql_info"),
562    // ── STREAMING SQL ─────────────────────────────────────────────────────────
563    FeatureEntry::new(
564        "streaming.continuous_select",
565        "STREAMING",
566        "Continuous SELECT over unbounded input",
567        S,
568    ),
569    FeatureEntry::new(
570        "streaming.window_agg",
571        "STREAMING",
572        "Windowed aggregations over streaming input",
573        S,
574    ),
575    FeatureEntry::new(
576        "streaming.watermark",
577        "STREAMING",
578        "Event-time watermarks for late-data handling",
579        S,
580    ),
581    FeatureEntry::new(
582        "streaming.interval_join",
583        "STREAMING",
584        "Streaming-to-streaming interval join",
585        S,
586    ),
587    FeatureEntry::new(
588        "streaming.cep",
589        "STREAMING",
590        "MATCH_RECOGNIZE CEP over streaming input",
591        S,
592    ),
593    FeatureEntry::new(
594        "streaming.dedup",
595        "STREAMING",
596        "Streaming deduplication (dropDuplicates)",
597        S,
598    ),
599    FeatureEntry::new(
600        "streaming.sink_modes",
601        "STREAMING",
602        "Append / Update / Complete output modes",
603        S,
604    ),
605    // ── INTROSPECTION ─────────────────────────────────────────────────────────
606    FeatureEntry::new(
607        "introspection.describe",
608        "INTROSPECTION",
609        "DESCRIBE / DESC / SHOW COLUMNS table schema",
610        S,
611    ),
612    FeatureEntry::new(
613        "introspection.explain",
614        "INTROSPECTION",
615        "EXPLAIN [LOGICAL|PHYSICAL|ANALYZE] query plans",
616        S,
617    ),
618    FeatureEntry::new(
619        "introspection.information_schema",
620        "INTROSPECTION",
621        "information_schema.{tables,columns,views,df_settings,routines,parameters,schemata}",
622        S,
623    ),
624    FeatureEntry::new(
625        "ddl.live_table",
626        "DDL",
627        "CREATE / REFRESH / DROP LIVE TABLE via session.sql()",
628        S,
629    ),
630];
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    #[test]
637    fn feature_matrix_is_non_empty() {
638        assert!(!feature_matrix().is_empty());
639    }
640
641    #[test]
642    fn all_ids_are_unique() {
643        let ids: Vec<&str> = feature_matrix().iter().map(|e| e.id).collect();
644        let mut seen = std::collections::HashSet::new();
645        for id in &ids {
646            assert!(seen.insert(*id), "duplicate feature id: {id}");
647        }
648    }
649
650    #[test]
651    fn features_for_category_returns_subset() {
652        let join_features = features_for_category("JOIN");
653        assert!(!join_features.is_empty());
654        for f in &join_features {
655            assert!(f.category.to_uppercase().starts_with("JOIN"), "{}", f.id);
656        }
657    }
658
659    #[test]
660    fn features_by_status_supported_is_non_empty() {
661        let supported = features_by_status(FeatureStatus::Supported);
662        assert!(!supported.is_empty());
663    }
664
665    #[test]
666    fn feature_entry_display_includes_id_and_status() {
667        let entry = feature_matrix()
668            .iter()
669            .find(|e| e.id == "select.distinct")
670            .unwrap();
671        let s = entry.to_string();
672        assert!(s.contains("select.distinct"));
673        assert!(s.contains("supported"));
674    }
675
676    #[test]
677    fn feature_entry_display_with_note() {
678        let entry = feature_matrix().iter().find(|e| e.note.is_some()).unwrap();
679        let s = entry.to_string();
680        assert!(s.contains('('));
681    }
682
683    #[test]
684    fn feature_status_display() {
685        assert_eq!(FeatureStatus::Supported.to_string(), "supported");
686        assert_eq!(FeatureStatus::Partial.to_string(), "partial");
687        assert_eq!(FeatureStatus::Planned.to_string(), "planned");
688        assert_eq!(FeatureStatus::NotApplicable.to_string(), "n/a");
689    }
690
691    #[test]
692    fn flight_sql_features_present() {
693        let flight = features_for_category("FLIGHT");
694        assert!(flight.iter().any(|e| e.id == "flight.get_flight_info"));
695        assert!(flight.iter().any(|e| e.id == "flight.prepared_statements"));
696    }
697
698    #[test]
699    fn operation_features_present() {
700        let ops = features_for_category("OPERATION");
701        assert!(ops.iter().any(|e| e.id == "operation.cancel"));
702        assert!(ops.iter().any(|e| e.id == "operation.timeout"));
703    }
704}