1use crate::PlatformDialect;
48use razeri_plugin_contract::{
49 ConstraintEnforcement, DataType, DdlRenderer, DialectCapabilities, DialectFamily, MergeShape,
50 ProcedureForm, SqlRenderer, UNKNOWN_PLATFORM,
51};
52use std::fmt;
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
56pub enum Severity {
57 Blocking,
60 Unanswered,
63 Note,
65}
66
67impl fmt::Display for Severity {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.write_str(match self {
70 Self::Blocking => "must fix",
71 Self::Unanswered => "unanswered",
72 Self::Note => "worth a look",
73 })
74 }
75}
76
77#[derive(Debug, Clone)]
80pub struct Finding {
81 pub severity: Severity,
83 pub what: String,
85 pub fix: String,
87}
88
89#[derive(Debug, Clone, Copy)]
91pub struct Obligation {
92 pub what: &'static str,
94 pub command: &'static str,
96}
97
98#[derive(Debug, Clone)]
100pub struct Report {
101 pub dialect: String,
103 pub findings: Vec<Finding>,
105}
106
107impl Report {
108 #[must_use]
112 pub fn is_clean(&self) -> bool {
113 !self.findings.iter().any(|f| f.severity != Severity::Note)
114 }
115
116 #[must_use]
118 pub fn count(&self, severity: Severity) -> usize {
119 self.findings.iter().filter(|f| f.severity == severity).count()
120 }
121}
122
123impl fmt::Display for Report {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 let name = &self.dialect;
126 if self.findings.is_empty() {
127 return writeln!(
128 f,
129 "{name}: nothing to fix. Every question Razeri asks a dialect, this one answers."
130 );
131 }
132 let blocking = self.count(Severity::Blocking);
133 let unanswered = self.count(Severity::Unanswered);
134 let notes = self.count(Severity::Note);
135 writeln!(f, "{name}: {blocking} to fix, {unanswered} unanswered, {notes} worth a look.")?;
136 for finding in &self.findings {
137 writeln!(f)?;
138 writeln!(f, " [{}] {}", finding.severity, finding.what)?;
139 writeln!(f, " → {}", finding.fix)?;
140 }
141 Ok(())
142 }
143}
144
145#[must_use]
152pub fn check(dialect: &dyn PlatformDialect) -> Report {
153 check_with_registry(dialect, &crate::dialect::PlatformRegistry::with_auto_discovered())
154}
155
156#[must_use]
159pub fn check_with_registry(
160 dialect: &dyn PlatformDialect,
161 registry: &crate::dialect::PlatformRegistry,
162) -> Report {
163 let info = dialect.info();
164 let mut findings = Vec::new();
165
166 check_registration(&info, registry, &mut findings);
167 check_family(info.dialect_family, &mut findings);
168 check_required_rendering(dialect, &mut findings);
169 check_decisions(dialect, &mut findings);
170 check_formula_renderer(dialect, &mut findings);
171 check_paired_facts(dialect, &mut findings);
172
173 findings.sort_by_key(|f| f.severity);
174 Report { dialect: info.name.to_owned(), findings }
175}
176
177fn check_registration(
188 info: &crate::dialect::DialectInfo,
189 registry: &crate::dialect::PlatformRegistry,
190 findings: &mut Vec<Finding>,
191) {
192 let name = info.name;
193 if matches!(
194 crate::resolve::resolve_dialect_by_name_with_registry(name, registry),
195 Err(crate::ResolveError::UnknownDialect { .. })
196 ) {
197 findings.push(Finding {
198 severity: Severity::Blocking,
199 what: format!(
200 "the name `{name}` does not resolve — nothing in this program registered it"
201 ),
202 fix: "add `inventory::submit! { DialectRegistration::new(\"<name>\", || \
203 Arc::new(MyDialect)) }` to your crate, and a `pub fn force_link() {}` beside \
204 it that the program using your dialect calls once. A crate nothing links is a \
205 crate the registry cannot see."
206 .to_owned(),
207 });
208 return;
209 }
210 for spelling in info.spellings() {
211 match crate::resolve::resolve_dialect_by_name_with_registry(spelling, registry) {
212 Ok(found) if found.info().name == name => {}
213 Ok(found) => findings.push(Finding {
214 severity: Severity::Blocking,
215 what: format!("`{spelling}` reaches `{}` rather than `{name}`", found.info().name),
216 fix: format!(
217 "the registry holds `{name}` under another key. Register it under its own \
218 name, or drop `{spelling}` from its aliases."
219 ),
220 }),
221 Err(crate::ResolveError::AmbiguousName { claimed_by, .. }) => findings.push(Finding {
222 severity: Severity::Blocking,
223 what: format!(
224 "`{spelling}` is claimed by more than one loaded platform ({}), so it names \
225 none of them — Razeri refuses that spelling rather than guessing which one \
226 a design meant",
227 claimed_by.join(" and ")
228 ),
229 fix: format!(
230 "give this dialect a name no other platform answers to. A platform name is \
231 read off the loaded dialects' own declared spellings, so a spelling another \
232 platform already claims is never `{name}`'s."
233 ),
234 }),
235 Err(_) => findings.push(Finding {
236 severity: Severity::Blocking,
237 what: format!(
238 "`{spelling}` is declared as a spelling of `{name}` but reaches no platform"
239 ),
240 fix: format!(
241 "the `{name}` in the registry is a different build of this dialect and does \
242 not declare `{spelling}`. Register the one you are grading."
243 ),
244 }),
245 }
246 }
247}
248
249fn check_family(family: DialectFamily, findings: &mut Vec<Finding>) {
252 if matches!(family, DialectFamily::Custom(name) if name.trim().is_empty()) {
253 findings.push(Finding {
254 severity: Severity::Blocking,
255 what: "this dialect's family is a custom one with no name".to_owned(),
256 fix: "give `DialectFamily::Custom` your engine's own name — it is what a report \
257 prints and what a reader looks up."
258 .to_owned(),
259 });
260 }
261}
262
263fn check_required_rendering(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
265 let declarable = declarable_types();
266 let empty_column: Vec<String> = declarable
267 .iter()
268 .filter(|dt| SqlRenderer::render_data_type(dialect, dt).trim().is_empty())
269 .map(|dt| format!("{dt:?}"))
270 .collect();
271 if !empty_column.is_empty() {
272 findings.push(Finding {
273 severity: Severity::Blocking,
274 what: format!(
275 "`render_data_type` answers nothing for {} of the types a design can declare",
276 empty_column.len()
277 ),
278 fix: "every declarable type needs a column spelling on this engine, even if the \
279 spelling is a wider type that stores it. A blank answer becomes a column \
280 declaration with no type in it."
281 .to_owned(),
282 });
283 }
284 let empty_cast: Vec<String> = declarable
285 .iter()
286 .filter(|dt| SqlRenderer::render_cast_type(dialect, dt).trim().is_empty())
287 .map(|dt| format!("{dt:?}"))
288 .collect();
289 if !empty_cast.is_empty() {
290 findings.push(Finding {
291 severity: Severity::Blocking,
292 what: format!(
293 "`render_cast_type` answers nothing for {} declarable types",
294 empty_cast.len()
295 ),
296 fix: "the generated load opens by casting every source column to its declared type, \
297 so a blank answer fails the load on its first statement."
298 .to_owned(),
299 });
300 }
301 let quoted = SqlRenderer::quote_identifier(dialect, "order");
302 if quoted == "order" {
303 findings.push(Finding {
304 severity: Severity::Blocking,
305 what: "`quote_identifier` returns the identifier unchanged, so a reserved word \
306 reaches this engine bare"
307 .to_owned(),
308 fix: "wrap the identifier in whatever this engine quotes with — double quotes, \
309 backticks, or brackets."
310 .to_owned(),
311 });
312 }
313 if SqlRenderer::render_current_timestamp(dialect).trim().is_empty() {
314 findings.push(Finding {
315 severity: Severity::Blocking,
316 what: "`render_current_timestamp` answers nothing, and every generated load stamps \
317 a row with it"
318 .to_owned(),
319 fix: "return this engine's own expression for the current moment.".to_owned(),
320 });
321 }
322}
323
324fn check_decisions(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
326 let spelling = SqlRenderer::sid_spelling(dialect);
327 if spelling.platform == UNKNOWN_PLATFORM {
328 findings.push(Finding {
329 severity: Severity::Blocking,
330 what: "this dialect states no surrogate-key spelling, so every key it renders calls \
331 an `xxhash64` its engine very likely does not have"
332 .to_owned(),
333 fix: "override `SqlRenderer::sid_spelling`. Say what this engine calls LOWER and \
334 TRIM, how it joins strings, how it casts to text, and how it spells a 64-bit \
335 hash. If it has no portable hash, name an artifact your prerequisites install \
336 — never the nearest-looking built-in, which keys a warehouse to a number \
337 nothing else can reproduce."
338 .to_owned(),
339 });
340 }
341
342 match SqlRenderer::procedure_form(dialect) {
343 ProcedureForm::Agnostic if SqlRenderer::is_agnostic_dialect(dialect) => {}
347 ProcedureForm::Agnostic => findings.push(Finding {
348 severity: Severity::Unanswered,
349 what: "this dialect states no stored-procedure grammar, so Razeri emits its own \
350 annotated form — which no engine parses"
351 .to_owned(),
352 fix: "override `SqlRenderer::procedure_form` with the grammar this engine speaks, \
353 or with `ProcedureForm::Unsupported(\"…\")` and a sentence saying what it \
354 offers instead."
355 .to_owned(),
356 }),
357 ProcedureForm::Unsupported(orientation) => {
358 check_refusal_copy("the stored-procedure refusal", orientation, findings);
359 }
360 _ => {}
361 }
362
363 if let Some(orientation) = SqlRenderer::table_function_form(dialect).refusal() {
364 check_refusal_copy("the table-function refusal", orientation, findings);
365 }
366
367 if let ConstraintEnforcement::DeclaredOnly(declared) =
370 DdlRenderer::constraint_enforcement(dialect)
371 && declared.note.split_whitespace().count() < 5
372 {
373 findings.push(Finding {
374 severity: Severity::Blocking,
375 what: "this dialect says its engine accepts constraints it does not enforce, and \
376 the note explaining that says almost nothing"
377 .to_owned(),
378 fix: "a reader of your rendered DDL sees a PRIMARY KEY that looks like a \
379 guarantee. Say what this engine really does with the declaration, and where \
380 the guarantee has to come from instead."
381 .to_owned(),
382 });
383 }
384
385 match DialectCapabilities::merge_shape(dialect) {
391 MergeShape::None => findings.push(Finding {
392 severity: Severity::Unanswered,
393 what: "this dialect has not stated its upsert shape, so Razeri writes no generated \
394 load for it"
395 .to_owned(),
396 fix: "if this engine has a MERGE or an upsert statement, say which in \
397 `DialectCapabilities::merge_shape`. If it genuinely has neither, answer \
398 `MergeShape::Unsupported` with your engine's own sentence — a refusal an \
399 operator can act on, rather than a silence."
400 .to_owned(),
401 }),
402 MergeShape::Unsupported(orientation) => {
403 check_refusal_copy("the upsert refusal", orientation, findings);
404 }
405 _ => {}
406 }
407}
408
409fn check_formula_renderer(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
412 let formula = SqlRenderer::to_formula_renderer(dialect);
413 let dialect_name = SqlRenderer::dialect_name(dialect);
414 if formula.dialect_name() != dialect_name && dialect_name != "ansi" {
415 findings.push(Finding {
416 severity: Severity::Blocking,
417 what: "`to_formula_renderer` was not overridden, so every formula in every design \
418 renders through Razeri's ANSI defaults instead of this dialect"
419 .to_owned(),
420 fix: "return `Box::new(Self)`. It is one line, nothing fails without it, and every \
421 formula this engine sees is silently somebody else's."
422 .to_owned(),
423 });
424 }
425}
426
427fn check_refusal_copy(subject: &str, copy: &str, findings: &mut Vec<Finding>) {
429 if copy.trim().is_empty() {
430 findings.push(Finding {
431 severity: Severity::Blocking,
432 what: format!("{subject} has no words in it"),
433 fix: "say what this engine offers instead. A reader who hits a refusal needs the \
434 next thing to try, and it has to be about their engine."
435 .to_owned(),
436 });
437 return;
438 }
439 if copy.split_whitespace().count() < 5 {
440 findings.push(Finding {
441 severity: Severity::Note,
442 what: format!("{subject} is very short"),
443 fix: "a refusal earns its place by naming the alternative — what this engine has \
444 instead of the thing it cannot do."
445 .to_owned(),
446 });
447 }
448}
449
450fn check_paired_facts(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
452 let caps: &dyn DialectCapabilities = dialect;
453 let ren: &dyn SqlRenderer = dialect;
454 let ddl: &dyn DdlRenderer = dialect;
455 let mut disagreements: Vec<&'static str> = Vec::new();
456
457 macro_rules! pair {
458 ($m:ident, $renderer_value:expr) => {
459 if caps.$m() != $renderer_value {
460 disagreements.push(stringify!($m));
461 }
462 };
463 }
464 pair!(supports_on_conflict, ren.supports_on_conflict());
465 pair!(supports_insert_overwrite, ren.supports_insert_overwrite());
466 pair!(supports_update_from, ren.supports_update_from());
467 pair!(supports_named_windows, ren.supports_named_windows());
468 pair!(supports_compound_assignment, ren.supports_compound_assignment());
469 pair!(supports_window_frame_exclude, ren.supports_window_frame_exclude());
470 pair!(supports_merge_not_matched_by_source, ren.supports_merge_not_matched_by_source());
471 pair!(merge_not_matched_keyword, ren.merge_not_matched_keyword());
472 pair!(uses_alter_table_update, ren.uses_alter_table_update());
473 pair!(supports_multi_table_insert, ren.supports_multi_table_insert());
474 pair!(supports_cascade_drop, ddl.supports_cascade_drop());
475 pair!(supports_sync_drop, ddl.supports_sync_drop());
476 pair!(supports_force_drop, ddl.supports_force_drop());
477
478 if caps.supports_table_functions() != ren.table_function_form().refusal().is_none() {
487 disagreements.push("supports_table_functions");
488 }
489
490 if !disagreements.is_empty() {
491 findings.push(Finding {
492 severity: Severity::Blocking,
493 what: format!(
494 "this dialect declares {} fact(s) two ways and the two disagree: {}",
495 disagreements.len(),
496 disagreements.join(", ")
497 ),
498 fix: "each of these is declared once on the capability trait and once in the \
499 renderer hierarchy, and Razeri reads both. Decide which is true of your engine \
500 and move the other one to match."
501 .to_owned(),
502 });
503 }
504}
505
506#[must_use]
514pub const fn repository_obligations() -> &'static [Obligation] {
515 &[
516 Obligation {
517 what: "classify the new crate — every workspace member is named exactly once, and \
518 dialect crates are tier 1",
519 command: "scripts/check-tier-boundary.sh",
520 },
521 Obligation {
522 what: "freeze the DDL your platform renders for all eleven corpus designs",
523 command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize --test golden_ddl",
524 },
525 Obligation {
526 what: "freeze the derived-column projections your platform renders",
527 command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize --test expression_matrix",
528 },
529 Obligation {
530 what: "freeze the transaction envelope your platform renders",
531 command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize \
532 --test load_transaction_matrix",
533 },
534 Obligation {
535 what: "regenerate the typed platform constants for Python, TypeScript, Go, C# and \
536 Java",
537 command: "cargo run -p razeri-dialects --bin gen-platform-constants",
538 },
539 Obligation {
540 what: "give every registry-walking roster a row for your platform — they fail one \
541 at a time otherwise",
542 command: "cargo test -p razeri-materialize -p razeri-sql",
543 },
544 Obligation {
545 what: "prove the artifacts deploy and run: an execution leg per routine kind and \
546 per generated load is what finished means here",
547 command: "cargo test -p integration-tests",
548 },
549 ]
550}
551
552fn declarable_types() -> Vec<DataType> {
555 vec![
556 DataType::tinyint(),
557 DataType::smallint(),
558 DataType::int(),
559 DataType::bigint(),
560 DataType::double(),
561 DataType::boolean(),
562 DataType::text(),
563 DataType::varchar(200),
564 DataType::uuid(),
565 DataType::json(),
566 DataType::date(),
567 DataType::timestamp(),
568 DataType::timestamp_tz(),
569 DataType::decimal(18, 2),
570 ]
571}
572
573#[cfg(test)]
574mod tests {
575 use super::*;
576 use crate::dialect::DialectInfo;
577 use razeri_plugin_contract::{
578 DmlRenderer, ExpressionRenderer, FormulaRenderer, MergeRestrictions, ProcedureRenderer,
579 ReplicationRenderer, ScalarFunctionForm, SidSpelling, StorageRenderer, TaskRenderer,
580 TextCast,
581 };
582 use std::marker::PhantomData;
583
584 struct BrokenPlugin;
593
594 impl SqlRenderer for BrokenPlugin {
595 fn render_data_type(&self, _dt: &DataType) -> String {
596 String::new()
597 }
598 fn quote_identifier(&self, name: &str) -> String {
599 name.to_owned()
600 }
601 fn render_cast_type(&self, _dt: &DataType) -> String {
602 String::new()
603 }
604 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
605 name.to_owned()
606 }
607 fn render_current_timestamp(&self) -> String {
608 String::new()
609 }
610 fn dialect_name(&self) -> &'static str {
611 "brokendb"
612 }
613 fn supports_on_conflict(&self) -> bool {
616 true
617 }
618 }
619 impl ExpressionRenderer for BrokenPlugin {}
620 impl DdlRenderer for BrokenPlugin {}
621 impl DmlRenderer for BrokenPlugin {}
622 impl ProcedureRenderer for BrokenPlugin {}
623 impl FormulaRenderer for BrokenPlugin {}
624 impl TaskRenderer for BrokenPlugin {}
625 impl ReplicationRenderer for BrokenPlugin {}
626 impl StorageRenderer for BrokenPlugin {}
627 impl DialectCapabilities for BrokenPlugin {
628 fn supports_on_conflict(&self) -> bool {
629 false
630 }
631 }
632 impl PlatformDialect for BrokenPlugin {
633 fn info(&self) -> DialectInfo {
634 DialectInfo {
635 name: "brokendb",
636 display_name: "Broken DB",
637 aliases: &[],
638 dialect_family: DialectFamily::Custom(""),
639 family: "Broken",
640 is_cloud_native: false,
641 is_olap: false,
642 is_postgres_compatible: false,
643 }
644 }
645 }
646
647 trait FixtureName: Send + Sync + 'static {
656 const NAME: &'static str;
658 }
659
660 struct Unlinked;
663 impl FixtureName for Unlinked {
664 const NAME: &'static str = "healthydb";
665 }
666
667 struct Outside;
670 impl FixtureName for Outside {
671 const NAME: &'static str = "outsidedb";
672 }
673
674 struct Answered<N: FixtureName>(PhantomData<N>);
675
676 impl<N: FixtureName> Answered<N> {
677 const fn new() -> Self {
678 Self(PhantomData)
679 }
680 }
681
682 type HealthyPlugin = Answered<Unlinked>;
683 type OutsidePlugin = Answered<Outside>;
684
685 inventory::submit! {
686 crate::builder::DialectRegistration::new(
687 Outside::NAME,
688 || std::sync::Arc::new(OutsidePlugin::new()),
689 )
690 }
691
692 impl<N: FixtureName> SqlRenderer for Answered<N> {
693 fn render_data_type(&self, dt: &DataType) -> String {
694 razeri_plugin_contract::agnostic::render_data_type_ansi(dt)
695 }
696 fn quote_identifier(&self, name: &str) -> String {
697 format!("\"{name}\"")
698 }
699 fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
700 schema.map_or_else(
701 || self.quote_identifier(name),
702 |s| format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
703 )
704 }
705 fn render_current_timestamp(&self) -> String {
706 "NOW()".to_owned()
707 }
708 fn dialect_name(&self) -> &'static str {
709 N::NAME
710 }
711 fn sid_spelling(&self) -> SidSpelling {
712 SidSpelling {
713 platform: N::NAME,
714 hash: ("answered_xxhash64(", ")"),
715 cast_type: TextCast::Direct("STRING"),
716 through_helper: false,
717 ..razeri_plugin_contract::ANSI_SID_SPELLING
718 }
719 }
720 fn procedure_form(&self) -> ProcedureForm {
721 ProcedureForm::Unsupported(
722 "This engine has no stored procedures; write the load as a scheduled query \
723 instead.",
724 )
725 }
726 fn scalar_function_form(&self) -> ScalarFunctionForm {
727 ScalarFunctionForm::PostgreSql
728 }
729 fn to_formula_renderer(&self) -> Box<dyn FormulaRenderer> {
730 Box::new(Self::new())
731 }
732 }
733 impl<N: FixtureName> ExpressionRenderer for Answered<N> {}
734 impl<N: FixtureName> DdlRenderer for Answered<N> {}
735 impl<N: FixtureName> DmlRenderer for Answered<N> {}
736 impl<N: FixtureName> ProcedureRenderer for Answered<N> {}
737 impl<N: FixtureName> FormulaRenderer for Answered<N> {}
738 impl<N: FixtureName> TaskRenderer for Answered<N> {}
739 impl<N: FixtureName> ReplicationRenderer for Answered<N> {}
740 impl<N: FixtureName> StorageRenderer for Answered<N> {}
741 impl<N: FixtureName> DialectCapabilities for Answered<N> {
742 fn merge_shape(&self) -> MergeShape {
743 MergeShape::Merge(MergeRestrictions::none())
744 }
745 }
746 impl<N: FixtureName> PlatformDialect for Answered<N> {
747 fn info(&self) -> DialectInfo {
748 DialectInfo {
749 name: N::NAME,
750 display_name: "An Answered Engine",
751 aliases: &[],
754 dialect_family: DialectFamily::Custom(N::NAME),
755 family: "Answered",
756 is_cloud_native: false,
757 is_olap: false,
758 is_postgres_compatible: false,
759 }
760 }
761 }
762
763 struct Twin;
769
770 impl SqlRenderer for Twin {
771 fn render_data_type(&self, dt: &DataType) -> String {
772 razeri_plugin_contract::agnostic::render_data_type_ansi(dt)
773 }
774 fn quote_identifier(&self, name: &str) -> String {
775 format!("\"{name}\"")
776 }
777 fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
778 self.quote_identifier(name)
779 }
780 fn render_current_timestamp(&self) -> String {
781 "NOW()".to_owned()
782 }
783 }
784 impl ExpressionRenderer for Twin {}
785 impl DdlRenderer for Twin {}
786 impl DmlRenderer for Twin {}
787 impl ProcedureRenderer for Twin {}
788 impl FormulaRenderer for Twin {}
789 impl TaskRenderer for Twin {}
790 impl ReplicationRenderer for Twin {}
791 impl StorageRenderer for Twin {}
792 impl DialectCapabilities for Twin {}
793 impl PlatformDialect for Twin {
794 fn info(&self) -> DialectInfo {
795 DialectInfo {
796 name: "twindb",
797 display_name: "Twin DB",
798 aliases: &["healthydb"],
800 dialect_family: DialectFamily::Custom("twindb"),
801 family: "Twin",
802 is_cloud_native: false,
803 is_olap: false,
804 is_postgres_compatible: false,
805 }
806 }
807 }
808
809 fn mentions(report: &Report, needle: &str) -> bool {
811 report.findings.iter().any(|f| f.what.contains(needle) || f.fix.contains(needle))
812 }
813
814 #[test]
818 fn the_kit_catches_a_deliberately_broken_plugin() {
819 let report = check(&BrokenPlugin);
820 assert!(!report.is_clean(), "the broken plugin reported clean:\n{report}");
821
822 assert!(mentions(&report, "does not resolve"), "unregistered:\n{report}");
823 assert!(mentions(&report, "custom one with no name"), "unnamed family:\n{report}");
824 assert!(mentions(&report, "render_data_type"), "empty column types:\n{report}");
825 assert!(mentions(&report, "render_cast_type"), "empty cast types:\n{report}");
826 assert!(mentions(&report, "identifier unchanged"), "no quoting:\n{report}");
827 assert!(mentions(&report, "render_current_timestamp"), "no now():\n{report}");
828 assert!(mentions(&report, "surrogate-key spelling"), "unspelled key:\n{report}");
829 assert!(mentions(&report, "stored-procedure grammar"), "no procedure form:\n{report}");
830 assert!(mentions(&report, "upsert shape"), "no merge shape:\n{report}");
831 assert!(mentions(&report, "to_formula_renderer"), "formula renderer:\n{report}");
832 assert!(mentions(&report, "supports_on_conflict"), "paired fact:\n{report}");
833
834 assert!(
835 report.count(Severity::Blocking) >= 8,
836 "the broken plugin owes more than {} blocking fixes:\n{report}",
837 report.count(Severity::Blocking)
838 );
839 }
840
841 #[test]
845 fn the_kit_reports_only_registration_on_a_plugin_that_answered_everything() {
846 let report = check(&HealthyPlugin::new());
847 let unregistered: Vec<&Finding> =
848 report.findings.iter().filter(|f| f.what.contains("does not resolve")).collect();
849 assert_eq!(unregistered.len(), 1, "the registration finding is the honest one here");
850 assert_eq!(
851 report.findings.len(),
852 1,
853 "a plugin that answered every question owes nothing else:\n{report}"
854 );
855 }
856
857 #[test]
863 fn a_dialect_registered_under_its_own_name_reaches_a_clean_report() {
864 assert!(
865 crate::resolve_dialect_by_name(Outside::NAME).is_ok(),
866 "the fixture registers through the registry, which is the whole path being graded"
867 );
868 let registry = crate::dialect::PlatformRegistry::with_auto_discovered();
872 assert!(
873 registry
874 .list()
875 .iter()
876 .filter(|info| info.name != Outside::NAME)
877 .all(|info| !info.answers_to(Outside::NAME)),
878 "the fixture's name must be one no other platform claims"
879 );
880
881 let report = check(&OutsidePlugin::new());
882 assert!(report.findings.is_empty(), "nothing is owed here:\n{report}");
883 assert!(report.is_clean(), "and the report says so:\n{report}");
884 assert_eq!(report.count(Severity::Blocking), 0, "{report}");
885 assert!(
886 report.to_string().contains("nothing to fix"),
887 "a clean dialect is told it is clean:\n{report}"
888 );
889 }
890
891 #[test]
898 fn a_platform_is_never_reachable_by_an_alias_but_not_by_its_own_name() {
899 let registry = crate::dialect::PlatformRegistry::with_auto_discovered();
900 for info in registry.list() {
901 assert_eq!(
902 info.spellings().first().copied(),
903 Some(info.name),
904 "a platform's own name leads its spellings"
905 );
906 assert!(info.answers_to(info.name), "{} answers to its own name", info.name);
907 }
908 }
909
910 #[test]
916 fn a_name_razeri_reads_as_another_platform_is_blocking_and_says_which() {
917 let mut registry = crate::dialect::PlatformRegistry::new();
918 registry.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
919 registry.register("twindb".to_owned(), std::sync::Arc::new(Twin));
920
921 let report = check_with_registry(&Twin, ®istry);
922 let collision = report
923 .findings
924 .iter()
925 .find(|f| f.what.contains("claimed by more than one loaded platform"))
926 .unwrap_or_else(|| panic!("the collision is reported:\n{report}"));
927 assert_eq!(collision.severity, Severity::Blocking, "{collision:?}");
928 assert!(collision.what.contains("healthydb"), "the other platform is named: {collision:?}");
929 assert!(collision.what.contains("twindb"), "{collision:?}");
930 assert!(collision.fix.contains("name no other platform answers to"), "{collision:?}");
931 assert!(
932 !mentions(&report, "does not resolve"),
933 "a collision is not an unregistered platform:\n{report}"
934 );
935 }
936
937 #[test]
941 fn registration_is_silent_on_a_platform_reached_under_its_own_name() {
942 let mut registry = crate::dialect::PlatformRegistry::new();
943 registry.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
944
945 let mut findings = Vec::new();
946 check_registration(&HealthyPlugin::new().info(), ®istry, &mut findings);
947 assert!(findings.is_empty(), "{findings:?}");
948
949 for spelling in ["healthydb", "HealthyDB", " healthydb "] {
950 assert!(
951 crate::resolve::resolve_dialect_by_name_with_registry(spelling, ®istry).is_ok(),
952 "'{spelling}' is the same platform"
953 );
954 }
955 assert!(
956 crate::resolve::resolve_dialect_by_name_with_registry("nosuchdb", ®istry).is_err()
957 );
958 }
959
960 #[test]
965 fn a_dialect_the_host_loaded_reports_clean_against_its_own_registry() {
966 let mut registry = crate::dialect::PlatformRegistry::new();
967 registry.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
968
969 let report = check_with_registry(&HealthyPlugin::new(), ®istry);
970 assert!(
971 report.is_clean(),
972 "a loaded dialect that answered everything owes nothing:\n{report}"
973 );
974 }
975
976 #[test]
980 fn no_fix_line_sends_an_author_into_razeris_own_tree() {
981 let mut collided = crate::dialect::PlatformRegistry::new();
982 collided.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
983 collided.register("twindb".to_owned(), std::sync::Arc::new(Twin));
984
985 let mut fixes: Vec<String> = Vec::new();
986 for report in [
987 check(&BrokenPlugin),
988 check(&HealthyPlugin::new()),
989 check(&OutsidePlugin::new()),
990 check_with_registry(&Twin, &collided),
993 check_with_registry(&Twin, &crate::dialect::PlatformRegistry::new()),
994 ] {
995 fixes.extend(report.findings.iter().map(|f| f.fix.clone()));
996 }
997 assert!(fixes.len() >= 10, "the sweep reached the findings: {}", fixes.len());
998 for fix in &fixes {
999 for path in ["schemas/", "crates/", ".json", "scripts/", "razeri-design"] {
1000 assert!(
1001 !fix.contains(path),
1002 "a fix line names `{path}`, which is not in the author's crate:\n {fix}"
1003 );
1004 }
1005 }
1006 }
1007
1008 #[test]
1012 fn a_stated_upsert_refusal_is_graded_as_copy_and_a_silent_one_as_unanswered() {
1013 let mut mute = Vec::new();
1014 check_refusal_copy("the upsert refusal", "no merge", &mut mute);
1015 assert_eq!(mute.len(), 1, "a three-word refusal is a wall: {mute:?}");
1016 assert!(mute[0].what.contains("the upsert refusal"), "{mute:?}");
1017
1018 let silence = check(&BrokenPlugin);
1019 assert!(
1020 silence
1021 .findings
1022 .iter()
1023 .any(|f| f.severity == Severity::Unanswered && f.what.contains("upsert shape")),
1024 "a dialect that never answered is unanswered, not wrong:\n{silence}"
1025 );
1026 assert!(
1027 silence.findings.iter().any(|f| f.fix.contains("MergeShape::Unsupported")),
1028 "the fix line names the way to state a measured refusal:\n{silence}"
1029 );
1030 }
1031
1032 #[test]
1036 fn the_report_reads_as_a_summary_then_a_fix_list() {
1037 let text = check(&BrokenPlugin).to_string();
1038 let first = text.lines().next().expect("a summary line");
1039 assert!(first.starts_with("brokendb:"), "{first}");
1040 assert!(first.contains("to fix"), "{first}");
1041 assert!(text.contains(" → "), "every finding names the fix:\n{text}");
1042 }
1043
1044 #[test]
1047 fn every_repository_obligation_names_a_command_and_a_reason() {
1048 let obligations = repository_obligations();
1049 assert!(obligations.len() >= 5);
1050 for o in obligations {
1051 assert!(!o.command.trim().is_empty(), "{o:?}");
1052 assert!(o.what.split_whitespace().count() >= 5, "{o:?}");
1053 }
1054 }
1055}