uqa-engine 0.4.0

Engine: schema-aware table store, catalog restore, transactions
//
// Unified Query Algebra
//
// Copyright (c) 2023-2026 Cognica, Inc.
//

//! Retain the analysis relation lock through read sampling, validation and short physical publication.

mod sampling;
#[cfg(test)]
mod tests;

use std::sync::atomic::Ordering;

use uqa_sql::ast::{ColumnType, GeneratedColumnKind};
use uqa_storage::{StorageBackendError, StorageBackendResult};

use crate::statistics::{now_ms, MaintenanceState};
use crate::{ColumnStatsMap, Engine};

struct AutomaticAnalysis {
    object_id: [u8; 16],
    maintenance: MaintenanceState,
    row_count: u64,
    statistics: ColumnStatsMap,
}

fn automatic_column(ty: &ColumnType) -> bool {
    match ty {
        ColumnType::Domain { base, .. } => automatic_column(base),
        ColumnType::Bytea
        | ColumnType::Vector(_)
        | ColumnType::Tensor(_)
        | ColumnType::Json
        | ColumnType::JsonB
        | ColumnType::Array(_)
        | ColumnType::AnyArray
        | ColumnType::Record => false,
        _ => true,
    }
}

impl Engine {
    pub(crate) fn run_automatic_analyze(&self, name: &str) -> StorageBackendResult<bool> {
        let _statement = self.runtime.statement_gate.lock();
        if self.storage.backend.is_none() {
            return Ok(false);
        }
        self.with_storage_maintenance_scope(|engine| {
            let Some(target) = uqa_execution::maintenance::analyze::prepare_optional_target(
                &engine.analyze_execution_context(false),
                name,
            )
            .map_err(|error| StorageBackendError::backend("automatic ANALYZE locking", error))?
            else {
                return Ok(false);
            };
            let Some(analysis) = engine.collect_automatic_analysis(&target.name)? else {
                return Ok(false);
            };
            engine.publish_automatic_analysis(&target.name, analysis)
        })
    }

    fn collect_automatic_analysis(
        &self,
        name: &str,
    ) -> StorageBackendResult<Option<AutomaticAnalysis>> {
        let Some(table) = self.try_table(name)? else {
            return Ok(None);
        };
        let Some(catalog) = self.storage.catalog.as_deref() else {
            return Ok(None);
        };
        let maintenance = MaintenanceState::load_for(catalog, name, table.object_id())?;
        let missing = maintenance.missing(table.column_stats.read().is_empty());
        if !maintenance.due(
            missing,
            now_ms(),
            crate::statistics::value_size::FORMAT_VERSION,
        ) {
            return Ok(None);
        }
        let columns = table
            .columns
            .read()
            .iter()
            .filter(|column| {
                automatic_column(&column.ty)
                    && !column
                        .generated
                        .as_ref()
                        .is_some_and(|generated| generated.kind == GeneratedColumnKind::Virtual)
            })
            .map(|column| column.name.clone())
            .collect::<Vec<_>>();
        let (statistics, row_count) = sampling::collect(self, name, &columns)?;
        Ok(Some(AutomaticAnalysis {
            object_id: table.object_id(),
            maintenance,
            row_count,
            statistics,
        }))
    }

    fn publish_automatic_analysis(
        &self,
        name: &str,
        analysis: AutomaticAnalysis,
    ) -> StorageBackendResult<bool> {
        self.with_storage_maintenance_scope(|engine| {
            let Some(target) = uqa_execution::maintenance::analyze::prepare_optional_target(
                &engine.analyze_execution_context(false),
                name,
            )
            .map_err(|error| StorageBackendError::backend("automatic ANALYZE locking", error))?
            else {
                return Ok(false);
            };
            if target.object_id != analysis.object_id {
                return Ok(false);
            }
            let name = target.name.as_str();
            if !engine.try_prepare_storage_maintenance_writer()? {
                return Ok(false);
            }
            let Some(table) = engine.try_table(name)? else {
                return Ok(false);
            };
            let Some(catalog) = engine.storage.catalog.as_deref() else {
                return Ok(false);
            };
            // Identity and generation must still match. A concurrent write,
            // explicit ANALYZE, or DROP/recreate leaves newer work pending.
            if table.object_id() != analysis.object_id
                || MaintenanceState::load_for(catalog, name, table.object_id())?
                    != analysis.maintenance
            {
                return Ok(false);
            }
            Self::persist_column_stats(
                catalog,
                name,
                &analysis.statistics,
                table.object_id(),
                analysis.row_count,
            )?;
            *table.column_stats.write() = analysis.statistics;
            table.column_stats_loaded.store(true, Ordering::Release);
            table.column_stats_dirty.store(false, Ordering::Release);
            engine.note_table_data_changed();
            Ok(true)
        })
    }
}