pub mod filter;
pub mod introspect;
pub mod ir;
pub mod overlay;
pub mod row;
pub mod select;
pub mod translate;
pub mod write;
pub use introspect::{introspect_to_schema_json, schema_def_from_rows};
pub use overlay::merge_schema;
pub use row::restore_rows_json;
pub use translate::translate;
use crate::schema::Schema;
pub(crate) fn scalar_column(schema: &Schema, field: &str) -> Option<String> {
if field.contains('.') {
let (head, _) = field.split_once('.')?;
let head_type = schema.fields.get(head).map(|f| f.field_type.as_str());
if matches!(head_type, Some("object") | Some("array")) {
return None;
}
return Some(field.to_string());
}
match schema.fields.get(field).map(|f| f.field_type.as_str()) {
Some("object") | Some("array") => None,
_ => Some(field.to_string()),
}
}
pub(crate) fn field_column_ref(schema: &Schema, field: &str) -> Option<ColumnRef> {
if let Some((head, rest)) = field.split_once('.') {
let head_type = schema.fields.get(head).map(|f| f.field_type.as_str());
match head_type {
Some("object") | Some("array") => {
let path: Vec<String> = rest.split('.').map(String::from).collect();
Some(ColumnRef::JsonPath(head.to_string(), path))
}
_ => Some(ColumnRef::Scalar(field.to_string())),
}
} else {
match schema.fields.get(field).map(|f| f.field_type.as_str()) {
Some("object") => Some(ColumnRef::Json(field.to_string(), JsonKind::Object)),
Some("array") => Some(ColumnRef::Json(field.to_string(), JsonKind::Array)),
_ => Some(ColumnRef::Scalar(field.to_string())),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsonKind {
Object,
Array,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ColumnRef {
Scalar(String),
Json(String, JsonKind),
JsonPath(String, Vec<String>),
}
pub(crate) fn field_is_bool(schema: &Schema, field: &str) -> bool {
schema
.fields
.get(field)
.map(|f| {
f.field_type.eq_ignore_ascii_case("bool")
|| f.field_type.eq_ignore_ascii_case("boolean")
})
.unwrap_or(false)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
Mysql,
Postgres,
Sqlite,
}
impl Backend {
pub fn parse(s: &str) -> Result<Backend, String> {
match s.to_lowercase().as_str() {
"mysql" => Ok(Backend::Mysql),
"postgres" | "pg" | "postgresql" => Ok(Backend::Postgres),
"sqlite" => Ok(Backend::Sqlite),
_ => Err(format!("不支持的后端: {}", s)),
}
}
pub fn quote_ident(&self, ident: &str) -> String {
match self {
Backend::Mysql => format!("`{}`", ident.replace('`', "``")),
_ => format!("\"{}\"", ident.replace('"', "\"\"")),
}
}
pub fn double_type(&self) -> &'static str {
match self {
Backend::Mysql => "DOUBLE",
Backend::Postgres => "DOUBLE PRECISION",
Backend::Sqlite => "REAL",
}
}
pub fn qualified_table(&self, namespace: Option<&str>, table: &str) -> String {
match namespace.filter(|s| !s.is_empty()) {
Some(ns) => format!("{}.{}", self.quote_ident(ns), self.quote_ident(table)),
None => self.quote_ident(table),
}
}
pub fn placeholder(&self, _index: usize) -> String {
match self {
Backend::Postgres => format!("${}", _index + 1),
_ => "?".to_string(),
}
}
pub fn as_str(&self) -> &'static str {
match self {
Backend::Mysql => "mysql",
Backend::Postgres => "postgres",
Backend::Sqlite => "sqlite",
}
}
pub fn supports_returning(&self) -> bool {
!matches!(self, Backend::Mysql)
}
pub fn json_type_name(&self) -> &'static str {
match self {
Backend::Mysql => "JSON",
Backend::Postgres => "jsonb",
Backend::Sqlite => "TEXT",
}
}
pub fn json_extract_scalar(&self, col: &str, path: &[&str]) -> String {
match self {
Backend::Mysql => format!(
"JSON_UNQUOTE(JSON_EXTRACT({}, '{}'))",
col,
json_path_dollar(path)
),
Backend::Postgres => format!("({} #>> '{{{}}}')", col, path.join(",")),
Backend::Sqlite => format!("json_extract({}, '{}')", col, json_path_dollar(path)),
}
}
pub fn json_array_contains(&self, col: &str, ph: &str) -> String {
match self {
Backend::Mysql => format!("JSON_CONTAINS({}, CAST({} AS JSON))", col, ph),
Backend::Postgres => format!("{} @> {}::jsonb", col, ph),
Backend::Sqlite => format!(
"EXISTS (SELECT 1 FROM json_each({}) WHERE json_each.value = {})",
col, ph
),
}
}
pub fn json_value_eq(&self, col: &str, ph: &str, negate: bool) -> String {
let op = if negate { "<>" } else { "=" };
match self {
Backend::Mysql => format!("{} {} CAST({} AS JSON)", col, op, ph),
Backend::Postgres => format!("{} {} {}::jsonb", col, op, ph),
Backend::Sqlite => format!("json({}) {} json({})", col, op, ph),
}
}
}
fn json_path_dollar(path: &[&str]) -> String {
let mut s = String::from("$");
for seg in path {
s.push('.');
s.push_str(seg);
}
s
}