use razeri_plugin_contract::DialectFamily;
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 trait PlatformDialect:
SqlRenderer
+ ExpressionRenderer
+ DdlRenderer
+ DmlRenderer
+ ProcedureRenderer
+ FormulaRenderer
+ TaskRenderer
+ DialectCapabilities
+ ReplicationRenderer
+ StorageRenderer
{
fn info(&self) -> DialectInfo;
}
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,
},
}
}
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,
},
}
}
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,
},
}
}
}
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()
}
}
#[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"]);
}
}