use crate::PlatformDialect;
use razeri_plugin_contract::{
ConstraintEnforcement, DataType, DdlRenderer, DialectCapabilities, DialectFamily, MergeShape,
ProcedureForm, SqlRenderer, UNKNOWN_PLATFORM,
};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Blocking,
Unanswered,
Note,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Blocking => "must fix",
Self::Unanswered => "unanswered",
Self::Note => "worth a look",
})
}
}
#[derive(Debug, Clone)]
pub struct Finding {
pub severity: Severity,
pub what: String,
pub fix: String,
}
#[derive(Debug, Clone, Copy)]
pub struct Obligation {
pub what: &'static str,
pub command: &'static str,
}
#[derive(Debug, Clone)]
pub struct Report {
pub dialect: String,
pub findings: Vec<Finding>,
}
impl Report {
#[must_use]
pub fn is_clean(&self) -> bool {
!self.findings.iter().any(|f| f.severity != Severity::Note)
}
#[must_use]
pub fn count(&self, severity: Severity) -> usize {
self.findings.iter().filter(|f| f.severity == severity).count()
}
}
impl fmt::Display for Report {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = &self.dialect;
if self.findings.is_empty() {
return writeln!(
f,
"{name}: nothing to fix. Every question Razeri asks a dialect, this one answers."
);
}
let blocking = self.count(Severity::Blocking);
let unanswered = self.count(Severity::Unanswered);
let notes = self.count(Severity::Note);
writeln!(f, "{name}: {blocking} to fix, {unanswered} unanswered, {notes} worth a look.")?;
for finding in &self.findings {
writeln!(f)?;
writeln!(f, " [{}] {}", finding.severity, finding.what)?;
writeln!(f, " → {}", finding.fix)?;
}
Ok(())
}
}
#[must_use]
pub fn check(dialect: &dyn PlatformDialect) -> Report {
let info = dialect.info();
let mut findings = Vec::new();
check_registration(info.name, &mut findings);
check_family(info.dialect_family, &mut findings);
check_required_rendering(dialect, &mut findings);
check_decisions(dialect, &mut findings);
check_formula_renderer(dialect, &mut findings);
check_paired_facts(dialect, &mut findings);
findings.sort_by_key(|f| f.severity);
Report { dialect: info.name.to_owned(), findings }
}
fn check_registration(name: &str, findings: &mut Vec<Finding>) {
if crate::resolve_dialect_by_name(name).is_err() {
findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"the name `{name}` does not resolve — nothing in this program registered it"
),
fix: "add `inventory::submit! { DialectRegistration::new(\"<name>\", || \
Arc::new(MyDialect)) }` to your crate, and a `pub fn force_link() {}` beside \
it that the program using your dialect calls once. A crate nothing links is a \
crate the registry cannot see."
.to_owned(),
});
return;
}
if !crate::resolve::alias_spellings()
.iter()
.any(|(spelling, canonical)| *spelling == name && *canonical == name)
{
findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"`{name}` is registered but the resolver's spelling table does not carry it, so \
a design naming this platform is refused at the door"
),
fix: "add your platform's accepted spellings to the resolver's alias table, including its \
own \
name, and add the same spellings to `schemas/razeri-design.schema.json`'s \
platform list — a test holds the two set-equal."
.to_owned(),
});
}
}
fn check_family(family: DialectFamily, findings: &mut Vec<Finding>) {
if matches!(family, DialectFamily::Custom(name) if name.trim().is_empty()) {
findings.push(Finding {
severity: Severity::Blocking,
what: "this dialect's family is a custom one with no name".to_owned(),
fix: "give `DialectFamily::Custom` your engine's own name — it is what a report \
prints and what a reader looks up."
.to_owned(),
});
}
}
fn check_required_rendering(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
let declarable = declarable_types();
let empty_column: Vec<String> = declarable
.iter()
.filter(|dt| SqlRenderer::render_data_type(dialect, dt).trim().is_empty())
.map(|dt| format!("{dt:?}"))
.collect();
if !empty_column.is_empty() {
findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"`render_data_type` answers nothing for {} of the types a design can declare",
empty_column.len()
),
fix: "every declarable type needs a column spelling on this engine, even if the \
spelling is a wider type that stores it. A blank answer becomes a column \
declaration with no type in it."
.to_owned(),
});
}
let empty_cast: Vec<String> = declarable
.iter()
.filter(|dt| SqlRenderer::render_cast_type(dialect, dt).trim().is_empty())
.map(|dt| format!("{dt:?}"))
.collect();
if !empty_cast.is_empty() {
findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"`render_cast_type` answers nothing for {} declarable types",
empty_cast.len()
),
fix: "the generated load opens by casting every source column to its declared type, \
so a blank answer fails the load on its first statement."
.to_owned(),
});
}
let quoted = SqlRenderer::quote_identifier(dialect, "order");
if quoted == "order" {
findings.push(Finding {
severity: Severity::Blocking,
what: "`quote_identifier` returns the identifier unchanged, so a reserved word \
reaches this engine bare"
.to_owned(),
fix: "wrap the identifier in whatever this engine quotes with — double quotes, \
backticks, or brackets."
.to_owned(),
});
}
if SqlRenderer::render_current_timestamp(dialect).trim().is_empty() {
findings.push(Finding {
severity: Severity::Blocking,
what: "`render_current_timestamp` answers nothing, and every generated load stamps \
a row with it"
.to_owned(),
fix: "return this engine's own expression for the current moment.".to_owned(),
});
}
}
fn check_decisions(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
let spelling = SqlRenderer::sid_spelling(dialect);
if spelling.platform == UNKNOWN_PLATFORM {
findings.push(Finding {
severity: Severity::Blocking,
what: "this dialect states no surrogate-key spelling, so every key it renders calls \
an `xxhash64` its engine very likely does not have"
.to_owned(),
fix: "override `SqlRenderer::sid_spelling`. Say what this engine calls LOWER and \
TRIM, how it joins strings, how it casts to text, and how it spells a 64-bit \
hash. If it has no portable hash, name an artifact your prerequisites install \
— never the nearest-looking built-in, which keys a warehouse to a number \
nothing else can reproduce."
.to_owned(),
});
}
match SqlRenderer::procedure_form(dialect) {
ProcedureForm::Agnostic if SqlRenderer::is_agnostic_dialect(dialect) => {}
ProcedureForm::Agnostic => findings.push(Finding {
severity: Severity::Unanswered,
what: "this dialect states no stored-procedure grammar, so Razeri emits its own \
annotated form — which no engine parses"
.to_owned(),
fix: "override `SqlRenderer::procedure_form` with the grammar this engine speaks, \
or with `ProcedureForm::Unsupported(\"…\")` and a sentence saying what it \
offers instead."
.to_owned(),
}),
ProcedureForm::Unsupported(orientation) => {
check_refusal_copy("the stored-procedure refusal", orientation, findings);
}
_ => {}
}
if let Some(orientation) = SqlRenderer::table_function_form(dialect).refusal() {
check_refusal_copy("the table-function refusal", orientation, findings);
}
if let ConstraintEnforcement::DeclaredOnly(declared) =
DdlRenderer::constraint_enforcement(dialect)
&& declared.note.split_whitespace().count() < 5
{
findings.push(Finding {
severity: Severity::Blocking,
what: "this dialect says its engine accepts constraints it does not enforce, and \
the note explaining that says almost nothing"
.to_owned(),
fix: "a reader of your rendered DDL sees a PRIMARY KEY that looks like a \
guarantee. Say what this engine really does with the declaration, and where \
the guarantee has to come from instead."
.to_owned(),
});
}
match DialectCapabilities::merge_shape(dialect) {
MergeShape::None => findings.push(Finding {
severity: Severity::Unanswered,
what: "this dialect has not stated its upsert shape, so Razeri writes no generated \
load for it"
.to_owned(),
fix: "if this engine has a MERGE or an upsert statement, say which in \
`DialectCapabilities::merge_shape`. If it genuinely has neither, answer \
`MergeShape::Unsupported` with your engine's own sentence — a refusal an \
operator can act on, rather than a silence."
.to_owned(),
}),
MergeShape::Unsupported(orientation) => {
check_refusal_copy("the upsert refusal", orientation, findings);
}
_ => {}
}
}
fn check_formula_renderer(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
let formula = SqlRenderer::to_formula_renderer(dialect);
let dialect_name = SqlRenderer::dialect_name(dialect);
if formula.dialect_name() != dialect_name && dialect_name != "ansi" {
findings.push(Finding {
severity: Severity::Blocking,
what: "`to_formula_renderer` was not overridden, so every formula in every design \
renders through Razeri's ANSI defaults instead of this dialect"
.to_owned(),
fix: "return `Box::new(Self)`. It is one line, nothing fails without it, and every \
formula this engine sees is silently somebody else's."
.to_owned(),
});
}
}
fn check_refusal_copy(subject: &str, copy: &str, findings: &mut Vec<Finding>) {
if copy.trim().is_empty() {
findings.push(Finding {
severity: Severity::Blocking,
what: format!("{subject} has no words in it"),
fix: "say what this engine offers instead. A reader who hits a refusal needs the \
next thing to try, and it has to be about their engine."
.to_owned(),
});
return;
}
if copy.split_whitespace().count() < 5 {
findings.push(Finding {
severity: Severity::Note,
what: format!("{subject} is very short"),
fix: "a refusal earns its place by naming the alternative — what this engine has \
instead of the thing it cannot do."
.to_owned(),
});
}
}
fn check_paired_facts(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
let caps: &dyn DialectCapabilities = dialect;
let ren: &dyn SqlRenderer = dialect;
let ddl: &dyn DdlRenderer = dialect;
let mut disagreements: Vec<&'static str> = Vec::new();
macro_rules! pair {
($m:ident, $renderer_value:expr) => {
if caps.$m() != $renderer_value {
disagreements.push(stringify!($m));
}
};
}
pair!(supports_on_conflict, ren.supports_on_conflict());
pair!(supports_insert_overwrite, ren.supports_insert_overwrite());
pair!(supports_update_from, ren.supports_update_from());
pair!(supports_named_windows, ren.supports_named_windows());
pair!(supports_compound_assignment, ren.supports_compound_assignment());
pair!(supports_window_frame_exclude, ren.supports_window_frame_exclude());
pair!(supports_merge_not_matched_by_source, ren.supports_merge_not_matched_by_source());
pair!(merge_not_matched_keyword, ren.merge_not_matched_keyword());
pair!(uses_alter_table_update, ren.uses_alter_table_update());
pair!(supports_multi_table_insert, ren.supports_multi_table_insert());
pair!(supports_cascade_drop, ddl.supports_cascade_drop());
pair!(supports_sync_drop, ddl.supports_sync_drop());
pair!(supports_force_drop, ddl.supports_force_drop());
if caps.supports_table_functions() != ren.table_function_form().refusal().is_none() {
disagreements.push("supports_table_functions");
}
if !disagreements.is_empty() {
findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"this dialect declares {} fact(s) two ways and the two disagree: {}",
disagreements.len(),
disagreements.join(", ")
),
fix: "each of these is declared once on the capability trait and once in the \
renderer hierarchy, and Razeri reads both. Decide which is true of your engine \
and move the other one to match."
.to_owned(),
});
}
}
#[must_use]
pub const fn repository_obligations() -> &'static [Obligation] {
&[
Obligation {
what: "classify the new crate — every workspace member is named exactly once, and \
dialect crates are tier 1",
command: "scripts/check-tier-boundary.sh",
},
Obligation {
what: "freeze the DDL your platform renders for all eleven corpus designs",
command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize --test golden_ddl",
},
Obligation {
what: "freeze the derived-column projections your platform renders",
command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize --test expression_matrix",
},
Obligation {
what: "freeze the transaction envelope your platform renders",
command: "UPDATE_GOLDEN=1 cargo test -p razeri-materialize \
--test load_transaction_matrix",
},
Obligation {
what: "regenerate the typed platform constants for Python, TypeScript, Go, C# and \
Java",
command: "cargo run -p razeri-dialects --bin gen-platform-constants",
},
Obligation {
what: "give every registry-walking roster a row for your platform — they fail one \
at a time otherwise",
command: "cargo test -p razeri-materialize -p razeri-sql",
},
Obligation {
what: "prove the artifacts deploy and run: an execution leg per routine kind and \
per generated load is what finished means here",
command: "cargo test -p integration-tests",
},
]
}
fn declarable_types() -> Vec<DataType> {
vec![
DataType::tinyint(),
DataType::smallint(),
DataType::int(),
DataType::bigint(),
DataType::double(),
DataType::boolean(),
DataType::text(),
DataType::varchar(200),
DataType::uuid(),
DataType::json(),
DataType::date(),
DataType::timestamp(),
DataType::timestamp_tz(),
DataType::decimal(18, 2),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialect::DialectInfo;
use razeri_plugin_contract::{
DmlRenderer, ExpressionRenderer, FormulaRenderer, MergeRestrictions, ProcedureRenderer,
ReplicationRenderer, ScalarFunctionForm, SidSpelling, StorageRenderer, TaskRenderer,
TextCast,
};
struct BrokenPlugin;
impl SqlRenderer for BrokenPlugin {
fn render_data_type(&self, _dt: &DataType) -> String {
String::new()
}
fn quote_identifier(&self, name: &str) -> String {
name.to_owned()
}
fn render_cast_type(&self, _dt: &DataType) -> String {
String::new()
}
fn qualify_name(&self, _schema: Option<&str>, name: &str) -> String {
name.to_owned()
}
fn render_current_timestamp(&self) -> String {
String::new()
}
fn dialect_name(&self) -> &'static str {
"brokendb"
}
fn supports_on_conflict(&self) -> bool {
true
}
}
impl ExpressionRenderer for BrokenPlugin {}
impl DdlRenderer for BrokenPlugin {}
impl DmlRenderer for BrokenPlugin {}
impl ProcedureRenderer for BrokenPlugin {}
impl FormulaRenderer for BrokenPlugin {}
impl TaskRenderer for BrokenPlugin {}
impl ReplicationRenderer for BrokenPlugin {}
impl StorageRenderer for BrokenPlugin {}
impl DialectCapabilities for BrokenPlugin {
fn supports_on_conflict(&self) -> bool {
false
}
}
impl PlatformDialect for BrokenPlugin {
fn info(&self) -> DialectInfo {
DialectInfo {
name: "brokendb",
display_name: "Broken DB",
dialect_family: DialectFamily::Custom(""),
family: "Broken",
is_cloud_native: false,
is_olap: false,
is_postgres_compatible: false,
}
}
}
struct HealthyPlugin;
impl SqlRenderer for HealthyPlugin {
fn render_data_type(&self, dt: &DataType) -> String {
razeri_plugin_contract::agnostic::render_data_type_ansi(dt)
}
fn quote_identifier(&self, name: &str) -> String {
format!("\"{name}\"")
}
fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
schema.map_or_else(
|| self.quote_identifier(name),
|s| format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
)
}
fn render_current_timestamp(&self) -> String {
"NOW()".to_owned()
}
fn dialect_name(&self) -> &'static str {
"healthydb"
}
fn sid_spelling(&self) -> SidSpelling {
SidSpelling {
platform: "healthydb",
hash: ("healthy_xxhash64(", ")"),
cast_type: TextCast::Direct("STRING"),
through_helper: false,
..razeri_plugin_contract::ANSI_SID_SPELLING
}
}
fn procedure_form(&self) -> ProcedureForm {
ProcedureForm::Unsupported(
"Healthy DB has no stored procedures; write the load as a scheduled query \
instead.",
)
}
fn scalar_function_form(&self) -> ScalarFunctionForm {
ScalarFunctionForm::PostgreSql
}
fn to_formula_renderer(&self) -> Box<dyn FormulaRenderer> {
Box::new(Self)
}
}
impl ExpressionRenderer for HealthyPlugin {}
impl DdlRenderer for HealthyPlugin {}
impl DmlRenderer for HealthyPlugin {}
impl ProcedureRenderer for HealthyPlugin {}
impl FormulaRenderer for HealthyPlugin {}
impl TaskRenderer for HealthyPlugin {}
impl ReplicationRenderer for HealthyPlugin {}
impl StorageRenderer for HealthyPlugin {}
impl DialectCapabilities for HealthyPlugin {
fn merge_shape(&self) -> MergeShape {
MergeShape::Merge(MergeRestrictions::none())
}
}
impl PlatformDialect for HealthyPlugin {
fn info(&self) -> DialectInfo {
DialectInfo {
name: "healthydb",
display_name: "Healthy DB",
dialect_family: DialectFamily::Custom("healthydb"),
family: "Healthy",
is_cloud_native: false,
is_olap: false,
is_postgres_compatible: false,
}
}
}
fn mentions(report: &Report, needle: &str) -> bool {
report.findings.iter().any(|f| f.what.contains(needle) || f.fix.contains(needle))
}
#[test]
fn the_kit_catches_a_deliberately_broken_plugin() {
let report = check(&BrokenPlugin);
assert!(!report.is_clean(), "the broken plugin reported clean:\n{report}");
assert!(mentions(&report, "does not resolve"), "unregistered:\n{report}");
assert!(mentions(&report, "custom one with no name"), "unnamed family:\n{report}");
assert!(mentions(&report, "render_data_type"), "empty column types:\n{report}");
assert!(mentions(&report, "render_cast_type"), "empty cast types:\n{report}");
assert!(mentions(&report, "identifier unchanged"), "no quoting:\n{report}");
assert!(mentions(&report, "render_current_timestamp"), "no now():\n{report}");
assert!(mentions(&report, "surrogate-key spelling"), "unspelled key:\n{report}");
assert!(mentions(&report, "stored-procedure grammar"), "no procedure form:\n{report}");
assert!(mentions(&report, "upsert shape"), "no merge shape:\n{report}");
assert!(mentions(&report, "to_formula_renderer"), "formula renderer:\n{report}");
assert!(mentions(&report, "supports_on_conflict"), "paired fact:\n{report}");
assert!(
report.count(Severity::Blocking) >= 8,
"the broken plugin owes more than {} blocking fixes:\n{report}",
report.count(Severity::Blocking)
);
}
#[test]
fn the_kit_reports_only_registration_on_a_plugin_that_answered_everything() {
let report = check(&HealthyPlugin);
let unregistered: Vec<&Finding> =
report.findings.iter().filter(|f| f.what.contains("does not resolve")).collect();
assert_eq!(unregistered.len(), 1, "the registration finding is the honest one here");
assert_eq!(
report.findings.len(),
1,
"a plugin that answered every question owes nothing else:\n{report}"
);
}
#[test]
fn a_stated_upsert_refusal_is_graded_as_copy_and_a_silent_one_as_unanswered() {
let mut mute = Vec::new();
check_refusal_copy("the upsert refusal", "no merge", &mut mute);
assert_eq!(mute.len(), 1, "a three-word refusal is a wall: {mute:?}");
assert!(mute[0].what.contains("the upsert refusal"), "{mute:?}");
let silence = check(&BrokenPlugin);
assert!(
silence
.findings
.iter()
.any(|f| f.severity == Severity::Unanswered && f.what.contains("upsert shape")),
"a dialect that never answered is unanswered, not wrong:\n{silence}"
);
assert!(
silence.findings.iter().any(|f| f.fix.contains("MergeShape::Unsupported")),
"the fix line names the way to state a measured refusal:\n{silence}"
);
}
#[test]
fn the_report_reads_as_a_summary_then_a_fix_list() {
let text = check(&BrokenPlugin).to_string();
let first = text.lines().next().expect("a summary line");
assert!(first.starts_with("brokendb:"), "{first}");
assert!(first.contains("to fix"), "{first}");
assert!(text.contains(" → "), "every finding names the fix:\n{text}");
}
#[test]
fn every_repository_obligation_names_a_command_and_a_reason() {
let obligations = repository_obligations();
assert!(obligations.len() >= 5);
for o in obligations {
assert!(!o.command.trim().is_empty(), "{o:?}");
assert!(o.what.split_whitespace().count() >= 5, "{o:?}");
}
}
}