1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FeatureStatus {
12 Supported,
14 Partial,
16 Planned,
18 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#[derive(Debug, Clone)]
41pub struct FeatureEntry {
42 pub id: &'static str,
44 pub category: &'static str,
46 pub description: &'static str,
48 pub status: FeatureStatus,
50 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
86pub fn feature_matrix() -> &'static [FeatureEntry] {
90 FEATURES
91}
92
93pub 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
102pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}