use crate::PlatformDialect;
use razeri_plugin_contract::{
ColumnEvolution, ConstraintEnforcement, DataType, DdlRenderer, DialectCapabilities,
DialectFamily, EntityRef, MergeRestrictions, MergeShape, ProcedureForm, RoutineKind,
RoutineSpec, ScalarFunctionForm, SliceRef, SliceReplacement, SqlRenderer, StorageRenderer,
TableFunctionForm, UNKNOWN_PLATFORM,
data_type::{ArrayDataType, MapDataType, StructDataType, StructField},
storage_strategy::{
CompressionStrategy, IndexStrategy, PartitionGranularity, PartitionStrategy, StoragePlan,
},
};
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)]
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 {
check_with_registry(dialect, &crate::dialect::PlatformRegistry::with_auto_discovered())
}
#[must_use]
pub fn check_with_registry(
dialect: &dyn PlatformDialect,
registry: &crate::dialect::PlatformRegistry,
) -> Report {
let info = dialect.info();
let mut findings = Vec::new();
check_registration(&info, registry, &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_evolution(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(
info: &crate::dialect::DialectInfo,
registry: &crate::dialect::PlatformRegistry,
findings: &mut Vec<Finding>,
) {
let name = info.name;
if matches!(
crate::resolve::resolve_dialect_by_name_with_registry(name, registry),
Err(crate::ResolveError::UnknownDialect { .. })
) {
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;
}
for spelling in info.spellings() {
match crate::resolve::resolve_dialect_by_name_with_registry(spelling, registry) {
Ok(found) if found.info().name == name => {}
Ok(found) => findings.push(Finding {
severity: Severity::Blocking,
what: format!("`{spelling}` reaches `{}` rather than `{name}`", found.info().name),
fix: format!(
"the registry holds `{name}` under another key. Register it under its own \
name, or drop `{spelling}` from its aliases."
),
}),
Err(crate::ResolveError::AmbiguousName { claimed_by, .. }) => findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"`{spelling}` is claimed by more than one loaded platform ({}), so it names \
none of them — Razeri refuses that spelling rather than guessing which one \
a design meant",
claimed_by.join(" and ")
),
fix: format!(
"give this dialect a name no other platform answers to. A platform name is \
read off the loaded dialects' own declared spellings, so a spelling another \
platform already claims is never `{name}`'s."
),
}),
Err(_) => findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"`{spelling}` is declared as a spelling of `{name}` but reaches no platform"
),
fix: format!(
"the `{name}` in the registry is a different build of this dialect and does \
not declare `{spelling}`. Register the one you are grading."
),
}),
}
}
}
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_encoded_file_relation(
dialect: &dyn PlatformDialect,
probe: &str,
findings: &mut Vec<Finding>,
) {
let Some(relation) =
SqlRenderer::delimited_file_relation_in_encoding(dialect, probe, "windows-1252")
else {
return;
};
if !relation.contains(probe) {
findings.push(Finding {
severity: Severity::Blocking,
what: "`delimited_file_relation_in_encoding` returns a relation that does not carry \
the path it was given"
.to_owned(),
fix: "put the literal you were handed where your engine's file relation takes a \
path. It arrives already quoted and escaped."
.to_owned(),
});
}
if SqlRenderer::delimited_file_relation(dialect, probe).is_none() {
findings.push(Finding {
severity: Severity::Blocking,
what: "this dialect states how to read a file in a named encoding and states no way \
to read a file at all"
.to_owned(),
fix: "answer `delimited_file_relation` as well, or answer neither. Razeri asks the \
first question before the second — a source whose bytes are UTF-8 never \
reaches the encoding arm — so an engine that answers only the second has \
written a relation nothing will ask it for."
.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(),
});
}
if let Some(supplied) = SqlRenderer::sid_helper_body(dialect)
&& supplied.body.trim().is_empty()
{
findings.push(Finding {
severity: Severity::Blocking,
what: "this dialect says its surrogate-key helper has a body Razeri cannot compose, \
and then supplies an empty one"
.to_owned(),
fix: "return the body that computes XXH64 with seed 0 over the UTF-8 bytes of the \
one argument, or answer `None` and let Razeri compose the derivation from \
your `sid_spelling`. An empty body deploys without error and keys every row \
to nothing."
.to_owned(),
});
}
let probe = "'/razeri/probe.csv'";
if let Some(relation) = SqlRenderer::delimited_file_relation(dialect, probe)
&& !relation.contains(probe)
{
findings.push(Finding {
severity: Severity::Blocking,
what: "`delimited_file_relation` returns a relation that does not carry the path it \
was given"
.to_owned(),
fix: "put the literal you were handed where your engine's file relation takes a \
path. It arrives already quoted and escaped. A relation that drops it reads \
one file for every entity the design binds, and the generated fill loads the \
wrong rows without failing."
.to_owned(),
});
}
check_encoded_file_relation(dialect, probe, findings);
check_routine_forms(dialect, findings);
check_slice_replacement(dialect, 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);
}
MergeShape::Merge(restrictions) => check_merge_restrictions(restrictions, findings),
_ => {}
}
}
fn check_slice_replacement(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
const VALUE: &str = "$razeri_slice_probe";
let plan = StoragePlan {
partition: PartitionStrategy::RangeByTimeDimension {
field: "razeri_probe_day".to_owned(),
granularity: PartitionGranularity::Day,
retention: None,
look_ahead: None,
window: razeri_plugin_contract::storage_strategy::PartitionWindow::default(),
},
compression: CompressionStrategy::None,
index: IndexStrategy::None,
};
let day = DataType::date();
let columns = ["razeri_probe_day"];
let entity = EntityRef {
schema: Some("razeri_probe"),
name: "razeri_probe_table",
is_identity_table: false,
columns: &columns,
partition_column_type: Some(&day),
};
let slice = SliceRef { column: "razeri_probe_day", value: VALUE };
let Some(answer) = StorageRenderer::render_slice_replacement(dialect, entity, &plan, slice)
else {
return;
};
let carries = match &answer {
SliceReplacement::Emptied(statements) => {
!statements.is_empty() && statements.iter().all(|sql| sql.contains(VALUE))
}
SliceReplacement::WrittenByInsert(clause) => clause.contains(VALUE),
};
if carries {
return;
}
findings.push(Finding {
severity: Severity::Blocking,
what: "`render_slice_replacement` returns a statement that does not carry the slice it \
was handed"
.to_owned(),
fix: "put the value you were given where your statement names the slice, and build any \
partition NAME from it the same way your extend procedure built it. A replacement \
that drops the value clears a slice this call was not given — or every slice — \
and the load reports success over what it emptied."
.to_owned(),
});
}
fn check_evolution(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
let answers = DialectCapabilities::evolution_answers(dialect);
let boolean = DataType::boolean();
let long = DataType::varchar(40);
if DdlRenderer::render_column_evolution(
dialect,
&ColumnEvolution::AddColumn {
schema: Some("s"),
table: "t",
column: "c",
data_type: &boolean,
},
)
.is_none()
{
findings.push(Finding {
severity: Severity::Blocking,
what: "this dialect writes no statement for a column that is new, which is the commonest change a deployed warehouse ever sees"
.to_owned(),
fix: "answer `ColumnEvolution::AddColumn` in `render_column_evolution`, or drop the override and take the contract's `ALTER TABLE … ADD COLUMN …`, which is this spelling on every engine Razeri reaches. An engine that genuinely has no way to add a column is worth saying out loud in the crate's own documentation."
.to_owned(),
});
}
if answers.parameterized_string_widening_is_lossless
&& DdlRenderer::render_column_evolution(
dialect,
&ColumnEvolution::WidenColumn {
schema: Some("s"),
table: "t",
column: "c",
data_type: &long,
},
)
.is_none()
{
findings.push(Finding {
severity: Severity::Note,
what: "this dialect says its engine widens a string's declared length in place, and writes no statement for the change"
.to_owned(),
fix: "answer `ColumnEvolution::WidenColumn` in `render_column_evolution` with this engine's spelling of it. Without it Razeri tells an operator the widening costs nothing and then hands them nothing to run, which reads as Razeri not having finished rather than as a statement they own."
.to_owned(),
});
}
if answers.nested_field_addition_is_lossless
&& DdlRenderer::render_column_evolution(
dialect,
&ColumnEvolution::AddNestedField {
schema: Some("s"),
table: "t",
column: "c",
field: "f",
data_type: &long,
},
)
.is_none()
{
findings.push(Finding {
severity: Severity::Note,
what: "this dialect says its engine keeps every value when a record gains a field, and writes no statement for the change"
.to_owned(),
fix: "answer `ColumnEvolution::AddNestedField` in `render_column_evolution`. Some engines name the new field; some restate the whole column's type, which is why the change carries both the field's path and the column's type with the field in it."
.to_owned(),
});
}
}
fn check_merge_restrictions(restrictions: MergeRestrictions, findings: &mut Vec<Finding>) {
if restrictions.slice_pruning_needs_literal {
findings.push(Finding {
severity: Severity::Note,
what: "this dialect says its planner reads one slice of a partitioned table only \
from a literal, so a load scoped to one day cannot simply bind the day"
.to_owned(),
fix: "Razeri will put the day into the statement text through this engine's \
dynamic-execution form rather than binding it to the load procedure's \
parameter. Confirm that is what your engine wants: the statement is composed \
once per call and its text differs per day, which some planners cache and \
some do not. If a bound parameter does prune here, drop the restriction and \
Razeri binds it like everywhere else."
.to_owned(),
});
}
}
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_routine_forms(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
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);
}
ProcedureForm::Custom(grammar) => check_custom_grammar(
CustomGrammar {
kind: RoutineKind::Procedure,
grammar,
form_method: "procedure_form",
render_method: "render_procedure",
noun: "procedures",
},
|spec| SqlRenderer::render_procedure(dialect, spec),
findings,
),
_ => {}
}
match SqlRenderer::scalar_function_form(dialect) {
ScalarFunctionForm::Unsupported(orientation) => {
check_refusal_copy("the scalar-function refusal", orientation, findings);
}
ScalarFunctionForm::Custom(grammar) => check_custom_grammar(
CustomGrammar {
kind: RoutineKind::ScalarFunction,
grammar,
form_method: "scalar_function_form",
render_method: "render_scalar_function",
noun: "scalar functions",
},
|spec| SqlRenderer::render_scalar_function(dialect, spec),
findings,
),
_ => {}
}
match SqlRenderer::table_function_form(dialect) {
TableFunctionForm::Unsupported(orientation) => {
check_refusal_copy("the table-function refusal", orientation, findings);
}
TableFunctionForm::Custom(grammar) => check_custom_grammar(
CustomGrammar {
kind: RoutineKind::TableFunction,
grammar,
form_method: "table_function_form",
render_method: "render_table_function",
noun: "table functions",
},
|spec| SqlRenderer::render_table_function(dialect, spec),
findings,
),
_ => {}
}
check_parameter_type_spelling(dialect, findings);
}
fn check_parameter_type_spelling(dialect: &dyn PlatformDialect, findings: &mut Vec<Finding>) {
let spells_its_own = SqlRenderer::procedure_form(dialect).custom_grammar().is_some()
|| SqlRenderer::scalar_function_form(dialect).custom_grammar().is_some()
|| SqlRenderer::table_function_form(dialect).custom_grammar().is_some();
if !spells_its_own {
return;
}
let qualified = DataType::varchar(100);
let column = SqlRenderer::render_data_type(dialect, &qualified);
let parameter = SqlRenderer::render_parameter_data_type(dialect, &qualified);
if column == parameter && column.contains('(') {
findings.push(Finding {
severity: Severity::Note,
what: format!(
"a parameter of this routine's would be declared `{parameter}`, the same as a column of that type"
),
fix: "that is right for most engines, so this is only worth a look. If yours refuses a length or a precision in a parameter list — Oracle does, and answers PLS-00103 at the opening parenthesis — override `SqlRenderer::render_parameter_data_type` with the bare spelling. Local declarations keep the length either way."
.to_owned(),
});
}
}
#[derive(Clone, Copy)]
struct CustomGrammar {
kind: RoutineKind,
grammar: &'static str,
form_method: &'static str,
render_method: &'static str,
noun: &'static str,
}
fn check_custom_grammar(
claim: CustomGrammar,
render: impl Fn(&RoutineSpec) -> Option<String>,
findings: &mut Vec<Finding>,
) {
let CustomGrammar { kind, grammar, form_method, render_method, noun } = claim;
if grammar.trim().is_empty() {
findings.push(Finding {
severity: Severity::Blocking,
what: format!("`{form_method}` says Custom, and names no grammar"),
fix: "carry your grammar's own name — `Custom(\"PL/SQL\")`. It is what a reader \
of a report, and of the comment Razeri writes where a routine could not be \
rendered, is told this engine speaks."
.to_owned(),
});
}
let mut probe = RoutineSpec::new(kind, "razeri_conformance_probe");
probe.or_replace = true;
if render(&probe).is_none_or(|rendered| rendered.trim().is_empty()) {
findings.push(Finding {
severity: Severity::Blocking,
what: format!(
"`{form_method}` says Custom({grammar:?}), so this dialect must render {noun} \
itself — and `{render_method}` returns nothing"
),
fix: format!(
"override `SqlRenderer::{render_method}`. It receives the routine Razeri \
composed — name, parameters with their types already in your spelling, body, \
comment — and returns the whole CREATE statement your engine parses. Razeri \
emits what you return and writes nothing of its own: a near-neighbour grammar \
fails on deploy, and a refusal would say this engine has no {noun} when it \
has them in a grammar of its own."
),
});
}
}
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(),
});
}
}
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),
DataType::Array(ArrayDataType::new(DataType::varchar(100))),
DataType::Map(MapDataType::new(DataType::varchar(50), DataType::bigint())),
DataType::Struct(StructDataType::new(vec![
StructField::new("street", DataType::varchar(200)),
StructField::new("zip", DataType::varchar(10)),
])),
DataType::Array(ArrayDataType::new(DataType::Struct(StructDataType::new(vec![
StructField::new("key", DataType::varchar(100)),
StructField::new("value", DataType::varchar(1000)),
])))),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dialect::DialectInfo;
use razeri_plugin_contract::{
Concat, DmlRenderer, ExpressionRenderer, FormulaRenderer, MergeRestrictions,
ProcedureRenderer, ReplicationRenderer, ScalarFunctionForm, SidSpelling, StorageRenderer,
TaskRenderer, TextCast,
};
use std::marker::PhantomData;
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",
aliases: &[],
dialect_family: DialectFamily::Custom(""),
family: "Broken",
is_cloud_native: false,
is_olap: false,
is_postgres_compatible: false,
}
}
}
trait FixtureName: Send + Sync + 'static {
const NAME: &'static str;
const CONCAT: Concat = Concat::Function("CONCAT");
}
struct Unlinked;
impl FixtureName for Unlinked {
const NAME: &'static str = "healthydb";
}
struct Outside;
impl FixtureName for Outside {
const NAME: &'static str = "outsidedb";
}
struct BinaryConcat;
impl FixtureName for BinaryConcat {
const NAME: &'static str = "binaryconcatdb";
const CONCAT: Concat = Concat::FunctionBinary("CONCAT");
}
struct Answered<N: FixtureName>(PhantomData<N>);
impl<N: FixtureName> Answered<N> {
const fn new() -> Self {
Self(PhantomData)
}
}
type HealthyPlugin = Answered<Unlinked>;
type OutsidePlugin = Answered<Outside>;
inventory::submit! {
crate::builder::DialectRegistration::new(
Outside::NAME,
|| std::sync::Arc::new(OutsidePlugin::new()),
)
}
impl<N: FixtureName> SqlRenderer for Answered<N> {
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 {
N::NAME
}
fn sid_spelling(&self) -> SidSpelling {
SidSpelling {
platform: N::NAME,
hash: ("answered_xxhash64(", ")"),
cast_type: TextCast::Direct("STRING"),
concat: N::CONCAT,
through_helper: false,
..razeri_plugin_contract::ANSI_SID_SPELLING
}
}
fn procedure_form(&self) -> ProcedureForm {
ProcedureForm::Unsupported(
"This engine 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::new())
}
}
impl<N: FixtureName> ExpressionRenderer for Answered<N> {}
impl<N: FixtureName> DdlRenderer for Answered<N> {}
impl<N: FixtureName> DmlRenderer for Answered<N> {}
impl<N: FixtureName> ProcedureRenderer for Answered<N> {}
impl<N: FixtureName> FormulaRenderer for Answered<N> {}
impl<N: FixtureName> TaskRenderer for Answered<N> {}
impl<N: FixtureName> ReplicationRenderer for Answered<N> {}
impl<N: FixtureName> StorageRenderer for Answered<N> {}
impl<N: FixtureName> DialectCapabilities for Answered<N> {
fn merge_shape(&self) -> MergeShape {
MergeShape::Merge(MergeRestrictions::none())
}
}
impl<N: FixtureName> PlatformDialect for Answered<N> {
fn info(&self) -> DialectInfo {
DialectInfo {
name: N::NAME,
display_name: "An Answered Engine",
aliases: &[],
dialect_family: DialectFamily::Custom(N::NAME),
family: "Answered",
is_cloud_native: false,
is_olap: false,
is_postgres_compatible: false,
}
}
}
struct Twin;
impl SqlRenderer for Twin {
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 {
self.quote_identifier(name)
}
fn render_current_timestamp(&self) -> String {
"NOW()".to_owned()
}
}
impl ExpressionRenderer for Twin {}
impl DdlRenderer for Twin {}
impl DmlRenderer for Twin {}
impl ProcedureRenderer for Twin {}
impl FormulaRenderer for Twin {}
impl TaskRenderer for Twin {}
impl ReplicationRenderer for Twin {}
impl StorageRenderer for Twin {}
impl DialectCapabilities for Twin {}
impl PlatformDialect for Twin {
fn info(&self) -> DialectInfo {
DialectInfo {
name: "twindb",
display_name: "Twin DB",
aliases: &["healthydb"],
dialect_family: DialectFamily::Custom("twindb"),
family: "Twin",
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::new());
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 the_kit_takes_a_two_argument_concat_as_an_answer_like_any_other() {
let binary = Answered::<BinaryConcat>::new();
let report = check(&binary);
assert_eq!(
report.findings.len(),
check(&HealthyPlugin::new()).findings.len(),
"the join form is a decision, so stating this one owes nothing extra:\n{report}"
);
assert!(
!report.to_string().contains("joins"),
"nothing about the join is outstanding:\n{report}"
);
let key = razeri_plugin_contract::sid::sid_expression(
SqlRenderer::sid_spelling(&binary),
None,
&["a".to_owned(), "b".to_owned(), "c".to_owned()],
);
assert!(
key.contains("CONCAT(CONCAT("),
"a two-argument engine gets one call per pair:\n{key}"
);
}
#[test]
fn a_stated_merge_restriction_is_reported_and_a_default_one_is_not() {
let mut quiet = Vec::new();
check_merge_restrictions(MergeRestrictions::none(), &mut quiet);
assert!(quiet.is_empty(), "a dialect with no stated restriction is asked nothing");
let mut stated = Vec::new();
check_merge_restrictions(MergeRestrictions::requiring_a_literal_slice(), &mut stated);
assert_eq!(stated.len(), 1, "the stated restriction is reported once");
assert_eq!(stated[0].severity, Severity::Note, "nothing is broken; it is worth a look");
assert!(
stated[0].what.contains("literal"),
"the finding names the restriction:\n{}",
stated[0].what
);
assert!(
stated[0].fix.contains("dynamic-execution"),
"and says what Razeri does with the answer:\n{}",
stated[0].fix
);
let mut two_clauses = Vec::new();
check_merge_restrictions(MergeRestrictions::requiring_two_when_clauses(), &mut two_clauses);
assert!(two_clauses.is_empty(), "one restriction is not the other");
}
#[test]
fn a_dialect_registered_under_its_own_name_reaches_a_clean_report() {
assert!(
crate::resolve_dialect_by_name(Outside::NAME).is_ok(),
"the fixture registers through the registry, which is the whole path being graded"
);
let registry = crate::dialect::PlatformRegistry::with_auto_discovered();
assert!(
registry
.list()
.iter()
.filter(|info| info.name != Outside::NAME)
.all(|info| !info.answers_to(Outside::NAME)),
"the fixture's name must be one no other platform claims"
);
let report = check(&OutsidePlugin::new());
assert!(report.findings.is_empty(), "nothing is owed here:\n{report}");
assert!(report.is_clean(), "and the report says so:\n{report}");
assert_eq!(report.count(Severity::Blocking), 0, "{report}");
assert!(
report.to_string().contains("nothing to fix"),
"a clean dialect is told it is clean:\n{report}"
);
}
#[test]
fn a_platform_is_never_reachable_by_an_alias_but_not_by_its_own_name() {
let registry = crate::dialect::PlatformRegistry::with_auto_discovered();
for info in registry.list() {
assert_eq!(
info.spellings().first().copied(),
Some(info.name),
"a platform's own name leads its spellings"
);
assert!(info.answers_to(info.name), "{} answers to its own name", info.name);
}
}
#[test]
fn a_name_razeri_reads_as_another_platform_is_blocking_and_says_which() {
let mut registry = crate::dialect::PlatformRegistry::new();
registry.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
registry.register("twindb".to_owned(), std::sync::Arc::new(Twin));
let report = check_with_registry(&Twin, ®istry);
let collision = report
.findings
.iter()
.find(|f| f.what.contains("claimed by more than one loaded platform"))
.unwrap_or_else(|| panic!("the collision is reported:\n{report}"));
assert_eq!(collision.severity, Severity::Blocking, "{collision:?}");
assert!(collision.what.contains("healthydb"), "the other platform is named: {collision:?}");
assert!(collision.what.contains("twindb"), "{collision:?}");
assert!(collision.fix.contains("name no other platform answers to"), "{collision:?}");
assert!(
!mentions(&report, "does not resolve"),
"a collision is not an unregistered platform:\n{report}"
);
}
#[test]
fn registration_is_silent_on_a_platform_reached_under_its_own_name() {
let mut registry = crate::dialect::PlatformRegistry::new();
registry.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
let mut findings = Vec::new();
check_registration(&HealthyPlugin::new().info(), ®istry, &mut findings);
assert!(findings.is_empty(), "{findings:?}");
for spelling in ["healthydb", "HealthyDB", " healthydb "] {
assert!(
crate::resolve::resolve_dialect_by_name_with_registry(spelling, ®istry).is_ok(),
"'{spelling}' is the same platform"
);
}
assert!(
crate::resolve::resolve_dialect_by_name_with_registry("nosuchdb", ®istry).is_err()
);
}
#[test]
fn a_dialect_the_host_loaded_reports_clean_against_its_own_registry() {
let mut registry = crate::dialect::PlatformRegistry::new();
registry.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
let report = check_with_registry(&HealthyPlugin::new(), ®istry);
assert!(
report.is_clean(),
"a loaded dialect that answered everything owes nothing:\n{report}"
);
}
#[test]
fn no_fix_line_sends_an_author_into_razeris_own_tree() {
let mut collided = crate::dialect::PlatformRegistry::new();
collided.register("healthydb".to_owned(), std::sync::Arc::new(HealthyPlugin::new()));
collided.register("twindb".to_owned(), std::sync::Arc::new(Twin));
let mut fixes: Vec<String> = Vec::new();
for report in [
check(&BrokenPlugin),
check(&HealthyPlugin::new()),
check(&OutsidePlugin::new()),
check_with_registry(&Twin, &collided),
check_with_registry(&Twin, &crate::dialect::PlatformRegistry::new()),
] {
fixes.extend(report.findings.iter().map(|f| f.fix.clone()));
}
assert!(fixes.len() >= 10, "the sweep reached the findings: {}", fixes.len());
for fix in &fixes {
for path in ["schemas/", "crates/", ".json", "scripts/", "razeri-design"] {
assert!(
!fix.contains(path),
"a fix line names `{path}`, which is not in the author's crate:\n {fix}"
);
}
}
}
#[test]
fn a_custom_grammar_without_a_render_method_is_blocking_on_all_three_kinds() {
for (kind, form_method, render_method) in [
(RoutineKind::Procedure, "procedure_form", "render_procedure"),
(RoutineKind::ScalarFunction, "scalar_function_form", "render_scalar_function"),
(RoutineKind::TableFunction, "table_function_form", "render_table_function"),
] {
let mut promised = Vec::new();
check_custom_grammar(
CustomGrammar {
kind,
grammar: "PL/SQL",
form_method,
render_method,
noun: "routines",
},
|_| None,
&mut promised,
);
assert_eq!(promised.len(), 1, "{form_method}: {promised:?}");
assert_eq!(promised[0].severity, Severity::Blocking, "{promised:?}");
assert!(promised[0].what.contains(form_method), "{promised:?}");
assert!(promised[0].what.contains("PL/SQL"), "{promised:?}");
assert!(promised[0].fix.contains(render_method), "{promised:?}");
let mut kept = Vec::new();
check_custom_grammar(
CustomGrammar {
kind,
grammar: "PL/SQL",
form_method,
render_method,
noun: "routines",
},
|spec| Some(format!("CREATE {} …", spec.name)),
&mut kept,
);
assert!(kept.is_empty(), "{form_method}: a kept promise is not a finding: {kept:?}");
}
}
#[test]
fn a_custom_render_method_that_returns_nothing_is_blocking() {
let mut findings = Vec::new();
check_custom_grammar(
CustomGrammar {
kind: RoutineKind::Procedure,
grammar: "PL/SQL",
form_method: "procedure_form",
render_method: "render_procedure",
noun: "procedures",
},
|_| Some(" \n".to_owned()),
&mut findings,
);
assert_eq!(findings.len(), 1, "{findings:?}");
assert_eq!(findings[0].severity, Severity::Blocking, "{findings:?}");
}
#[test]
fn a_custom_grammar_with_no_name_is_blocking() {
let mut findings = Vec::new();
check_custom_grammar(
CustomGrammar {
kind: RoutineKind::ScalarFunction,
grammar: " ",
form_method: "scalar_function_form",
render_method: "render_scalar_function",
noun: "scalar functions",
},
|spec| Some(format!("CREATE FUNCTION {}", spec.name)),
&mut findings,
);
assert_eq!(findings.len(), 1, "{findings:?}");
assert!(findings[0].what.contains("names no grammar"), "{findings:?}");
}
#[test]
fn a_stated_key_body_that_is_blank_and_a_file_relation_that_drops_its_path_are_blocking() {
struct Overstated;
impl SqlRenderer for Overstated {
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 {
name.to_owned()
}
fn render_current_timestamp(&self) -> String {
"CURRENT_TIMESTAMP".to_owned()
}
fn dialect_name(&self) -> &'static str {
"overstateddb"
}
fn sid_helper_body(&self) -> Option<razeri_plugin_contract::SidHelperBody> {
Some(razeri_plugin_contract::SidHelperBody {
language: razeri_plugin_contract::SidHelperLanguage::JavaScript,
body: " \n ",
})
}
fn delimited_file_relation(&self, _path_literal: &str) -> Option<String> {
Some("read_csv_auto('/tmp/whatever.csv')".to_owned())
}
}
impl ExpressionRenderer for Overstated {}
impl DdlRenderer for Overstated {}
impl DmlRenderer for Overstated {}
impl ProcedureRenderer for Overstated {}
impl FormulaRenderer for Overstated {}
impl TaskRenderer for Overstated {}
impl ReplicationRenderer for Overstated {}
impl StorageRenderer for Overstated {}
impl DialectCapabilities for Overstated {}
impl PlatformDialect for Overstated {
fn info(&self) -> DialectInfo {
DialectInfo {
name: "overstateddb",
display_name: "Overstated DB",
aliases: &[],
dialect_family: DialectFamily::Custom(""),
family: "Overstated",
is_cloud_native: false,
is_olap: false,
is_postgres_compatible: false,
}
}
}
let report = check(&Overstated);
for (fragment, why) in [
("supplies an empty one", "a blank helper body"),
("does not carry the path", "a file relation that drops its path"),
] {
let found = report
.findings
.iter()
.find(|f| f.what.contains(fragment))
.unwrap_or_else(|| panic!("{why} is not graded:\n{report}"));
assert_eq!(found.severity, Severity::Blocking, "{why}: {found:?}");
}
let quiet = check(&HealthyPlugin::new());
for fragment in ["supplies an empty one", "does not carry the path"] {
assert!(
!quiet.findings.iter().any(|f| f.what.contains(fragment)),
"silence is the default and owes nothing:\n{quiet}"
);
}
}
#[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}");
}
}