use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use scythe_backend::manifest::{BackendManifest, load_manifest};
use scythe_backend::types::resolve_type;
use scythe_codegen::GeneratedCode;
use scythe_codegen::backends::get_backend;
const RANGE_CAPABLE_ENGINES: &[&str] = &["postgresql"];
const MANIFEST_FLOOR: usize = 102;
const DECLARATION_FLOOR: usize = 19;
struct ManifestNote {
manifest: &'static str,
reason: &'static str,
}
const RANGE_CAPABILITY_EXCEPTIONS: &[ManifestNote] = &[];
const RANGE_SPELLING_EXCEPTIONS: &[ManifestNote] = &[
ManifestNote {
manifest: "python-psycopg3.toml",
reason: "asyncpg and psycopg3 each ship their own Range class -- asyncpg.Range and \
psycopg.types.range.Range -- confirmed live to be two distinct classes (asyncpg.Range \
is not psycopg.types.range.Range, and each driver decodes a range column into its own \
class, never the other's). Normalizing python-psycopg3 onto asyncpg.Range[{T}] would \
produce a type hint the psycopg driver never actually returns.",
},
ManifestNote {
manifest: "rust-tokio-postgres.toml",
reason: "rust-sqlx spells range as sqlx::postgres::types::PgRange<{T}>, a real type sqlx-postgres \
ships with Type/Encode/Decode impls. tokio-postgres's own type crate (postgres-types \
0.2.14) has no equivalent, so rust-tokio-postgres.rs hand-rolls its own PgRange<T> -- a \
FromSql/ToSql pair built on postgres_protocol::types::range_from_sql/range_to_sql, \
emitted directly into the generated file rather than imported from a crate. Normalizing \
onto sqlx's fully-qualified spelling would name a type this backend's dependency tree \
does not have.",
},
];
struct RangeDeclaration {
file: String,
language: String,
engine: String,
pattern: Option<String>,
}
fn manifests_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("manifests")
}
fn all_declarations() -> Vec<RangeDeclaration> {
let dir = manifests_dir();
let mut files: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|error| panic!("manifests dir {} must be readable: {error}", dir.display()))
.map(|entry| entry.expect("readable dir entry").path())
.filter(|path| path.extension().is_some_and(|ext| ext == "toml"))
.collect();
files.sort();
assert!(
files.len() >= MANIFEST_FLOOR,
"expected at least {MANIFEST_FLOOR} manifests under {}, found {} -- the sweep has gone \
stale and this file is no longer checking anything",
dir.display(),
files.len()
);
files.iter().map(|path| declaration_for(path)).collect()
}
fn declaration_for(path: &Path) -> RangeDeclaration {
let manifest: BackendManifest =
load_manifest(path).unwrap_or_else(|error| panic!("{} must parse: {error}", path.display()));
RangeDeclaration {
file: path
.file_name()
.and_then(|name| name.to_str())
.expect("manifest file name must be UTF-8")
.to_string(),
language: manifest.backend.language.clone(),
engine: manifest.backend.engine.clone(),
pattern: manifest.types.containers.get("range").cloned(),
}
}
fn unmatched_exceptions(
list: &[ManifestNote],
eligible: &BTreeSet<&str>,
kind: &str,
eligibility: &str,
) -> Vec<String> {
list.iter()
.filter(|note| !eligible.contains(note.manifest))
.map(|note| {
format!(
"STALE {kind} EXCEPTION: {}: listed, but it {eligibility} -- delete its entry \
(reason on file: {})",
note.manifest, note.reason
)
})
.collect()
}
#[test]
fn range_is_declared_exactly_for_engines_that_have_a_range_type() {
let declarations = all_declarations();
let mut failures = Vec::new();
for declaration in &declarations {
let capable = RANGE_CAPABLE_ENGINES.contains(&declaration.engine.as_str());
let declared = declaration.pattern.is_some();
let listed = RANGE_CAPABILITY_EXCEPTIONS
.iter()
.find(|note| note.manifest == declaration.file.as_str());
match (declared == capable, listed) {
(true, Some(note)) => failures.push(format!(
"STALE CAPABILITY EXCEPTION: {}: listed, but its range declaration now agrees \
with engine `{}` -- delete its entry (reason on file: {})",
declaration.file, declaration.engine, note.reason
)),
(false, None) if declared => failures.push(format!(
"UNSUPPORTED ENGINE: {} declares `range = {:?}` but engine `{}` has no range \
type -- drop the key, or add it to RANGE_CAPABILITY_EXCEPTIONS with evidence \
that the engine gained one",
declaration.file,
declaration.pattern.as_deref().unwrap_or_default(),
declaration.engine
)),
(false, None) => failures.push(format!(
"MISSING: {} omits `range` but engine `{}` has a range type -- resolve_type will \
fail with UnknownContainer(\"range\") on any range column",
declaration.file, declaration.engine
)),
_ => {}
}
}
let all_files: BTreeSet<&str> = declarations
.iter()
.map(|declaration| declaration.file.as_str())
.collect();
failures.extend(unmatched_exceptions(
RANGE_CAPABILITY_EXCEPTIONS,
&all_files,
"CAPABILITY",
"names no manifest under manifests/",
));
assert!(
failures.is_empty(),
"{} of {} manifests need attention:\n{}",
failures.len(),
declarations.len(),
failures.join("\n")
);
let declaring = declarations
.iter()
.filter(|declaration| declaration.pattern.is_some())
.count();
assert!(
declaring >= DECLARATION_FLOOR,
"expected at least {DECLARATION_FLOOR} manifests to declare `range`, found {declaring} -- \
either RANGE_CAPABLE_ENGINES was narrowed without updating this floor, or the sweep \
stopped seeing the PostgreSQL manifests"
);
}
#[test]
fn each_language_declares_one_range_spelling() {
let declarations = all_declarations();
let mut failures = Vec::new();
let mut by_language: BTreeMap<&str, Vec<&RangeDeclaration>> = BTreeMap::new();
for declaration in &declarations {
if declaration.pattern.is_some() {
by_language
.entry(declaration.language.as_str())
.or_default()
.push(declaration);
}
}
for (language, group) in &by_language {
let (exempt, plain): (Vec<&RangeDeclaration>, Vec<&RangeDeclaration>) =
group.iter().copied().partition(|declaration| {
RANGE_SPELLING_EXCEPTIONS
.iter()
.any(|note| note.manifest == declaration.file.as_str())
});
let spellings: BTreeSet<&str> = plain
.iter()
.filter_map(|declaration| declaration.pattern.as_deref())
.collect();
if spellings.len() > 1 {
let detail: Vec<String> = plain
.iter()
.map(|declaration| {
format!(
" {} = {:?}",
declaration.file,
declaration.pattern.as_deref().unwrap_or_default()
)
})
.collect();
failures.push(format!(
"DIVERGENT SPELLING: {language} declares {} different `range` mappings; a \
language family gets one:\n{}",
spellings.len(),
detail.join("\n")
));
}
let Some(baseline) = spellings.iter().next().copied() else {
continue;
};
for declaration in &exempt {
if declaration.pattern.as_deref() == Some(baseline) {
let note = RANGE_SPELLING_EXCEPTIONS
.iter()
.find(|note| note.manifest == declaration.file.as_str())
.expect("partitioned as exempt, so it is listed");
failures.push(format!(
"STALE SPELLING EXCEPTION: {}: listed, but it now spells `range` as {baseline:?}, \
the same as the rest of {language} -- delete its entry (reason on file: {})",
declaration.file, note.reason
));
}
}
}
let declaring: BTreeSet<&str> = declarations
.iter()
.filter(|declaration| declaration.pattern.is_some())
.map(|declaration| declaration.file.as_str())
.collect();
failures.extend(unmatched_exceptions(
RANGE_SPELLING_EXCEPTIONS,
&declaring,
"SPELLING",
"no longer declares `range` at all, so it can never diverge",
));
assert!(
!by_language.is_empty(),
"no manifest declares `range` at all -- the sweep matched nothing and this test would \
pass vacuously"
);
assert!(
failures.is_empty(),
"{} language families need attention:\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn rust_tokio_postgres_range_emits_a_working_pgrange_wrapper() {
let backend = get_backend("rust-tokio-postgres", "postgresql").expect("rust-tokio-postgres/postgresql");
let resolved = resolve_type("range<int32>", backend.manifest(), false).expect("range<int32> must resolve");
assert_eq!(
resolved.as_ref(),
"PgRange<i32>",
"the manifest's range mapping must substitute {{T}} the same way every other container does"
);
let no_range = [GeneratedCode::build(|c| {
c.row_struct = Some("pub struct GetUserRow {\n pub id: i32,\n}\n".to_string());
})];
let header_without = backend.file_header_for_results(&no_range);
assert!(
!header_without.contains("PgRange"),
"a file with no range column must not carry the PgRange wrapper:\n{header_without}"
);
let with_range = [GeneratedCode::build(|c| {
c.row_struct = Some(format!("pub struct GetSpanRow {{\n pub span: {resolved},\n}}\n"));
})];
let header_with = backend.file_header_for_results(&with_range);
for needle in [
"pub enum PgRange<T>",
"Empty,",
"Range {",
"std::ops::Bound<T>",
"impl<'a, T> tokio_postgres::types::FromSql<'a> for PgRange<T>",
"tokio_postgres::types::Kind::Range(element)",
"postgres_protocol::types::range_from_sql(raw)?",
"postgres_protocol::types::Range::Empty => Ok(PgRange::Empty)",
"impl<T> tokio_postgres::types::ToSql for PgRange<T>",
"postgres_protocol::types::empty_range_to_sql(out)",
"postgres_protocol::types::range_to_sql(",
"tokio_postgres::types::to_sql_checked!();",
] {
assert!(
header_with.contains(needle),
"PgRange support text must contain {needle:?}, got:\n{header_with}"
);
}
let two_range_fragments = [
GeneratedCode::build(|c| {
c.row_struct = Some(format!("pub struct GetSpanRow {{\n pub span: {resolved},\n}}\n"));
}),
GeneratedCode::build(|c| {
c.model_struct = Some(format!("pub struct Booking {{\n pub during: {resolved},\n}}\n"));
}),
];
let header_twice = backend.file_header_for_results(&two_range_fragments);
assert_eq!(
header_twice.matches("pub enum PgRange<T>").count(),
1,
"PgRange must be defined exactly once per file regardless of how many fragments use it:\n{header_twice}"
);
}