use sqlparser::ast::Statement;
use sqlparser::dialect::{PostgreSqlDialect, SQLiteDialect};
use sqlparser::parser::Parser;
use super::error::ParseError;
use super::segments::{SqlObjectName, SqlSegment, SqlStatementKind, segment_sql};
use super::table_recovery::recover_table_sql;
use super::{postgres, sqlite};
use crate::dialects::Dialect;
use crate::states::types::EntityKind;
use crate::states::{
ExtensionDef, FunctionDef, Index, Schema, Table, TriggerDef, ViewDef, schema_qualified_key,
};
pub(crate) struct ParseContext {
pub(crate) schema: Schema,
pending_indexes: Vec<(String, Index)>,
}
impl ParseContext {
pub(crate) fn new() -> Self {
Self {
schema: Schema::default(),
pending_indexes: Vec::new(),
}
}
pub(crate) fn insert_table(&mut self, table: (String, Table)) -> Result<(), ParseError> {
let (key, table) = table;
if self.schema.tables.contains_key(&key) {
return Err(ParseError::DuplicateTable(key));
}
self.schema.tables.insert(key, table);
Ok(())
}
pub(crate) fn push_index(&mut self, index: (String, Index)) {
self.pending_indexes.push(index);
}
fn preserve_opaque_index_source(&mut self, raw: &str) {
let Some((_, index)) = self.pending_indexes.last_mut() else {
return;
};
if index.is_opaque() {
*index = Index::from_raw(index.name.clone(), raw.to_string());
}
}
fn finish(self) -> Result<Schema, ParseError> {
self.finish_raw()
}
fn finish_raw(mut self) -> Result<Schema, ParseError> {
for (table_name, index) in self.pending_indexes {
match self.schema.tables.get_mut(&table_name) {
Some(table) => table.indexes.push(index),
None => return Err(ParseError::UnknownTable { table: table_name }),
}
}
Ok(self.schema)
}
}
pub fn parse_sql(sql: &str, dialect: Dialect) -> Result<Schema, ParseError> {
Ok(parse_sql_raw(sql, dialect)?.prepare(dialect)?)
}
pub(crate) fn parse_sql_raw(sql: &str, dialect: Dialect) -> Result<Schema, ParseError> {
let segments = segment_sql(sql, dialect)?;
if matches!(dialect, Dialect::Mysql) {
return Err(ParseError::UnsupportedDialect("mysql".to_string()));
}
let mut ctx = ParseContext::new();
for segment in segments {
ensure_schema_segment(&segment, dialect)?;
let statements = match parse_segment(&segment.sql, dialect) {
Ok(statements) => statements,
Err(error) => {
if recover_modeled_table(&segment, &mut ctx, dialect)? {
continue;
}
if lower_raw_segment(&segment, &mut ctx, dialect).is_ok() {
continue;
}
return Err(ParseError::parse_in_segment(dialect, &segment, error));
}
};
for stmt in &statements {
ensure_modeled_create_statement(stmt, dialect)?;
let lowered = match dialect {
Dialect::Postgres => postgres::lower_statement(stmt, &mut ctx),
Dialect::Sqlite => sqlite::lower_statement(stmt, &mut ctx),
Dialect::Mysql => unreachable!("mysql returned above"),
};
if let Err(error) = lowered {
if matches!(segment.kind, Some(SqlStatementKind::Ddl(ref ddl)) if ddl.entity == EntityKind::Table)
{
return Err(error);
}
lower_raw_segment(&segment, &mut ctx, dialect)?;
} else if matches!(
segment.kind,
Some(SqlStatementKind::Ddl(ref ddl)) if ddl.entity == EntityKind::Index
) {
ctx.preserve_opaque_index_source(&segment.sql);
}
}
}
ctx.finish()
}
fn ensure_schema_segment(segment: &SqlSegment, dialect: Dialect) -> Result<(), ParseError> {
if matches!(segment.kind, Some(SqlStatementKind::Ddl(_))) {
return Ok(());
}
Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"schema SQL must be a CREATE statement for a known Gaman entity kind",
))
}
fn parse_segment(
sql: &str,
dialect: Dialect,
) -> Result<Vec<Statement>, sqlparser::parser::ParserError> {
match dialect {
Dialect::Postgres => Parser::parse_sql(&PostgreSqlDialect {}, sql),
Dialect::Sqlite => Parser::parse_sql(&SQLiteDialect {}, sql),
Dialect::Mysql => unreachable!("mysql returned before segment parsing"),
}
}
fn recover_modeled_table(
segment: &SqlSegment,
ctx: &mut ParseContext,
dialect: Dialect,
) -> Result<bool, ParseError> {
let Some(SqlStatementKind::Ddl(ddl)) = &segment.kind else {
return Ok(false);
};
if ddl.entity != EntityKind::Table {
return Ok(false);
}
let Some(recovered) = recover_table_sql(&segment.sql, dialect) else {
return Ok(false);
};
let Ok(statements) = parse_segment(&recovered.core_sql, dialect) else {
return Ok(false);
};
for statement in &statements {
ensure_modeled_create_statement(statement, dialect)?;
match dialect {
Dialect::Postgres => postgres::lower_statement(statement, ctx)?,
Dialect::Sqlite => sqlite::lower_statement(statement, ctx)?,
Dialect::Mysql => unreachable!("mysql returned before table recovery"),
}
}
attach_recovered_options(ddl.name.as_ref(), recovered, ctx);
Ok(true)
}
fn attach_recovered_options(
name: Option<&SqlObjectName>,
recovered: super::table_recovery::RecoveredTableSql,
ctx: &mut ParseContext,
) {
let Some(name) = name else {
return;
};
let (name, schema) = object_name_parts(name);
let key = schema_qualified_key(&name, schema.as_deref());
let Some(table) = ctx.schema.tables.get_mut(&key) else {
return;
};
let mut header = table.options.header_raw.clone();
let mut tail = table.options.tail_raw.clone();
header.extend(recovered.header_options);
tail.extend(recovered.tail_options);
table.options = crate::states::TableOptionsMeta::from_parts(header, tail);
}
fn lower_raw_segment(
segment: &SqlSegment,
ctx: &mut ParseContext,
dialect: Dialect,
) -> Result<(), ParseError> {
let Some(SqlStatementKind::Ddl(ddl)) = &segment.kind else {
return Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"schema SQL must be CREATE statements for known Gaman entity kinds",
));
};
let Some(name) = &ddl.name else {
return Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"raw fallback requires a recoverable object name",
));
};
match ddl.entity {
EntityKind::Table => Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"CREATE TABLE must parse into a modeled table; opaque tables are not supported",
)),
EntityKind::Index => lower_raw_index(segment, ctx, dialect, name, ddl.owner.as_ref()),
EntityKind::Trigger => lower_raw_trigger(segment, ctx, dialect, name, ddl.owner.as_ref()),
EntityKind::Function => {
let (name, schema) = object_name_parts(name);
let key = schema_qualified_key(&name, schema.as_deref());
let mut function = FunctionDef::from_raw(name, segment.sql.clone());
function.schema = schema;
ctx.schema.functions.insert(key, function);
Ok(())
}
EntityKind::View => {
let (name, schema) = object_name_parts(name);
let key = schema_qualified_key(&name, schema.as_deref());
let mut view = ViewDef::from_raw(name, segment.sql.clone());
view.schema = schema;
ctx.schema.views.insert(key, view);
Ok(())
}
EntityKind::Extension => {
let (name, schema) = object_name_parts(name);
let key = schema_qualified_key(&name, schema.as_deref());
let mut extension = ExtensionDef::from_raw(name, segment.sql.clone());
extension.schema = schema;
ctx.schema.extensions.insert(key, extension);
Ok(())
}
EntityKind::Enum | EntityKind::Column | EntityKind::ForeignKey | EntityKind::Constraint => {
Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"this entity kind must be recovered from modeled SQL, not raw fallback",
))
}
}
}
fn lower_raw_index(
segment: &SqlSegment,
ctx: &mut ParseContext,
dialect: Dialect,
name: &SqlObjectName,
owner: Option<&SqlObjectName>,
) -> Result<(), ParseError> {
let Some(owner) = owner else {
return Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"opaque index fallback requires a recoverable target table after ON",
));
};
let (table, schema) = object_name_parts(owner);
let table_name = schema_qualified_key(&table, schema.as_deref());
let (index_name, _) = object_name_parts(name);
ctx.push_index((table_name, Index::from_raw(index_name, segment.sql.clone())));
Ok(())
}
fn lower_raw_trigger(
segment: &SqlSegment,
ctx: &mut ParseContext,
dialect: Dialect,
name: &SqlObjectName,
owner: Option<&SqlObjectName>,
) -> Result<(), ParseError> {
let Some(owner) = owner else {
return Err(ParseError::unsupported(
dialect,
segment.sql.clone(),
"opaque trigger fallback requires a recoverable target table after ON",
));
};
let (table, schema) = object_name_parts(owner);
let table_name = schema_qualified_key(&table, schema.as_deref());
let table =
ctx.schema
.tables
.get_mut(&table_name)
.ok_or_else(|| ParseError::UnknownTriggerTable {
table: table_name.clone(),
})?;
let (trigger_name, _) = object_name_parts(name);
table
.triggers
.push(TriggerDef::from_raw(trigger_name, segment.sql.clone()));
Ok(())
}
fn object_name_parts(name: &SqlObjectName) -> (String, Option<String>) {
match name.parts.as_slice() {
[schema, name] if schema != "public" => (name.clone(), Some(schema.clone())),
[_, name] => (name.clone(), None),
[name] => (name.clone(), None),
_ => (name.raw.clone(), None),
}
}
fn ensure_modeled_create_statement(stmt: &Statement, dialect: Dialect) -> Result<(), ParseError> {
if is_modeled_create_statement(stmt) {
return Ok(());
}
Err(ParseError::unsupported(
dialect,
stmt.to_string(),
"only CREATE statements for modeled schema entities are parsed",
))
}
fn is_modeled_create_statement(stmt: &Statement) -> bool {
matches!(
stmt,
Statement::CreateExtension(_)
| Statement::CreateFunction(_)
| Statement::CreateIndex(_)
| Statement::CreateTable(_)
| Statement::CreateTrigger(_)
| Statement::CreateType { .. }
| Statement::CreateView(_)
)
}
#[cfg(test)]
mod recovery_tests {
use super::*;
#[test]
fn parse_sql_recovers_unlogged_table() {
let schema = parse_sql(
"CREATE UNLOGGED TABLE events (id integer NOT NULL)",
Dialect::Postgres,
)
.expect("parse table");
let table = schema.tables.get("events").expect("events table");
assert_eq!(table.columns.len(), 1);
assert_eq!(table.options.header_raw, ["UNLOGGED"]);
}
#[test]
fn raw_index_uses_classified_owner() {
let schema = parse_sql(
"CREATE TABLE app.users (email text); CREATE INDEX users_email_idx ON app.users ((lower(email)))",
Dialect::Postgres,
)
.expect("parse schema");
let table = schema.tables.get("app.users").expect("qualified table");
assert!(
table
.indexes
.iter()
.any(|index| index.name == "users_email_idx")
);
}
}