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.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 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 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 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 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 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 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 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 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}