pub mod filter;
pub mod introspect;
pub mod ir;
pub mod overlay;
pub mod raw;
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 raw::{compile_raw_stmt, RawStmt};
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)
}
fn is_reserved_physical(name: &str) -> bool {
name == "_id" || name.starts_with("__")
}
pub(crate) fn physical_of(name: &str) -> String {
if is_reserved_physical(name) {
name.to_string()
} else {
crate::naming::to_snake(&crate::naming::canonical(name))
}
}
#[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(crate) fn pcol(&self, logical: &str) -> String {
self.quote_ident(&physical_of(logical))
}
pub fn double_type(&self) -> &'static str {
match self {
Backend::Mysql => "DOUBLE",
Backend::Postgres => "DOUBLE PRECISION",
Backend::Sqlite => "REAL",
}
}
pub fn qualified_table(
&self,
database: Option<&str>,
schema: Option<&str>,
table: &str,
) -> String {
let t = self.quote_ident(&physical_of(table));
let qual = match self {
Backend::Postgres => schema,
_ => database,
};
match qual.filter(|s| !s.is_empty()) {
Some(q) => format!("{}.{}", self.quote_ident(q), t),
None => t,
}
}
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]) -> Result<String, String> {
for seg in path {
validate_json_path_seg(seg)?;
}
match self {
Backend::Mysql => Ok(format!(
"JSON_UNQUOTE(JSON_EXTRACT({}, '{}'))",
col,
json_path_dollar(path)?
)),
Backend::Postgres => {
let segs: Vec<String> = path.iter().map(|s| pg_array_literal_seg(s)).collect();
Ok(format!("({} #>> '{{{}}}')", col, segs.join(",")))
}
Backend::Sqlite => Ok(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 validate_json_path_seg(seg: &str) -> Result<(), String> {
if seg.is_empty() {
return Err("ERR_GQL_PARSE:JSON 点号路径段为空(非法路径段)".to_string());
}
let ok = seg.chars().all(|c| {
c.is_ascii_alphanumeric() || c == '_' || c == '-' || ('\u{4e00}'..='\u{9fff}').contains(&c)
});
if ok {
Ok(())
} else {
Err(format!(
"ERR_GQL_PARSE:JSON 点号路径段 \"{seg}\" 含非法字符(仅允许字母/数字/下划线/连字符/中文)"
))
}
}
fn pg_array_literal_seg(seg: &str) -> String {
let needs_quote = seg.is_empty()
|| seg
.chars()
.any(|c| matches!(c, '"' | '\\' | ',' | '{' | '}' | ' '));
if !needs_quote {
return seg.to_string();
}
let mut out = String::from("\"");
for ch in seg.chars() {
if ch == '"' || ch == '\\' {
out.push('\\');
}
out.push(ch);
}
out.push('"');
out
}
fn json_path_dollar(path: &[&str]) -> Result<String, String> {
let mut s = String::from("$");
for seg in path {
validate_json_path_seg(seg)?;
s.push('.');
for ch in seg.chars() {
match ch {
'\'' => s.push_str("''"),
'\\' => s.push_str("\\\\"),
_ => s.push(ch),
}
}
}
Ok(s)
}