use razeri_plugin_contract::DialectFamily;
use razeri_plugin_contract::EngineVersion;
use razeri_plugin_contract::{
DdlRenderer, DialectCapabilities, DmlRenderer, ExpressionRenderer, FormulaRenderer,
ProcedureRenderer, ReplicationRenderer, SqlRenderer, StorageRenderer, TaskRenderer,
};
use std::collections::HashMap;
use std::sync::Arc;
pub use razeri_plugin_contract;
#[derive(Debug, Clone, serde::Serialize)]
pub struct DialectInfo {
pub name: &'static str,
pub display_name: &'static str,
pub aliases: &'static [&'static str],
pub dialect_family: DialectFamily,
pub family: &'static str,
pub is_cloud_native: bool,
pub is_olap: bool,
pub is_postgres_compatible: bool,
pub versions: &'static [EngineVersion],
pub deploy_script: Option<DeployScript>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum DeployScript {
Duckdb,
Sqlite3,
Sqlplus,
Hdbsql,
Mysql,
Psql,
InvokeSqlcmd,
Client(DeployClient),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct DeployClient {
pub command: &'static str,
pub run_file: &'static [&'static str],
pub read_rows: Option<&'static [&'static str]>,
}
pub trait PlatformDialect:
SqlRenderer
+ ExpressionRenderer
+ DdlRenderer
+ DmlRenderer
+ ProcedureRenderer
+ FormulaRenderer
+ TaskRenderer
+ DialectCapabilities
+ ReplicationRenderer
+ StorageRenderer
+ DialectHandle
{
fn info(&self) -> DialectInfo;
fn at_version(
self: Arc<Self>,
version: &EngineVersion,
) -> Result<Arc<dyn PlatformDialect>, UnsupportedVersion> {
if self.engine_version().as_ref() == Some(version) {
return Ok(self.into_dialect());
}
Err(UnsupportedVersion::of(&self.info(), &version.to_string()))
}
fn engine_version(&self) -> Option<EngineVersion> {
self.info().versions.first().copied()
}
}
pub trait DialectHandle {
fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect>;
}
impl<T: PlatformDialect + 'static> DialectHandle for T {
fn into_dialect(self: Arc<Self>) -> Arc<dyn PlatformDialect> {
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{}", self.sentence())]
pub struct UnsupportedVersion {
pub engine: String,
pub display_name: String,
pub asked: String,
pub answers_for: Vec<EngineVersion>,
}
impl UnsupportedVersion {
#[must_use]
pub fn of(info: &DialectInfo, asked: &str) -> Self {
Self {
engine: info.name.to_owned(),
display_name: info.display_name.to_owned(),
asked: asked.to_owned(),
answers_for: info.versions.to_vec(),
}
}
#[must_use]
pub fn sentence(&self) -> String {
let Self { engine, display_name, asked, answers_for } = self;
let Some(floor) = answers_for.first() else {
return format!(
"razeri renders {display_name} for no version a design names, and not at \
{asked}: write `{engine}`"
);
};
let versions: Vec<String> = answers_for.iter().map(ToString::to_string).collect();
let names: Vec<String> = versions.iter().map(|v| format!("`{engine}@{v}`")).collect();
if versions.len() == 1 {
return format!(
"razeri renders {display_name} at {floor} alone, and not at {asked}: write \
`{engine}`"
);
}
format!(
"razeri renders {display_name} at {}, and not at {asked}: write {}, or `{engine}` \
for {floor}, the oldest",
in_words(&versions, "and"),
in_words(&names, "or"),
)
}
}
fn in_words(items: &[String], last: &str) -> String {
match items {
[] => String::new(),
[only] => only.clone(),
[rest @ .., tail] => format!("{} {last} {tail}", rest.join(", ")),
}
}
impl DialectInfo {
#[must_use]
pub fn spellings(&self) -> Vec<&'static str> {
let mut out = Vec::with_capacity(self.aliases.len() + 1);
out.push(self.name);
for alias in self.aliases {
if !out.contains(alias) {
out.push(alias);
}
}
out
}
#[must_use]
pub fn answers_to(&self, spelling: &str) -> bool {
let wanted = spelling.trim();
self.spellings().iter().any(|known| known.eq_ignore_ascii_case(wanted))
}
}
pub struct PlatformRegistry {
dialects: HashMap<String, Arc<dyn PlatformDialect>>,
}
impl PlatformRegistry {
#[must_use]
pub fn new() -> Self {
Self { dialects: HashMap::new() }
}
pub fn register(&mut self, name: String, dialect: Arc<dyn PlatformDialect>) {
self.dialects.insert(name, dialect);
}
#[must_use]
pub fn get_by_name(&self, name: &str) -> Option<&Arc<dyn PlatformDialect>> {
self.dialects.get(name)
}
#[must_use]
pub fn resolve(&self, target: &str) -> Option<Arc<dyn PlatformDialect>> {
self.dialects.get(target).cloned()
}
#[must_use]
pub fn list(&self) -> Vec<DialectInfo> {
self.dialects.values().map(|d| d.info()).collect()
}
#[must_use]
pub fn registered_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.dialects.keys().cloned().collect();
names.sort();
names
}
#[must_use]
pub fn with_auto_discovered() -> Self {
use crate::builder::DialectRegistration;
let mut registry = Self::new();
for reg in inventory::iter::<DialectRegistration> {
let dialect = (reg.factory)();
let name = dialect.info().name.to_string();
registry.register(name, dialect);
}
for reg in inventory::iter::<crate::builder::PluginRegistration> {
let mut linked = crate::builder::PluginRegistryBuilder::new();
(reg.register)(&mut linked);
for (name, dialect) in linked.build().dialects.dialects {
registry.register(name, dialect);
}
}
registry
}
}
impl Default for PlatformRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use razeri_plugin_contract::DataType;
struct TestDialect {
info: DialectInfo,
}
impl TestDialect {
fn postgres() -> Self {
Self {
info: DialectInfo {
name: "postgres",
display_name: "PostgreSQL",
aliases: &["postgresql", "pg"],
dialect_family: DialectFamily::PostgreSql,
family: "PostgreSQL",
is_cloud_native: false,
is_olap: false,
is_postgres_compatible: true,
versions: &[],
deploy_script: None,
},
}
}
fn redshift() -> Self {
Self {
info: DialectInfo {
name: "redshift",
display_name: "Amazon Redshift",
aliases: &["amazon_redshift", "rs"],
dialect_family: DialectFamily::PostgreSql,
family: "PostgreSQL",
is_cloud_native: true,
is_olap: true,
is_postgres_compatible: true,
versions: &[],
deploy_script: None,
},
}
}
fn snowflake() -> Self {
Self {
info: DialectInfo {
name: "snowflake",
display_name: "Snowflake",
aliases: &["sf"],
dialect_family: DialectFamily::Snowflake,
family: "Snowflake",
is_cloud_native: true,
is_olap: true,
is_postgres_compatible: false,
versions: &[],
deploy_script: None,
},
}
}
}
impl SqlRenderer for TestDialect {
fn render_data_type(&self, _dt: &DataType) -> String {
"TEXT".to_string()
}
fn quote_identifier(&self, name: &str) -> String {
format!("\"{name}\"")
}
fn qualify_name(&self, schema: Option<&str>, name: &str) -> String {
match schema {
Some(s) => format!("{}.{}", self.quote_identifier(s), self.quote_identifier(name)),
None => self.quote_identifier(name),
}
}
fn render_current_timestamp(&self) -> String {
"CURRENT_TIMESTAMP".to_string()
}
}
impl ExpressionRenderer for TestDialect {
fn render_concat(&self, parts: &[String]) -> String {
parts.join(" || ")
}
}
impl DdlRenderer for TestDialect {}
impl DmlRenderer for TestDialect {}
impl ProcedureRenderer for TestDialect {}
impl FormulaRenderer for TestDialect {}
impl TaskRenderer for TestDialect {}
impl ReplicationRenderer for TestDialect {}
impl StorageRenderer for TestDialect {}
impl DialectCapabilities for TestDialect {
fn supports_identity(&self) -> bool {
true
}
fn supports_sequences(&self) -> bool {
true
}
fn supports_stored_procedures(&self) -> bool {
true
}
}
impl PlatformDialect for TestDialect {
fn info(&self) -> DialectInfo {
self.info.clone()
}
}
const FLOOR_ONLY: &[EngineVersion] = &[EngineVersion::declared("15")];
#[test]
fn a_dialect_with_one_answer_is_itself_at_its_floor_and_refuses_another() {
let dialect: Arc<dyn PlatformDialect> = Arc::new(TestDialect {
info: DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() },
});
assert_eq!(dialect.engine_version(), Some(EngineVersion::declared("15")));
let at_floor =
Arc::clone(&dialect).at_version(&EngineVersion::declared("15")).expect("its floor");
assert!(Arc::ptr_eq(&at_floor, &dialect), "a dialect with one answer returns itself");
let Err(refused) = Arc::clone(&dialect).at_version(&EngineVersion::declared("17")) else {
panic!("17 is not declared")
};
assert_eq!(refused.answers_for, vec![EngineVersion::declared("15")]);
let said = refused.to_string();
assert!(said.contains("PostgreSQL"), "{said}");
assert!(said.contains("15"), "{said}");
assert!(said.contains("`postgres`"), "names the spelling that works: {said}");
}
#[test]
fn a_service_that_chooses_its_own_version_answers_for_none() {
let dialect = TestDialect::snowflake();
assert!(dialect.info().versions.is_empty());
assert_eq!(dialect.engine_version(), None);
}
#[test]
fn an_unsupported_version_names_every_version_and_how_to_write_each() {
let refusal = UnsupportedVersion {
engine: "postgres".to_owned(),
display_name: "PostgreSQL".to_owned(),
asked: "12".to_owned(),
answers_for: ["15", "16", "17"].iter().map(|v| EngineVersion::declared(v)).collect(),
};
let said = refusal.to_string();
assert!(said.contains("15, 16 and 17"), "{said}");
assert!(said.contains("not at 12"), "{said}");
assert!(said.contains("`postgres@17`"), "{said}");
assert!(said.contains("`postgres` for 15"), "the name alone is the floor: {said}");
}
#[test]
fn the_versions_ride_along_in_the_discovery_payload() {
let info = DialectInfo { versions: FLOOR_ONLY, ..TestDialect::postgres().info() };
let json = serde_json::to_value(&info).expect("DialectInfo serializes");
assert_eq!(json["versions"], serde_json::json!(["15"]));
}
#[test]
fn register_dialect_resolves_by_name() {
let mut registry = PlatformRegistry::new();
registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
assert!(registry.get_by_name("postgres").is_some());
}
#[test]
fn register_extension_resolves_by_name() {
let mut registry = PlatformRegistry::new();
registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
assert!(registry.get_by_name("redshift").is_some());
}
#[test]
fn resolve_tries_name_lookup() {
let mut registry = PlatformRegistry::new();
registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
assert!(registry.resolve("postgres").is_some());
assert!(registry.resolve("redshift").is_some());
assert!(registry.resolve("nonexistent").is_none());
}
#[test]
fn list_returns_all_registered_dialects() {
let mut registry = PlatformRegistry::new();
registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
let list = registry.list();
assert_eq!(list.len(), 3);
}
#[test]
fn duplicate_registration_overwrites() {
let mut registry = PlatformRegistry::new();
registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
assert!(registry.get_by_name("postgres").is_some());
let list = registry.list();
assert_eq!(list.len(), 1);
}
#[test]
fn dialect_info_contains_expected_metadata() {
let dialect = TestDialect::postgres();
let info = dialect.info();
assert_eq!(info.name, "postgres");
assert_eq!(info.display_name, "PostgreSQL");
assert_eq!(info.dialect_family, DialectFamily::PostgreSql);
}
#[test]
fn classification_fields_reflect_dialect_characteristics() {
let pg = TestDialect::postgres();
let pg_info = pg.info();
assert_eq!(pg_info.family, "PostgreSQL");
assert!(pg_info.is_postgres_compatible);
assert!(!pg_info.is_cloud_native);
assert!(!pg_info.is_olap);
let sf = TestDialect::snowflake();
let sf_info = sf.info();
assert_eq!(sf_info.family, "Snowflake");
assert!(sf_info.is_cloud_native);
assert!(sf_info.is_olap);
assert!(!sf_info.is_postgres_compatible);
let rd = TestDialect::redshift();
let rd_info = rd.info();
assert!(rd_info.is_cloud_native);
assert!(rd_info.is_olap);
assert!(rd_info.is_postgres_compatible);
}
#[test]
fn empty_registry_returns_none() {
let registry = PlatformRegistry::new();
assert!(registry.get_by_name("postgres").is_none());
assert!(registry.resolve("postgres").is_none());
assert!(registry.list().is_empty());
}
#[test]
fn registered_names_are_sorted() {
let mut registry = PlatformRegistry::new();
registry.register("snowflake".to_string(), Arc::new(TestDialect::snowflake()));
registry.register("postgres".to_string(), Arc::new(TestDialect::postgres()));
registry.register("redshift".to_string(), Arc::new(TestDialect::redshift()));
assert_eq!(registry.registered_names(), vec!["postgres", "redshift", "snowflake"]);
}
#[test]
fn dialect_info_serializes_for_discovery() {
let info = TestDialect::snowflake().info();
let json = serde_json::to_value(&info).expect("DialectInfo serializes");
assert_eq!(json["name"], "snowflake");
assert_eq!(json["display_name"], "Snowflake");
assert_eq!(json["dialect_family"], "snowflake");
assert_eq!(json["is_cloud_native"], true);
assert_eq!(json["is_postgres_compatible"], false);
assert_eq!(json["aliases"], serde_json::json!(["sf"]));
}
#[test]
fn spellings_start_with_the_dialects_own_name() {
let info = TestDialect::postgres().info();
assert_eq!(info.spellings(), vec!["postgres", "postgresql", "pg"]);
}
#[test]
fn a_dialect_answers_to_its_name_and_its_aliases_in_any_case() {
let info = TestDialect::postgres().info();
assert!(info.answers_to("postgres"));
assert!(info.answers_to("pg"));
assert!(info.answers_to(" PostgreSQL "));
assert!(!info.answers_to("snowflake"));
}
#[test]
fn an_alias_repeating_the_name_is_listed_once() {
let info = DialectInfo { aliases: &["postgres", "pg"], ..TestDialect::postgres().info() };
assert_eq!(info.spellings(), vec!["postgres", "pg"]);
}
}