Skip to main content

krishiv_sql/
coverage.rs

1#![forbid(unsafe_code)]
2//! Phase 60 coverage harness — the Spark-reference SQL surface as a **measured
3//! number**, not a vibe.
4//!
5//! Three things live here:
6//!
7//! 1. **The Spark-checklist suite** ([`CHECKLIST`]): one representative case per
8//!    batch-claimed feature in the [`crate::grammar`] matrix. Runnable cases are
9//!    executed against a fixed in-memory fixture and asserted to succeed;
10//!    infra-gated features (Iceberg DML, Flight SQL protocol, the distributed
11//!    runtime) point at where they are proven instead.
12//! 2. **The matrix-to-test CI rule**: every non-`n/a`, non-`planned` **batch**
13//!    cell must have a checklist case — enforced by
14//!    [`tests::every_claimed_batch_feature_has_a_checklist_case`].
15//! 3. **The published KPI + generated pages**: [`coverage_report`] computes the
16//!    pass-rate, and the reference / honesty markdown pages are regenerated from
17//!    the matrix and drift-guarded against the checked-in copies.
18
19use crate::grammar::{Engine, FeatureStatus, feature_matrix};
20
21/// Evidence that a batch-claimed feature works.
22#[derive(Debug, Clone, Copy)]
23pub enum Coverage {
24    /// Executable against the in-memory fixture; the suite runs it and asserts
25    /// it plans + executes without error.
26    Sql(&'static str),
27    /// Proven by a named test or integration suite, or infra-gated (Iceberg,
28    /// Flight SQL protocol, distributed runtime, streaming engine). Its presence
29    /// satisfies the matrix-to-test rule; the reference is the audit trail.
30    Elsewhere(&'static str),
31}
32
33/// A single checklist case tying a matrix feature id to its evidence.
34#[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
53/// The fixture DDL run before the SQL checklist cases: `t(id, name, ts)` and
54/// `u(id, val)` as session tables.
55pub 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
61/// The Spark-reference checklist: one case per batch-claimed matrix feature.
62pub static CHECKLIST: &[ChecklistCase] = &[
63    // ── SELECT ───────────────────────────────────────────────────────────────
64    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    // ── GROUP BY ──────────────────────────────────────────────────────────────
102    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    // ── JOIN ──────────────────────────────────────────────────────────────────
123    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    // ── HINTS ─────────────────────────────────────────────────────────────────
148    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    // ── WINDOW FUNCTIONS ──────────────────────────────────────────────────────
157    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    // ── CTE ───────────────────────────────────────────────────────────────────
207    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    // ── SET OPS ───────────────────────────────────────────────────────────────
220    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    // ── LATERAL / UNNEST ──────────────────────────────────────────────────────
234    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    // ── PIVOT ─────────────────────────────────────────────────────────────────
244    elsewhere("pivot.pivot", "pivot_sql.rs PIVOT rewrite coverage"),
245    elsewhere("pivot.unpivot", "pivot_sql.rs UNPIVOT rewrite coverage"),
246    // ── FUNCTIONS: JSON ───────────────────────────────────────────────────────
247    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    // ── FUNCTIONS: higher-order ───────────────────────────────────────────────
256    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    // ── FUNCTIONS: Spark scalar aliases ───────────────────────────────────────
273    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    // ── DML ───────────────────────────────────────────────────────────────────
287    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    // ── DDL ───────────────────────────────────────────────────────────────────
307    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    // ── SESSION ───────────────────────────────────────────────────────────────
340    sql(
341        "stmt.set_reset",
342        "SET datafusion.execution.batch_size = 4096",
343    ),
344    // `USE public` keeps the default schema where the fixtures live, so it does
345    // not pollute later cases (its own unit test proves the schema switch).
346    sql("stmt.use", "USE public"),
347    // ── SHOW ──────────────────────────────────────────────────────────────────
348    sql("show.tables_databases_functions", "SHOW DATABASES"),
349    // ── TEMPORAL ──────────────────────────────────────────────────────────────
350    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    // ── PREPARED ──────────────────────────────────────────────────────────────
363    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    // ── OPERATION ─────────────────────────────────────────────────────────────
381    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    // ── ERROR ─────────────────────────────────────────────────────────────────
392    elsewhere("error.sqlstate", "sqlstate.rs SQLSTATE coverage"),
393    elsewhere(
394        "error.error_position",
395        "DataFusion message-only error position",
396    ),
397    // ── FLIGHT SQL ────────────────────────────────────────────────────────────
398    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    // ── STREAMING ─────────────────────────────────────────────────────────────
420    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    // ── INTROSPECTION ─────────────────────────────────────────────────────────
437    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/// A published coverage summary (the phase KPI).
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub struct CoverageReport {
448    /// Matrix features with a `Supported` or `Partial` **batch** cell.
449    pub batch_claimed: usize,
450    /// Of those, how many have a checklist case.
451    pub batch_covered: usize,
452    /// Checklist cases that are directly executed (`Coverage::Sql`).
453    pub executable_cases: usize,
454    /// FUNCTIONS-category features supported (batch) out of the total —
455    /// the function-parity dimension.
456    pub functions_supported: usize,
457    pub functions_total: usize,
458}
459
460impl CoverageReport {
461    /// Batch coverage as a percentage (0–100).
462    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
470/// Compute the coverage KPI from the matrix + checklist.
471pub 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    /// The matrix-to-test CI rule: every claimed batch cell has a checklist case.
530    #[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        // Every claimed batch feature must have a case (100% by construction of
547        // the rule above); assert the KPI is computed and the executable subset
548        // is substantial.
549        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    /// The public SQL reference page is generated from the matrix, never
571    /// hand-written; regenerate with
572    /// `KRISHIV_BLESS_SQL_DOCS=1 cargo test -p krishiv-sql coverage`.
573    #[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    /// The Krishiv-vs-Spark honesty page is generated from the matrix too.
590    #[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    /// Execute every `Coverage::Sql` checklist case against the fixture and
607    /// assert it plans + runs. This is the Spark-checklist suite proper.
608    #[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}