use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SqlStatementModel {
pub verb: SqlSemanticVerb,
pub tables: Vec<TableUse>,
pub reads: Vec<ColumnUse>,
pub writes: Vec<ColumnUse>,
pub projection: Vec<ProjectionItem>,
pub alias_scope: AliasScope,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SqlSemanticModel {
pub statements: Vec<SqlStatementModel>,
}
impl SqlSemanticModel {
pub fn push(&mut self, m: SqlStatementModel) -> usize {
let pos = self.statements.len();
self.statements.push(m);
pos
}
pub fn iter(&self) -> impl Iterator<Item = (usize, &SqlStatementModel)> {
self.statements.iter().enumerate()
}
#[must_use]
pub fn distinct_tables(&self) -> Vec<(String, String)> {
let mut out = std::collections::BTreeSet::new();
for s in &self.statements {
for t in &s.tables {
out.insert((t.schema.clone(), t.table.clone()));
}
}
out.into_iter().collect()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SqlSemanticVerb {
#[default]
Select,
Insert,
Update,
Delete,
MergeUpdate,
MergeInsert,
MergeDelete,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TableUse {
pub schema: String,
pub table: String,
pub alias: String,
pub usage: TableUsageKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TableUsageKind {
Read,
Write,
ReadWrite,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ColumnUse {
pub qualifier: String,
pub column: String,
pub resolution: ColumnResolution,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ColumnResolution {
Resolved,
StarExpansion,
Unresolved,
#[default]
Pending,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectionItem {
pub alias: String,
pub expression_text: String,
pub is_star: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AliasScope {
pub bindings: Vec<AliasBinding>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AliasBinding {
pub alias: String,
pub schema: String,
pub table: String,
}
impl AliasScope {
pub fn bind(&mut self, alias: &str, schema: &str, table: &str) {
self.bindings.retain(|b| b.alias != alias);
self.bindings.push(AliasBinding {
alias: alias.into(),
schema: schema.into(),
table: table.into(),
});
}
#[must_use]
pub fn resolve(&self, alias: &str) -> Option<(&str, &str)> {
let needle = alias.to_ascii_uppercase();
self.bindings
.iter()
.rev()
.find(|b| b.alias.eq_ignore_ascii_case(&needle))
.map(|b| (b.schema.as_str(), b.table.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn table(schema: &str, name: &str, alias: &str, usage: TableUsageKind) -> TableUse {
TableUse {
schema: schema.into(),
table: name.into(),
alias: alias.into(),
usage,
}
}
fn col(qual: &str, name: &str) -> ColumnUse {
ColumnUse {
qualifier: qual.into(),
column: name.into(),
resolution: ColumnResolution::Pending,
}
}
#[test]
fn default_model_is_empty_select() {
let m = SqlStatementModel::default();
assert_eq!(m.verb, SqlSemanticVerb::Select);
assert!(m.tables.is_empty());
assert!(m.projection.is_empty());
assert!(m.alias_scope.bindings.is_empty());
}
#[test]
fn push_returns_position_and_appends() {
let mut m = SqlSemanticModel::default();
let p0 = m.push(SqlStatementModel::default());
let p1 = m.push(SqlStatementModel::default());
assert_eq!(p0, 0);
assert_eq!(p1, 1);
assert_eq!(m.statements.len(), 2);
}
#[test]
fn distinct_tables_dedupes_across_statements() {
let mut model = SqlSemanticModel::default();
let mut s = SqlStatementModel::default();
s.tables
.push(table("HR", "EMPLOYEES", "e", TableUsageKind::Read));
model.push(s.clone());
model.push(s); assert_eq!(model.distinct_tables().len(), 1);
}
#[test]
fn distinct_tables_keeps_distinct_schema_table_pairs() {
let mut model = SqlSemanticModel::default();
let mut s1 = SqlStatementModel::default();
s1.tables
.push(table("HR", "EMPLOYEES", "", TableUsageKind::Read));
let mut s2 = SqlStatementModel::default();
s2.tables
.push(table("HR", "DEPARTMENTS", "", TableUsageKind::Read));
model.push(s1);
model.push(s2);
let distinct = model.distinct_tables();
assert_eq!(distinct.len(), 2);
}
#[test]
fn alias_scope_bind_and_resolve() {
let mut scope = AliasScope::default();
scope.bind("e", "HR", "EMPLOYEES");
scope.bind("d", "HR", "DEPARTMENTS");
assert_eq!(scope.resolve("e"), Some(("HR", "EMPLOYEES")));
assert_eq!(scope.resolve("E"), Some(("HR", "EMPLOYEES")));
assert_eq!(scope.resolve("d"), Some(("HR", "DEPARTMENTS")));
assert_eq!(scope.resolve("x"), None);
}
#[test]
fn alias_scope_shadows_duplicate_alias() {
let mut scope = AliasScope::default();
scope.bind("t", "HR", "EMPLOYEES");
scope.bind("t", "HR", "DEPARTMENTS");
assert_eq!(scope.resolve("t"), Some(("HR", "DEPARTMENTS")));
assert_eq!(scope.bindings.len(), 1);
}
#[test]
fn column_resolution_default_is_pending() {
let c = col("e", "salary");
assert_eq!(c.resolution, ColumnResolution::Pending);
}
#[test]
fn projection_item_carries_alias_and_star_flag() {
let p = ProjectionItem {
alias: "name_lower".into(),
expression_text: "LOWER(e.name)".into(),
is_star: false,
};
assert!(!p.is_star);
let star = ProjectionItem {
alias: String::new(),
expression_text: "*".into(),
is_star: true,
};
assert!(star.is_star);
}
#[test]
fn merge_verbs_are_distinct_from_select() {
assert_ne!(SqlSemanticVerb::MergeUpdate, SqlSemanticVerb::Select);
assert_ne!(SqlSemanticVerb::MergeInsert, SqlSemanticVerb::MergeUpdate);
}
#[test]
fn round_trip_through_serde() {
let mut model = SqlSemanticModel::default();
let mut s = SqlStatementModel {
verb: SqlSemanticVerb::Update,
..SqlStatementModel::default()
};
s.tables
.push(table("HR", "EMPLOYEES", "e", TableUsageKind::Write));
s.writes.push(col("e", "salary"));
s.alias_scope.bind("e", "HR", "EMPLOYEES");
model.push(s);
let json = serde_json::to_string(&model).unwrap();
let back: SqlSemanticModel = serde_json::from_str(&json).unwrap();
assert_eq!(back, model);
assert!(json.contains("\"verb\":\"update\""));
}
#[test]
fn iter_yields_each_statement_with_index() {
let mut model = SqlSemanticModel::default();
model.push(SqlStatementModel::default());
model.push(SqlStatementModel::default());
model.push(SqlStatementModel::default());
let collected: Vec<usize> = model.iter().map(|(i, _)| i).collect();
assert_eq!(collected, vec![0, 1, 2]);
}
}