use std::collections::BTreeMap;
use crate::catalog::read_catalog;
use crate::data::{self, Value};
use crate::file::{FileError, PageReader};
use crate::format::ObjectType;
use crate::table;
const ATTR_TYPE: u8 = 1;
const ATTR_PARAMETER: u8 = 2;
const ATTR_FLAG: u8 = 3;
const ATTR_TABLE: u8 = 5;
const ATTR_COLUMN: u8 = 6;
const ATTR_JOIN: u8 = 7;
const ATTR_WHERE: u8 = 8;
const ATTR_GROUPBY: u8 = 9;
const ATTR_HAVING: u8 = 10;
const ATTR_ORDERBY: u8 = 11;
const SELECT_STAR: i16 = 0x01;
const DISTINCT: i16 = 0x02;
const OWNER_ACCESS: i16 = 0x04;
const DISTINCT_ROW: i16 = 0x08;
const TOP: i16 = 0x10;
const PERCENT: i16 = 0x20;
const UNION_FLAG: i16 = 0x02;
const APPEND_VALUE_FLAG: i16 = -0x8000; const CROSSTAB_PIVOT_FLAG: i16 = 0x01;
const CROSSTAB_NORMAL_FLAG: i16 = 0x02;
const UNION_PART1: &str = "X7YZ_____1";
const UNION_PART2: &str = "X7YZ_____2";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryType {
Select = 1,
MakeTable = 2,
Append = 3,
Update = 4,
Delete = 5,
Crosstab = 6,
Ddl = 7,
Passthrough = 8,
Union = 9,
}
impl QueryType {
fn from_flag(flag: i16) -> Option<Self> {
match flag {
1 => Some(Self::Select),
2 => Some(Self::MakeTable),
3 => Some(Self::Append),
4 => Some(Self::Update),
5 => Some(Self::Delete),
6 => Some(Self::Crosstab),
7 => Some(Self::Ddl),
8 => Some(Self::Passthrough),
9 => Some(Self::Union),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct QueryDef {
pub name: String,
pub query_type: QueryType,
rows: Vec<QueryRow>,
}
#[derive(Debug, Clone)]
struct QueryRow {
attribute: u8,
expression: Option<String>,
name1: Option<String>,
name2: Option<String>,
flag: Option<i16>,
extra: Option<i32>,
}
fn get_long(row: &[Value], idx: usize) -> Option<i32> {
match row.get(idx) {
Some(Value::Long(v)) => Some(*v),
_ => None,
}
}
fn get_byte(row: &[Value], idx: usize) -> Option<u8> {
match row.get(idx) {
Some(Value::Byte(v)) => Some(*v),
_ => None,
}
}
fn get_binary(row: &[Value], idx: usize) -> Option<&[u8]> {
match row.get(idx) {
Some(Value::Binary(b)) => Some(b),
_ => None,
}
}
fn get_text(row: &[Value], idx: usize) -> Option<&str> {
match row.get(idx) {
Some(Value::Text(s)) if !s.is_empty() => Some(s),
_ => None,
}
}
fn get_int(row: &[Value], idx: usize) -> Option<i16> {
match row.get(idx) {
Some(Value::Int(v)) => Some(*v),
_ => None,
}
}
struct QueryColumnIndexes {
object_id: usize,
attribute: usize,
order: Option<usize>,
name1: Option<usize>,
name2: Option<usize>,
expression: Option<usize>,
flag: Option<usize>,
extra: Option<usize>,
}
fn resolve_query_columns(columns: &[table::ColumnDef]) -> Result<QueryColumnIndexes, FileError> {
let mut object_id = None;
let mut attribute = None;
let mut order = None;
let mut name1 = None;
let mut name2 = None;
let mut expression = None;
let mut flag = None;
let mut extra = None;
for (i, col) in columns.iter().enumerate() {
match col.name.as_str() {
"ObjectId" => object_id = Some(i),
"Attribute" => attribute = Some(i),
"Order" => order = Some(i),
"Name1" => name1 = Some(i),
"Name2" => name2 = Some(i),
"Expression" => expression = Some(i),
"Flag" => flag = Some(i),
"LvExtra" => extra = Some(i),
_ => {}
}
}
let object_id = object_id.ok_or(FileError::InvalidTableDef {
reason: "MSysQueries missing ObjectId column",
})?;
let attribute = attribute.ok_or(FileError::InvalidTableDef {
reason: "MSysQueries missing Attribute column",
})?;
Ok(QueryColumnIndexes {
object_id,
attribute,
order,
name1,
name2,
expression,
flag,
extra,
})
}
struct RawRow {
attribute: u8,
order: Vec<u8>,
name1: Option<String>,
name2: Option<String>,
expression: Option<String>,
flag: Option<i16>,
extra: Option<i32>,
}
fn build_query_defs(
groups: BTreeMap<i32, Vec<RawRow>>,
query_name_map: &BTreeMap<u32, String>,
) -> Vec<QueryDef> {
let mut queries = Vec::new();
for (object_id, mut raw_rows) in groups {
raw_rows.sort_by(|a, b| a.order.cmp(&b.order));
let page_key = (object_id as u32) & 0x00FF_FFFF;
let name = match query_name_map.get(&page_key) {
Some(n) => n.clone(),
None => continue,
};
let type_row = raw_rows.iter().find(|r| r.attribute == ATTR_TYPE);
let query_type = match type_row.and_then(|r| r.flag).and_then(QueryType::from_flag) {
Some(qt) => qt,
None if name.starts_with("~sq_") => continue,
None => QueryType::Select,
};
let rows: Vec<QueryRow> = raw_rows
.into_iter()
.map(|r| QueryRow {
attribute: r.attribute,
expression: r.expression,
name1: r.name1,
name2: r.name2,
flag: r.flag,
extra: r.extra,
})
.collect();
queries.push(QueryDef {
name,
query_type,
rows,
});
}
queries
}
pub fn read_queries(reader: &mut PageReader) -> Result<Vec<QueryDef>, FileError> {
let catalog = read_catalog(reader)?;
let queries_entry = catalog.iter().find(|e| {
e.name == "MSysQueries"
&& matches!(e.object_type, ObjectType::Table | ObjectType::SystemTable)
});
let queries_page = match queries_entry {
Some(e) => e.table_page,
None => return Ok(Vec::new()),
};
let tdef = table::read_table_def(reader, "MSysQueries", queries_page)?;
let result = data::read_table_rows(reader, &tdef)?;
result.warn_skipped("MSysQueries");
let ci = resolve_query_columns(&tdef.columns)?;
let mut groups: BTreeMap<i32, Vec<RawRow>> = BTreeMap::new();
for row in &result.rows {
let object_id = match get_long(row, ci.object_id) {
Some(v) => v,
None => continue,
};
let attribute = match get_byte(row, ci.attribute) {
Some(v) => v,
None => continue,
};
let order = ci
.order
.and_then(|i| get_binary(row, i))
.map(|b| b.to_vec())
.unwrap_or_default();
let name1 = ci
.name1
.and_then(|i| get_text(row, i))
.map(|s| s.to_string());
let name2 = ci
.name2
.and_then(|i| get_text(row, i))
.map(|s| s.to_string());
let expression = ci.expression.and_then(|i| get_text(row, i)).and_then(|s| {
let trimmed = s.trim_end_matches('\0');
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
});
let flag = ci.flag.and_then(|i| get_int(row, i));
let extra = ci.extra.and_then(|i| get_long(row, i));
groups.entry(object_id).or_default().push(RawRow {
attribute,
order,
name1,
name2,
expression,
flag,
extra,
});
}
let query_name_map: BTreeMap<u32, String> = catalog
.iter()
.filter(|e| e.object_type == ObjectType::Query)
.map(|e| (e.table_page, e.name.clone()))
.collect();
Ok(build_query_defs(groups, &query_name_map))
}
pub fn query_to_sql(qdef: &QueryDef) -> String {
let mut builder = String::new();
let supports_standard = !matches!(qdef.query_type, QueryType::Passthrough | QueryType::Ddl);
if supports_standard {
let params = format_parameters(&qdef.rows);
if !params.is_empty() {
builder.push_str("PARAMETERS ");
builder.push_str(¶ms);
builder.push_str(";\n");
}
}
match qdef.query_type {
QueryType::Select => sql_select(&mut builder, &qdef.rows),
QueryType::Delete => sql_delete(&mut builder, &qdef.rows),
QueryType::Update => sql_update(&mut builder, &qdef.rows),
QueryType::Append => sql_append(&mut builder, &qdef.rows),
QueryType::MakeTable => sql_make_table(&mut builder, &qdef.rows),
QueryType::Crosstab => sql_crosstab(&mut builder, &qdef.rows),
QueryType::Union => sql_union(&mut builder, &qdef.rows),
QueryType::Passthrough => sql_passthrough(&mut builder, &qdef.rows),
QueryType::Ddl => sql_ddl(&mut builder, &qdef.rows),
}
if supports_standard {
if has_flag(&qdef.rows, OWNER_ACCESS) {
builder.push_str("\nWITH OWNERACCESS OPTION");
}
builder.push(';');
}
builder
}
fn rows_by_attr(rows: &[QueryRow], attr: u8) -> Vec<&QueryRow> {
rows.iter().filter(|r| r.attribute == attr).collect()
}
fn flag_row(rows: &[QueryRow]) -> Option<&QueryRow> {
rows.iter().find(|r| r.attribute == ATTR_FLAG)
}
fn has_flag(rows: &[QueryRow], mask: i16) -> bool {
flag_row(rows)
.and_then(|r| r.flag)
.map(|f| (f & mask) != 0)
.unwrap_or(false)
}
fn type_row(rows: &[QueryRow]) -> Option<&QueryRow> {
rows.iter().find(|r| r.attribute == ATTR_TYPE)
}
fn where_expr(rows: &[QueryRow]) -> Option<&str> {
rows.iter()
.find(|r| r.attribute == ATTR_WHERE)
.and_then(|r| r.expression.as_deref())
}
fn having_expr(rows: &[QueryRow]) -> Option<&str> {
rows.iter()
.find(|r| r.attribute == ATTR_HAVING)
.and_then(|r| r.expression.as_deref())
}
fn needs_quoting(s: &str) -> bool {
s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_')
}
fn is_quoted(s: &str) -> bool {
s.len() >= 2 && s.starts_with('[') && s.ends_with(']')
}
fn to_quoted_expr(s: &str) -> String {
if is_quoted(s) {
s.to_string()
} else {
format!("[{}]", s.replace(']', "]]"))
}
}
fn to_optional_quoted(full_expr: &str, is_identifier: bool) -> String {
if is_identifier {
full_expr
.split('.')
.map(|part| {
if needs_quoting(part) {
to_quoted_expr(part)
} else {
part.to_string()
}
})
.collect::<Vec<_>>()
.join(".")
} else if needs_quoting(full_expr) {
to_quoted_expr(full_expr)
} else {
full_expr.to_string()
}
}
fn to_alias(alias: Option<&str>) -> String {
match alias {
Some(a) => format!(" AS {}", to_optional_quoted(a, false)),
None => String::new(),
}
}
fn param_type_name(flag: i16) -> Option<&'static str> {
match flag {
0 => Some("Value"),
1 => Some("Bit"),
10 => Some("Text"),
2 => Some("Byte"),
3 => Some("Short"),
4 => Some("Long"),
5 => Some("Currency"),
6 => Some("IEEESingle"),
7 => Some("IEEEDouble"),
8 => Some("DateTime"),
9 => Some("Binary"),
11 => Some("LongBinary"),
15 => Some("Guid"),
_ => None,
}
}
fn format_parameters(rows: &[QueryRow]) -> String {
let param_rows = rows_by_attr(rows, ATTR_PARAMETER);
let parts: Vec<String> = param_rows
.iter()
.filter_map(|r| {
let name1 = r.name1.as_deref()?;
let flag = r.flag?;
let type_name = param_type_name(flag)?;
let mut s = format!("{name1} {type_name}");
if flag == 10 {
if let Some(extra) = r.extra {
if extra > 0 {
s.push_str(&format!("({extra})"));
}
}
}
Some(s)
})
.collect();
parts.join(", ")
}
fn get_select_type(rows: &[QueryRow]) -> String {
if has_flag(rows, DISTINCT) {
return "DISTINCT".to_string();
}
if has_flag(rows, DISTINCT_ROW) {
return "DISTINCTROW".to_string();
}
if has_flag(rows, TOP) {
let n = flag_row(rows)
.and_then(|r| r.name1.as_deref())
.unwrap_or("");
let mut s = format!("TOP {n}");
if has_flag(rows, PERCENT) {
s.push_str(" PERCENT");
}
return s;
}
String::new()
}
fn get_select_columns(rows: &[QueryRow], filter: impl Fn(&QueryRow) -> bool) -> String {
let column_rows = rows_by_attr(rows, ATTR_COLUMN);
let mut parts: Vec<String> = column_rows
.iter()
.filter(|r| filter(r))
.filter_map(|r| {
let expr = r.expression.as_deref()?;
let mut s = expr.to_string();
s.push_str(&to_alias(r.name1.as_deref()));
Some(s)
})
.collect();
if has_flag(rows, SELECT_STAR) {
parts.push("*".to_string());
}
parts.join(", ")
}
fn get_groupings(rows: &[QueryRow], filter: impl Fn(&QueryRow) -> bool) -> String {
let gb_rows = rows_by_attr(rows, ATTR_GROUPBY);
let parts: Vec<&str> = gb_rows
.iter()
.filter(|r| filter(r))
.filter_map(|r| r.expression.as_deref())
.collect();
parts.join(", ")
}
fn get_orderings(rows: &[QueryRow]) -> String {
let ob_rows = rows_by_attr(rows, ATTR_ORDERBY);
let parts: Vec<String> = ob_rows
.iter()
.filter_map(|r| {
let expr = r.expression.as_deref()?;
let mut s = expr.to_string();
if r.name1
.as_deref()
.map(|n| n.eq_ignore_ascii_case("D"))
.unwrap_or(false)
{
s.push_str(" DESC");
}
Some(s)
})
.collect();
parts.join(", ")
}
enum TableSource {
Simple {
name: String,
expr: String,
},
Join {
from: Box<TableSource>,
to: Box<TableSource>,
join_type: i16,
on_conditions: Vec<String>,
},
}
impl TableSource {
fn contains_table(&self, table: &str) -> bool {
match self {
TableSource::Simple { name, .. } => name.eq_ignore_ascii_case(table),
TableSource::Join { from, to, .. } => {
from.contains_table(table) || to.contains_table(table)
}
}
}
fn same_join(&mut self, jtype: i16, on: &str) -> bool {
match self {
TableSource::Join {
join_type,
on_conditions,
..
} => {
if *join_type == jtype {
on_conditions.insert(0, on.to_string());
true
} else {
false
}
}
TableSource::Simple { .. } => false,
}
}
fn to_sql(&self, is_top_level: bool) -> String {
match self {
TableSource::Simple { expr, .. } => expr.clone(),
TableSource::Join {
from,
to,
join_type,
on_conditions,
} => {
let join_str = match join_type {
1 => " INNER JOIN ",
2 => " LEFT JOIN ",
3 => " RIGHT JOIN ",
_ => " JOIN ",
};
let mut sb = String::new();
if !is_top_level {
sb.push('(');
}
sb.push_str(&from.to_sql(false));
sb.push_str(join_str);
sb.push_str(&to.to_sql(false));
sb.push_str(" ON ");
let multi = on_conditions.len() > 1;
if multi {
sb.push('(');
}
sb.push_str(&on_conditions.join(") AND ("));
if multi {
sb.push(')');
}
if !is_top_level {
sb.push(')');
}
sb
}
}
}
}
fn build_from_tables(rows: &[QueryRow]) -> Vec<String> {
let table_rows = rows_by_attr(rows, ATTR_TABLE);
let join_rows = rows_by_attr(rows, ATTR_JOIN);
let mut sources: Vec<TableSource> = Vec::new();
for trow in &table_rows {
let mut expr = String::new();
if let Some(ref e) = trow.expression {
expr.push_str(&to_quoted_expr(e));
expr.push('.');
}
if let Some(ref n1) = trow.name1 {
expr.push_str(&to_optional_quoted(n1, true));
}
if let Some(ref n2) = trow.name2 {
expr.push_str(&to_alias(Some(n2)));
}
let key = trow
.name2
.as_deref()
.or(trow.name1.as_deref())
.unwrap_or("")
.to_string();
sources.push(TableSource::Simple { name: key, expr });
}
for jrow in &join_rows {
let from_table = match jrow.name1.as_deref() {
Some(s) => s,
None => continue,
};
let to_table = match jrow.name2.as_deref() {
Some(s) => s,
None => continue,
};
let on_expr = match jrow.expression.as_deref() {
Some(s) => s,
None => continue,
};
let jtype = jrow.flag.unwrap_or(1);
let mut from_idx = None;
let mut to_idx = None;
let mut same_source = false;
for (i, ts) in sources.iter().enumerate() {
if from_idx.is_none() && ts.contains_table(from_table) {
from_idx = Some(i);
if to_idx.is_none() && ts.contains_table(to_table) {
to_idx = from_idx;
same_source = true;
break;
}
} else if to_idx.is_none() && ts.contains_table(to_table) {
to_idx = Some(i);
}
if from_idx.is_some() && to_idx.is_some() {
break;
}
}
if same_source {
if let Some(idx) = from_idx {
if sources[idx].same_join(jtype, on_expr) {
continue;
}
continue;
}
}
let from_ts;
let to_ts;
match (from_idx, to_idx) {
(Some(fi), Some(ti)) => {
if fi > ti {
from_ts = sources.remove(fi);
to_ts = sources.remove(ti);
} else {
to_ts = sources.remove(ti);
from_ts = sources.remove(fi);
}
}
(Some(fi), None) => {
from_ts = sources.remove(fi);
to_ts = TableSource::Simple {
name: to_table.to_string(),
expr: to_optional_quoted(to_table, true),
};
}
(None, Some(ti)) => {
from_ts = TableSource::Simple {
name: from_table.to_string(),
expr: to_optional_quoted(from_table, true),
};
to_ts = sources.remove(ti);
}
(None, None) => {
from_ts = TableSource::Simple {
name: from_table.to_string(),
expr: to_optional_quoted(from_table, true),
};
to_ts = TableSource::Simple {
name: to_table.to_string(),
expr: to_optional_quoted(to_table, true),
};
}
}
sources.push(TableSource::Join {
from: Box::new(from_ts),
to: Box::new(to_ts),
join_type: jtype,
on_conditions: vec![on_expr.to_string()],
});
}
sources.iter().map(|ts| ts.to_sql(true)).collect()
}
fn append_select_body(
builder: &mut String,
rows: &[QueryRow],
use_prefix: bool,
into_target: Option<&str>,
col_filter: &dyn Fn(&QueryRow) -> bool,
gb_filter: &dyn Fn(&QueryRow) -> bool,
) {
if use_prefix {
builder.push_str("SELECT ");
let sel_type = get_select_type(rows);
if !sel_type.is_empty() {
builder.push_str(&sel_type);
builder.push(' ');
}
}
builder.push_str(&get_select_columns(rows, col_filter));
if let Some(target) = into_target {
builder.push_str(" INTO ");
builder.push_str(&to_optional_quoted(target, true));
}
let from = build_from_tables(rows);
if !from.is_empty() {
builder.push_str("\nFROM ");
builder.push_str(&from.join(", "));
}
if let Some(w) = where_expr(rows) {
builder.push_str("\nWHERE ");
builder.push_str(w);
}
let gb = get_groupings(rows, gb_filter);
if !gb.is_empty() {
builder.push_str("\nGROUP BY ");
builder.push_str(&gb);
}
if let Some(h) = having_expr(rows) {
builder.push_str("\nHAVING ");
builder.push_str(h);
}
let ord = get_orderings(rows);
if !ord.is_empty() {
builder.push_str("\nORDER BY ");
builder.push_str(&ord);
}
}
fn sql_select(builder: &mut String, rows: &[QueryRow]) {
append_select_body(builder, rows, true, None, &|_| true, &|_| true);
}
fn sql_delete(builder: &mut String, rows: &[QueryRow]) {
builder.push_str("DELETE ");
append_select_body(builder, rows, false, None, &|_| true, &|_| true);
}
fn sql_update(builder: &mut String, rows: &[QueryRow]) {
builder.push_str("UPDATE ");
let from = build_from_tables(rows);
builder.push_str(&from.join(", "));
let column_rows = rows_by_attr(rows, ATTR_COLUMN);
let set_parts: Vec<String> = column_rows
.iter()
.filter_map(|r| {
let name2 = r.name2.as_deref()?;
let expr = r.expression.as_deref()?;
Some(format!("{} = {}", to_optional_quoted(name2, true), expr))
})
.collect();
if !set_parts.is_empty() {
builder.push_str("\nSET ");
builder.push_str(&set_parts.join(", "));
}
if let Some(w) = where_expr(rows) {
builder.push_str("\nWHERE ");
builder.push_str(w);
}
}
fn sql_append(builder: &mut String, rows: &[QueryRow]) {
let target = type_row(rows)
.and_then(|r| r.name1.as_deref())
.unwrap_or("");
builder.push_str("INSERT INTO ");
builder.push_str(&to_optional_quoted(target, true));
let all_col_rows = rows_by_attr(rows, ATTR_COLUMN);
let target_cols: Vec<String> = all_col_rows
.iter()
.filter(|r| r.name2.is_some())
.filter_map(|r| Some(to_optional_quoted(r.name2.as_deref()?, true)))
.collect();
if !target_cols.is_empty() {
builder.push_str(" (");
builder.push_str(&target_cols.join(", "));
builder.push(')');
}
let value_rows: Vec<&&QueryRow> = all_col_rows
.iter()
.filter(|r| {
r.flag
.map(|f| (f & APPEND_VALUE_FLAG) != 0)
.unwrap_or(false)
})
.collect();
builder.push('\n');
if !value_rows.is_empty() {
let values: Vec<&str> = value_rows
.iter()
.filter_map(|r| r.expression.as_deref())
.collect();
builder.push_str("VALUES (");
builder.push_str(&values.join(", "));
builder.push(')');
} else {
let not_value = |r: &QueryRow| {
!r.flag
.map(|f| (f & APPEND_VALUE_FLAG) != 0)
.unwrap_or(false)
};
append_select_body(builder, rows, true, None, ¬_value, &|_| true);
}
}
fn sql_make_table(builder: &mut String, rows: &[QueryRow]) {
let target = type_row(rows)
.and_then(|r| r.name1.as_deref())
.unwrap_or("");
append_select_body(builder, rows, true, Some(target), &|_| true, &|_| true);
}
fn sql_crosstab(builder: &mut String, rows: &[QueryRow]) {
let all_col_rows = rows_by_attr(rows, ATTR_COLUMN);
let transform_row = all_col_rows.iter().find(|r| {
let f = r.flag.unwrap_or(0);
(f & (CROSSTAB_PIVOT_FLAG | CROSSTAB_NORMAL_FLAG)) == 0
});
if let Some(trow) = transform_row {
if let Some(ref expr) = trow.expression {
builder.push_str("TRANSFORM ");
builder.push_str(expr);
builder.push_str(&to_alias(trow.name1.as_deref()));
builder.push('\n');
}
}
let normal_col = |r: &QueryRow| {
r.flag
.map(|f| (f & CROSSTAB_NORMAL_FLAG) != 0)
.unwrap_or(false)
};
let normal_gb = |r: &QueryRow| {
r.flag
.map(|f| (f & CROSSTAB_NORMAL_FLAG) != 0)
.unwrap_or(false)
};
append_select_body(builder, rows, true, None, &normal_col, &normal_gb);
let pivot_row = all_col_rows.iter().find(|r| {
r.flag
.map(|f| (f & CROSSTAB_PIVOT_FLAG) != 0)
.unwrap_or(false)
});
if let Some(prow) = pivot_row {
if let Some(ref expr) = prow.expression {
builder.push_str("\nPIVOT ");
builder.push_str(expr);
}
}
}
fn sql_union(builder: &mut String, rows: &[QueryRow]) {
let table_rows = rows_by_attr(rows, ATTR_TABLE);
let part1 = table_rows
.iter()
.find(|r| r.name2.as_deref() == Some(UNION_PART1))
.and_then(|r| r.expression.as_deref());
let part2 = table_rows
.iter()
.find(|r| r.name2.as_deref() == Some(UNION_PART2))
.and_then(|r| r.expression.as_deref());
if let Some(p1) = part1 {
let cleaned = clean_union_string(p1);
builder.push_str(&cleaned);
}
builder.push_str("\nUNION ");
if !has_flag(rows, UNION_FLAG) {
builder.push_str("ALL ");
}
if let Some(p2) = part2 {
let cleaned = clean_union_string(p2);
builder.push_str(&cleaned);
}
let ord = get_orderings(rows);
if !ord.is_empty() {
builder.push_str("\nORDER BY ");
builder.push_str(&ord);
}
}
fn clean_union_string(s: &str) -> String {
let trimmed = s.trim();
let mut result = String::with_capacity(trimmed.len());
let mut prev_newline = false;
for c in trimmed.chars() {
if c == '\r' || c == '\n' {
if !prev_newline {
result.push('\n');
prev_newline = true;
}
} else {
result.push(c);
prev_newline = false;
}
}
result
}
fn sql_passthrough(builder: &mut String, rows: &[QueryRow]) {
if let Some(expr) = type_row(rows).and_then(|r| r.expression.as_deref()) {
builder.push_str(expr);
}
}
fn sql_ddl(builder: &mut String, rows: &[QueryRow]) {
if let Some(expr) = type_row(rows).and_then(|r| r.expression.as_deref()) {
builder.push_str(expr);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_type_from_flag() {
assert_eq!(QueryType::from_flag(1), Some(QueryType::Select));
assert_eq!(QueryType::from_flag(2), Some(QueryType::MakeTable));
assert_eq!(QueryType::from_flag(3), Some(QueryType::Append));
assert_eq!(QueryType::from_flag(4), Some(QueryType::Update));
assert_eq!(QueryType::from_flag(5), Some(QueryType::Delete));
assert_eq!(QueryType::from_flag(6), Some(QueryType::Crosstab));
assert_eq!(QueryType::from_flag(7), Some(QueryType::Ddl));
assert_eq!(QueryType::from_flag(8), Some(QueryType::Passthrough));
assert_eq!(QueryType::from_flag(9), Some(QueryType::Union));
assert_eq!(QueryType::from_flag(0), None);
assert_eq!(QueryType::from_flag(99), None);
}
#[test]
fn join_type_str() {
let ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 1,
on_conditions: vec!["T1.id = T2.id".to_string()],
};
assert_eq!(ts.to_sql(true), "T1 INNER JOIN T2 ON T1.id = T2.id");
let ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 2,
on_conditions: vec!["T1.id = T2.id".to_string()],
};
assert_eq!(ts.to_sql(true), "T1 LEFT JOIN T2 ON T1.id = T2.id");
let ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 3,
on_conditions: vec!["T1.id = T2.id".to_string()],
};
assert_eq!(ts.to_sql(true), "T1 RIGHT JOIN T2 ON T1.id = T2.id");
}
#[test]
fn join_multi_on() {
let ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 1,
on_conditions: vec!["T1.a = T2.a".to_string(), "T1.b = T2.b".to_string()],
};
assert_eq!(
ts.to_sql(true),
"T1 INNER JOIN T2 ON (T1.a = T2.a) AND (T1.b = T2.b)"
);
}
#[test]
fn join_nested_parens() {
let inner = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 1,
on_conditions: vec!["T1.id = T2.id".to_string()],
};
let outer = TableSource::Join {
from: Box::new(inner),
to: Box::new(TableSource::Simple {
name: "T3".to_string(),
expr: "T3".to_string(),
}),
join_type: 2,
on_conditions: vec!["T1.id = T3.id".to_string()],
};
assert_eq!(
outer.to_sql(true),
"(T1 INNER JOIN T2 ON T1.id = T2.id) LEFT JOIN T3 ON T1.id = T3.id"
);
}
#[test]
fn quoting_simple() {
assert_eq!(to_optional_quoted("Table1", true), "Table1");
assert_eq!(to_optional_quoted("col1", true), "col1");
}
#[test]
fn quoting_with_space() {
assert_eq!(
to_optional_quoted("Another Table", false),
"[Another Table]"
);
}
#[test]
fn quoting_dotted_identifier() {
assert_eq!(to_optional_quoted("Table1.col1", true), "Table1.col1");
}
#[test]
fn quoting_already_quoted() {
assert_eq!(to_optional_quoted("[Table1]", true), "[Table1]");
}
#[test]
fn quoting_bracket_escape() {
assert_eq!(to_quoted_expr("col]x"), "[col]]x]");
}
fn test_data_path(relative: &str) -> Option<std::path::PathBuf> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::PathBuf::from(manifest_dir)
.join("../../testdata")
.join(relative);
if path.exists() {
Some(path)
} else {
None
}
}
macro_rules! skip_if_missing {
($path:expr) => {
match test_data_path($path) {
Some(p) => p,
None => {
eprintln!("SKIP: test data not found: {}", $path);
return;
}
}
};
}
#[test]
fn read_queries_count() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
assert_eq!(queries.len(), 9, "should have 9 queries");
}
#[test]
fn read_queries_names() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let names: Vec<&str> = queries.iter().map(|q| q.name.as_str()).collect();
assert!(names.contains(&"SelectQuery"));
assert!(names.contains(&"DeleteQuery"));
assert!(names.contains(&"UpdateQuery"));
assert!(names.contains(&"AppendQuery"));
assert!(names.contains(&"MakeTableQuery"));
assert!(names.contains(&"CrosstabQuery"));
assert!(names.contains(&"UnionQuery"));
assert!(names.contains(&"PassthroughQuery"));
assert!(names.contains(&"DataDefinitionQuery"));
}
#[test]
fn read_queries_types() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
for q in &queries {
let expected = match q.name.as_str() {
"SelectQuery" => QueryType::Select,
"DeleteQuery" => QueryType::Delete,
"UpdateQuery" => QueryType::Update,
"AppendQuery" => QueryType::Append,
"MakeTableQuery" => QueryType::MakeTable,
"CrosstabQuery" => QueryType::Crosstab,
"UnionQuery" => QueryType::Union,
"PassthroughQuery" => QueryType::Passthrough,
"DataDefinitionQuery" => QueryType::Ddl,
other => panic!("unexpected query name: {other}"),
};
assert_eq!(q.query_type, expected, "type mismatch for {}", q.name);
}
}
fn find_query<'a>(queries: &'a [QueryDef], name: &str) -> &'a QueryDef {
queries.iter().find(|q| q.name == name).unwrap()
}
#[test]
fn sql_delete_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "DeleteQuery");
let sql = query_to_sql(q);
assert!(
sql.starts_with("DELETE "),
"should start with DELETE: {sql}"
);
assert!(
sql.contains("Table1.col1"),
"should contain Table1.col1: {sql}"
);
assert!(
sql.contains("FROM Table1"),
"should contain FROM Table1: {sql}"
);
assert!(
sql.contains("WHERE (((Table1.col1)>\"blah\"))"),
"should contain WHERE clause: {sql}"
);
assert!(sql.ends_with(';'), "should end with semicolon: {sql}");
}
#[test]
fn sql_select_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "SelectQuery");
let sql = query_to_sql(q);
assert!(
sql.starts_with("SELECT DISTINCT "),
"should start with SELECT DISTINCT: {sql}"
);
assert!(sql.contains("Table1.*"), "should contain Table1.*: {sql}");
assert!(
sql.contains("Table2.col1"),
"should contain Table2.col1: {sql}"
);
assert!(
sql.contains("LEFT JOIN Table3 ON Table1.col1 = Table3.col1"),
"should contain LEFT JOIN: {sql}"
);
assert!(
sql.contains("INNER JOIN Table2"),
"should contain INNER JOIN: {sql}"
);
assert!(
sql.contains("ORDER BY Table2.col1"),
"should contain ORDER BY: {sql}"
);
}
#[test]
fn sql_append_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "AppendQuery");
let sql = query_to_sql(q);
assert!(
sql.starts_with("INSERT INTO Table3"),
"should start with INSERT INTO Table3: {sql}"
);
assert!(
sql.contains("(col2, col2, col3)"),
"should contain target columns: {sql}"
);
assert!(sql.contains("SELECT "), "should contain SELECT: {sql}");
assert!(
sql.contains("INNER JOIN Table2 ON [Table1].[col1]=[Table2].[col1]"),
"should contain JOIN: {sql}"
);
}
#[test]
fn sql_update_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "UpdateQuery");
let sql = query_to_sql(q);
assert!(
sql.contains("PARAMETERS User Name Text;"),
"should contain PARAMETERS: {sql}"
);
assert!(
sql.contains("UPDATE Table1"),
"should contain UPDATE Table1: {sql}"
);
assert!(sql.contains("SET "), "should contain SET: {sql}");
assert!(
sql.contains("Table1.col1 = \"foo\""),
"should contain col1 assignment: {sql}"
);
}
#[test]
fn sql_make_table_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "MakeTableQuery");
let sql = query_to_sql(q);
assert!(
sql.contains("INTO Table4"),
"should contain INTO Table4: {sql}"
);
assert!(sql.contains("SELECT "), "should contain SELECT: {sql}");
assert!(
sql.contains("Max(Table2.col1) AS MaxOfcol1"),
"should contain aggregate: {sql}"
);
assert!(
sql.contains("GROUP BY Table2.col2, Table3.col2"),
"should contain GROUP BY: {sql}"
);
}
#[test]
fn sql_crosstab_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "CrosstabQuery");
let sql = query_to_sql(q);
assert!(
sql.starts_with("TRANSFORM "),
"should start with TRANSFORM: {sql}"
);
assert!(
sql.contains("Count([Table2].[col2]) AS CountOfcol2"),
"should contain TRANSFORM expression: {sql}"
);
assert!(
sql.contains("PIVOT [Table1].[col1]"),
"should contain PIVOT: {sql}"
);
}
#[test]
fn no_queries_returns_empty() {
let path = skip_if_missing!("V2003/testV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
assert!(queries.is_empty(), "testV2003.mdb should have no queries");
}
fn make_col(name: &str) -> table::ColumnDef {
table::ColumnDef {
name: name.to_string(),
col_type: crate::format::ColumnType::Long,
col_num: 0,
var_col_num: 0,
fixed_offset: 0,
col_size: 4,
flags: 0,
is_fixed: true,
precision: 0,
scale: 0,
}
}
#[test]
fn resolve_all_columns_present() {
let columns = vec![
make_col("ObjectId"),
make_col("Attribute"),
make_col("Order"),
make_col("Name1"),
make_col("Name2"),
make_col("Expression"),
make_col("Flag"),
make_col("LvExtra"),
];
let ci = resolve_query_columns(&columns).unwrap();
assert_eq!(ci.object_id, 0);
assert_eq!(ci.attribute, 1);
assert_eq!(ci.order, Some(2));
assert_eq!(ci.name1, Some(3));
assert_eq!(ci.name2, Some(4));
assert_eq!(ci.expression, Some(5));
assert_eq!(ci.flag, Some(6));
assert_eq!(ci.extra, Some(7));
}
#[test]
fn resolve_missing_object_id() {
let columns = vec![make_col("Attribute")];
assert!(resolve_query_columns(&columns).is_err());
}
#[test]
fn resolve_missing_attribute() {
let columns = vec![make_col("ObjectId")];
assert!(resolve_query_columns(&columns).is_err());
}
#[test]
fn resolve_optional_columns_missing() {
let columns = vec![make_col("ObjectId"), make_col("Attribute")];
let ci = resolve_query_columns(&columns).unwrap();
assert_eq!(ci.object_id, 0);
assert_eq!(ci.attribute, 1);
assert_eq!(ci.order, None);
assert_eq!(ci.name1, None);
assert_eq!(ci.expression, None);
}
#[test]
fn resolve_unknown_column_ignored() {
let columns = vec![
make_col("ObjectId"),
make_col("Attribute"),
make_col("UnknownCol"),
];
let ci = resolve_query_columns(&columns).unwrap();
assert_eq!(ci.object_id, 0);
assert_eq!(ci.attribute, 1);
assert_eq!(ci.order, None);
}
#[test]
fn param_type_name_all_flags() {
assert_eq!(param_type_name(0), Some("Value"));
assert_eq!(param_type_name(1), Some("Bit"));
assert_eq!(param_type_name(2), Some("Byte"));
assert_eq!(param_type_name(3), Some("Short"));
assert_eq!(param_type_name(4), Some("Long"));
assert_eq!(param_type_name(5), Some("Currency"));
assert_eq!(param_type_name(6), Some("IEEESingle"));
assert_eq!(param_type_name(7), Some("IEEEDouble"));
assert_eq!(param_type_name(8), Some("DateTime"));
assert_eq!(param_type_name(9), Some("Binary"));
assert_eq!(param_type_name(10), Some("Text"));
assert_eq!(param_type_name(11), Some("LongBinary"));
assert_eq!(param_type_name(15), Some("Guid"));
assert_eq!(param_type_name(-1), None);
assert_eq!(param_type_name(99), None);
assert_eq!(param_type_name(12), None);
assert_eq!(param_type_name(13), None);
assert_eq!(param_type_name(14), None);
}
#[test]
fn clean_union_string_basic() {
let result = clean_union_string(" SELECT * FROM T1 ");
assert_eq!(result, "SELECT * FROM T1");
}
#[test]
fn clean_union_string_collapses_newlines() {
let input = "SELECT *\n\n\nFROM T1\r\n\r\nWHERE 1=1";
let result = clean_union_string(input);
assert_eq!(result, "SELECT *\nFROM T1\nWHERE 1=1");
}
#[test]
fn clean_union_string_cr_only() {
let result = clean_union_string("A\r\r\rB");
assert_eq!(result, "A\nB");
}
#[test]
fn get_byte_wrong_type() {
let row = vec![Value::Long(42)];
assert_eq!(get_byte(&row, 0), None);
}
#[test]
fn get_binary_wrong_type() {
let row = vec![Value::Long(42)];
assert_eq!(get_binary(&row, 0), None);
}
#[test]
fn get_text_empty_string() {
let row = vec![Value::Text(String::new())];
assert_eq!(get_text(&row, 0), None);
}
#[test]
fn get_int_wrong_type() {
let row = vec![Value::Long(42)];
assert_eq!(get_int(&row, 0), None);
}
#[test]
fn get_long_wrong_type() {
let row = vec![Value::Text("hello".into())];
assert_eq!(get_long(&row, 0), None);
}
#[test]
fn get_select_type_distinct_row() {
let rows = vec![QueryRow {
attribute: ATTR_FLAG,
expression: None,
name1: None,
name2: None,
flag: Some(DISTINCT_ROW),
extra: None,
}];
assert_eq!(get_select_type(&rows), "DISTINCTROW");
}
#[test]
fn get_select_type_top() {
let rows = vec![QueryRow {
attribute: ATTR_FLAG,
expression: None,
name1: Some("10".to_string()),
name2: None,
flag: Some(TOP),
extra: None,
}];
assert_eq!(get_select_type(&rows), "TOP 10");
}
#[test]
fn get_select_type_top_percent() {
let rows = vec![QueryRow {
attribute: ATTR_FLAG,
expression: None,
name1: Some("25".to_string()),
name2: None,
flag: Some(TOP | PERCENT),
extra: None,
}];
assert_eq!(get_select_type(&rows), "TOP 25 PERCENT");
}
#[test]
fn get_select_type_none() {
let rows = vec![QueryRow {
attribute: ATTR_FLAG,
expression: None,
name1: None,
name2: None,
flag: Some(0),
extra: None,
}];
assert_eq!(get_select_type(&rows), "");
}
#[test]
fn format_parameters_text_with_size() {
let rows = vec![QueryRow {
attribute: ATTR_PARAMETER,
expression: None,
name1: Some("Param1".to_string()),
name2: None,
flag: Some(10), extra: Some(255),
}];
let result = format_parameters(&rows);
assert_eq!(result, "Param1 Text(255)");
}
#[test]
fn format_parameters_text_no_size() {
let rows = vec![QueryRow {
attribute: ATTR_PARAMETER,
expression: None,
name1: Some("Param1".to_string()),
name2: None,
flag: Some(10), extra: Some(0), }];
let result = format_parameters(&rows);
assert_eq!(result, "Param1 Text");
}
#[test]
fn format_parameters_multiple() {
let rows = vec![
QueryRow {
attribute: ATTR_PARAMETER,
expression: None,
name1: Some("P1".to_string()),
name2: None,
flag: Some(4), extra: None,
},
QueryRow {
attribute: ATTR_PARAMETER,
expression: None,
name1: Some("P2".to_string()),
name2: None,
flag: Some(8), extra: None,
},
];
let result = format_parameters(&rows);
assert_eq!(result, "P1 Long, P2 DateTime");
}
#[test]
fn get_orderings_desc() {
let rows = vec![QueryRow {
attribute: ATTR_ORDERBY,
expression: Some("Table1.col1".to_string()),
name1: Some("D".to_string()),
name2: None,
flag: None,
extra: None,
}];
assert_eq!(get_orderings(&rows), "Table1.col1 DESC");
}
#[test]
fn get_orderings_asc() {
let rows = vec![QueryRow {
attribute: ATTR_ORDERBY,
expression: Some("Table1.col1".to_string()),
name1: None,
name2: None,
flag: None,
extra: None,
}];
assert_eq!(get_orderings(&rows), "Table1.col1");
}
#[test]
fn same_join_matching_type() {
let mut ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 1,
on_conditions: vec!["T1.a = T2.a".to_string()],
};
assert!(ts.same_join(1, "T1.b = T2.b"));
if let TableSource::Join { on_conditions, .. } = &ts {
assert_eq!(on_conditions[0], "T1.b = T2.b");
assert_eq!(on_conditions[1], "T1.a = T2.a");
}
}
#[test]
fn same_join_different_type() {
let mut ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 1,
on_conditions: vec!["T1.a = T2.a".to_string()],
};
assert!(!ts.same_join(2, "T1.b = T2.b")); }
#[test]
fn same_join_on_simple() {
let mut ts = TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
};
assert!(!ts.same_join(1, "T1.a = T2.a")); }
#[test]
fn join_unknown_type() {
let ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 99,
on_conditions: vec!["T1.id = T2.id".to_string()],
};
assert_eq!(ts.to_sql(true), "T1 JOIN T2 ON T1.id = T2.id");
}
#[test]
fn sql_append_values() {
let rows = vec![
QueryRow {
attribute: ATTR_TYPE,
expression: None,
name1: Some("TargetTable".to_string()),
name2: None,
flag: Some(3), extra: None,
},
QueryRow {
attribute: ATTR_COLUMN,
expression: Some("42".to_string()),
name1: None,
name2: Some("col1".to_string()),
flag: Some(APPEND_VALUE_FLAG),
extra: None,
},
QueryRow {
attribute: ATTR_COLUMN,
expression: Some("'hello'".to_string()),
name1: None,
name2: Some("col2".to_string()),
flag: Some(APPEND_VALUE_FLAG),
extra: None,
},
];
let mut builder = String::new();
sql_append(&mut builder, &rows);
assert!(builder.contains("INSERT INTO TargetTable"));
assert!(builder.contains("(col1, col2)"));
assert!(builder.contains("VALUES (42, 'hello')"));
}
#[test]
fn query_to_sql_owner_access() {
let qdef = QueryDef {
name: "TestQuery".to_string(),
query_type: QueryType::Select,
rows: vec![
QueryRow {
attribute: ATTR_TYPE,
expression: None,
name1: None,
name2: None,
flag: Some(1),
extra: None,
},
QueryRow {
attribute: ATTR_FLAG,
expression: None,
name1: None,
name2: None,
flag: Some(SELECT_STAR | OWNER_ACCESS),
extra: None,
},
QueryRow {
attribute: ATTR_TABLE,
expression: None,
name1: Some("Table1".to_string()),
name2: None,
flag: None,
extra: None,
},
],
};
let sql = query_to_sql(&qdef);
assert!(
sql.contains("WITH OWNERACCESS OPTION"),
"should contain OWNER_ACCESS: {sql}"
);
assert!(sql.contains("*"), "should contain SELECT *: {sql}");
}
#[test]
fn sql_union_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "UnionQuery");
let sql = query_to_sql(q);
assert!(sql.contains("UNION"), "should contain UNION: {sql}");
}
#[test]
fn sql_passthrough_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "PassthroughQuery");
let sql = query_to_sql(q);
assert!(!sql.is_empty(), "passthrough query should produce SQL");
}
#[test]
fn sql_ddl_query() {
let path = skip_if_missing!("V2003/queryTestV2003.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
let q = find_query(&queries, "DataDefinitionQuery");
let sql = query_to_sql(q);
assert!(!sql.is_empty(), "DDL query should produce SQL");
}
#[test]
fn build_from_tables_with_alias() {
let rows = vec![QueryRow {
attribute: ATTR_TABLE,
expression: None,
name1: Some("Table1".to_string()),
name2: Some("T1".to_string()),
flag: None,
extra: None,
}];
let result = build_from_tables(&rows);
assert_eq!(result, vec!["Table1 AS T1"]);
}
#[test]
fn build_from_tables_with_external_db() {
let rows = vec![QueryRow {
attribute: ATTR_TABLE,
expression: Some("C:\\path\\to\\db.mdb".to_string()),
name1: Some("Table1".to_string()),
name2: None,
flag: None,
extra: None,
}];
let result = build_from_tables(&rows);
assert_eq!(result.len(), 1);
assert!(result[0].contains("[C:\\path\\to\\db.mdb]"));
assert!(result[0].contains("Table1"));
}
#[test]
fn build_from_tables_join_missing_name() {
let rows = vec![
QueryRow {
attribute: ATTR_TABLE,
expression: None,
name1: Some("T1".to_string()),
name2: None,
flag: None,
extra: None,
},
QueryRow {
attribute: ATTR_JOIN,
expression: Some("T1.id = T2.id".to_string()),
name1: None, name2: Some("T2".to_string()),
flag: Some(1),
extra: None,
},
];
let result = build_from_tables(&rows);
assert_eq!(result.len(), 1);
assert_eq!(result[0], "T1");
}
#[test]
fn build_from_tables_join_both_not_found() {
let rows = vec![QueryRow {
attribute: ATTR_JOIN,
expression: Some("A.id = B.id".to_string()),
name1: Some("A".to_string()),
name2: Some("B".to_string()),
flag: Some(1),
extra: None,
}];
let result = build_from_tables(&rows);
assert_eq!(result.len(), 1);
assert!(result[0].contains("INNER JOIN"));
}
#[test]
fn contains_table_simple() {
let ts = TableSource::Simple {
name: "Table1".to_string(),
expr: "Table1".to_string(),
};
assert!(ts.contains_table("Table1"));
assert!(ts.contains_table("table1")); assert!(!ts.contains_table("Table2"));
}
#[test]
fn contains_table_nested_join() {
let ts = TableSource::Join {
from: Box::new(TableSource::Simple {
name: "T1".to_string(),
expr: "T1".to_string(),
}),
to: Box::new(TableSource::Simple {
name: "T2".to_string(),
expr: "T2".to_string(),
}),
join_type: 1,
on_conditions: vec!["T1.id = T2.id".to_string()],
};
assert!(ts.contains_table("T1"));
assert!(ts.contains_table("T2"));
assert!(!ts.contains_table("T3"));
}
#[test]
fn to_alias_none() {
assert_eq!(to_alias(None), "");
}
#[test]
fn to_alias_some() {
assert_eq!(to_alias(Some("Alias1")), " AS Alias1");
}
#[test]
fn is_quoted_true() {
assert!(is_quoted("[Table1]"));
}
#[test]
fn is_quoted_false() {
assert!(!is_quoted("Table1"));
assert!(!is_quoted("[T"));
assert!(!is_quoted("T]"));
assert!(!is_quoted(""));
}
#[test]
fn needs_quoting_simple() {
assert!(!needs_quoting("Table1"));
assert!(needs_quoting("My Table"));
assert!(needs_quoting("col-1"));
}
#[test]
fn sql_select_empty() {
let rows = vec![QueryRow {
attribute: ATTR_FLAG,
expression: None,
name1: None,
name2: None,
flag: Some(0),
extra: None,
}];
let mut builder = String::new();
sql_select(&mut builder, &rows);
assert!(builder.starts_with("SELECT "));
}
#[test]
fn read_queries_form_prop_test() {
let path = skip_if_missing!("formPropTest.accdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
assert_eq!(
queries.len(),
2,
"formPropTest.accdb should have 2 user queries"
);
for q in &queries {
assert!(
!q.name.starts_with("~sq_"),
"embedded query should be filtered: {}",
q.name
);
assert_eq!(
q.query_type,
QueryType::Select,
"query '{}' should default to Select",
q.name
);
}
let jp_query = queries.iter().find(|q| q.name == "jp_クエリ_02");
assert!(
jp_query.is_some(),
"should contain jp_クエリ_02, found: {:?}",
queries.iter().map(|q| &q.name).collect::<Vec<_>>()
);
let sql = query_to_sql(jp_query.unwrap());
assert!(
sql.contains("jp_テーブル2"),
"jp_クエリ_02 SQL should reference jp_テーブル2, got: {sql}"
);
}
#[test]
fn read_queries_jet3() {
let path = skip_if_missing!("V1997/queryTestV1997.mdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
assert!(
queries.len() >= 9,
"queryTestV1997.mdb should have at least 9 queries, got {}",
queries.len()
);
let names: Vec<&str> = queries.iter().map(|q| q.name.as_str()).collect();
assert!(names.contains(&"SelectQuery"), "should contain SelectQuery");
assert!(names.contains(&"UnionQuery"), "should contain UnionQuery");
for q in &queries {
let expected = match q.name.as_str() {
"SelectQuery" => QueryType::Select,
"DeleteQuery" => QueryType::Delete,
"UpdateQuery" => QueryType::Update,
"AppendQuery" => QueryType::Append,
"MakeTableQuery" => QueryType::MakeTable,
"CrosstabQuery" => QueryType::Crosstab,
"UnionQuery" => QueryType::Union,
"PassthroughQuery" => QueryType::Passthrough,
"DataDefinitionQuery" => QueryType::Ddl,
other => panic!("unexpected query name: {other}"),
};
assert_eq!(q.query_type, expected, "type mismatch for {}", q.name);
}
}
#[test]
fn read_queries_ace14() {
let path = skip_if_missing!("V2010/queryTestV2010.accdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
assert!(
queries.len() >= 9,
"queryTestV2010.accdb should have at least 9 queries, got {}",
queries.len()
);
let names: Vec<&str> = queries.iter().map(|q| q.name.as_str()).collect();
assert!(names.contains(&"SelectQuery"), "should contain SelectQuery");
assert!(names.contains(&"UnionQuery"), "should contain UnionQuery");
for q in &queries {
let expected = match q.name.as_str() {
"SelectQuery" => QueryType::Select,
"DeleteQuery" => QueryType::Delete,
"UpdateQuery" => QueryType::Update,
"AppendQuery" => QueryType::Append,
"MakeTableQuery" => QueryType::MakeTable,
"CrosstabQuery" => QueryType::Crosstab,
"UnionQuery" => QueryType::Union,
"PassthroughQuery" => QueryType::Passthrough,
"DataDefinitionQuery" => QueryType::Ddl,
other => panic!("unexpected query name: {other}"),
};
assert_eq!(q.query_type, expected, "type mismatch for {}", q.name);
}
}
#[test]
fn read_queries_v2007() {
let path = skip_if_missing!("V2007/queryTestV2007.accdb");
let mut reader = PageReader::open(&path).unwrap();
let queries = read_queries(&mut reader).unwrap();
assert!(
queries.len() >= 9,
"queryTestV2007.accdb should have at least 9 queries, got {}",
queries.len()
);
for q in &queries {
let expected = match q.name.as_str() {
"SelectQuery" => QueryType::Select,
"DeleteQuery" => QueryType::Delete,
"UpdateQuery" => QueryType::Update,
"AppendQuery" => QueryType::Append,
"MakeTableQuery" => QueryType::MakeTable,
"CrosstabQuery" => QueryType::Crosstab,
"UnionQuery" => QueryType::Union,
"PassthroughQuery" => QueryType::Passthrough,
"DataDefinitionQuery" => QueryType::Ddl,
other => panic!("unexpected query name: {other}"),
};
assert_eq!(q.query_type, expected, "type mismatch for {}", q.name);
}
}
}