1#![forbid(unsafe_code)]
2use crate::grammar::{Engine, FeatureStatus, feature_matrix};
20
21#[derive(Debug, Clone, Copy)]
23pub enum Coverage {
24 Sql(&'static str),
27 Elsewhere(&'static str),
31}
32
33#[derive(Debug, Clone, Copy)]
35pub struct ChecklistCase {
36 pub feature_id: &'static str,
37 pub coverage: Coverage,
38}
39
40const fn sql(feature_id: &'static str, q: &'static str) -> ChecklistCase {
41 ChecklistCase {
42 feature_id,
43 coverage: Coverage::Sql(q),
44 }
45}
46const fn elsewhere(feature_id: &'static str, why: &'static str) -> ChecklistCase {
47 ChecklistCase {
48 feature_id,
49 coverage: Coverage::Elsewhere(why),
50 }
51}
52
53pub const FIXTURE_T: &str = "CREATE TABLE t AS SELECT * FROM (VALUES \
56 (1, 'a', TIMESTAMP '2024-01-01 00:00:00'), \
57 (2, 'b', TIMESTAMP '2024-01-01 00:01:30'), \
58 (3, 'a', TIMESTAMP '2024-01-01 00:03:00')) v(id, name, ts)";
59pub const FIXTURE_U: &str = "CREATE TABLE u AS SELECT * FROM (VALUES (1, 10), (2, 20)) v(id, val)";
60
61pub static CHECKLIST: &[ChecklistCase] = &[
63 sql("select.projection", "SELECT id, name AS n FROM t"),
65 sql("select.star", "SELECT * FROM t"),
66 sql("select.distinct", "SELECT DISTINCT name FROM t"),
67 sql("select.where", "SELECT id FROM t WHERE id > 1"),
68 sql(
69 "select.order_by",
70 "SELECT id FROM t ORDER BY id DESC NULLS LAST",
71 ),
72 sql(
73 "select.limit_offset",
74 "SELECT id FROM t ORDER BY id LIMIT 1 OFFSET 1",
75 ),
76 sql(
77 "select.having",
78 "SELECT name, count(*) c FROM t GROUP BY name HAVING count(*) >= 1",
79 ),
80 sql(
81 "select.case",
82 "SELECT CASE WHEN id > 1 THEN 'p' ELSE 'n' END AS c FROM t",
83 ),
84 sql(
85 "select.cast",
86 "SELECT CAST(id AS VARCHAR) a, TRY_CAST(name AS INT) b FROM t",
87 ),
88 sql(
89 "select.subquery_scalar",
90 "SELECT id, (SELECT max(id) FROM t) m FROM t",
91 ),
92 sql(
93 "select.subquery_exists",
94 "SELECT id FROM t WHERE EXISTS (SELECT 1 FROM u WHERE u.id = t.id)",
95 ),
96 sql(
97 "select.subquery_in",
98 "SELECT id FROM t WHERE id IN (SELECT id FROM u)",
99 ),
100 sql("select.values", "SELECT * FROM (VALUES (1), (2)) v(x)"),
101 sql(
103 "groupby.basic",
104 "SELECT name, count(*) c FROM t GROUP BY name",
105 ),
106 sql(
107 "groupby.rollup",
108 "SELECT name, count(*) c FROM t GROUP BY ROLLUP(name)",
109 ),
110 sql(
111 "groupby.cube",
112 "SELECT name, count(*) c FROM t GROUP BY CUBE(name)",
113 ),
114 sql(
115 "groupby.grouping_sets",
116 "SELECT name, count(*) c FROM t GROUP BY GROUPING SETS ((name), ())",
117 ),
118 sql(
119 "groupby.grouping_function",
120 "SELECT name, GROUPING(name) g FROM t GROUP BY ROLLUP(name)",
121 ),
122 sql("join.inner", "SELECT * FROM t JOIN u ON t.id = u.id"),
124 sql(
125 "join.left_outer",
126 "SELECT * FROM t LEFT JOIN u ON t.id = u.id",
127 ),
128 sql(
129 "join.right_outer",
130 "SELECT * FROM t RIGHT JOIN u ON t.id = u.id",
131 ),
132 sql(
133 "join.full_outer",
134 "SELECT * FROM t FULL JOIN u ON t.id = u.id",
135 ),
136 sql("join.cross", "SELECT t.id, u.val FROM t CROSS JOIN u"),
137 sql("join.natural", "SELECT * FROM t NATURAL JOIN u"),
138 sql("join.using", "SELECT * FROM t JOIN u USING (id)"),
139 elsewhere(
140 "join.lateral",
141 "sql_tests.rs lateral-join coverage; correlation shape varies",
142 ),
143 sql(
144 "join.broadcast_hint",
145 "SELECT /*+ BROADCAST(u) */ t.id FROM t JOIN u ON t.id = u.id",
146 ),
147 sql(
149 "hints.join_strategy",
150 "SELECT /*+ MERGE(u) */ t.id FROM t JOIN u ON t.id = u.id",
151 ),
152 sql(
153 "hints.repartition",
154 "SELECT /*+ REPARTITION(4) */ id FROM t",
155 ),
156 sql("window.over", "SELECT id, sum(id) OVER () s FROM t"),
158 sql(
159 "window.partition_by",
160 "SELECT id, sum(id) OVER (PARTITION BY name) s FROM t",
161 ),
162 sql(
163 "window.order_by",
164 "SELECT id, row_number() OVER (ORDER BY id) r FROM t",
165 ),
166 sql(
167 "window.rows_range",
168 "SELECT id, sum(id) OVER (ORDER BY id ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) s FROM t",
169 ),
170 sql(
171 "window.rank_dense_rank",
172 "SELECT rank() OVER (ORDER BY id) a, dense_rank() OVER (ORDER BY id) b, row_number() OVER (ORDER BY id) c FROM t",
173 ),
174 sql(
175 "window.lead_lag",
176 "SELECT lead(id) OVER (ORDER BY id) a, lag(id) OVER (ORDER BY id) b FROM t",
177 ),
178 sql(
179 "window.first_last_value",
180 "SELECT first_value(id) OVER (ORDER BY id) a, last_value(id) OVER (ORDER BY id) b FROM t",
181 ),
182 sql(
183 "window.nth_value",
184 "SELECT nth_value(id, 1) OVER (ORDER BY id) a FROM t",
185 ),
186 sql(
187 "window.ntile",
188 "SELECT ntile(2) OVER (ORDER BY id) a FROM t",
189 ),
190 sql(
191 "window.cume_dist_percent",
192 "SELECT cume_dist() OVER (ORDER BY id) a, percent_rank() OVER (ORDER BY id) b FROM t",
193 ),
194 elsewhere(
195 "window.tumble",
196 "streaming_tvf.rs batch TUMBLE (needs an Int64 epoch-ms descriptor column)",
197 ),
198 elsewhere(
199 "window.hop",
200 "streaming_tvf.rs + streaming_window_plan.rs HOP coverage",
201 ),
202 elsewhere(
203 "window.session",
204 "streaming_window_plan.rs SESSION coverage",
205 ),
206 sql(
208 "cte.non_recursive",
209 "WITH c AS (SELECT id FROM t) SELECT * FROM c",
210 ),
211 sql(
212 "cte.recursive",
213 "WITH RECURSIVE c(n) AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM c WHERE n < 3) SELECT * FROM c",
214 ),
215 sql(
216 "cte.multiple",
217 "WITH a AS (SELECT 1 x), b AS (SELECT 2 y) SELECT * FROM a, b",
218 ),
219 sql(
221 "set.union_all",
222 "SELECT id FROM t UNION ALL SELECT id FROM u",
223 ),
224 sql(
225 "set.union_distinct",
226 "SELECT id FROM t UNION SELECT id FROM u",
227 ),
228 sql(
229 "set.intersect",
230 "SELECT id FROM t INTERSECT SELECT id FROM u",
231 ),
232 sql("set.except", "SELECT id FROM t EXCEPT SELECT id FROM u"),
233 sql("lateral.unnest", "SELECT unnest([1, 2, 3]) AS e"),
235 sql(
236 "lateral.generate_series",
237 "SELECT * FROM generate_series(1, 3)",
238 ),
239 elsewhere(
240 "lateral.cross_join_unnest",
241 "unnest_sql.rs CROSS JOIN UNNEST coverage",
242 ),
243 elsewhere("pivot.pivot", "pivot_sql.rs PIVOT rewrite coverage"),
245 elsewhere("pivot.unpivot", "pivot_sql.rs UNPIVOT rewrite coverage"),
246 sql(
248 "functions.json.get_json_object",
249 "SELECT get_json_object('{\"a\":{\"b\":7}}', '$.a.b') AS v",
250 ),
251 sql(
252 "functions.json.json_array_length",
253 "SELECT json_array_length('[1,2,3,4]') AS n",
254 ),
255 sql(
257 "functions.hof.transform",
258 "SELECT transform([1, 2, 3], x -> x * 2) AS r",
259 ),
260 sql(
261 "functions.hof.filter",
262 "SELECT filter([1, 2, 3, 4], x -> x % 2 = 0) AS r",
263 ),
264 sql(
265 "functions.hof.exists",
266 "SELECT any_match([1, 2, 3], x -> x > 2) AS r",
267 ),
268 sql(
269 "functions.hof.forall",
270 "SELECT forall([2, 4, 6], x -> x % 2 = 0) AS r",
271 ),
272 sql(
274 "functions.spark.nvl",
275 "SELECT nvl(NULL, 1) a, nvl2(1, 2, 3) b",
276 ),
277 sql(
278 "functions.spark.substring_index",
279 "SELECT substring_index('a.b.c', '.', 2) AS s",
280 ),
281 sql(
282 "functions.spark.date_format",
283 "SELECT date_format(TIMESTAMP '2024-03-07 09:05:00', 'yyyy-MM-dd') AS d",
284 ),
285 sql("functions.spark.crc32", "SELECT crc32('Spark') AS c"),
286 elsewhere("dml.copy_to", "DataFusion-native COPY TO (writes a file)"),
288 sql("dml.insert_into", "INSERT INTO u SELECT 9, 90"),
289 elsewhere(
290 "dml.insert_overwrite",
291 "lakehouse Iceberg INSERT OVERWRITE coverage",
292 ),
293 elsewhere(
294 "dml.delete",
295 "lakehouse Iceberg DELETE coverage (Iceberg-gated)",
296 ),
297 elsewhere(
298 "dml.update",
299 "lakehouse Iceberg UPDATE coverage (Iceberg-gated)",
300 ),
301 elsewhere("dml.merge", "lakehouse MERGE coverage (Iceberg-gated)"),
302 elsewhere(
303 "dml.iceberg_merge",
304 "lakehouse atomic Iceberg MERGE coverage",
305 ),
306 elsewhere(
308 "ddl.create_external_table",
309 "sql_tests.rs CREATE EXTERNAL TABLE (needs a file)",
310 ),
311 sql("ddl.create_view", "CREATE VIEW cov_v AS SELECT 1 AS x"),
312 elsewhere(
313 "ddl.create_function",
314 "create_function_ddl.rs CREATE FUNCTION coverage",
315 ),
316 sql("ddl.drop_table", "DROP TABLE IF EXISTS cov_absent_table"),
317 sql("ddl.drop_view", "DROP VIEW IF EXISTS cov_absent_view"),
318 sql(
319 "ddl.create_table_as",
320 "CREATE TABLE cov_ctas AS SELECT 1 AS x",
321 ),
322 elsewhere(
323 "ddl.partitioned_by",
324 "lakehouse PARTITIONED BY writer coverage (Iceberg-gated)",
325 ),
326 elsewhere(
327 "ddl.alter_table",
328 "lakehouse ALTER TABLE schema-evolution coverage",
329 ),
330 sql(
331 "ddl.create_schema",
332 "CREATE SCHEMA IF NOT EXISTS cov_schema",
333 ),
334 elsewhere("ddl.live_table", "live_table.rs LIVE TABLE DDL coverage"),
335 elsewhere(
336 "ddl.connector_source_sink",
337 "krishiv-api connector-registry DDL coverage",
338 ),
339 sql(
341 "stmt.set_reset",
342 "SET datafusion.execution.batch_size = 4096",
343 ),
344 sql("stmt.use", "USE public"),
347 sql("show.tables_databases_functions", "SHOW DATABASES"),
349 elsewhere(
351 "temporal.as_of",
352 "lakehouse/as_of.rs time-travel coverage (Iceberg-gated)",
353 ),
354 elsewhere(
355 "temporal.match_recognize",
356 "cep_sql.rs MATCH_RECOGNIZE coverage",
357 ),
358 elsewhere(
359 "temporal.system_time",
360 "lakehouse FOR SYSTEM_TIME AS OF coverage",
361 ),
362 elsewhere(
364 "prepared.create",
365 "krishiv-flight-sql prepared-statement protocol coverage",
366 ),
367 elsewhere(
368 "prepared.execute",
369 "krishiv-flight-sql prepared-statement protocol coverage",
370 ),
371 elsewhere(
372 "prepared.close",
373 "krishiv-flight-sql prepared-statement protocol coverage",
374 ),
375 elsewhere(
376 "prepared.parameters",
377 "krishiv-flight-sql parameter-binding coverage",
378 ),
379 sql("prepared.sql_text", "PREPARE cov_p AS SELECT 1"),
380 elsewhere(
382 "operation.id",
383 "krishiv-runtime operation-tracking coverage",
384 ),
385 elsewhere("operation.cancel", "krishiv-runtime cancel coverage"),
386 elsewhere(
387 "operation.timeout",
388 "krishiv-runtime per-query timeout coverage",
389 ),
390 elsewhere("operation.progress", "krishiv-runtime progress coverage"),
391 elsewhere("error.sqlstate", "sqlstate.rs SQLSTATE coverage"),
393 elsewhere(
394 "error.error_position",
395 "DataFusion message-only error position",
396 ),
397 elsewhere(
399 "flight.get_flight_info",
400 "krishiv-flight-sql service coverage",
401 ),
402 elsewhere("flight.do_get", "krishiv-flight-sql service coverage"),
403 elsewhere(
404 "flight.prepared_statements",
405 "krishiv-flight-sql service coverage",
406 ),
407 elsewhere("flight.do_action", "krishiv-flight-sql service coverage"),
408 elsewhere("flight.get_sql_info", "krishiv-flight-sql service coverage"),
409 elsewhere("flight.auth", "krishiv-flight-sql auth coverage"),
410 elsewhere("flight.policy", "krishiv-flight-sql policy coverage"),
411 elsewhere(
412 "flight.transactions",
413 "krishiv-flight-sql transaction coverage",
414 ),
415 elsewhere(
416 "flight.schemas",
417 "krishiv-flight-sql catalog-introspection coverage",
418 ),
419 elsewhere(
421 "streaming.continuous_select",
422 "streaming.rs continuous-select coverage",
423 ),
424 elsewhere(
425 "streaming.window_agg",
426 "streaming_window_plan.rs windowed-agg coverage",
427 ),
428 elsewhere("streaming.watermark", "streaming engine watermark coverage"),
429 elsewhere(
430 "streaming.interval_join",
431 "streaming interval-join coverage",
432 ),
433 elsewhere("streaming.cep", "cep_sql.rs streaming CEP coverage"),
434 elsewhere("streaming.dedup", "streaming dedup coverage"),
435 elsewhere("streaming.sink_modes", "streaming sink-mode coverage"),
436 sql("introspection.describe", "DESCRIBE t"),
438 sql("introspection.explain", "EXPLAIN SELECT 1"),
439 sql(
440 "introspection.information_schema",
441 "SELECT count(*) c FROM information_schema.tables",
442 ),
443];
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub struct CoverageReport {
448 pub batch_claimed: usize,
450 pub batch_covered: usize,
452 pub executable_cases: usize,
454 pub functions_supported: usize,
457 pub functions_total: usize,
458}
459
460impl CoverageReport {
461 pub fn batch_coverage_pct(&self) -> f64 {
463 if self.batch_claimed == 0 {
464 return 100.0;
465 }
466 (self.batch_covered as f64 / self.batch_claimed as f64) * 100.0
467 }
468}
469
470pub fn coverage_report() -> CoverageReport {
472 let mut batch_claimed = 0usize;
473 let mut batch_covered = 0usize;
474 let mut functions_supported = 0usize;
475 let mut functions_total = 0usize;
476
477 for e in feature_matrix() {
478 if e.category == "FUNCTIONS" {
479 functions_total += 1;
480 if e.batch == FeatureStatus::Supported {
481 functions_supported += 1;
482 }
483 }
484 if e.status_for(Engine::Batch).is_claimed() {
485 batch_claimed += 1;
486 if CHECKLIST.iter().any(|c| c.feature_id == e.id) {
487 batch_covered += 1;
488 }
489 }
490 }
491
492 let executable_cases = CHECKLIST
493 .iter()
494 .filter(|c| matches!(c.coverage, Coverage::Sql(_)))
495 .count();
496
497 CoverageReport {
498 batch_claimed,
499 batch_covered,
500 executable_cases,
501 functions_supported,
502 functions_total,
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn checklist_ids_are_valid_and_unique() {
512 let matrix_ids: std::collections::HashSet<&str> =
513 feature_matrix().iter().map(|e| e.id).collect();
514 let mut seen = std::collections::HashSet::new();
515 for c in CHECKLIST {
516 assert!(
517 matrix_ids.contains(c.feature_id),
518 "checklist references unknown feature id: {}",
519 c.feature_id
520 );
521 assert!(
522 seen.insert(c.feature_id),
523 "duplicate checklist case for: {}",
524 c.feature_id
525 );
526 }
527 }
528
529 #[test]
531 fn every_claimed_batch_feature_has_a_checklist_case() {
532 for e in feature_matrix() {
533 if e.status_for(Engine::Batch).is_claimed() {
534 assert!(
535 CHECKLIST.iter().any(|c| c.feature_id == e.id),
536 "batch-claimed feature '{}' has no checklist case (matrix-to-test rule)",
537 e.id
538 );
539 }
540 }
541 }
542
543 #[test]
544 fn coverage_report_meets_batch_gate() {
545 let r = coverage_report();
546 assert_eq!(r.batch_covered, r.batch_claimed);
550 assert!(
551 r.executable_cases >= 45,
552 "executable cases: {}",
553 r.executable_cases
554 );
555 assert!(
556 r.functions_supported >= 8,
557 "functions supported: {}",
558 r.functions_supported
559 );
560 }
561
562 fn workspace_doc(rel: &str) -> std::path::PathBuf {
563 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
564 .parent()
565 .and_then(|p| p.parent())
566 .expect("workspace root")
567 .join(rel)
568 }
569
570 #[test]
574 fn committed_reference_page_matches_matrix() {
575 let path = workspace_doc("docs/reference/sql-feature-matrix.md");
576 let expected = crate::grammar::generate_reference_markdown();
577 if std::env::var("KRISHIV_BLESS_SQL_DOCS").is_ok() {
578 std::fs::write(&path, &expected).expect("write reference page");
579 return;
580 }
581 let committed = std::fs::read_to_string(&path).unwrap_or_default();
582 assert_eq!(
583 committed, expected,
584 "docs/reference/sql-feature-matrix.md is out of date; regenerate with \
585 KRISHIV_BLESS_SQL_DOCS=1 cargo test -p krishiv-sql coverage"
586 );
587 }
588
589 #[test]
591 fn committed_honesty_page_matches_matrix() {
592 let path = workspace_doc("docs/reference/krishiv-vs-spark-sql.md");
593 let expected = crate::grammar::generate_honesty_markdown();
594 if std::env::var("KRISHIV_BLESS_SQL_DOCS").is_ok() {
595 std::fs::write(&path, &expected).expect("write honesty page");
596 return;
597 }
598 let committed = std::fs::read_to_string(&path).unwrap_or_default();
599 assert_eq!(
600 committed, expected,
601 "docs/reference/krishiv-vs-spark-sql.md is out of date; regenerate with \
602 KRISHIV_BLESS_SQL_DOCS=1 cargo test -p krishiv-sql coverage"
603 );
604 }
605
606 #[tokio::test]
609 async fn spark_checklist_sql_cases_execute() {
610 let engine = crate::SqlEngine::new();
611 engine
612 .sql(FIXTURE_T)
613 .await
614 .expect("fixture t")
615 .collect()
616 .await
617 .expect("t rows");
618 engine
619 .sql(FIXTURE_U)
620 .await
621 .expect("fixture u")
622 .collect()
623 .await
624 .expect("u rows");
625
626 let mut failures: Vec<String> = Vec::new();
627 for c in CHECKLIST {
628 if let Coverage::Sql(q) = c.coverage {
629 match engine.sql(q).await {
630 Ok(df) => {
631 if let Err(e) = df.collect().await {
632 failures.push(format!("[{}] exec: {e}", c.feature_id));
633 }
634 }
635 Err(e) => failures.push(format!("[{}] plan: {e}", c.feature_id)),
636 }
637 }
638 }
639 assert!(
640 failures.is_empty(),
641 "checklist failures:\n{}",
642 failures.join("\n")
643 );
644 }
645}