Skip to main content

radixdb_api/
public_read.rs

1//! Public, read-only ORM admission and deterministic keyset pagination.
2//!
3//! This API never accepts SQL text. The ORM IR is validated before rendering,
4//! then the executor independently validates the rendered SELECT while holding
5//! the catalog fence used for authorization and complete result consumption.
6
7use std::fmt;
8
9use base64::Engine as _;
10use chrono::{NaiveDate, SecondsFormat};
11use radixdb_core::{DataType, Error, Row, Value};
12use radixdb_executor::{
13    BoundPublicReadPolicy, PublicReadLimits, PublicReadRelationBinding, PublicReadRelationSpec,
14};
15use radixdb_orm::{
16    BinaryOperator, ColumnRef, Expression, FloatValue, IrDocument, JoinKind, NullPlacement,
17    Operation, OrderBy, Projection, Relation, Select, SortDirection, TypedValue,
18};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21
22use crate::orm::typed_values_to_core;
23use crate::{Database, ObjectId, ServerExecutionContext};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum PublicReadErrorCode {
28    InvalidIr,
29    UnsupportedShape,
30    InvalidCursor,
31    Policy,
32    Authorization,
33    ResourceLimit,
34    Execution,
35}
36
37impl PublicReadErrorCode {
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Self::InvalidIr => "public_read.invalid_ir",
41            Self::UnsupportedShape => "public_read.unsupported_shape",
42            Self::InvalidCursor => "public_read.invalid_cursor",
43            Self::Policy => "public_read.policy",
44            Self::Authorization => "public_read.authorization",
45            Self::ResourceLimit => "public_read.resource_limit",
46            Self::Execution => "public_read.execution",
47        }
48    }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct PublicReadError {
53    code: PublicReadErrorCode,
54    fingerprint: Option<String>,
55    detail: &'static str,
56}
57
58impl PublicReadError {
59    fn new(code: PublicReadErrorCode, fingerprint: Option<&str>, detail: &'static str) -> Self {
60        Self {
61            code,
62            fingerprint: fingerprint.map(str::to_owned),
63            detail,
64        }
65    }
66
67    pub const fn code(&self) -> PublicReadErrorCode {
68        self.code
69    }
70
71    pub fn fingerprint(&self) -> Option<&str> {
72        self.fingerprint.as_deref()
73    }
74
75    pub const fn detail(&self) -> &'static str {
76        self.detail
77    }
78}
79
80impl fmt::Display for PublicReadError {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(formatter, "{}: {}", self.code.as_str(), self.detail)?;
83        if let Some(fingerprint) = &self.fingerprint {
84            write!(formatter, " [fingerprint={fingerprint}]")?;
85        }
86        Ok(())
87    }
88}
89
90impl std::error::Error for PublicReadError {}
91
92pub type PublicReadResult<T> = std::result::Result<T, PublicReadError>;
93
94#[derive(Debug, Clone, PartialEq)]
95pub struct PublicReadRequest {
96    pub document: IrDocument,
97    pub page_size: usize,
98    pub cursor: Option<PublicReadCursor>,
99}
100
101impl PublicReadRequest {
102    pub fn new(document: IrDocument, page_size: usize) -> Self {
103        Self {
104            document,
105            page_size,
106            cursor: None,
107        }
108    }
109
110    pub fn after(mut self, cursor: PublicReadCursor) -> Self {
111        self.cursor = Some(cursor);
112        self
113    }
114}
115
116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct PublicReadCursor {
118    pub fingerprint: String,
119    /// Binds the cursor to the original parameter values without exposing
120    /// them in the public diagnostic fingerprint.
121    pub scope_digest: String,
122    pub keys: Vec<PublicReadCursorKey>,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
126pub struct PublicReadCursorKey {
127    pub relation_ordinal: u32,
128    pub column_id: String,
129    pub value: TypedValue,
130}
131
132#[derive(Debug, Clone, PartialEq)]
133pub struct PublicReadPage {
134    pub columns: Vec<String>,
135    pub rows: Vec<Row>,
136    pub next_cursor: Option<PublicReadCursor>,
137    pub fingerprint: String,
138    pub database_id: [u8; 16],
139    pub catalog_id: [u8; 16],
140    pub catalog_generation: u64,
141}
142
143#[derive(Debug, Clone)]
144struct RelationOccurrence<'a> {
145    ordinal: u32,
146    qualifier: String,
147    binding: &'a PublicReadRelationBinding,
148}
149
150#[derive(Debug, Clone)]
151struct KeyProjection {
152    relation_ordinal: u32,
153    qualifier: String,
154    column_id: ObjectId,
155    column_name: String,
156    output_index: usize,
157    data_type: DataType,
158}
159
160impl Database {
161    /// Bind deployment configuration names to durable catalog identities.
162    /// Reusing the returned policy after DROP/recreate or incompatible ALTER
163    /// fails closed; names are never silently rebound during a request.
164    pub fn bind_public_read_policy(
165        &self,
166        relations: &[PublicReadRelationSpec],
167        functions: &[String],
168    ) -> PublicReadResult<BoundPublicReadPolicy> {
169        self.with_connection_executor(|executor| {
170            executor.bind_public_read_policy(relations, functions)
171        })
172        .map_err(|error| public_error(error, None))
173    }
174
175    /// Execute one bounded public ORM SELECT for an authenticated principal.
176    /// There is intentionally no public-read method accepting raw SQL.
177    pub fn public_read(
178        &self,
179        request: &PublicReadRequest,
180        context: &ServerExecutionContext,
181        policy: &BoundPublicReadPolicy,
182        limits: PublicReadLimits,
183    ) -> PublicReadResult<PublicReadPage> {
184        let limits = limits
185            .validate()
186            .map_err(|error| public_error(error, None))?;
187        if request.page_size == 0 || request.page_size > limits.max_page_size {
188            return Err(PublicReadError::new(
189                PublicReadErrorCode::ResourceLimit,
190                None,
191                "page size is outside the admitted limit",
192            ));
193        }
194        request.document.validate().map_err(|_| {
195            PublicReadError::new(PublicReadErrorCode::InvalidIr, None, "invalid ORM envelope")
196        })?;
197        let Operation::Select { query } = &request.document.payload else {
198            return Err(PublicReadError::new(
199                PublicReadErrorCode::UnsupportedShape,
200                None,
201                "only ORM SELECT is admitted",
202            ));
203        };
204        validate_ir_subset(query, limits)?;
205        let occurrences = relation_occurrences(query.from.as_ref(), policy)?;
206        let keys = bind_key_projections(query, &occurrences)?;
207        let base = request.document.to_sql().map_err(|_| {
208            PublicReadError::new(PublicReadErrorCode::InvalidIr, None, "ORM rendering failed")
209        })?;
210        let fingerprint = public_fingerprint(&base.shape_fingerprint, &occurrences, &keys);
211        let scope_digest = cursor_scope_digest(&fingerprint, &base.parameters);
212        validate_cursor(request.cursor.as_ref(), &fingerprint, &scope_digest, &keys)?;
213
214        let mut admitted = query.clone();
215        install_keyset_boundary(&mut admitted, request.cursor.as_ref(), &keys);
216        admitted.order_by = keys
217            .iter()
218            .map(|key| OrderBy {
219                expression: Expression::Column {
220                    column: ColumnRef::qualified(&key.qualifier, &key.column_name),
221                },
222                direction: SortDirection::Asc,
223                nulls: Some(NullPlacement::Last),
224            })
225            .collect();
226        admitted.limit = Some((request.page_size + 1) as u64);
227        admitted.offset = None;
228        let admitted = IrDocument::new(Operation::Select { query: admitted });
229        let compiled = admitted.to_sql().map_err(|_| {
230            PublicReadError::new(
231                PublicReadErrorCode::InvalidIr,
232                Some(&fingerprint),
233                "ORM rendering failed",
234            )
235        })?;
236        let params = typed_values_to_core(&compiled.parameters).map_err(|_| {
237            PublicReadError::new(
238                PublicReadErrorCode::InvalidIr,
239                Some(&fingerprint),
240                "typed ORM parameter is invalid",
241            )
242        })?;
243        let materialized = self
244            .with_connection_executor(|executor| {
245                executor.execute_public_read_sql(
246                    &compiled.sql,
247                    params.into(),
248                    context.inner(),
249                    policy,
250                    limits,
251                )
252            })
253            .map_err(|error| public_error(error, Some(&fingerprint)))?;
254
255        let mut rows = materialized.rows;
256        let has_more = rows.len() > request.page_size;
257        if has_more {
258            rows.truncate(request.page_size);
259        }
260        let next_cursor = if has_more {
261            rows.last()
262                .map(|row| cursor_from_row(&fingerprint, &scope_digest, row, &keys))
263                .transpose()?
264        } else {
265            None
266        };
267        Ok(PublicReadPage {
268            columns: materialized.columns,
269            rows,
270            next_cursor,
271            fingerprint,
272            database_id: materialized.database_id,
273            catalog_id: materialized.catalog_id,
274            catalog_generation: materialized.catalog_generation,
275        })
276    }
277}
278
279fn validate_ir_subset(select: &Select, limits: PublicReadLimits) -> PublicReadResult<()> {
280    if !select.ctes.is_empty()
281        || select.recursive
282        || select.distinct
283        || !select.distinct_on.is_empty()
284        || select.group_by.is_some()
285        || select.having.is_some()
286        || !select.windows.is_empty()
287        || !select.set_operations.is_empty()
288        || !select.order_by.is_empty()
289        || select.limit.is_some()
290        || select.offset.is_some()
291    {
292        return Err(unsupported("server owns ordering and pagination"));
293    }
294    if select.projection.is_empty() || select.projection.len() > limits.max_projection {
295        return Err(resource("projection width is outside the admitted limit"));
296    }
297    let mut joins = 0usize;
298    let mut filter_nodes = 0usize;
299    validate_relation(
300        select.from.as_ref(),
301        &mut joins,
302        limits.max_navigation_depth,
303        &mut filter_nodes,
304    )?;
305    if joins > limits.max_joins {
306        return Err(resource("JOIN count exceeds the admitted limit"));
307    }
308    if let Some(filter) = &select.filter {
309        validate_expression(filter, limits.max_navigation_depth, &mut filter_nodes)?;
310    }
311    if filter_nodes > limits.max_filter_nodes {
312        return Err(resource("filter complexity exceeds the admitted limit"));
313    }
314    for projection in &select.projection {
315        let mut ignored = 0;
316        validate_expression(
317            &projection.expression,
318            limits.max_navigation_depth,
319            &mut ignored,
320        )?;
321    }
322    Ok(())
323}
324
325fn validate_relation(
326    relation: Option<&Relation>,
327    joins: &mut usize,
328    max_navigation_depth: usize,
329    predicate_nodes: &mut usize,
330) -> PublicReadResult<()> {
331    let Some(relation) = relation else {
332        return Err(unsupported("a physical relation source is required"));
333    };
334    match relation {
335        Relation::Table { .. } => Ok(()),
336        Relation::Join {
337            left,
338            right,
339            kind,
340            on,
341        } => {
342            if !matches!(kind, JoinKind::Inner | JoinKind::Cross) {
343                return Err(unsupported("only INNER and CROSS JOIN are admitted"));
344            }
345            if matches!(kind, JoinKind::Inner) && on.is_none() {
346                return Err(unsupported("INNER JOIN requires an ON predicate"));
347            }
348            *joins = joins.saturating_add(1);
349            validate_relation(Some(left), joins, max_navigation_depth, predicate_nodes)?;
350            validate_relation(Some(right), joins, max_navigation_depth, predicate_nodes)?;
351            if let Some(on) = on {
352                validate_expression(on, max_navigation_depth, predicate_nodes)?;
353            }
354            Ok(())
355        }
356        Relation::Cte { .. } | Relation::Derived { .. } | Relation::Values { .. } => Err(
357            unsupported("CTE, derived and VALUES sources are not admitted"),
358        ),
359    }
360}
361
362fn validate_expression(
363    expression: &Expression,
364    max_navigation_depth: usize,
365    nodes: &mut usize,
366) -> PublicReadResult<()> {
367    *nodes = nodes.saturating_add(1);
368    match expression {
369        Expression::Column { .. } | Expression::Literal { .. } => Ok(()),
370        Expression::Unary { expression, .. }
371        | Expression::Cast { expression, .. }
372        | Expression::IsNull { expression, .. } => {
373            validate_expression(expression, max_navigation_depth, nodes)
374        }
375        Expression::Binary { left, right, .. } => {
376            validate_expression(left, max_navigation_depth, nodes)?;
377            validate_expression(right, max_navigation_depth, nodes)
378        }
379        Expression::Function { arguments, .. } | Expression::Tuple { values: arguments } => {
380            for argument in arguments {
381                validate_expression(argument, max_navigation_depth, nodes)?;
382            }
383            Ok(())
384        }
385        Expression::Case {
386            operand,
387            branches,
388            otherwise,
389        } => {
390            if let Some(operand) = operand {
391                validate_expression(operand, max_navigation_depth, nodes)?;
392            }
393            for branch in branches {
394                validate_expression(&branch.when, max_navigation_depth, nodes)?;
395                validate_expression(&branch.then, max_navigation_depth, nodes)?;
396            }
397            if let Some(otherwise) = otherwise {
398                validate_expression(otherwise, max_navigation_depth, nodes)?;
399            }
400            Ok(())
401        }
402        Expression::Between {
403            expression,
404            lower,
405            upper,
406            ..
407        } => {
408            validate_expression(expression, max_navigation_depth, nodes)?;
409            validate_expression(lower, max_navigation_depth, nodes)?;
410            validate_expression(upper, max_navigation_depth, nodes)
411        }
412        Expression::InList {
413            expression, values, ..
414        } => {
415            validate_expression(expression, max_navigation_depth, nodes)?;
416            for value in values {
417                validate_expression(value, max_navigation_depth, nodes)?;
418            }
419            Ok(())
420        }
421        Expression::Navigation { path, .. } => {
422            if path.is_empty() || path.len() > max_navigation_depth {
423                Err(resource("navigation depth is outside the admitted limit"))
424            } else {
425                Ok(())
426            }
427        }
428        Expression::Star { .. }
429        | Expression::Aggregate { .. }
430        | Expression::Window { .. }
431        | Expression::InSubquery { .. }
432        | Expression::Exists { .. }
433        | Expression::ScalarSubquery { .. }
434        | Expression::Grouping { .. } => Err(unsupported(
435            "stars, aggregates, windows, grouping and subqueries are not admitted",
436        )),
437    }
438}
439
440fn relation_occurrences<'a>(
441    relation: Option<&Relation>,
442    policy: &'a BoundPublicReadPolicy,
443) -> PublicReadResult<Vec<RelationOccurrence<'a>>> {
444    fn collect<'a>(
445        relation: &Relation,
446        policy: &'a BoundPublicReadPolicy,
447        output: &mut Vec<RelationOccurrence<'a>>,
448    ) -> PublicReadResult<()> {
449        match relation {
450            Relation::Table { name, alias } => {
451                let binding = policy.relation(name).ok_or_else(|| {
452                    PublicReadError::new(
453                        PublicReadErrorCode::Policy,
454                        None,
455                        "relation is not explicitly published",
456                    )
457                })?;
458                let ordinal = u32::try_from(output.len()).map_err(|_| {
459                    resource("relation occurrence count exceeds the supported domain")
460                })?;
461                output.push(RelationOccurrence {
462                    ordinal,
463                    qualifier: alias.as_deref().unwrap_or(name).to_lowercase(),
464                    binding,
465                });
466                Ok(())
467            }
468            Relation::Join { left, right, .. } => {
469                collect(left, policy, output)?;
470                collect(right, policy, output)
471            }
472            _ => Err(unsupported("relation source is outside the public subset")),
473        }
474    }
475
476    let mut output = Vec::new();
477    collect(
478        relation.ok_or_else(|| unsupported("a relation source is required"))?,
479        policy,
480        &mut output,
481    )?;
482    let mut aliases = std::collections::BTreeSet::new();
483    if output
484        .iter()
485        .any(|occurrence| !aliases.insert(occurrence.qualifier.clone()))
486    {
487        return Err(unsupported("relation aliases must be unique"));
488    }
489    Ok(output)
490}
491
492fn bind_key_projections(
493    select: &Select,
494    occurrences: &[RelationOccurrence<'_>],
495) -> PublicReadResult<Vec<KeyProjection>> {
496    let mut output = Vec::new();
497    for occurrence in occurrences {
498        for key in &occurrence.binding.primary_key {
499            let indices = select
500                .projection
501                .iter()
502                .enumerate()
503                .filter_map(|(index, projection)| {
504                    projection_matches_key(projection, occurrence, &key.name).then_some(index)
505                })
506                .collect::<Vec<_>>();
507            if indices.len() != 1 {
508                return Err(unsupported(
509                    "every pagination key must be projected directly exactly once",
510                ));
511            }
512            output.push(KeyProjection {
513                relation_ordinal: occurrence.ordinal,
514                qualifier: occurrence.qualifier.clone(),
515                column_id: key.object_id,
516                column_name: key.name.clone(),
517                output_index: indices[0],
518                data_type: key.data_type,
519            });
520        }
521    }
522    Ok(output)
523}
524
525fn projection_matches_key(
526    projection: &Projection,
527    occurrence: &RelationOccurrence<'_>,
528    key_name: &str,
529) -> bool {
530    let Expression::Column { column } = &projection.expression else {
531        return false;
532    };
533    if !column.name.eq_ignore_ascii_case(key_name) {
534        return false;
535    }
536    match &column.relation {
537        Some(relation) => relation.eq_ignore_ascii_case(&occurrence.qualifier),
538        None => occurrence.ordinal == 0,
539    }
540}
541
542fn public_fingerprint(
543    shape: &str,
544    occurrences: &[RelationOccurrence<'_>],
545    keys: &[KeyProjection],
546) -> String {
547    let mut hash = Sha256::new();
548    hash.update(b"radixdb.public-read\0");
549    hash.update(shape.as_bytes());
550    for occurrence in occurrences {
551        hash.update(occurrence.ordinal.to_le_bytes());
552        hash.update(occurrence.binding.object_id.as_bytes());
553        hash.update(occurrence.binding.definition_revision.to_le_bytes());
554        for column in &occurrence.binding.columns {
555            hash.update(column.object_id.as_bytes());
556        }
557    }
558    for key in keys {
559        hash.update(key.relation_ordinal.to_le_bytes());
560        hash.update(key.column_id.as_bytes());
561    }
562    format!("{:x}", hash.finalize())
563}
564
565fn cursor_scope_digest(fingerprint: &str, parameters: &[TypedValue]) -> String {
566    let mut hash = Sha256::new();
567    hash.update(b"radixdb.public-read.cursor-scope\0");
568    hash.update(fingerprint.as_bytes());
569    hash.update(serde_json::to_vec(parameters).expect("validated ORM parameters are serializable"));
570    format!("{:x}", hash.finalize())
571}
572
573fn validate_cursor(
574    cursor: Option<&PublicReadCursor>,
575    fingerprint: &str,
576    scope_digest: &str,
577    keys: &[KeyProjection],
578) -> PublicReadResult<()> {
579    let Some(cursor) = cursor else {
580        return Ok(());
581    };
582    if cursor.fingerprint != fingerprint
583        || cursor.scope_digest != scope_digest
584        || cursor.keys.len() != keys.len()
585    {
586        return Err(invalid_cursor(fingerprint));
587    }
588    for (cursor, key) in cursor.keys.iter().zip(keys) {
589        if cursor.relation_ordinal != key.relation_ordinal
590            || cursor.column_id != key.column_id.to_string()
591            || !typed_value_matches(&cursor.value, key.data_type)
592            || matches!(cursor.value, TypedValue::Null(_))
593        {
594            return Err(invalid_cursor(fingerprint));
595        }
596    }
597    Ok(())
598}
599
600fn typed_value_matches(value: &TypedValue, data_type: DataType) -> bool {
601    matches!(
602        (value, data_type),
603        (TypedValue::Integer(_), DataType::Integer)
604            | (TypedValue::Float(_), DataType::Float)
605            | (TypedValue::Text(_), DataType::Text)
606            | (TypedValue::Boolean(_), DataType::Boolean)
607            | (TypedValue::Timestamp(_), DataType::Timestamp)
608            | (TypedValue::Date(_), DataType::Date)
609            | (TypedValue::Json(_), DataType::Json)
610            | (TypedValue::Uuid(_), DataType::Uuid)
611            | (TypedValue::Bytes(_), DataType::Bytes)
612            | (TypedValue::Decimal(_), DataType::Decimal)
613            | (TypedValue::Vector(_), DataType::Vector)
614    )
615}
616
617fn install_keyset_boundary(
618    select: &mut Select,
619    cursor: Option<&PublicReadCursor>,
620    keys: &[KeyProjection],
621) {
622    let Some(cursor) = cursor else {
623        return;
624    };
625    let mut disjunction = None;
626    for index in 0..keys.len() {
627        let mut conjunction = None;
628        for (equal_index, equal_key) in keys.iter().enumerate().take(index) {
629            let equality = comparison(
630                equal_key,
631                BinaryOperator::Eq,
632                cursor.keys[equal_index].value.clone(),
633            );
634            conjunction = Some(and(conjunction, equality));
635        }
636        let greater = comparison(
637            &keys[index],
638            BinaryOperator::Gt,
639            cursor.keys[index].value.clone(),
640        );
641        let arm = and(conjunction, greater);
642        disjunction = Some(or(disjunction, arm));
643    }
644    if let Some(boundary) = disjunction {
645        select.filter = Some(match select.filter.take() {
646            Some(existing) => Expression::Binary {
647                left: Box::new(existing),
648                operator: BinaryOperator::And,
649                right: Box::new(boundary),
650            },
651            None => boundary,
652        });
653    }
654}
655
656fn comparison(key: &KeyProjection, operator: BinaryOperator, value: TypedValue) -> Expression {
657    Expression::Binary {
658        left: Box::new(Expression::Column {
659            column: ColumnRef::qualified(&key.qualifier, &key.column_name),
660        }),
661        operator,
662        right: Box::new(Expression::Literal { value }),
663    }
664}
665
666fn and(left: Option<Expression>, right: Expression) -> Expression {
667    left.map_or(right.clone(), |left| Expression::Binary {
668        left: Box::new(left),
669        operator: BinaryOperator::And,
670        right: Box::new(right),
671    })
672}
673
674fn or(left: Option<Expression>, right: Expression) -> Expression {
675    left.map_or(right.clone(), |left| Expression::Binary {
676        left: Box::new(left),
677        operator: BinaryOperator::Or,
678        right: Box::new(right),
679    })
680}
681
682fn cursor_from_row(
683    fingerprint: &str,
684    scope_digest: &str,
685    row: &Row,
686    keys: &[KeyProjection],
687) -> PublicReadResult<PublicReadCursor> {
688    let mut cursor_keys = Vec::with_capacity(keys.len());
689    for key in keys {
690        let value = row
691            .get(key.output_index)
692            .ok_or_else(|| invalid_cursor(fingerprint))?;
693        cursor_keys.push(PublicReadCursorKey {
694            relation_ordinal: key.relation_ordinal,
695            column_id: key.column_id.to_string(),
696            value: core_to_typed(value).ok_or_else(|| invalid_cursor(fingerprint))?,
697        });
698    }
699    Ok(PublicReadCursor {
700        fingerprint: fingerprint.to_owned(),
701        scope_digest: scope_digest.to_owned(),
702        keys: cursor_keys,
703    })
704}
705
706fn core_to_typed(value: &Value) -> Option<TypedValue> {
707    match value {
708        Value::Null(_) => None,
709        Value::Integer(value) => Some(TypedValue::Integer(*value)),
710        Value::Float(value) => Some(TypedValue::Float(FloatValue::from(*value))),
711        Value::Text(value) => Some(TypedValue::Text(value.as_str().to_owned())),
712        Value::Boolean(value) => Some(TypedValue::Boolean(*value)),
713        Value::Timestamp(value) => Some(TypedValue::Timestamp(
714            value.to_rfc3339_opts(SecondsFormat::Nanos, true),
715        )),
716        Value::Extension(_) => match value.data_type() {
717            DataType::Json => serde_json::from_str(value.as_json()?)
718                .ok()
719                .map(TypedValue::Json),
720            DataType::Vector => value.as_vector_f32().map(TypedValue::Vector),
721            DataType::Uuid => value.as_uuid_bytes().map(|bytes| {
722                TypedValue::Uuid(uuid::Uuid::from_bytes(bytes).hyphenated().to_string())
723            }),
724            DataType::Decimal => {
725                value
726                    .as_decimal_parts()
727                    .map(|(coefficient, _precision, scale)| {
728                        TypedValue::Decimal(decimal_string(coefficient, scale))
729                    })
730            }
731            DataType::Date => value.as_date_days().and_then(|days| {
732                let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)?;
733                epoch
734                    .checked_add_signed(chrono::Duration::days(i64::from(days)))
735                    .map(|date| TypedValue::Date(date.format("%Y-%m-%d").to_string()))
736            }),
737            DataType::Bytes => value.as_bytes_value().map(|bytes| {
738                TypedValue::Bytes(base64::engine::general_purpose::STANDARD.encode(bytes))
739            }),
740            _ => None,
741        },
742    }
743}
744
745fn decimal_string(coefficient: i128, scale: u8) -> String {
746    if scale == 0 {
747        return coefficient.to_string();
748    }
749    let negative = coefficient < 0;
750    let digits = coefficient.unsigned_abs().to_string();
751    let scale = usize::from(scale);
752    let padded = if digits.len() <= scale {
753        format!("{}{}", "0".repeat(scale + 1 - digits.len()), digits)
754    } else {
755        digits
756    };
757    let split = padded.len() - scale;
758    format!(
759        "{}{}.{}",
760        if negative { "-" } else { "" },
761        &padded[..split],
762        &padded[split..]
763    )
764}
765
766fn public_error(error: Error, fingerprint: Option<&str>) -> PublicReadError {
767    let rendered = error.to_string().to_lowercase();
768    let code = if rendered.contains("authorization") || rendered.contains("permission") {
769        PublicReadErrorCode::Authorization
770    } else if rendered.contains("budget") || rendered.contains("limit") {
771        PublicReadErrorCode::ResourceLimit
772    } else if rendered.contains("public read") || rendered.contains("catalog") {
773        PublicReadErrorCode::Policy
774    } else {
775        PublicReadErrorCode::Execution
776    };
777    let detail = match code {
778        PublicReadErrorCode::Authorization => "authorization denied",
779        PublicReadErrorCode::ResourceLimit => "resource limit exceeded",
780        PublicReadErrorCode::Policy => "public policy or catalog binding rejected the request",
781        _ => "query execution failed",
782    };
783    PublicReadError::new(code, fingerprint, detail)
784}
785
786fn unsupported(detail: &'static str) -> PublicReadError {
787    PublicReadError::new(PublicReadErrorCode::UnsupportedShape, None, detail)
788}
789
790fn resource(detail: &'static str) -> PublicReadError {
791    PublicReadError::new(PublicReadErrorCode::ResourceLimit, None, detail)
792}
793
794fn invalid_cursor(fingerprint: &str) -> PublicReadError {
795    PublicReadError::new(
796        PublicReadErrorCode::InvalidCursor,
797        Some(fingerprint),
798        "cursor does not match the admitted query and catalog identities",
799    )
800}
801
802impl ServerExecutionContext {
803    /// Construct a request context for an authenticated stable Principal.
804    pub fn for_principal(principal_id: ObjectId) -> Self {
805        Self {
806            inner: radixdb_executor::ExecutionContext::new().with_principal_id(principal_id),
807        }
808    }
809}
810
811#[cfg(test)]
812#[path = "public_read_tests.rs"]
813mod tests;