1#![forbid(unsafe_code)]
2#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum FeatureStatus {
21 Supported,
23 Partial,
25 Planned,
27 NotApplicable,
29}
30
31impl FeatureStatus {
32 pub fn as_str(self) -> &'static str {
33 match self {
34 Self::Supported => "supported",
35 Self::Partial => "partial",
36 Self::Planned => "planned",
37 Self::NotApplicable => "n/a",
38 }
39 }
40
41 pub fn is_claimed(self) -> bool {
44 matches!(self, Self::Supported | Self::Partial)
45 }
46}
47
48impl std::fmt::Display for FeatureStatus {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 f.write_str(self.as_str())
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Engine {
57 Batch,
58 Streaming,
59 Incremental,
60}
61
62impl Engine {
63 pub const ALL: [Engine; 3] = [Engine::Batch, Engine::Streaming, Engine::Incremental];
64
65 pub fn as_str(self) -> &'static str {
66 match self {
67 Engine::Batch => "batch",
68 Engine::Streaming => "streaming",
69 Engine::Incremental => "incremental",
70 }
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Placement {
82 EmbeddedApi,
84 Sql,
86 Distributed,
88}
89
90impl Placement {
91 pub fn as_str(self) -> &'static str {
92 match self {
93 Self::EmbeddedApi => "embedded API",
94 Self::Sql => "SQL front door",
95 Self::Distributed => "distributed runtime",
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct FeatureEntry {
103 pub id: &'static str,
105 pub category: &'static str,
107 pub description: &'static str,
109 pub batch: FeatureStatus,
111 pub streaming: FeatureStatus,
113 pub incremental: FeatureStatus,
115 pub note: Option<&'static str>,
117 pub placement: Option<&'static [Placement]>,
122}
123
124impl FeatureEntry {
125 const fn new(
126 id: &'static str,
127 category: &'static str,
128 description: &'static str,
129 batch: FeatureStatus,
130 streaming: FeatureStatus,
131 incremental: FeatureStatus,
132 ) -> Self {
133 Self {
134 id,
135 category,
136 description,
137 batch,
138 streaming,
139 incremental,
140 note: None,
141 placement: None,
142 }
143 }
144
145 const fn batch_only(
147 id: &'static str,
148 category: &'static str,
149 description: &'static str,
150 batch: FeatureStatus,
151 ) -> Self {
152 Self::new(id, category, description, batch, NA, NA)
153 }
154
155 const fn with_note(mut self, note: &'static str) -> Self {
156 self.note = Some(note);
157 self
158 }
159
160 const fn placed(mut self, placement: &'static [Placement]) -> Self {
162 self.placement = Some(placement);
163 self
164 }
165
166 pub fn placement_restricted(&self) -> bool {
170 matches!(self.placement, Some(p) if p.len() < 3)
171 }
172
173 pub fn status_for(&self, engine: Engine) -> FeatureStatus {
175 match engine {
176 Engine::Batch => self.batch,
177 Engine::Streaming => self.streaming,
178 Engine::Incremental => self.incremental,
179 }
180 }
181}
182
183impl std::fmt::Display for FeatureEntry {
184 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
185 write!(
186 f,
187 "[batch:{} streaming:{} incremental:{}] {} — {}",
188 self.batch, self.streaming, self.incremental, self.id, self.description
189 )?;
190 if let Some(note) = self.note {
191 write!(f, " ({note})")?;
192 }
193 Ok(())
194 }
195}
196
197pub fn feature_matrix() -> &'static [FeatureEntry] {
201 FEATURES
202}
203
204pub fn features_for_category(category: &str) -> Vec<&'static FeatureEntry> {
206 let cat_upper = category.to_uppercase();
207 FEATURES
208 .iter()
209 .filter(|e| e.category.to_uppercase().starts_with(&cat_upper))
210 .collect()
211}
212
213pub fn features_by_status(status: FeatureStatus) -> Vec<&'static FeatureEntry> {
216 FEATURES.iter().filter(|e| e.batch == status).collect()
217}
218
219pub fn generate_reference_markdown() -> String {
223 let mut out = String::new();
224 out.push_str("# Krishiv SQL feature matrix\n\n");
225 out.push_str(
226 "_Generated from `krishiv-sql/src/grammar.rs` — do not edit by hand._\n\n\
227 Each feature is dimensioned across the three Krishiv execution engines: \
228 **batch** (DataFusion + extensions), **streaming** (continuous windows), \
229 and **incremental** (IVM). `n/a` means the feature does not apply to that \
230 engine.\n\n",
231 );
232
233 let mut categories: Vec<&'static str> = Vec::new();
235 for e in FEATURES {
236 if !categories.contains(&e.category) {
237 categories.push(e.category);
238 }
239 }
240
241 for cat in categories {
242 out.push_str(&format!("## {cat}\n\n"));
243 out.push_str("| Feature | Description | Batch | Streaming | Incremental | Notes |\n");
244 out.push_str("|---|---|---|---|---|---|\n");
245 for e in FEATURES.iter().filter(|e| e.category == cat) {
246 let mut notes = String::new();
247 if let Some(placement) = e.placement {
248 let surfaces: Vec<&str> = placement.iter().map(|p| p.as_str()).collect();
249 notes.push_str(&format!("**placement: {} only.** ", surfaces.join(" + ")));
250 }
251 notes.push_str(e.note.unwrap_or(""));
252 out.push_str(&format!(
253 "| `{}` | {} | {} | {} | {} | {} |\n",
254 e.id,
255 e.description,
256 e.batch,
257 e.streaming,
258 e.incremental,
259 notes.trim_end()
260 ));
261 }
262 out.push('\n');
263 }
264
265 out.push_str("## Embedded-API-only streaming operators\n\n");
271 out.push_str(
272 "These operators are real and tested but reachable **only** from the embedded \
273 Rust `StreamingDataFrame`/process API and its Python mirror — they are not \
274 compiled from SQL, and the distributed `stream:loop` runtime executes only \
275 `WindowExecutionSpec` shapes (windows, window-join, CEP). A job built on them \
276 cannot run distributed today.\n\n",
277 );
278 for (name, desc) in EMBEDDED_ONLY_OPERATORS {
279 out.push_str(&format!("- **{name}** — {desc}\n"));
280 }
281 out.push('\n');
282 out
283}
284
285pub const EMBEDDED_ONLY_OPERATORS: &[(&str, &str)] = &[
290 (
291 "temporal join",
292 "event-time temporal (versioned lookup) join between two streams",
293 ),
294 (
295 "side outputs",
296 "route late/rejected/tagged rows to a secondary stream",
297 ),
298 (
299 "broadcast state",
300 "low-volume control stream broadcast to all tasks of a keyed stream",
301 ),
302 (
303 "connected streams",
304 "two-input operators sharing state across both inputs",
305 ),
306 (
307 "ProcessFunction + timers",
308 "per-key user logic with registered event/processing-time timers",
309 ),
310];
311
312pub fn generate_honesty_markdown() -> String {
316 let mut out = String::new();
317 out.push_str("# Krishiv SQL vs Spark SQL\n\n");
318 out.push_str(
319 "_Generated from `krishiv-sql/src/grammar.rs` — do not edit by hand._\n\n\
320 Krishiv targets Spark-SQL reference parity as a **measured** number. This page \
321 is the honest ledger: what maps 1:1, what differs semantically, and what is \
322 absent, derived from the feature matrix.\n\n",
323 );
324
325 out.push_str("## Documented semantic differences\n\n");
328 out.push_str(
329 "- **`date_format(ts, fmt)` pattern letters.** Krishiv uses **Spark/Java** \
330 `DateTimeFormatter` letters (`yyyy-MM-dd`), not chrono/strftime (`%Y-%m-%d`). \
331 Supported letters translate exactly; unsupported letters (era `G`, timezone \
332 `z`/`X`) raise a clear error instead of emitting wrong output.\n\
333 - **`exists(array, x -> …)`.** The `exists(` spelling is shadowed by the \
334 EXISTS-subquery keyword in the parser; use `any_match(array, x -> …)` (the \
335 byte-identical implementation) for the Spark higher-order `exists`.\n\
336 - **Lambda / array-literal syntax.** The SQL front door parses with a \
337 lambda-capable dialect so `transform(arr, x -> …)` and `[1, 2, 3]` work; \
338 the array constructor is `make_array(...)` / `[...]` (Spark's `array(...)` \
339 maps to these).\n\
340 - **ANSI mode, integral division, NULL ordering** follow DataFusion \
341 semantics, which match Spark ANSI mode for the covered surface; divergences \
342 are tracked as matrix notes.\n\n",
343 );
344
345 out.push_str("## Maps 1:1 (supported, no semantic caveat)\n\n");
347 for e in FEATURES
348 .iter()
349 .filter(|e| e.batch == FeatureStatus::Supported && e.note.is_none())
350 {
351 out.push_str(&format!("- `{}` — {}\n", e.id, e.description));
352 }
353 out.push('\n');
354
355 out.push_str("## Supported with caveats (partial or noted)\n\n");
357 for e in FEATURES.iter().filter(|e| {
358 (e.batch == FeatureStatus::Partial) || (e.batch.is_claimed() && e.note.is_some())
359 }) {
360 out.push_str(&format!(
361 "- `{}` — {} _({})_\n",
362 e.id,
363 e.description,
364 e.note.unwrap_or("partial")
365 ));
366 }
367 out.push('\n');
368
369 out.push_str("## Absent (planned — itemized shortfall)\n\n");
371 for e in FEATURES.iter().filter(|e| {
372 e.batch == FeatureStatus::Planned
373 && e.streaming != FeatureStatus::Supported
374 && e.incremental != FeatureStatus::Supported
375 }) {
376 out.push_str(&format!(
377 "- `{}` — {} _({})_\n",
378 e.id,
379 e.description,
380 e.note.unwrap_or("planned")
381 ));
382 }
383 out.push('\n');
384 out
385}
386
387const S: FeatureStatus = FeatureStatus::Supported;
388const P: FeatureStatus = FeatureStatus::Partial;
389const PL: FeatureStatus = FeatureStatus::Planned;
390const NA: FeatureStatus = FeatureStatus::NotApplicable;
391
392static FEATURES: &[FeatureEntry] = &[
393 FeatureEntry::new("select.projection", "SELECT", "Column projection and aliases", S, P, P),
398 FeatureEntry::new("select.star", "SELECT", "SELECT * expansion", S, P, P),
399 FeatureEntry::new("select.distinct", "SELECT", "SELECT DISTINCT deduplication", S, NA, P),
400 FeatureEntry::new("select.where", "SELECT", "WHERE predicate filtering", S, P, P),
401 FeatureEntry::new(
402 "select.order_by",
403 "SELECT",
404 "ORDER BY with ASC/DESC and NULLS FIRST/LAST",
405 S,
406 NA,
407 NA,
408 )
409 .with_note("streaming/IVM: unbounded ordering is not a maintainable operator"),
410 FeatureEntry::new("select.limit_offset", "SELECT", "LIMIT / OFFSET pagination", S, NA, NA),
411 FeatureEntry::new("select.having", "SELECT", "HAVING post-aggregation filter", S, P, P),
412 FeatureEntry::new(
413 "select.case",
414 "SELECT",
415 "CASE WHEN … THEN … ELSE … END expressions",
416 S,
417 P,
418 P,
419 ),
420 FeatureEntry::new("select.cast", "SELECT", "CAST(expr AS type) and TRY_CAST", S, P, P),
421 FeatureEntry::new(
422 "select.subquery_scalar",
423 "SELECT",
424 "Scalar subqueries in projection/predicate",
425 S,
426 NA,
427 P,
428 ),
429 FeatureEntry::new(
430 "select.subquery_exists",
431 "SELECT",
432 "EXISTS / NOT EXISTS correlated subqueries",
433 S,
434 NA,
435 P,
436 ),
437 FeatureEntry::new("select.subquery_in", "SELECT", "IN / NOT IN subqueries", S, NA, P),
438 FeatureEntry::new("select.values", "SELECT", "VALUES clause for inline data", S, NA, P),
439 FeatureEntry::new("groupby.basic", "GROUP BY", "Basic GROUP BY column list", S, P, P)
441 .with_note("streaming: only inside a window TVF (windowed aggregation)"),
442 FeatureEntry::new("groupby.rollup", "GROUP BY", "ROLLUP grouping sets", S, NA, P),
443 FeatureEntry::new("groupby.cube", "GROUP BY", "CUBE grouping sets", S, NA, P),
444 FeatureEntry::new("groupby.grouping_sets", "GROUP BY", "Explicit GROUPING SETS", S, NA, P),
445 FeatureEntry::new(
446 "groupby.grouping_function",
447 "GROUP BY",
448 "GROUPING() function for NULL disambiguation",
449 S,
450 NA,
451 P,
452 ),
453 FeatureEntry::new("join.inner", "JOIN", "INNER JOIN (equi and non-equi)", S, NA, P),
455 FeatureEntry::new("join.left_outer", "JOIN", "LEFT OUTER JOIN", S, NA, P),
456 FeatureEntry::new("join.right_outer", "JOIN", "RIGHT OUTER JOIN", S, NA, P),
457 FeatureEntry::new("join.full_outer", "JOIN", "FULL OUTER JOIN", S, NA, P),
458 FeatureEntry::new("join.cross", "JOIN", "CROSS JOIN", S, NA, P),
459 FeatureEntry::new("join.natural", "JOIN", "NATURAL JOIN (column-name matching)", S, NA, P),
460 FeatureEntry::new("join.using", "JOIN", "JOIN … USING (column_list)", S, NA, P),
461 FeatureEntry::new("join.lateral", "JOIN", "LATERAL JOIN / CROSS JOIN LATERAL", S, NA, NA),
462 FeatureEntry::new(
463 "join.interval",
464 "JOIN",
465 "Streaming interval join on event-time bounds",
466 PL,
467 P,
468 NA,
469 )
470 .with_note(
471 "DataFrame-only today (audit §9b): the interval-join operator has no SQL planning path, \
472 so batch SQL cannot express it (Planned); the streaming operator exists (Partial). \
473 Corrected from the prior over-claim of batch Supported.",
474 ),
475 FeatureEntry::new(
476 "join.temporal_as_of",
477 "JOIN",
478 "Temporal AS OF point-in-time join",
479 PL,
480 NA,
481 NA,
482 )
483 .with_note(
484 "no SQL temporal-join planning path: `lakehouse/as_of.rs` is table time-travel \
485 (temporal.as_of), not a temporal join. Marked Planned rather than the prior Supported.",
486 ),
487 FeatureEntry::new(
488 "join.broadcast_hint",
489 "JOIN",
490 "/*+ BROADCAST(t) */ optimizer hint",
491 P,
492 NA,
493 NA,
494 )
495 .with_note("hint parsed and recorded; broadcast decision is cost-based (see hints.* entries)"),
496 FeatureEntry::new(
498 "hints.join_strategy",
499 "HINTS",
500 "/*+ MERGE|SHUFFLE_HASH|BROADCAST(t) */ join-strategy hints",
501 P,
502 NA,
503 NA,
504 )
505 .with_note("parsed always; honored where the executor supports the strategy (Phase 52/54), recorded either way"),
506 FeatureEntry::new(
507 "hints.repartition",
508 "HINTS",
509 "/*+ REPARTITION(n)|COALESCE(n) */ partitioning hints",
510 P,
511 NA,
512 NA,
513 )
514 .with_note("parsed and recorded; applied where the distributed planner supports it"),
515 FeatureEntry::new("window.over", "WINDOW", "OVER () window function clauses", S, NA, NA),
517 FeatureEntry::new("window.partition_by", "WINDOW", "PARTITION BY inside OVER", S, NA, NA),
518 FeatureEntry::new("window.order_by", "WINDOW", "ORDER BY inside OVER", S, NA, NA),
519 FeatureEntry::new("window.rows_range", "WINDOW", "ROWS / RANGE frame specification", S, NA, NA),
520 FeatureEntry::new(
521 "window.rank_dense_rank",
522 "WINDOW",
523 "RANK(), DENSE_RANK(), ROW_NUMBER()",
524 S,
525 NA,
526 NA,
527 ),
528 FeatureEntry::new("window.lead_lag", "WINDOW", "LEAD() and LAG()", S, NA, NA),
529 FeatureEntry::new(
530 "window.first_last_value",
531 "WINDOW",
532 "FIRST_VALUE() and LAST_VALUE()",
533 S,
534 NA,
535 NA,
536 ),
537 FeatureEntry::new("window.nth_value", "WINDOW", "NTH_VALUE()", S, NA, NA),
538 FeatureEntry::new("window.ntile", "WINDOW", "NTILE(n)", S, NA, NA),
539 FeatureEntry::new(
540 "window.cume_dist_percent",
541 "WINDOW",
542 "CUME_DIST() and PERCENT_RANK()",
543 S,
544 NA,
545 NA,
546 ),
547 FeatureEntry::new(
548 "window.tumble",
549 "WINDOW",
550 "TUMBLE(col, interval) streaming window",
551 S,
552 S,
553 P,
554 )
555 .with_note("batch rewrites the TVF to scalar UDFs (streaming_tvf.rs); streaming compiles it natively"),
556 FeatureEntry::new("window.hop", "WINDOW", "HOP(col, slide, size) sliding window", S, S, P),
557 FeatureEntry::new("window.session", "WINDOW", "Session window on inactivity gap", S, S, NA),
558 FeatureEntry::new("cte.non_recursive", "CTE", "WITH … AS (…) non-recursive CTEs", S, P, P),
560 FeatureEntry::new(
561 "cte.recursive",
562 "CTE",
563 "WITH RECURSIVE … (UNION ALL base + recursive)",
564 S,
565 NA,
566 NA,
567 ),
568 FeatureEntry::new("cte.multiple", "CTE", "Multiple CTEs in one WITH clause", S, P, P),
569 FeatureEntry::new("set.union_all", "SET", "UNION ALL", S, P, P),
571 FeatureEntry::new("set.union_distinct", "SET", "UNION (DISTINCT)", S, NA, P),
572 FeatureEntry::new("set.intersect", "SET", "INTERSECT", S, NA, P),
573 FeatureEntry::new("set.except", "SET", "EXCEPT", S, NA, P),
574 FeatureEntry::batch_only("lateral.unnest", "LATERAL", "UNNEST(array_col) in FROM clause", S),
576 FeatureEntry::batch_only(
577 "lateral.generate_series",
578 "LATERAL",
579 "generate_series() table function",
580 S,
581 ),
582 FeatureEntry::batch_only(
583 "lateral.cross_join_unnest",
584 "LATERAL",
585 "CROSS JOIN UNNEST(…) AS t(col)",
586 S,
587 ),
588 FeatureEntry::batch_only("pivot.pivot", "PIVOT", "PIVOT(agg FOR col IN (v1, v2, …))", S),
590 FeatureEntry::batch_only("pivot.unpivot", "PIVOT", "UNPIVOT(value FOR col IN (c1, c2, …))", S),
591 FeatureEntry::batch_only(
593 "functions.json.get_json_object",
594 "FUNCTIONS",
595 "get_json_object(json, path) Spark JSONPath extraction",
596 S,
597 ),
598 FeatureEntry::batch_only(
599 "functions.json.json_array_length",
600 "FUNCTIONS",
601 "json_array_length(json) top-level array element count",
602 S,
603 ),
604 FeatureEntry::batch_only(
605 "functions.json.from_to_json",
606 "FUNCTIONS",
607 "from_json / to_json struct⇄JSON conversion",
608 PL,
609 )
610 .with_note(
611 "requires a typed arrow⇄JSON converter + a Spark-DDL schema parser with Spark's \
612 version-specific null-field/timestamp rules; itemized shortfall, not shipped approximate",
613 ),
614 FeatureEntry::batch_only(
615 "functions.json.json_tuple",
616 "FUNCTIONS",
617 "json_tuple(json, k1, k2, …) multi-key extraction (generator)",
618 PL,
619 )
620 .with_note("needs table-generating/LATERAL VIEW machinery; use get_json_object per key today"),
621 FeatureEntry::batch_only(
622 "functions.json.schema_of_json",
623 "FUNCTIONS",
624 "schema_of_json(json) infer a DDL schema string",
625 PL,
626 ),
627 FeatureEntry::batch_only(
629 "functions.hof.transform",
630 "FUNCTIONS",
631 "transform(array, x -> …) — Spark alias for array_transform",
632 S,
633 ),
634 FeatureEntry::batch_only(
635 "functions.hof.filter",
636 "FUNCTIONS",
637 "filter(array, x -> …) — Spark alias for array_filter",
638 S,
639 ),
640 FeatureEntry::batch_only(
641 "functions.hof.exists",
642 "FUNCTIONS",
643 "exists / any_match(array, x -> …) predicate-any",
644 P,
645 )
646 .with_note(
647 "any_match is reachable; the `exists(...)` spelling is shadowed by the EXISTS-subquery \
648 keyword in the parser (documented dialect difference)",
649 ),
650 FeatureEntry::batch_only(
651 "functions.hof.forall",
652 "FUNCTIONS",
653 "forall(array, x -> …) predicate-all (new, exact all-match)",
654 S,
655 ),
656 FeatureEntry::batch_only(
657 "functions.hof.aggregate_zip_map",
658 "FUNCTIONS",
659 "aggregate/reduce, zip_with, map_filter, transform_keys/values",
660 PL,
661 )
662 .with_note("require DataFusion's multi-step lambda / map-lambda protocol; itemized shortfall"),
663 FeatureEntry::batch_only(
665 "functions.spark.nvl",
666 "FUNCTIONS",
667 "nvl / nvl2 null-coalescing (DataFusion-native, exact)",
668 S,
669 ),
670 FeatureEntry::batch_only(
671 "functions.spark.substring_index",
672 "FUNCTIONS",
673 "substring_index(str, delim, count) (DataFusion-native, exact)",
674 S,
675 ),
676 FeatureEntry::batch_only(
677 "functions.spark.date_format",
678 "FUNCTIONS",
679 "date_format(ts, fmt) with **Spark** pattern letters (yyyy-MM-dd)",
680 S,
681 )
682 .with_note(
683 "supported Spark pattern letters translate exactly to chrono; unsupported letters \
684 (era/timezone) error clearly rather than emitting wrong output. Differs from \
685 DataFusion's chrono-pattern date_format — see honesty page.",
686 ),
687 FeatureEntry::batch_only(
688 "functions.spark.crc32",
689 "FUNCTIONS",
690 "crc32(expr) IEEE CRC-32 as BIGINT (exact)",
691 S,
692 ),
693 FeatureEntry::batch_only(
694 "functions.spark.hash_generators",
695 "FUNCTIONS",
696 "xxhash64, stack, posexplode, inline",
697 PL,
698 )
699 .with_note(
700 "xxhash64 needs byte-exact replication of Spark's seed-42 typed hashing; \
701 stack/posexplode/inline need generator machinery — itemized shortfall",
702 ),
703 FeatureEntry::batch_only("dml.copy_to", "DML", "COPY (query) TO 'path' (FORMAT …)", S)
705 .with_note("inherited from DataFusion's native parser/planner; no Krishiv-side code involved"),
706 FeatureEntry::new("dml.insert_into", "DML", "INSERT INTO table SELECT …", S, NA, NA),
707 FeatureEntry::batch_only(
708 "dml.insert_overwrite",
709 "DML",
710 "INSERT OVERWRITE (full partition replace)",
711 S,
712 ),
713 FeatureEntry::batch_only("dml.delete", "DML", "DELETE FROM table WHERE …", P)
714 .with_note("supported on Iceberg tables; in-memory and Parquet tables require rewrite"),
715 FeatureEntry::batch_only("dml.update", "DML", "UPDATE table SET col = … WHERE …", P)
716 .with_note("supported on Iceberg tables via MERGE rewrite"),
717 FeatureEntry::batch_only(
718 "dml.merge",
719 "DML",
720 "MERGE INTO target USING source ON … WHEN MATCHED …",
721 S,
722 ),
723 FeatureEntry::batch_only(
724 "dml.iceberg_merge",
725 "DML",
726 "Atomic Iceberg MERGE with row-level deletes",
727 S,
728 ),
729 FeatureEntry::batch_only("dml.truncate", "DML", "TRUNCATE TABLE (Iceberg + memory)", PL)
730 .with_note("itemized shortfall: TRUNCATE is not yet wired for memory/Iceberg session tables"),
731 FeatureEntry::batch_only(
733 "ddl.create_external_table",
734 "DDL",
735 "CREATE EXTERNAL TABLE … STORED AS …",
736 S,
737 ),
738 FeatureEntry::batch_only("ddl.create_view", "DDL", "CREATE VIEW name AS SELECT …", S),
739 FeatureEntry::batch_only(
740 "ddl.create_function",
741 "DDL",
742 "CREATE FUNCTION … LANGUAGE SQL|PYTHON",
743 S,
744 ),
745 FeatureEntry::batch_only("ddl.drop_table", "DDL", "DROP TABLE [IF EXISTS]", S),
746 FeatureEntry::batch_only("ddl.drop_view", "DDL", "DROP VIEW [IF EXISTS]", S),
747 FeatureEntry::batch_only("ddl.create_table_as", "DDL", "CREATE TABLE … AS SELECT (CTAS)", S)
748 .with_note("durable Iceberg landing (G17) when the target resolves to a registered Iceberg catalog; session table otherwise"),
749 FeatureEntry::batch_only(
750 "ddl.partitioned_by",
751 "DDL",
752 "CREATE TABLE … PARTITIONED BY (col | bucket/truncate/year/month/day/hour(col)) AS SELECT",
753 S,
754 )
755 .with_note("Iceberg catalog tables only; transforms follow the Iceberg partition spec"),
756 FeatureEntry::batch_only("ddl.alter_table", "DDL", "ALTER TABLE ADD/DROP COLUMN, RENAME", P)
757 .with_note("Iceberg schema evolution via ALTER TABLE is supported"),
758 FeatureEntry::batch_only("ddl.create_schema", "DDL", "CREATE SCHEMA name", S)
759 .with_note("inherited from DataFusion's native catalog; no Krishiv-side code involved"),
760 FeatureEntry::new(
761 "ddl.create_materialized_view",
762 "DDL",
763 "CREATE [OR REPLACE] MATERIALIZED VIEW … AS SELECT → IVM view (REFRESH/DROP)",
764 NA,
765 NA,
766 S,
767 )
768 .with_note(
769 "Phase 60 SQL-DDL-for-IVM: ANSI/Spark synonym routed onto the same IVM engine as \
770 CREATE MATERIALIZED INCREMENTAL VIEW; REFRESH/DROP MATERIALIZED VIEW lifecycle; \
771 engine primitive under the platform's governed pipelines",
772 ),
773 FeatureEntry::new(
774 "ddl.create_streaming_table",
775 "DDL",
776 "CREATE [OR REPLACE] STREAMING TABLE … AS SELECT → continuous job",
777 NA,
778 PL,
779 NA,
780 )
781 .with_note(
782 "Phase 60: SQL front door + planner validation land (the body lowers through the shared \
783 streaming compiler); continuous-job execution is coordinator-gated — a cluster-attached \
784 session submits the validated plan via the continuous-stream registration API",
785 ),
786 FeatureEntry::batch_only(
787 "ddl.live_table",
788 "DDL",
789 "CREATE / REFRESH / DROP LIVE TABLE via session.sql()",
790 S,
791 ),
792 FeatureEntry::batch_only(
794 "ddl.connector_source_sink",
795 "DDL",
796 "CREATE SOURCE/SINK … WITH (connector=…) resolved through the connector registry",
797 P,
798 )
799 .with_note(
800 "registry-backed dispatch replacing the parquet-only hardcoded factory (audit §8b); \
801 supported kinds come from connector descriptors, unsupported kinds fail loudly",
802 ),
803 FeatureEntry::batch_only("stmt.set_reset", "SESSION", "SET / RESET / SET TIMEZONE session config", S)
805 .with_note("DataFusion-native session config"),
806 FeatureEntry::batch_only("stmt.use", "SESSION", "USE [CATALOG|SCHEMA] current-namespace", S)
807 .with_note("Phase 60: mutates the session default catalog/schema"),
808 FeatureEntry::batch_only(
809 "stmt.cache",
810 "SESSION",
811 "CACHE / UNCACHE / CLEAR CACHE TABLE (session materialization)",
812 PL,
813 )
814 .with_note("itemized shortfall: needs a session-scoped materialization + provider swap/restore"),
815 FeatureEntry::batch_only(
817 "show.tables_databases_functions",
818 "SHOW",
819 "SHOW TABLES | DATABASES | SCHEMAS | FUNCTIONS | COLUMNS",
820 P,
821 )
822 .with_note(
823 "TABLES/FUNCTIONS/COLUMNS are DataFusion-native; DATABASES/SCHEMAS added in Phase 60 \
824 (information_schema.schemata). SHOW PARTITIONS (Iceberg) and SHOW VIEWS remain the gap.",
825 ),
826 FeatureEntry::batch_only(
827 "describe.function_database_query",
828 "DESCRIBE",
829 "DESCRIBE FUNCTION | DATABASE | QUERY",
830 PL,
831 )
832 .with_note("DESCRIBE <table> is native; FUNCTION/DATABASE/QUERY are the itemized shortfall"),
833 FeatureEntry::batch_only("temporal.as_of", "TEMPORAL", "AS OF TIMESTAMP point-in-time queries", S),
835 FeatureEntry::new(
836 "temporal.match_recognize",
837 "TEMPORAL",
838 "MATCH_RECOGNIZE pattern matching over ordered rows",
839 P,
840 P,
841 NA,
842 )
843 .with_note(
844 "streaming CEP subset: PARTITION BY / ORDER BY / PATTERN (…) / WITHIN <duration>; \
845 DEFINE (pattern-variable predicates) and MEASURES (computed output) clauses are the \
846 remaining gap vs Oracle/Flink's full grammar",
847 ),
848 FeatureEntry::batch_only(
849 "temporal.system_time",
850 "TEMPORAL",
851 "FOR SYSTEM_TIME AS OF (Iceberg time-travel)",
852 P,
853 )
854 .with_note("alias for AS OF on Iceberg tables"),
855 FeatureEntry::batch_only(
857 "prepared.create",
858 "PREPARED",
859 "CREATE PREPARED STATEMENT via Flight SQL action",
860 S,
861 ),
862 FeatureEntry::batch_only("prepared.execute", "PREPARED", "Execute prepared statement by handle", S),
863 FeatureEntry::batch_only(
864 "prepared.close",
865 "PREPARED",
866 "CLOSE PREPARED STATEMENT to release server memory",
867 S,
868 ),
869 FeatureEntry::batch_only(
870 "prepared.parameters",
871 "PREPARED",
872 "Positional parameter binding ($1, $2, …)",
873 S,
874 )
875 .with_note("local PreparedStatement::bind and Flight SQL DoPut parameter batches"),
876 FeatureEntry::batch_only(
877 "prepared.sql_text",
878 "PREPARED",
879 "PREPARE name AS …; EXECUTE name(…); DEALLOCATE name",
880 S,
881 )
882 .with_note("inherited from DataFusion's native parser/planner (session-scoped named plans)"),
883 FeatureEntry::new("operation.id", "OPERATION", "Operation IDs for query tracking", S, S, S),
885 FeatureEntry::new("operation.cancel", "OPERATION", "Cancel a running operation by ID", S, S, S),
886 FeatureEntry::new("operation.timeout", "OPERATION", "Per-query execution timeout", S, NA, NA),
887 FeatureEntry::new(
888 "operation.progress",
889 "OPERATION",
890 "Query progress reporting via QueryHandle",
891 S,
892 S,
893 S,
894 ),
895 FeatureEntry::batch_only("error.sqlstate", "ERROR", "SQLSTATE codes on error responses", S),
897 FeatureEntry::batch_only("error.error_position", "ERROR", "Source line/column in error messages", P)
898 .with_note("DataFusion provides message but not structured position"),
899 FeatureEntry::batch_only(
901 "flight.get_flight_info",
902 "FLIGHT SQL",
903 "GetFlightInfo for statement execution",
904 S,
905 ),
906 FeatureEntry::batch_only("flight.do_get", "FLIGHT SQL", "DoGet streaming result delivery", S),
907 FeatureEntry::batch_only(
908 "flight.prepared_statements",
909 "FLIGHT SQL",
910 "Prepared statement create/execute/close",
911 S,
912 ),
913 FeatureEntry::batch_only(
914 "flight.do_action",
915 "FLIGHT SQL",
916 "DoAction for custom Krishiv operations",
917 S,
918 ),
919 FeatureEntry::batch_only(
920 "flight.get_sql_info",
921 "FLIGHT SQL",
922 "GetSqlInfo capability introspection",
923 S,
924 ),
925 FeatureEntry::batch_only("flight.auth", "FLIGHT SQL", "Bearer token authentication", S),
926 FeatureEntry::batch_only("flight.policy", "FLIGHT SQL", "Table-level access policy enforcement", S),
927 FeatureEntry::batch_only(
928 "flight.transactions",
929 "FLIGHT SQL",
930 "BEGIN/COMMIT/ROLLBACK transactions",
931 P,
932 )
933 .with_note("Flight SQL BeginTransaction/EndTransaction actions; SQL BEGIN/COMMIT not routed"),
934 FeatureEntry::batch_only(
935 "flight.schemas",
936 "FLIGHT SQL",
937 "GetDbSchemas / GetTables catalog introspection",
938 P,
939 )
940 .with_note("tables listed via Krishiv catalog; schema introspection via get_sql_info"),
941 FeatureEntry::new(
943 "streaming.continuous_select",
944 "STREAMING",
945 "Continuous SELECT over unbounded input",
946 NA,
947 S,
948 NA,
949 ),
950 FeatureEntry::new(
951 "streaming.window_agg",
952 "STREAMING",
953 "Windowed aggregations over streaming input",
954 NA,
955 S,
956 P,
957 ),
958 FeatureEntry::new(
959 "streaming.watermark",
960 "STREAMING",
961 "Event-time watermarks for late-data handling",
962 NA,
963 S,
964 NA,
965 ),
966 FeatureEntry::new(
967 "streaming.interval_join",
968 "STREAMING",
969 "Streaming-to-streaming interval join",
970 NA,
971 S,
972 NA,
973 )
974 .placed(&[Placement::EmbeddedApi, Placement::Distributed])
975 .with_note(
976 "no SQL planning path; embedded PerKeyIntervalJoin, and distributed only as the \
977 watermark window-join WindowExecutionSpec shape",
978 ),
979 FeatureEntry::new("streaming.cep", "STREAMING", "MATCH_RECOGNIZE CEP over streaming input", NA, S, NA),
980 FeatureEntry::new(
981 "streaming.dedup",
982 "STREAMING",
983 "Streaming deduplication (dropDuplicates)",
984 NA,
985 S,
986 NA,
987 )
988 .placed(&[Placement::EmbeddedApi])
989 .with_note(
990 "embedded API only — not compiled from SQL and not a distributed \
991 stream:loop shape (audit §9b)",
992 ),
993 FeatureEntry::new(
994 "streaming.sink_modes",
995 "STREAMING",
996 "Append / Update / Complete output modes",
997 NA,
998 S,
999 P,
1000 ),
1001 FeatureEntry::batch_only(
1003 "introspection.describe",
1004 "INTROSPECTION",
1005 "DESCRIBE / DESC / SHOW COLUMNS table schema",
1006 S,
1007 ),
1008 FeatureEntry::batch_only(
1009 "introspection.explain",
1010 "INTROSPECTION",
1011 "EXPLAIN [LOGICAL|PHYSICAL|ANALYZE] query plans",
1012 S,
1013 ),
1014 FeatureEntry::batch_only(
1015 "introspection.information_schema",
1016 "INTROSPECTION",
1017 "information_schema.{tables,columns,views,df_settings,routines,parameters,schemata}",
1018 S,
1019 ),
1020];
1021
1022#[cfg(test)]
1023mod tests {
1024 use super::*;
1025
1026 #[test]
1027 fn feature_matrix_is_non_empty() {
1028 assert!(!feature_matrix().is_empty());
1029 }
1030
1031 #[test]
1032 fn all_ids_are_unique() {
1033 let mut seen = std::collections::HashSet::new();
1034 for e in feature_matrix() {
1035 assert!(seen.insert(e.id), "duplicate feature id: {}", e.id);
1036 }
1037 }
1038
1039 #[test]
1040 fn every_entry_has_at_least_one_non_na_engine() {
1041 for e in feature_matrix() {
1043 assert!(
1044 e.batch != NA || e.streaming != NA || e.incremental != NA,
1045 "feature {} is n/a in every engine",
1046 e.id
1047 );
1048 }
1049 }
1050
1051 #[test]
1052 fn features_for_category_returns_subset() {
1053 let join_features = features_for_category("JOIN");
1054 assert!(!join_features.is_empty());
1055 for f in &join_features {
1056 assert!(f.category.to_uppercase().starts_with("JOIN"), "{}", f.id);
1057 }
1058 }
1059
1060 #[test]
1061 fn features_by_status_supported_is_non_empty() {
1062 assert!(!features_by_status(FeatureStatus::Supported).is_empty());
1063 }
1064
1065 #[test]
1066 fn feature_entry_display_includes_id_and_engines() {
1067 let entry = feature_matrix()
1068 .iter()
1069 .find(|e| e.id == "window.tumble")
1070 .unwrap();
1071 let s = entry.to_string();
1072 assert!(s.contains("window.tumble"));
1073 assert!(s.contains("batch:supported"));
1074 assert!(s.contains("streaming:supported"));
1075 }
1076
1077 #[test]
1078 fn generated_reference_has_all_three_engine_columns() {
1079 let md = generate_reference_markdown();
1080 assert!(md.contains("| Batch | Streaming | Incremental |"));
1081 assert!(md.contains("`window.tumble`"));
1083 assert!(md.contains("`functions.hof.forall`"));
1084 }
1085
1086 #[test]
1087 fn drift_check_ctas_is_supported_not_partial() {
1088 let ctas = feature_matrix()
1090 .iter()
1091 .find(|e| e.id == "ddl.create_table_as")
1092 .unwrap();
1093 assert_eq!(ctas.batch, FeatureStatus::Supported);
1094 }
1095
1096 #[test]
1097 fn drift_check_interval_join_has_no_batch_sql_path() {
1098 let ij = feature_matrix()
1101 .iter()
1102 .find(|e| e.id == "join.interval")
1103 .unwrap();
1104 assert_eq!(ij.batch, FeatureStatus::Planned);
1105 assert_eq!(ij.streaming, FeatureStatus::Partial);
1106 }
1107
1108 #[test]
1109 fn embedded_only_operators_carry_the_placement_marker() {
1110 let dedup = feature_matrix()
1114 .iter()
1115 .find(|e| e.id == "streaming.dedup")
1116 .unwrap();
1117 assert_eq!(dedup.placement, Some(&[Placement::EmbeddedApi][..]));
1118 assert!(dedup.placement_restricted());
1119
1120 let ij = feature_matrix()
1121 .iter()
1122 .find(|e| e.id == "streaming.interval_join")
1123 .unwrap();
1124 let p = ij
1125 .placement
1126 .expect("streaming.interval_join must carry a placement set");
1127 assert!(p.contains(&Placement::EmbeddedApi) && p.contains(&Placement::Distributed));
1128 assert!(
1129 !p.contains(&Placement::Sql),
1130 "no SQL planning path exists for interval join"
1131 );
1132 }
1133
1134 #[test]
1135 fn placement_restricted_entries_always_explain_themselves() {
1136 for e in feature_matrix() {
1138 if e.placement_restricted() {
1139 assert!(
1140 e.note.is_some(),
1141 "feature {} restricts placement but has no note",
1142 e.id
1143 );
1144 }
1145 }
1146 }
1147
1148 #[test]
1149 fn generated_reference_renders_placement_and_embedded_only_ledger() {
1150 let md = generate_reference_markdown();
1151 assert!(md.contains("**placement: embedded API only.**"));
1152 assert!(md.contains("**placement: embedded API + distributed runtime only.**"));
1153 assert!(md.contains("## Embedded-API-only streaming operators"));
1154 for (name, _) in EMBEDDED_ONLY_OPERATORS {
1155 assert!(md.contains(name), "embedded-only ledger is missing {name}");
1156 }
1157 }
1158}