use super::{ExecutorState, extract_pk_from_row, now_micros};
use crate::catalog::namespace_key;
use crate::catalog::schema::TableSchema;
use crate::commit::validation::Mutation;
use crate::error::AedbError;
use crate::storage::encoded_key::EncodedKey;
use crate::storage::keyspace::NamespaceId;
use crate::version_store::CommitDelta;
use tracing::warn;
pub(crate) fn refresh_async_indexes(
state: &mut ExecutorState,
pending_delta: Option<&CommitDelta>,
) -> Result<bool, AedbError> {
let target_seq = state.visible_head_seq;
let mut changed = false;
for ((ns, table_name, index_name), def) in &state.catalog.async_indexes {
let key = (
NamespaceId::Project(ns.clone()),
table_name.clone(),
index_name.clone(),
);
let current = state
.keyspace
.async_indexes
.get(&key)
.map(|v| v.materialized_seq)
.unwrap_or(0);
if current >= target_seq {
continue;
}
let schema = state
.catalog
.tables
.get(&(ns.clone(), table_name.clone()))
.ok_or_else(|| AedbError::Validation("table missing".into()))?;
let existing_projection = state.keyspace.take_async_projection(&key.0, &key.1, &key.2);
let (mut projection_rows, mut materialized_seq) = existing_projection
.map(|projection| (projection.rows, projection.materialized_seq))
.unwrap_or_default();
if current == 0
&& projection_rows.is_empty()
&& let Some(table) = state.keyspace.table_by_namespace_key(ns, table_name)
{
for (pk, stored) in &table.rows {
let row = state.keyspace.materialize_row(stored)?;
let projected = project_row(&row, schema, &def.projected_columns)?;
projection_rows.insert(pk.clone(), projected);
}
materialized_seq = target_seq;
}
if materialized_seq < target_seq {
let Some(deltas) = state
.version_store
.deltas_since(materialized_seq, target_seq)
else {
warn!(
from_seq = materialized_seq,
to_seq = target_seq,
"async projection delta history evicted; rebuilding projection"
);
projection_rows.clear();
if let Some(table) = state.keyspace.table_by_namespace_key(ns, table_name) {
for (pk, stored) in &table.rows {
let row = state.keyspace.materialize_row(stored)?;
let projected = project_row(&row, schema, &def.projected_columns)?;
projection_rows.insert(pk.clone(), projected);
}
}
materialized_seq = target_seq;
state.keyspace.insert_async_projection(
key.0.clone(),
key.1.clone(),
key.2.clone(),
crate::storage::keyspace::AsyncProjectionData {
rows: projection_rows,
materialized_seq,
},
);
changed = true;
continue;
};
let mut requires_rebuild = false;
for delta in deltas {
for mutation in &delta.mutations {
if matches!(
&mutation,
Mutation::TableIncU256 {
project_id,
scope_id,
table_name: mutation_table,
..
} | Mutation::TableDecU256 {
project_id,
scope_id,
table_name: mutation_table,
..
} | Mutation::DeleteWhere {
project_id,
scope_id,
table_name: mutation_table,
..
} | Mutation::UpdateWhere {
project_id,
scope_id,
table_name: mutation_table,
..
} | Mutation::UpdateWhereExpr {
project_id,
scope_id,
table_name: mutation_table,
..
} | Mutation::UpdateFields {
project_id,
scope_id,
table_name: mutation_table,
..
} if namespace_key(project_id, scope_id) == *ns
&& mutation_table == table_name
) {
requires_rebuild = true;
break;
}
apply_projection_delta(
mutation,
ns,
table_name,
schema,
&def.projected_columns,
&mut projection_rows,
)?;
}
materialized_seq = delta.seq;
}
if let Some(delta) = pending_delta
&& delta.seq > materialized_seq
&& delta.seq <= target_seq
{
for mutation in &delta.mutations {
apply_projection_delta(
mutation,
ns,
table_name,
schema,
&def.projected_columns,
&mut projection_rows,
)?;
}
if requires_rebuild {
break;
}
materialized_seq = delta.seq;
}
if requires_rebuild {
projection_rows.clear();
if let Some(table) = state.keyspace.table_by_namespace_key(ns, table_name) {
for (pk, stored) in &table.rows {
let row = state.keyspace.materialize_row(stored)?;
let projected = project_row(&row, schema, &def.projected_columns)?;
projection_rows.insert(pk.clone(), projected);
}
}
materialized_seq = target_seq;
}
}
state.keyspace.insert_async_projection(
key.0,
key.1,
key.2,
crate::storage::keyspace::AsyncProjectionData {
rows: projection_rows,
materialized_seq,
},
);
changed = true;
}
if refresh_kv_projection_tables(state, pending_delta)? {
changed = true;
}
Ok(changed)
}
fn refresh_kv_projection_tables(
state: &mut ExecutorState,
pending_delta: Option<&CommitDelta>,
) -> Result<bool, AedbError> {
let target_seq = state.visible_head_seq;
let mut changed = false;
let projections: Vec<(String, String, String)> = state
.catalog
.kv_projections
.iter()
.map(|((project_id, scope_id), def)| {
(project_id.clone(), scope_id.clone(), def.table_name.clone())
})
.collect();
for (project_id, scope_id, table_name) in projections {
let ns = namespace_key(&project_id, &scope_id);
let current = state
.keyspace
.table_structural_version(&project_id, &scope_id, &table_name);
if current >= target_seq {
continue;
}
if current == 0 {
rebuild_kv_projection_rows(state, &project_id, &scope_id, &table_name, target_seq)?;
changed = true;
continue;
}
let Some(deltas) = state.version_store.deltas_since(current, target_seq) else {
rebuild_kv_projection_rows(state, &project_id, &scope_id, &table_name, target_seq)?;
changed = true;
continue;
};
for delta in deltas {
for mutation in &delta.mutations {
apply_kv_projection_delta(
state,
mutation,
&ns,
&project_id,
&scope_id,
&table_name,
)?;
}
set_kv_projection_structural_version(
state,
&project_id,
&scope_id,
&table_name,
delta.seq,
);
}
if let Some(delta) = pending_delta
&& delta.seq > current
&& delta.seq <= target_seq
{
for mutation in &delta.mutations {
apply_kv_projection_delta(
state,
mutation,
&ns,
&project_id,
&scope_id,
&table_name,
)?;
}
set_kv_projection_structural_version(
state,
&project_id,
&scope_id,
&table_name,
delta.seq,
);
}
changed = true;
}
Ok(changed)
}
fn rebuild_kv_projection_rows(
state: &mut ExecutorState,
project_id: &str,
scope_id: &str,
table_name: &str,
materialized_seq: u64,
) -> Result<(), AedbError> {
let entries = state
.keyspace
.try_kv_scan_prefix(project_id, scope_id, &[], usize::MAX)?;
let ns = namespace_key(project_id, scope_id);
if let Some(table) = state.keyspace.table_by_namespace_key_mut(&ns, table_name) {
table.rows.clear();
table.row_versions.clear();
table.structural_version = materialized_seq;
} else {
state
.keyspace
.table_mut(project_id, scope_id, table_name)
.structural_version = materialized_seq;
}
for (key, entry) in entries {
upsert_kv_projection_row(
state,
project_id,
scope_id,
table_name,
KvProjectionRow {
key,
value: entry.value,
commit_seq: entry.version,
updated_at: materialized_seq,
},
)?;
}
Ok(())
}
fn apply_kv_projection_delta(
state: &mut ExecutorState,
mutation: &Mutation,
namespace: &str,
project_id: &str,
scope_id: &str,
table_name: &str,
) -> Result<(), AedbError> {
match mutation {
Mutation::KvSet {
project_id: p,
scope_id: s,
key,
value,
} if namespace_key(p, s) == namespace => {
upsert_kv_projection_row(
state,
project_id,
scope_id,
table_name,
KvProjectionRow {
key: key.clone(),
value: value.clone(),
commit_seq: state.keyspace.try_kv_version(project_id, scope_id, key)?,
updated_at: now_micros(),
},
)?;
}
Mutation::KvIncU256 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvDecU256 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvAddU256Ex {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvSubU256Ex {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvMaxU256 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvMinU256 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvMutateU256 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvAddU64Ex {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvSubU64Ex {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvMaxU64 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvMinU64 {
project_id: p,
scope_id: s,
key,
..
}
| Mutation::KvMutateU64 {
project_id: p,
scope_id: s,
key,
..
} if namespace_key(p, s) == namespace => {
if let Some(entry) = state.keyspace.try_kv_get(project_id, scope_id, key)? {
upsert_kv_projection_row(
state,
project_id,
scope_id,
table_name,
KvProjectionRow {
key: key.clone(),
value: entry.value,
commit_seq: entry.version,
updated_at: now_micros(),
},
)?;
}
}
Mutation::KvDel {
project_id: p,
scope_id: s,
key,
} if namespace_key(p, s) == namespace => {
let _ = state.keyspace.delete_row(
project_id,
scope_id,
table_name,
&[crate::catalog::types::Value::Blob(key.clone())],
now_micros(),
);
}
Mutation::Ddl(crate::catalog::DdlOperation::DropScope {
project_id: p,
scope_id: s,
if_exists: true,
..
}) if namespace_key(p, s) == namespace => {
state.keyspace.drop_table(project_id, scope_id, table_name);
}
Mutation::Ddl(crate::catalog::DdlOperation::DropProject { project_id: p, .. })
if namespace.starts_with(&(p.to_string() + "::")) =>
{
state.keyspace.drop_table(project_id, scope_id, table_name);
}
Mutation::Ddl(crate::catalog::DdlOperation::DisableKvProjection {
project_id: p,
scope_id: s,
}) if namespace_key(p, s) == namespace => {
state.keyspace.drop_table(project_id, scope_id, table_name);
}
_ => {}
}
Ok(())
}
struct KvProjectionRow {
key: Vec<u8>,
value: Vec<u8>,
commit_seq: u64,
updated_at: u64,
}
fn upsert_kv_projection_row(
state: &mut ExecutorState,
project_id: &str,
scope_id: &str,
table_name: &str,
row: KvProjectionRow,
) -> Result<(), AedbError> {
let commit_seq_i64 = i64::try_from(row.commit_seq)
.map_err(|_| AedbError::Validation("commit_seq overflow".into()))?;
let updated_at = i64::try_from(row.updated_at)
.map_err(|_| AedbError::Validation("updated_at overflow".into()))?;
state.keyspace.upsert_row(
project_id,
scope_id,
table_name,
vec![crate::catalog::types::Value::Blob(row.key.clone())],
crate::catalog::types::Row {
values: vec![
crate::catalog::types::Value::Text(project_id.into()),
crate::catalog::types::Value::Text(scope_id.into()),
crate::catalog::types::Value::Blob(row.key),
crate::catalog::types::Value::Blob(row.value),
crate::catalog::types::Value::Integer(commit_seq_i64),
crate::catalog::types::Value::Timestamp(updated_at),
],
},
row.commit_seq,
);
Ok(())
}
fn set_kv_projection_structural_version(
state: &mut ExecutorState,
project_id: &str,
scope_id: &str,
table_name: &str,
seq: u64,
) {
let ns = namespace_key(project_id, scope_id);
if let Some(table) = state.keyspace.table_by_namespace_key_mut(&ns, table_name) {
table.structural_version = seq;
}
}
fn project_row(
row: &crate::catalog::types::Row,
schema: &TableSchema,
projected_columns: &[String],
) -> Result<crate::catalog::types::Row, AedbError> {
let mut values = Vec::with_capacity(projected_columns.len());
for col in projected_columns {
let column_index = schema
.columns
.iter()
.position(|c| c.name == *col)
.ok_or_else(|| AedbError::Validation(format!("projection column missing: {col}")))?;
values.push(row.values[column_index].clone());
}
Ok(crate::catalog::types::Row { values })
}
fn apply_projection_delta(
mutation: &Mutation,
ns: &str,
table_name: &str,
schema: &TableSchema,
projected_columns: &[String],
projection_rows: &mut im::OrdMap<EncodedKey, crate::catalog::types::Row>,
) -> Result<(), AedbError> {
match mutation {
Mutation::Insert {
project_id,
scope_id,
table_name: mutation_table,
primary_key,
row,
}
| Mutation::Upsert {
project_id,
scope_id,
table_name: mutation_table,
primary_key,
row,
} => {
if namespace_key(project_id, scope_id) == ns && mutation_table == table_name {
let projected = project_row(row, schema, projected_columns)?;
projection_rows.insert(EncodedKey::from_values(primary_key), projected);
}
}
Mutation::InsertBatch {
project_id,
scope_id,
table_name: mutation_table,
rows,
} => {
if namespace_key(project_id, scope_id) == ns && mutation_table == table_name {
for row in rows {
let pk = extract_pk_from_row(schema, row)?;
let projected = project_row(row, schema, projected_columns)?;
projection_rows.insert(EncodedKey::from_values(&pk), projected);
}
}
}
Mutation::Delete {
project_id,
scope_id,
table_name: mutation_table,
primary_key,
} => {
if namespace_key(project_id, scope_id) == ns && mutation_table == table_name {
projection_rows.remove(&EncodedKey::from_values(primary_key));
}
}
Mutation::Ddl(crate::catalog::DdlOperation::DropTable {
project_id,
scope_id,
table_name: dropped_table,
..
}) => {
if namespace_key(project_id, scope_id) == ns && dropped_table == table_name {
projection_rows.clear();
}
}
Mutation::Ddl(crate::catalog::DdlOperation::DropScope {
project_id,
scope_id,
..
}) => {
if namespace_key(project_id, scope_id) == ns {
projection_rows.clear();
}
}
Mutation::Ddl(crate::catalog::DdlOperation::DropProject { project_id, .. })
if ns.starts_with(&(project_id.to_owned() + "::")) =>
{
projection_rows.clear();
}
_ => {}
}
Ok(())
}