khive_runtime/operations/
resolve_prefix_in.rs1use khive_storage::{SqlRow, SqlStatement, SqlValue};
2use uuid::Uuid;
3
4use super::uuid_prefix_bounds;
5use crate::{KhiveRuntime, RuntimeError, RuntimeResult};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub struct PrefixScope<'a> {
15 pub namespace: Option<&'a str>,
18 pub live_only: bool,
20}
21
22impl KhiveRuntime {
23 pub async fn resolve_prefix_in(
44 &self,
45 table: &'static str,
46 id_column: &'static str,
47 scope: PrefixScope<'_>,
48 prefix: &str,
49 ) -> RuntimeResult<Option<Uuid>> {
50 let Some(statement) = prefix_statement(table, id_column, scope, prefix)? else {
51 return Ok(None);
52 };
53 let mut reader = self.sql().reader().await?;
54 decode_prefix_matches(prefix, reader.query_all(statement).await?)
55 }
56}
57
58fn quoted_identifier(identifier: &str, kind: &str) -> RuntimeResult<String> {
59 let mut bytes = identifier.bytes();
60 if !bytes
61 .next()
62 .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
63 || !bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
64 {
65 return Err(RuntimeError::InvalidInput(format!(
66 "resolve_prefix_in: invalid {kind} identifier"
67 )));
68 }
69 Ok(format!("\"{identifier}\""))
70}
71
72fn prefix_statement(
73 table: &str,
74 id_column: &str,
75 scope: PrefixScope<'_>,
76 prefix: &str,
77) -> RuntimeResult<Option<SqlStatement>> {
78 let table = quoted_identifier(table, "table")?;
79 let id_column = quoted_identifier(id_column, "column")?;
80 let Some((lower, upper)) = uuid_prefix_bounds(prefix) else {
81 return Ok(None);
82 };
83 let mut params = vec![SqlValue::Text(lower), SqlValue::Text(upper)];
84 let mut filters = String::new();
85 if let Some(namespace) = scope.namespace {
86 params.push(SqlValue::Text(namespace.to_owned()));
87 filters.push_str(" AND candidate.namespace = ?3");
88 }
89 if scope.live_only {
90 filters.push_str(" AND candidate.deleted_at IS NULL");
91 }
92 Ok(Some(
93 SqlStatement::new(
94 format!(
95 "SELECT DISTINCT candidate.{id_column} AS resolved_id FROM {table} AS candidate \
96 WHERE candidate.{id_column} >= ?1 AND candidate.{id_column} < ?2{filters} \
97 ORDER BY candidate.{id_column} LIMIT 2"
98 ),
99 params,
100 )
101 .labelled("resolve_prefix_in"),
102 ))
103}
104
105fn decode_prefix_matches(prefix: &str, rows: Vec<SqlRow>) -> RuntimeResult<Option<Uuid>> {
106 let matches = rows
107 .iter()
108 .map(|row| {
109 row.uuid("resolved_id")
110 .map_err(|error| RuntimeError::Internal(format!("stored UUID is invalid: {error}")))
111 })
112 .collect::<RuntimeResult<Vec<_>>>()?;
113 match matches.as_slice() {
114 [] => Ok(None),
115 [id] => Ok(Some(*id)),
116 _ => Err(RuntimeError::AmbiguousPrefix {
117 prefix: prefix.to_owned(),
118 matches,
119 }),
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use khive_storage::types::SqlColumn;
126
127 use super::*;
128
129 #[test]
130 fn mistyped_or_missing_result_column_is_an_error() {
131 for columns in [
132 vec![],
133 vec![SqlColumn {
134 name: "resolved_id".into(),
135 value: SqlValue::Integer(1),
136 }],
137 ] {
138 assert!(matches!(
139 decode_prefix_matches("aabbccdd", vec![SqlRow { columns }]),
140 Err(RuntimeError::Internal(_))
141 ));
142 }
143 }
144}