use std::collections::HashMap;
use drizzle_types::Dialect;
#[derive(Debug, Clone, Default)]
pub struct ParsedTable {
pub name: String,
pub attr: String,
pub fields: Vec<ParsedField>,
pub dialect: Dialect,
}
#[derive(Debug, Clone, Default)]
pub struct ParsedIndex {
pub name: String,
pub attr: String,
pub columns: Vec<String>,
pub dialect: Dialect,
}
#[derive(Debug, Clone, Default)]
pub struct ParsedSchema {
pub name: String,
pub members: HashMap<String, String>,
pub dialect: Dialect,
}
#[derive(Debug, Clone, Default)]
pub struct ParsedField {
pub name: String,
pub ty: String,
pub attrs: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ParseResult {
pub tables: HashMap<String, ParsedTable>,
pub indexes: HashMap<String, ParsedIndex>,
pub schema: Option<ParsedSchema>,
pub dialect: Dialect,
}
impl ParsedTable {
#[must_use]
pub fn field(&self, name: &str) -> Option<&ParsedField> {
self.fields.iter().find(|f| f.name == name)
}
#[must_use]
pub fn has_table_attr(&self, attr: &str) -> bool {
self.attr.contains(attr)
}
#[must_use]
pub fn attr_value(&self, key: &str) -> Option<String> {
extract_attr_value_from_attr(&self.attr, key)
}
#[must_use]
pub fn schema_name(&self) -> Option<String> {
self.attr_value("schema")
.map(|v| trim_wrapping_quotes(v.trim()).to_string())
}
#[must_use]
pub fn is_strict(&self) -> bool {
self.has_table_attr("strict")
}
#[must_use]
pub fn is_without_rowid(&self) -> bool {
self.has_table_attr("without_rowid")
}
#[must_use]
pub fn field_names(&self) -> Vec<&str> {
self.fields.iter().map(|f| f.name.as_str()).collect()
}
}
impl ParsedIndex {
#[must_use]
pub fn is_unique(&self) -> bool {
self.attr.contains("unique")
}
#[must_use]
pub fn is_concurrent(&self) -> bool {
self.attr.contains("concurrent")
}
#[must_use]
pub fn method(&self) -> Option<String> {
self.attr_value("method")
.map(|v| trim_wrapping_quotes(v.trim()).to_string())
}
#[must_use]
pub fn where_clause(&self) -> Option<String> {
self.attr_value("where")
.map(|v| trim_wrapping_quotes(v.trim()).to_string())
}
#[must_use]
pub fn table_name(&self) -> Option<&str> {
self.columns.first().and_then(|c| c.split("::").next())
}
fn attr_value(&self, key: &str) -> Option<String> {
extract_attr_value_from_attr(&self.attr, key)
}
}
impl ParsedField {
#[must_use]
pub fn has_attr(&self, attr: &str) -> bool {
self.attrs.iter().any(|a| a.contains(attr))
}
#[must_use]
pub fn attr_value(&self, key: &str) -> Option<String> {
for attr in &self.attrs {
if let Some(value) = Self::extract_attr_value(attr, key) {
return Some(value);
}
}
None
}
#[must_use]
pub fn attr_values(&self) -> HashMap<String, String> {
let mut result = HashMap::new();
for attr in &self.attrs {
if let Some(start) = attr.find('(')
&& let Some(end) = attr.rfind(')')
{
let content = &attr[start + 1..end];
for part in split_attr_parts(content) {
if let Some(eq_pos) = part.find('=') {
let key = part[..eq_pos].trim();
let value = part[eq_pos + 1..].trim();
result.insert(key.to_string(), value.to_string());
}
}
}
}
result
}
#[must_use]
pub fn column_attr(&self) -> String {
self.attrs.join(", ")
}
#[must_use]
pub fn is_nullable(&self) -> bool {
self.ty.starts_with("Option<")
}
#[must_use]
pub fn is_primary_key(&self) -> bool {
self.has_attr("primary")
}
#[must_use]
pub fn is_autoincrement(&self) -> bool {
self.has_attr("autoincrement")
}
#[must_use]
pub fn is_unique(&self) -> bool {
self.has_attr("unique")
}
#[must_use]
pub fn default_value(&self) -> Option<String> {
self.attr_value("default")
}
#[must_use]
pub fn references(&self) -> Option<String> {
self.attr_value("references")
}
#[must_use]
pub fn on_delete(&self) -> Option<String> {
self.attr_value("on_delete")
}
#[must_use]
pub fn on_update(&self) -> Option<String> {
self.attr_value("on_update")
}
fn extract_attr_value(attr: &str, key: &str) -> Option<String> {
extract_attr_value_from_attr(attr, key)
}
}
fn extract_attr_value_from_attr(attr: &str, key: &str) -> Option<String> {
let start = attr.find('(')?;
let end = attr.rfind(')')?;
let content = &attr[start + 1..end];
for part in split_attr_parts(content) {
let part = part.trim();
if let Some(eq_pos) = part.find('=') {
let k = part[..eq_pos].trim();
if k == key {
return Some(part[eq_pos + 1..].trim().to_string());
}
}
}
None
}
fn split_attr_parts(content: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut depth = 0usize;
let mut start = 0;
let mut in_single = false;
let mut in_double = false;
let mut escaped = false;
for (i, c) in content.char_indices() {
if escaped {
escaped = false;
continue;
}
match c {
'\\' if in_single || in_double => {
escaped = true;
}
'\'' if !in_double => {
in_single = !in_single;
}
'"' if !in_single => {
in_double = !in_double;
}
'(' | '<' | '[' | '{' if !in_single && !in_double => {
depth += 1;
}
')' | '>' | ']' | '}' if !in_single && !in_double => {
depth = depth.saturating_sub(1);
}
',' if depth == 0 && !in_single && !in_double => {
parts.push(content[start..i].trim());
start = i + 1;
}
_ => {}
}
}
if start < content.len() {
parts.push(content[start..].trim());
}
parts
}
fn trim_wrapping_quotes(value: &str) -> &str {
let bytes = value.as_bytes();
if bytes.len() >= 2
&& ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
|| (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''))
{
&value[1..value.len() - 1]
} else {
value
}
}
impl ParseResult {
#[must_use]
pub fn table(&self, name: &str, dialect: Dialect) -> Option<&ParsedTable> {
let key = format!("{}:{}", dialect_key(dialect), name);
self.tables.get(&key)
}
#[must_use]
pub fn index(&self, name: &str, dialect: Dialect) -> Option<&ParsedIndex> {
let key = format!("{}:{}", dialect_key(dialect), name);
self.indexes.get(&key)
}
pub fn tables_for_dialect(&self, dialect: Dialect) -> impl Iterator<Item = &ParsedTable> {
let prefix = format!("{}:", dialect_key(dialect));
self.tables
.iter()
.filter(move |(k, _)| k.starts_with(&prefix))
.map(|(_, v)| v)
}
pub fn indexes_for_dialect(&self, dialect: Dialect) -> impl Iterator<Item = &ParsedIndex> {
let prefix = format!("{}:", dialect_key(dialect));
self.indexes
.iter()
.filter(move |(k, _)| k.starts_with(&prefix))
.map(|(_, v)| v)
}
#[must_use]
pub fn table_names(&self) -> Vec<&str> {
self.tables
.keys()
.filter_map(|s| s.split(':').nth(1))
.collect()
}
#[must_use]
pub fn index_names(&self) -> Vec<&str> {
self.indexes
.keys()
.filter_map(|s| s.split(':').nth(1))
.collect()
}
}
const fn dialect_key(dialect: Dialect) -> &'static str {
match dialect {
Dialect::SQLite => "sqlite",
Dialect::PostgreSQL => "postgres",
Dialect::MySQL => "mysql",
}
}