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.insert_into", "DML", "INSERT INTO table SELECT …", S),
341    FeatureEntry::new(
342        "dml.insert_overwrite",
343        "DML",
344        "INSERT OVERWRITE (full partition replace)",
345        S,
346    ),
347    FeatureEntry::new("dml.delete", "DML", "DELETE FROM table WHERE …", P)
348        .with_note("supported on Iceberg tables; in-memory and Parquet tables require rewrite"),
349    FeatureEntry::new("dml.update", "DML", "UPDATE table SET col = … WHERE …", P)
350        .with_note("supported on Iceberg tables via MERGE rewrite"),
351    FeatureEntry::new(
352        "dml.merge",
353        "DML",
354        "MERGE INTO target USING source ON … WHEN MATCHED …",
355        S,
356    ),
357    FeatureEntry::new(
358        "dml.iceberg_merge",
359        "DML",
360        "Atomic Iceberg MERGE with row-level deletes",
361        S,
362    ),
363    // ── DDL ──────────────────────────────────────────────────────────────────
364    FeatureEntry::new(
365        "ddl.create_external_table",
366        "DDL",
367        "CREATE EXTERNAL TABLE … STORED AS …",
368        S,
369    ),
370    FeatureEntry::new("ddl.create_view", "DDL", "CREATE VIEW name AS SELECT …", S),
371    FeatureEntry::new(
372        "ddl.create_function",
373        "DDL",
374        "CREATE FUNCTION … LANGUAGE SQL|PYTHON",
375        S,
376    ),
377    FeatureEntry::new("ddl.drop_table", "DDL", "DROP TABLE [IF EXISTS]", S),
378    FeatureEntry::new("ddl.drop_view", "DDL", "DROP VIEW [IF EXISTS]", S),
379    FeatureEntry::new(
380        "ddl.create_table_as",
381        "DDL",
382        "CREATE TABLE … AS SELECT (CTAS)",
383        P,
384    )
385    .with_note("supported via INSERT OVERWRITE or external-table pattern"),
386    FeatureEntry::new(
387        "ddl.alter_table",
388        "DDL",
389        "ALTER TABLE ADD/DROP COLUMN, RENAME",
390        P,
391    )
392    .with_note("Iceberg schema evolution via ALTER TABLE is supported"),
393    // ── TEMPORAL ─────────────────────────────────────────────────────────────
394    FeatureEntry::new(
395        "temporal.as_of",
396        "TEMPORAL",
397        "AS OF TIMESTAMP point-in-time queries",
398        S,
399    ),
400    FeatureEntry::new(
401        "temporal.match_recognize",
402        "TEMPORAL",
403        "MATCH_RECOGNIZE pattern matching over ordered rows",
404        S,
405    ),
406    FeatureEntry::new(
407        "temporal.system_time",
408        "TEMPORAL",
409        "FOR SYSTEM_TIME AS OF (Iceberg time-travel)",
410        P,
411    )
412    .with_note("alias for AS OF on Iceberg tables"),
413    // ── PREPARED STATEMENTS ───────────────────────────────────────────────────
414    FeatureEntry::new(
415        "prepared.create",
416        "PREPARED",
417        "CREATE PREPARED STATEMENT via Flight SQL action",
418        S,
419    ),
420    FeatureEntry::new(
421        "prepared.execute",
422        "PREPARED",
423        "Execute prepared statement by handle",
424        S,
425    ),
426    FeatureEntry::new(
427        "prepared.close",
428        "PREPARED",
429        "CLOSE PREPARED STATEMENT to release server memory",
430        S,
431    ),
432    FeatureEntry::new(
433        "prepared.parameters",
434        "PREPARED",
435        "Positional parameter binding ($1, $2, …)",
436        S,
437    )
438    .with_note("local PreparedStatement::bind and Flight SQL DoPut parameter batches"),
439    // ── OPERATION CONTROL ────────────────────────────────────────────────────
440    FeatureEntry::new(
441        "operation.id",
442        "OPERATION",
443        "Operation IDs for query tracking",
444        S,
445    ),
446    FeatureEntry::new(
447        "operation.cancel",
448        "OPERATION",
449        "Cancel a running operation by ID",
450        S,
451    ),
452    FeatureEntry::new(
453        "operation.timeout",
454        "OPERATION",
455        "Per-query execution timeout",
456        S,
457    ),
458    FeatureEntry::new(
459        "operation.progress",
460        "OPERATION",
461        "Query progress reporting via QueryHandle",
462        S,
463    ),
464    // ── ERROR HANDLING ────────────────────────────────────────────────────────
465    FeatureEntry::new(
466        "error.sqlstate",
467        "ERROR",
468        "SQLSTATE codes on error responses",
469        S,
470    ),
471    FeatureEntry::new(
472        "error.error_position",
473        "ERROR",
474        "Source line/column in error messages",
475        P,
476    )
477    .with_note("DataFusion provides message but not structured position"),
478    // ── FLIGHT SQL ────────────────────────────────────────────────────────────
479    FeatureEntry::new(
480        "flight.get_flight_info",
481        "FLIGHT SQL",
482        "GetFlightInfo for statement execution",
483        S,
484    ),
485    FeatureEntry::new(
486        "flight.do_get",
487        "FLIGHT SQL",
488        "DoGet streaming result delivery",
489        S,
490    ),
491    FeatureEntry::new(
492        "flight.prepared_statements",
493        "FLIGHT SQL",
494        "Prepared statement create/execute/close",
495        S,
496    ),
497    FeatureEntry::new(
498        "flight.do_action",
499        "FLIGHT SQL",
500        "DoAction for custom Krishiv operations",
501        S,
502    ),
503    FeatureEntry::new(
504        "flight.get_sql_info",
505        "FLIGHT SQL",
506        "GetSqlInfo capability introspection",
507        S,
508    ),
509    FeatureEntry::new(
510        "flight.auth",
511        "FLIGHT SQL",
512        "Bearer token authentication",
513        S,
514    ),
515    FeatureEntry::new(
516        "flight.policy",
517        "FLIGHT SQL",
518        "Table-level access policy enforcement",
519        S,
520    ),
521    FeatureEntry::new(
522        "flight.transactions",
523        "FLIGHT SQL",
524        "BEGIN/COMMIT/ROLLBACK transactions",
525        P,
526    )
527    .with_note("Flight SQL BeginTransaction/EndTransaction actions; SQL BEGIN/COMMIT not routed"),
528    FeatureEntry::new(
529        "flight.schemas",
530        "FLIGHT SQL",
531        "GetDbSchemas / GetTables catalog introspection",
532        P,
533    )
534    .with_note("tables listed via Krishiv catalog; schema introspection via get_sql_info"),
535    // ── STREAMING SQL ─────────────────────────────────────────────────────────
536    FeatureEntry::new(
537        "streaming.continuous_select",
538        "STREAMING",
539        "Continuous SELECT over unbounded input",
540        S,
541    ),
542    FeatureEntry::new(
543        "streaming.window_agg",
544        "STREAMING",
545        "Windowed aggregations over streaming input",
546        S,
547    ),
548    FeatureEntry::new(
549        "streaming.watermark",
550        "STREAMING",
551        "Event-time watermarks for late-data handling",
552        S,
553    ),
554    FeatureEntry::new(
555        "streaming.interval_join",
556        "STREAMING",
557        "Streaming-to-streaming interval join",
558        S,
559    ),
560    FeatureEntry::new(
561        "streaming.cep",
562        "STREAMING",
563        "MATCH_RECOGNIZE CEP over streaming input",
564        S,
565    ),
566    FeatureEntry::new(
567        "streaming.dedup",
568        "STREAMING",
569        "Streaming deduplication (dropDuplicates)",
570        S,
571    ),
572    FeatureEntry::new(
573        "streaming.sink_modes",
574        "STREAMING",
575        "Append / Update / Complete output modes",
576        S,
577    ),
578    // ── INTROSPECTION ─────────────────────────────────────────────────────────
579    FeatureEntry::new(
580        "introspection.describe",
581        "INTROSPECTION",
582        "DESCRIBE / DESC / SHOW COLUMNS table schema",
583        S,
584    ),
585    FeatureEntry::new(
586        "introspection.explain",
587        "INTROSPECTION",
588        "EXPLAIN [LOGICAL|PHYSICAL|ANALYZE] query plans",
589        S,
590    ),
591    FeatureEntry::new(
592        "ddl.live_table",
593        "DDL",
594        "CREATE / REFRESH / DROP LIVE TABLE via session.sql()",
595        S,
596    ),
597];
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn feature_matrix_is_non_empty() {
605        assert!(!feature_matrix().is_empty());
606    }
607
608    #[test]
609    fn all_ids_are_unique() {
610        let ids: Vec<&str> = feature_matrix().iter().map(|e| e.id).collect();
611        let mut seen = std::collections::HashSet::new();
612        for id in &ids {
613            assert!(seen.insert(*id), "duplicate feature id: {id}");
614        }
615    }
616
617    #[test]
618    fn features_for_category_returns_subset() {
619        let join_features = features_for_category("JOIN");
620        assert!(!join_features.is_empty());
621        for f in &join_features {
622            assert!(f.category.to_uppercase().starts_with("JOIN"), "{}", f.id);
623        }
624    }
625
626    #[test]
627    fn features_by_status_supported_is_non_empty() {
628        let supported = features_by_status(FeatureStatus::Supported);
629        assert!(!supported.is_empty());
630    }
631
632    #[test]
633    fn feature_entry_display_includes_id_and_status() {
634        let entry = feature_matrix()
635            .iter()
636            .find(|e| e.id == "select.distinct")
637            .unwrap();
638        let s = entry.to_string();
639        assert!(s.contains("select.distinct"));
640        assert!(s.contains("supported"));
641    }
642
643    #[test]
644    fn feature_entry_display_with_note() {
645        let entry = feature_matrix().iter().find(|e| e.note.is_some()).unwrap();
646        let s = entry.to_string();
647        assert!(s.contains('('));
648    }
649
650    #[test]
651    fn feature_status_display() {
652        assert_eq!(FeatureStatus::Supported.to_string(), "supported");
653        assert_eq!(FeatureStatus::Partial.to_string(), "partial");
654        assert_eq!(FeatureStatus::Planned.to_string(), "planned");
655        assert_eq!(FeatureStatus::NotApplicable.to_string(), "n/a");
656    }
657
658    #[test]
659    fn flight_sql_features_present() {
660        let flight = features_for_category("FLIGHT");
661        assert!(flight.iter().any(|e| e.id == "flight.get_flight_info"));
662        assert!(flight.iter().any(|e| e.id == "flight.prepared_statements"));
663    }
664
665    #[test]
666    fn operation_features_present() {
667        let ops = features_for_category("OPERATION");
668        assert!(ops.iter().any(|e| e.id == "operation.cancel"));
669        assert!(ops.iter().any(|e| e.id == "operation.timeout"));
670    }
671}