#![allow(dead_code)]
use super::ir::{JsonType, SqlType};
pub trait Dialect: Send + Sync {
fn name(&self) -> &'static str;
fn placeholder(&self, idx: usize) -> String;
fn json_field(&self, base: &str, key: &str) -> String;
fn json_field_text(&self, base: &str, key: &str) -> String;
fn json_path(&self, base: &str, segments: &[&str]) -> String;
fn json_path_text(&self, base: &str, segments: &[&str]) -> String;
fn unnest_array(&self, expr: &str) -> String;
fn coalesce_array(&self, expr: &str) -> String;
fn json_type(&self, expr: &str) -> String;
fn json_agg(&self, expr: &str) -> String;
fn string_agg(&self, expr: &str, sep_param: &str) -> String;
fn bool_true(&self) -> &'static str;
fn bool_false(&self) -> &'static str;
fn lateral_keyword(&self) -> &'static str;
fn cast(&self, inner: &str, ty: SqlType) -> String;
fn has_json_type(&self, expr: &str, ty: JsonType) -> String;
fn truthy_predicate(&self, expr: &str) -> String;
fn last_path_segment(&self, s: &str) -> String;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct PgDialect;
impl Dialect for PgDialect {
fn name(&self) -> &'static str {
"postgres"
}
fn placeholder(&self, idx: usize) -> String {
format!("${idx}")
}
fn json_field(&self, base: &str, key: &str) -> String {
format!("{base}->'{key}'")
}
fn json_field_text(&self, base: &str, key: &str) -> String {
format!("{base}->>'{key}'")
}
fn json_path(&self, base: &str, segments: &[&str]) -> String {
if segments.len() == 1 {
self.json_field(base, segments[0])
} else {
format!("{base}#>'{{{}}}'", segments.join(","))
}
}
fn json_path_text(&self, base: &str, segments: &[&str]) -> String {
if segments.len() == 1 {
self.json_field_text(base, segments[0])
} else {
format!("{base}#>>'{{{}}}'", segments.join(","))
}
}
fn unnest_array(&self, expr: &str) -> String {
format!("jsonb_array_elements({expr})")
}
fn coalesce_array(&self, expr: &str) -> String {
format!("coalesce({expr}, '[]'::jsonb)")
}
fn json_type(&self, expr: &str) -> String {
format!("jsonb_typeof({expr})")
}
fn json_agg(&self, expr: &str) -> String {
format!("coalesce(jsonb_agg({expr}), '[]'::jsonb)")
}
fn string_agg(&self, expr: &str, sep_param: &str) -> String {
format!("string_agg({expr}, {sep_param})")
}
fn bool_true(&self) -> &'static str {
"true"
}
fn bool_false(&self) -> &'static str {
"false"
}
fn lateral_keyword(&self) -> &'static str {
"LATERAL "
}
fn cast(&self, inner: &str, ty: SqlType) -> String {
match ty {
SqlType::Text => format!("({inner})::text"),
SqlType::Integer => format!("(({inner})::bigint)::text"),
SqlType::Decimal => format!("(({inner})::numeric)::text"),
SqlType::Boolean => {
format!(
"CASE WHEN ({inner})::boolean IS TRUE THEN 'true' \
WHEN ({inner})::boolean IS FALSE THEN 'false' END"
)
}
SqlType::Json => format!("({inner})::jsonb"),
}
}
fn has_json_type(&self, expr: &str, ty: JsonType) -> String {
let name = match ty {
JsonType::Object => "object",
JsonType::Array => "array",
JsonType::String => "string",
JsonType::Number => "number",
JsonType::Boolean => "boolean",
JsonType::Null => "null",
};
format!("jsonb_typeof({expr}) = '{name}'")
}
fn truthy_predicate(&self, expr: &str) -> String {
format!("({expr})::boolean IS TRUE")
}
fn last_path_segment(&self, s: &str) -> String {
format!("regexp_replace({s}, '.*/', '')")
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SqliteDialect;
impl Dialect for SqliteDialect {
fn name(&self) -> &'static str {
"sqlite"
}
fn placeholder(&self, idx: usize) -> String {
format!("?{idx}")
}
fn json_field(&self, base: &str, key: &str) -> String {
format!("json_extract({base}, '$.{key}')")
}
fn json_field_text(&self, base: &str, key: &str) -> String {
self.json_field(base, key)
}
fn json_path(&self, base: &str, segments: &[&str]) -> String {
let mut path = String::from("$");
for seg in segments {
if seg.chars().all(|c| c.is_ascii_digit()) {
path.push('[');
path.push_str(seg);
path.push(']');
} else {
path.push('.');
path.push_str(seg);
}
}
format!("json_extract({base}, '{path}')")
}
fn json_path_text(&self, base: &str, segments: &[&str]) -> String {
self.json_path(base, segments)
}
fn unnest_array(&self, expr: &str) -> String {
format!("json_each({expr})")
}
fn coalesce_array(&self, expr: &str) -> String {
format!("coalesce({expr}, '[]')")
}
fn json_type(&self, expr: &str) -> String {
format!("json_type({expr})")
}
fn json_agg(&self, expr: &str) -> String {
format!("json_group_array({expr})")
}
fn string_agg(&self, expr: &str, sep_param: &str) -> String {
format!("group_concat({expr}, {sep_param})")
}
fn bool_true(&self) -> &'static str {
"1"
}
fn bool_false(&self) -> &'static str {
"0"
}
fn lateral_keyword(&self) -> &'static str {
""
}
fn cast(&self, inner: &str, ty: SqlType) -> String {
match ty {
SqlType::Text => format!("CAST({inner} AS TEXT)"),
SqlType::Integer => format!("CAST({inner} AS INTEGER)"),
SqlType::Decimal => format!("CAST({inner} AS REAL)"),
SqlType::Boolean => {
format!("CASE WHEN ({inner}) THEN 'true' WHEN NOT ({inner}) THEN 'false' END")
}
SqlType::Json => format!("json({inner})"),
}
}
fn has_json_type(&self, expr: &str, ty: JsonType) -> String {
let name = match ty {
JsonType::Object => "object",
JsonType::Array => "array",
JsonType::String => "text",
JsonType::Number => "integer", JsonType::Boolean => "true", JsonType::Null => "null",
};
format!("json_type({expr}) = '{name}'")
}
fn truthy_predicate(&self, expr: &str) -> String {
format!("({expr}) IS NOT NULL AND ({expr}) != 0 AND ({expr}) != 'false'")
}
fn last_path_segment(&self, s: &str) -> String {
format!("fhir_last_segment({s})")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pg_field_text() {
assert_eq!(PgDialect.json_field_text("r.data", "id"), "r.data->>'id'");
}
#[test]
fn pg_path_text_dotted() {
assert_eq!(
PgDialect.json_path_text("r.data", &["subject", "reference"]),
"r.data#>>'{subject,reference}'"
);
}
#[test]
fn sqlite_field() {
assert_eq!(
SqliteDialect.json_field("r.data", "id"),
"json_extract(r.data, '$.id')"
);
}
#[test]
fn sqlite_path_dotted() {
assert_eq!(
SqliteDialect.json_path("r.data", &["subject", "reference"]),
"json_extract(r.data, '$.subject.reference')"
);
}
#[test]
fn placeholder_forms() {
assert_eq!(PgDialect.placeholder(3), "$3");
assert_eq!(SqliteDialect.placeholder(3), "?3");
}
}