use crate::command::Command;
use crate::event::Event;
use crate::failure::FailureCode;
use crate::port::{PortError, StoragePort};
use crate::snapshot::UiStateSnapshot;
use crate::state::AppState;
mod relationships;
mod rename;
mod table_view_state;
mod tabs;
mod views;
pub struct Core {
pub(crate) state: AppState,
pub(crate) port: Box<dyn StoragePort>,
}
impl Core {
pub fn new(port: Box<dyn StoragePort>) -> Self {
let mut state = AppState::new();
state.drivers = port.driver_inventory();
Self { state, port }
}
pub fn state(&self) -> &AppState {
&self.state
}
pub fn inspect(&self) -> UiStateSnapshot {
UiStateSnapshot::of(&self.state)
}
pub fn run_query(&self, name: &str) -> Result<crate::query_engine::QueryResult, String> {
let Some(db) = &self.state.database else {
return Err("no database is open".to_owned());
};
let Some(def) = db
.queries
.iter()
.find(|q| q.name.eq_ignore_ascii_case(name))
.cloned()
else {
let native = db
.native_queries
.iter()
.find(|query| query.name.eq_ignore_ascii_case(name))
.ok_or_else(|| format!("query {name:?} not found"))?;
let mut sources = Vec::with_capacity(native.tables.len());
for table in &native.tables {
let Some(source) = db
.tables
.iter()
.find(|candidate| candidate.name.eq_ignore_ascii_case(table))
else {
return Err(format!(
"native query {:?}: table {table:?} not found",
native.name
));
};
let rows = self
.port
.read_rows(
&source.name,
crate::port::RowRange {
start: 0,
len: usize::MAX,
},
)
.map_err(|e| e.to_string())?;
sources.push((source, rows));
}
return crate::native_exec::run(native, &sources).map_err(|e| e.to_string());
};
let Some(source) = db
.tables
.iter()
.find(|t| t.name.eq_ignore_ascii_case(&def.table))
else {
return Err(format!("source table {:?} not found", def.table));
};
let rows = self
.port
.read_rows(
&source.name,
crate::port::RowRange {
start: 0,
len: usize::MAX,
},
)
.map_err(|e| e.to_string())?;
crate::query_engine::run_query(&def, source, &rows).map_err(|e| e.to_string())
}
pub fn native_query(&self, name: &str) -> Option<&crate::port::NativeQuery> {
self.state
.database
.as_ref()?
.native_queries
.iter()
.find(|query| query.name.eq_ignore_ascii_case(name))
}
pub fn form_filter(&self, form: &str) -> Option<&str> {
self.state
.form_filters
.iter()
.find(|(name, _)| name.eq_ignore_ascii_case(form))
.map(|(_, criteria)| criteria.as_str())
}
pub fn form_filter_rows(
&self,
form: &str,
rows: &[Vec<crate::schema::FieldValue>],
) -> Result<Vec<usize>, String> {
let Some(criteria) = self.form_filter(form) else {
return Ok((0..rows.len()).collect());
};
let criteria = criteria.to_owned();
let Some(db) = self.state.database.as_ref() else {
return Err("no database is open".to_owned());
};
let Some(form_def) = db.forms.iter().find(|f| f.name.eq_ignore_ascii_case(form)) else {
return Err(format!("form {form:?} not found"));
};
let Some(table) = db
.tables
.iter()
.find(|table| table.name.eq_ignore_ascii_case(&form_def.source))
else {
return Err(format!("source table {:?} not found", form_def.source));
};
let expression = crate::expr::parse(&criteria)
.map_err(|error| format!("filter {criteria:?}: {error}"))?;
let mut indices = Vec::new();
for (index, row) in rows.iter().enumerate() {
let mut scope = crate::expr::Scope::new();
for (field, value) in table.fields.iter().zip(row) {
scope.set(field.name.clone(), value.clone());
}
match expression.evaluate(&scope) {
Ok(crate::schema::FieldValue::Boolean(true)) => indices.push(index),
Ok(crate::schema::FieldValue::Boolean(false) | crate::schema::FieldValue::Null) => {
}
Ok(other) => {
return Err(format!(
"filter {criteria:?} evaluated to {other:?}, not a logical value"
));
}
Err(error) => return Err(format!("filter {criteria:?}: {error}")),
}
}
Ok(indices)
}
#[cfg(feature = "sql-engine")]
pub fn run_sql(&self, sql: &str) -> Result<crate::sql_types::SqlResult, String> {
let Some(db) = &self.state.database else {
return Err("no database is open".to_owned());
};
let mut sources = Vec::with_capacity(db.tables.len() + db.native_views.len());
for table in &db.tables {
let rows = self
.port
.read_rows(
&table.name,
crate::port::RowRange {
start: 0,
len: usize::MAX,
},
)
.map_err(|e| format!("materializing {:?} failed: {e}", table.name))?;
sources.push((table.clone(), rows));
}
for view in &db.native_views {
let rows = self
.port
.read_rows(
&view.name,
crate::port::RowRange {
start: 0,
len: usize::MAX,
},
)
.map_err(|e| format!("materializing view {:?} failed: {e}", view.name))?;
sources.push((
crate::schema::TableDef::new(&view.name, view.fields.clone()),
rows,
));
}
crate::sql_engine::run_sql(sql, &sources)
}
#[cfg(not(feature = "sql-engine"))]
pub fn run_sql(&self, _sql: &str) -> Result<crate::sql_types::SqlResult, String> {
Err(
"the SQL engine is not part of this build (enable the datacraft-core `sql-engine` feature)"
.to_owned(),
)
}
pub fn sql_result(&self) -> Option<&crate::sql_types::SqlResult> {
self.state.sql_result.as_ref()
}
fn run_sql_command(&mut self, sql: &str) -> Vec<Event> {
match self.run_sql(sql) {
Ok(result) => {
let rows = result.rows.len();
self.state.sql_result = Some(result);
vec![Event::SqlRan { rows }]
}
Err(reason) => {
self.state.sql_result = None;
failed(FailureCode::Parse, reason)
}
}
}
pub fn read_rows(
&self,
table: &str,
range: crate::port::RowRange,
) -> Result<Vec<Vec<crate::schema::FieldValue>>, PortError> {
self.port.read_rows(table, range)
}
pub fn table_definition(&self, table: &str) -> Option<&crate::schema::TableDef> {
self.state
.database
.as_ref()?
.tables
.iter()
.find(|candidate| candidate.name.eq_ignore_ascii_case(table))
}
pub fn dispatch(&mut self, command: Command) -> Vec<Event> {
let clears_history = clears_history(&command);
let events = match command {
Command::ToggleNavPane => {
self.state.nav_pane_visible = !self.state.nav_pane_visible;
vec![Event::NavPaneToggled {
visible: self.state.nav_pane_visible,
}]
}
Command::CreateRelationship { relationship } => self.create_relationship(relationship),
Command::DeleteRelationship { relationship } => self.delete_relationship(relationship),
Command::ToggleRelationshipMapper => {
self.state.relationship_mapper_open = !self.state.relationship_mapper_open;
vec![Event::RelationshipMapperToggled {
open: self.state.relationship_mapper_open,
}]
}
Command::ToggleSqlWindow => {
self.state.sql_window_open = !self.state.sql_window_open;
vec![Event::SqlWindowToggled {
open: self.state.sql_window_open,
}]
}
Command::ToggleImportWindow => {
self.state.import_window_open = !self.state.import_window_open;
Vec::new()
}
Command::PreviewImportFile { source } => self.preview_import_file(&source),
Command::ToggleExpressionWindow => {
self.state.expression_window_open = !self.state.expression_window_open;
vec![Event::ExpressionWindowToggled {
open: self.state.expression_window_open,
}]
}
Command::RunSql { sql } => self.run_sql_command(&sql),
Command::RunExpression {
context,
expression,
} => self.run_expression_command(context, &expression),
Command::CreateQuery { name } => self.create_query(&name),
Command::View(action) => self.view_command(action),
Command::SetQbeCell {
query,
row,
col,
value,
} => self.set_qbe_cell(&query, row, col, value),
Command::ClearQbeCriteria { query } => self.clear_qbe_criteria(&query),
Command::SetTableSort {
table,
field,
order,
} => self.set_table_sort(&table, &field, order),
Command::SetTableFilter {
table,
field,
criteria,
} => self.set_table_filter(&table, &field, criteria.as_deref()),
Command::ClearTableFilters { table } => self.clear_table_filters(&table),
Command::DeleteQuery { name } => self.delete_query(&name),
Command::RunQuery { query } => self.run_query_command(&query),
Command::CreateForm { name } => self.create_form(&name),
Command::DeleteForm { name } => self.delete_form(&name),
Command::AddFormControl { form, control } => self.add_control(&form, control),
Command::SetFormControlContent {
form,
control,
value,
} => self.set_control_content(&form, &control, &value),
Command::SetFormControlData(data) => self.set_control_data(&data),
Command::SetFormFilter(filter) => self.set_form_filter(&filter),
Command::SetFormControlRect {
form,
control,
rect,
} => self.set_control_rect(&form, &control, rect),
Command::DeleteFormControl { form, control } => self.delete_control(&form, &control),
Command::AddFormField { form, field } => self.add_form_field(&form, &field),
Command::SetFormSource { form, table } => self.set_form_source(&form, &table),
Command::CreateReport { name } => self.create_report(&name),
Command::DeleteReport { name } => self.delete_report(&name),
Command::AddReportControl { report, control } => {
self.add_report_control(&report, control)
}
Command::SetReportControlContent {
report,
control,
value,
} => self.set_report_control_content(&report, &control, &value),
Command::SetReportControlRect {
report,
control,
rect,
} => self.set_report_control_rect(&report, &control, rect),
Command::DeleteReportControl { report, control } => {
self.delete_report_control(&report, &control)
}
Command::AddReportField { report, field } => self.add_report_field(&report, &field),
Command::SetReportSource { report, table } => self.set_report_source(&report, &table),
Command::SetReportGrouping { report, group_by } => {
self.set_report_grouping(&report, group_by)
}
Command::LoadVbaModule { module } => self.load_vba_module(&module),
Command::AnalyzeVba => self.analyze_vba_command(),
Command::SaveDesignDocument { path } => self.save_design_document(&path),
Command::LoadDesignDocument { path } => self.load_design_document(&path),
Command::OpenDatabase {
path,
password,
source,
} => self.open_database(path, password, source),
Command::CloseDatabase => self.close_database(),
Command::CreateDatabase { path, format } => self.create_database(path, format),
Command::CreateDatabaseFromTemplate { path, template } => {
self.create_database_from_template(path, template)
}
Command::SaveDatabaseAs { path, password } => self.save_database_as(path, password),
Command::ChangePassword { new_password } => self.change_password(new_password),
Command::CreateTable { definition } => self.create_table(definition),
Command::ImportFile { source, table } => self.import_file(&source, &table),
Command::DropTable { table } => self.drop_table(&table),
Command::AddField { table, field } => {
self.alter_table(&table, crate::port::SchemaChange::AddField { field })
}
Command::DropField { table, field } => {
self.alter_table(&table, crate::port::SchemaChange::DropField { field })
}
Command::RenameField {
table,
field,
new_name,
} => self.alter_table(
&table,
crate::port::SchemaChange::RenameField { field, new_name },
),
Command::SetFieldProperties {
table,
field,
properties,
} => self.alter_table(
&table,
crate::port::SchemaChange::SetFieldProperties { field, properties },
),
Command::InsertRow { table, values } => self.insert_row(&table, &values),
Command::UpdateRow { table, row, values } => self.update_row(&table, row, &values),
Command::DeleteRow { table, row } => self.delete_row(&table, row),
Command::RenameObject { object, new_name } => self.rename_object(&object, &new_name),
Command::PreviewWrite { table, ops } => self.preview_write(&table, ops),
Command::ApplyWrite { token } => self.apply_write(token),
Command::RollbackWrite { token } => self.rollback_write(token),
Command::DiscardWrite { token } => self.discard_write(token),
Command::OpenObject(object) => self.open_object(object, None),
Command::OpenObjectInMode { object, mode } => self.open_object(object, Some(mode)),
Command::CloseTab { tab_id } => self.close_tab(tab_id),
Command::CloseOtherTabs { tab_id } => self.close_other_tabs(tab_id),
Command::CloseAllTabs => self.close_all_tabs(),
Command::SetActiveTab { tab_id } => self.set_active_tab(tab_id),
Command::SetTabMode { tab_id, mode } => self.set_tab_mode(tab_id, mode),
Command::Undo => self.undo(),
Command::Redo => self.redo(),
};
if clears_history {
let failed = events.iter().any(|event| {
matches!(
event,
Event::Failed { .. }
| Event::PasswordRequired { .. }
| Event::PasswordRejected { .. }
)
});
if !failed {
self.clear_history();
}
}
events
}
}
fn clears_history(command: &Command) -> bool {
matches!(
command,
Command::OpenDatabase { .. }
| Command::CloseDatabase
| Command::CreateTable { .. }
| Command::ImportFile { .. }
| Command::DropTable { .. }
| Command::AddField { .. }
| Command::DropField { .. }
| Command::RenameField { .. }
| Command::SetFieldProperties { .. }
| Command::ApplyWrite { .. }
| Command::RollbackWrite { .. }
) || matches!(
command,
Command::RenameObject { object, .. } if object.kind == crate::ObjectKind::Table
)
}
pub(crate) fn failed(code: crate::failure::FailureCode, reason: impl Into<String>) -> Vec<Event> {
vec![Event::Failed {
code,
reason: reason.into(),
}]
}