use crate::error::LaserError;
use crate::fork::{ForkError, ForkInfo, ForkKind};
use crate::laser::Laser;
use crate::query::{
AGDX_FORK_CREATE_CODE, AGDX_FORK_DELETE_CODE, AGDX_FORK_LIST_CODE, AGDX_FORK_PROMOTE_CODE,
AGDX_FORK_PUT_CODE, FORK_OP_VERSION,
};
use laser_wire::fork::{
ForkCreate, ForkDelete, ForkList, ForkOutcome, ForkPromote, ForkPut, ForkReply,
};
use laser_wire::framing::encode_named;
#[cfg(test)]
use laser_wire::limits::MAX_FORK_ID_BYTES;
use serde::Serialize;
use std::collections::BTreeMap;
impl Laser {
pub fn fork<'a>(&'a self, fork_id: impl Into<String>) -> ForkHandle<'a> {
ForkHandle {
laser: self,
fork_id: fork_id.into(),
}
}
pub async fn forks(&self) -> Result<Vec<ForkInfo>, LaserError> {
match self
.execute_fork(AGDX_FORK_LIST_CODE, &ForkList { v: FORK_OP_VERSION })
.await?
{
ForkOutcome::List(forks) => Ok(forks),
other => Err(unexpected("list", &other)),
}
}
pub(crate) async fn execute_fork(
&self,
code: u32,
request: &impl Serialize,
) -> Result<ForkOutcome, LaserError> {
let capabilities = self.capabilities().await;
if !capabilities.forks {
return Err(LaserError::unsupported(
"fork",
"forks are not served by this deployment",
));
}
if let Some(versions) = capabilities.versions
&& versions.fork != FORK_OP_VERSION
{
return Err(ForkError::Version {
expected: versions.fork,
got: FORK_OP_VERSION,
}
.into());
}
let payload = encode_named(request)
.map_err(|error| LaserError::Codec(format!("encode fork request: {error}")))?;
let payload = self.send_raw_with_response(code, payload).await?;
match crate::error::decode_managed_reply::<ForkReply>(&payload)? {
ForkReply::Ok(outcome) => Ok(outcome),
ForkReply::Err(error) => Err(error.into()),
_ => Err(LaserError::Protocol(
"fork: unknown reply variant".to_owned(),
)),
}
}
}
pub struct ForkHandle<'a> {
laser: &'a Laser,
fork_id: String,
}
impl<'a> ForkHandle<'a> {
pub fn id(&self) -> &str {
&self.fork_id
}
pub fn create(&self) -> ForkCreateRequest<'a> {
ForkCreateRequest {
laser: self.laser,
fork_id: self.fork_id.clone(),
parent: None,
kind: ForkKind::Continuous,
tables: Vec::new(),
}
}
pub async fn promote(&self) -> Result<usize, LaserError> {
match self
.laser
.execute_fork(
AGDX_FORK_PROMOTE_CODE,
&ForkPromote {
v: FORK_OP_VERSION,
fork_id: self.fork_id.clone(),
},
)
.await?
{
ForkOutcome::Promoted { rows } => Ok(rows),
other => Err(unexpected("promote", &other)),
}
}
pub async fn squash(&self) -> Result<bool, LaserError> {
match self
.laser
.execute_fork(
AGDX_FORK_DELETE_CODE,
&ForkDelete {
v: FORK_OP_VERSION,
fork_id: self.fork_id.clone(),
},
)
.await?
{
ForkOutcome::Deleted(existed) => Ok(existed),
other => Err(unexpected("squash", &other)),
}
}
pub fn put_row(
&self,
table: impl Into<String>,
partition_id: u32,
offset: u64,
) -> ForkPutRequest<'a> {
ForkPutRequest {
laser: self.laser,
fork_id: self.fork_id.clone(),
table: table.into(),
partition_id,
offset,
projection_id: String::new(),
projection_version: 0,
fields: BTreeMap::new(),
metadata: BTreeMap::new(),
payload: None,
embedding: None,
tombstone: false,
}
}
}
#[must_use = "call .send().await to create the fork"]
pub struct ForkCreateRequest<'a> {
laser: &'a Laser,
fork_id: String,
parent: Option<String>,
kind: ForkKind,
tables: Vec<String>,
}
impl<'a> ForkCreateRequest<'a> {
pub fn severed(mut self) -> Self {
self.kind = ForkKind::Severed;
self
}
pub fn continuous(mut self) -> Self {
self.kind = ForkKind::Continuous;
self
}
pub fn parent(mut self, parent: impl Into<String>) -> Self {
self.parent = Some(parent.into());
self
}
pub fn tables(mut self, tables: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.tables = tables.into_iter().map(Into::into).collect();
self
}
pub async fn send(self) -> Result<ForkInfo, LaserError> {
validated_fork_id(&self.fork_id)?;
let request = ForkCreate {
v: FORK_OP_VERSION,
fork_id: self.fork_id,
parent: self.parent,
kind: self.kind,
tables: self.tables,
};
match self
.laser
.execute_fork(AGDX_FORK_CREATE_CODE, &request)
.await?
{
ForkOutcome::Created(info) => Ok(info),
other => Err(unexpected("create", &other)),
}
}
}
#[must_use = "call .send().await to write the speculative row"]
pub struct ForkPutRequest<'a> {
laser: &'a Laser,
fork_id: String,
table: String,
partition_id: u32,
offset: u64,
projection_id: String,
projection_version: u32,
fields: BTreeMap<String, String>,
metadata: BTreeMap<String, String>,
payload: Option<Vec<u8>>,
embedding: Option<Vec<f32>>,
tombstone: bool,
}
impl<'a> ForkPutRequest<'a> {
pub fn projection(mut self, id: impl Into<String>, version: u32) -> Self {
self.projection_id = id.into();
self.projection_version = version;
self
}
pub fn field(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.fields.insert(name.into(), value.into());
self
}
pub fn metadata(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.metadata.insert(name.into(), value.into());
self
}
pub fn payload(mut self, payload: impl AsRef<[u8]>) -> Self {
self.payload = Some(payload.as_ref().to_vec());
self
}
pub fn embedding(mut self, embedding: impl IntoIterator<Item = f32>) -> Self {
self.embedding = Some(embedding.into_iter().collect());
self
}
pub fn tombstone(mut self) -> Self {
self.tombstone = true;
self
}
pub async fn send(self) -> Result<(), LaserError> {
let embedding = self
.embedding
.as_deref()
.map(embedding_literal)
.transpose()?;
let request = ForkPut {
v: FORK_OP_VERSION,
fork_id: self.fork_id,
table: self.table,
partition_id: self.partition_id,
offset: self.offset,
projection_id: self.projection_id,
projection_version: self.projection_version,
fields: self.fields,
metadata: self.metadata,
payload: self.payload,
embedding,
tombstone: self.tombstone,
};
match self
.laser
.execute_fork(AGDX_FORK_PUT_CODE, &request)
.await?
{
ForkOutcome::Written => Ok(()),
other => Err(unexpected("put_row", &other)),
}
}
}
fn unexpected(op: &str, outcome: &ForkOutcome) -> LaserError {
LaserError::Protocol(format!("fork {op}: unexpected outcome {outcome:?}"))
}
fn validated_fork_id(fork_id: &str) -> Result<(), LaserError> {
laser_wire::fork::validate_fork_id(fork_id).map_err(|error| LaserError::Invalid(error.0))
}
fn embedding_literal(embedding: &[f32]) -> Result<String, LaserError> {
if embedding.iter().any(|component| !component.is_finite()) {
return Err(LaserError::Invalid(
"an embedding component must be a finite number".to_owned(),
));
}
serde_json::to_string(embedding).map_err(|error| LaserError::Codec(error.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn given_an_over_long_or_empty_fork_id_when_validated_then_should_reject() {
assert!(validated_fork_id("").is_err());
assert!(validated_fork_id("experiment-2026-q2").is_ok());
let too_long = "f".repeat(MAX_FORK_ID_BYTES + 1);
assert!(validated_fork_id(&too_long).is_err());
let at_cap = "f".repeat(MAX_FORK_ID_BYTES);
assert!(validated_fork_id(&at_cap).is_ok());
}
#[test]
fn given_fork_error_when_mapped_then_should_nest_the_typed_error() {
let error: LaserError = ForkError::NotFound("f9".to_owned()).into();
assert!(matches!(error, LaserError::Fork(ForkError::NotFound(_))));
assert!(error.is_not_found());
let unsupported: LaserError = ForkError::Unsupported("x".to_owned()).into();
assert!(matches!(
unsupported,
LaserError::Fork(ForkError::Unsupported(_))
));
assert!(unsupported.is_unsupported());
}
#[test]
fn given_an_embedding_when_encoded_then_should_be_a_json_array_and_reject_non_finite() {
assert_eq!(
embedding_literal(&[0.5, 1.0]).expect("finite components encode"),
"[0.5,1.0]"
);
assert!(matches!(
embedding_literal(&[f32::NAN]),
Err(LaserError::Invalid(_))
));
}
}