1use 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 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 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 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 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;