use serde::Serialize;
use super::operations::{
LogicalAggregate, LogicalDelete, LogicalRead, LogicalResourceOp, LogicalSearch, LogicalUpdate,
LogicalWrite,
};
use super::value::LogicalValue;
use crate::backend::BackendKind;
use crate::generation::CatalogManifest;
pub mod postgres;
#[cfg(any(feature = "mysql", test))]
pub mod mysql;
#[cfg(any(feature = "sqlite", test))]
pub mod sqlite;
#[cfg(any(feature = "mongodb", test))]
pub mod mongodb;
#[cfg(any(feature = "neo4j", test))]
pub mod neo4j;
#[cfg(any(feature = "clickhouse", test))]
pub mod clickhouse;
#[cfg(any(feature = "qdrant", test))]
pub mod qdrant;
#[cfg(any(feature = "elasticsearch", test))]
pub mod elasticsearch;
#[cfg(any(feature = "memcached", test))]
pub mod memcached;
#[cfg(any(feature = "mssql", test))]
pub mod mssql;
#[cfg(any(feature = "weaviate", test))]
pub mod weaviate;
#[cfg(any(feature = "pinecone", test))]
pub mod pinecone;
#[cfg(any(feature = "cassandra", test))]
pub mod cassandra;
#[cfg(any(feature = "azureblob", test))]
pub mod azureblob;
#[cfg(any(feature = "gcs", test))]
pub mod gcs;
#[cfg(any(feature = "redis", test))]
pub mod redis;
#[cfg(any(feature = "s3", test))]
pub mod s3;
mod sql_dialect;
mod util;
#[cfg(all(test, not(udb_portable)))]
mod live_tests;
pub trait Compiler: Send + Sync {
fn kind(&self) -> BackendKind;
fn supports_read_include(&self) -> bool {
false
}
fn compile_read(
&self,
_op: &LogicalRead,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(self.kind(), "read"))
}
fn compile_write(
&self,
_op: &LogicalWrite,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(self.kind(), "write"))
}
fn compile_update(
&self,
_op: &LogicalUpdate,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(self.kind(), "update"))
}
fn compile_delete(
&self,
_op: &LogicalDelete,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(self.kind(), "delete"))
}
fn compile_search(
&self,
_op: &LogicalSearch,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(self.kind(), "search"))
}
fn compile_resource_op(
&self,
_op: &LogicalResourceOp,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(
self.kind(),
"resource_op",
))
}
fn compile_aggregate(
&self,
_op: &LogicalAggregate,
_ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
Err(CompileError::operation_unsupported(
self.kind(),
"aggregate",
))
}
}
pub enum CompileOperation<'a> {
Read(&'a LogicalRead),
Write(&'a LogicalWrite),
Update(&'a LogicalUpdate),
Delete(&'a LogicalDelete),
Search(&'a LogicalSearch),
ResourceOp(&'a LogicalResourceOp),
Aggregate(&'a LogicalAggregate),
}
impl<'a> CompileOperation<'a> {
pub fn token(&self) -> &'static str {
match self {
Self::Read(_) => "read",
Self::Write(_) => "write",
Self::Update(_) => "update",
Self::Delete(_) => "delete",
Self::Search(_) => "search",
Self::ResourceOp(_) => "resource_op",
Self::Aggregate(_) => "aggregate",
}
}
}
fn compile_with<C: Compiler>(
compiler: C,
op: CompileOperation<'_>,
ctx: &CompileContext<'_>,
) -> Result<CompiledRendering, CompileError> {
if let CompileOperation::Read(read) = op
&& !read.include.is_empty()
&& !compiler.supports_read_include()
{
return Err(CompileError::OperatorUnsupported {
backend: compiler.kind(),
op: "include",
});
}
match op {
CompileOperation::Read(op) => compiler.compile_read(op, ctx),
CompileOperation::Write(op) => compiler.compile_write(op, ctx),
CompileOperation::Update(op) => compiler.compile_update(op, ctx),
CompileOperation::Delete(op) => compiler.compile_delete(op, ctx),
CompileOperation::Search(op) => compiler.compile_search(op, ctx),
CompileOperation::ResourceOp(op) => compiler.compile_resource_op(op, ctx),
CompileOperation::Aggregate(op) => compiler.compile_aggregate(op, ctx),
}
}
pub fn compile_for_backend(
kind: &BackendKind,
op: CompileOperation<'_>,
ctx: &CompileContext<'_>,
) -> Option<Result<CompiledRendering, CompileError>> {
match kind {
BackendKind::Postgres => Some(compile_with(postgres::PostgresCompiler, op, ctx)),
BackendKind::Mysql => {
#[cfg(any(feature = "mysql", test))]
{
Some(compile_with(mysql::MysqlCompiler, op, ctx))
}
#[cfg(not(any(feature = "mysql", test)))]
{
None
}
}
BackendKind::Sqlite => {
#[cfg(any(feature = "sqlite", test))]
{
Some(compile_with(sqlite::SqliteCompiler, op, ctx))
}
#[cfg(not(any(feature = "sqlite", test)))]
{
None
}
}
BackendKind::Mssql => {
#[cfg(any(feature = "mssql", test))]
{
Some(compile_with(mssql::MssqlCompiler, op, ctx))
}
#[cfg(not(any(feature = "mssql", test)))]
{
None
}
}
BackendKind::Clickhouse => {
#[cfg(any(feature = "clickhouse", test))]
{
Some(compile_with(clickhouse::ClickHouseCompiler, op, ctx))
}
#[cfg(not(any(feature = "clickhouse", test)))]
{
None
}
}
BackendKind::Mongodb => {
#[cfg(any(feature = "mongodb", test))]
{
Some(compile_with(mongodb::MongoDbCompiler, op, ctx))
}
#[cfg(not(any(feature = "mongodb", test)))]
{
None
}
}
BackendKind::Neo4j => {
#[cfg(any(feature = "neo4j", test))]
{
Some(compile_with(neo4j::Neo4jCompiler, op, ctx))
}
#[cfg(not(any(feature = "neo4j", test)))]
{
None
}
}
BackendKind::Qdrant => {
#[cfg(any(feature = "qdrant", test))]
{
Some(compile_with(qdrant::QdrantCompiler, op, ctx))
}
#[cfg(not(any(feature = "qdrant", test)))]
{
None
}
}
BackendKind::Elasticsearch => {
#[cfg(any(feature = "elasticsearch", test))]
{
Some(compile_with(elasticsearch::ElasticsearchCompiler, op, ctx))
}
#[cfg(not(any(feature = "elasticsearch", test)))]
{
None
}
}
BackendKind::Memcached => {
#[cfg(any(feature = "memcached", test))]
{
Some(compile_with(memcached::MemcachedCompiler, op, ctx))
}
#[cfg(not(any(feature = "memcached", test)))]
{
None
}
}
BackendKind::Weaviate => {
#[cfg(any(feature = "weaviate", test))]
{
Some(compile_with(weaviate::WeaviateCompiler, op, ctx))
}
#[cfg(not(any(feature = "weaviate", test)))]
{
None
}
}
BackendKind::Pinecone => {
#[cfg(any(feature = "pinecone", test))]
{
Some(compile_with(pinecone::PineconeCompiler, op, ctx))
}
#[cfg(not(any(feature = "pinecone", test)))]
{
None
}
}
BackendKind::Cassandra => {
#[cfg(any(feature = "cassandra", test))]
{
Some(compile_with(cassandra::CassandraCompiler, op, ctx))
}
#[cfg(not(any(feature = "cassandra", test)))]
{
None
}
}
BackendKind::AzureBlob => {
#[cfg(any(feature = "azureblob", test))]
{
Some(compile_with(azureblob::AzureBlobCompiler, op, ctx))
}
#[cfg(not(any(feature = "azureblob", test)))]
{
None
}
}
BackendKind::Gcs => {
#[cfg(any(feature = "gcs", test))]
{
Some(compile_with(gcs::GcsCompiler, op, ctx))
}
#[cfg(not(any(feature = "gcs", test)))]
{
None
}
}
BackendKind::Redis => {
#[cfg(any(feature = "redis", test))]
{
Some(compile_with(redis::RedisCompiler, op, ctx))
}
#[cfg(not(any(feature = "redis", test)))]
{
None
}
}
BackendKind::S3 | BackendKind::Minio => {
#[cfg(any(feature = "s3", test))]
{
Some(compile_with(s3::S3Compiler, op, ctx))
}
#[cfg(not(any(feature = "s3", test)))]
{
None
}
}
}
}
pub fn mediated_backends() -> Vec<BackendKind> {
let mut out = Vec::new();
out.push(BackendKind::Postgres);
#[cfg(any(feature = "mysql", test))]
out.push(BackendKind::Mysql);
#[cfg(any(feature = "sqlite", test))]
out.push(BackendKind::Sqlite);
#[cfg(any(feature = "mssql", test))]
out.push(BackendKind::Mssql);
#[cfg(any(feature = "clickhouse", test))]
out.push(BackendKind::Clickhouse);
#[cfg(any(feature = "mongodb", test))]
out.push(BackendKind::Mongodb);
#[cfg(any(feature = "neo4j", test))]
out.push(BackendKind::Neo4j);
#[cfg(any(feature = "qdrant", test))]
out.push(BackendKind::Qdrant);
#[cfg(any(feature = "elasticsearch", test))]
out.push(BackendKind::Elasticsearch);
#[cfg(any(feature = "memcached", test))]
out.push(BackendKind::Memcached);
#[cfg(any(feature = "weaviate", test))]
out.push(BackendKind::Weaviate);
#[cfg(any(feature = "pinecone", test))]
out.push(BackendKind::Pinecone);
#[cfg(any(feature = "cassandra", test))]
out.push(BackendKind::Cassandra);
#[cfg(any(feature = "azureblob", test))]
out.push(BackendKind::AzureBlob);
#[cfg(any(feature = "gcs", test))]
out.push(BackendKind::Gcs);
#[cfg(any(feature = "redis", test))]
out.push(BackendKind::Redis);
#[cfg(any(feature = "s3", test))]
{
out.push(BackendKind::S3);
out.push(BackendKind::Minio);
}
out
}
pub fn is_mediated_backend(kind: &BackendKind) -> bool {
mediated_backends().contains(kind)
}
#[derive(Debug, Clone, Copy)]
pub struct CompileContext<'a> {
pub manifest: &'a CatalogManifest,
pub tenant_id: Option<&'a str>,
pub project_id: Option<&'a str>,
pub backend_instance: Option<&'a str>,
}
impl<'a> CompileContext<'a> {
pub fn new(manifest: &'a CatalogManifest) -> Self {
Self {
manifest,
tenant_id: None,
project_id: None,
backend_instance: None,
}
}
pub fn with_tenant(mut self, tenant_id: &'a str) -> Self {
self.tenant_id = Some(tenant_id);
self
}
pub fn with_project(mut self, project_id: &'a str) -> Self {
self.project_id = Some(project_id);
self
}
pub fn with_instance(mut self, backend_instance: &'a str) -> Self {
self.backend_instance = Some(backend_instance);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum CompiledRendering {
Sql {
backend: BackendKind,
statement: String,
params: Vec<LogicalValue>,
},
Json {
backend: BackendKind,
method: HttpMethod,
path: String,
body: serde_json::Value,
},
KeyValue {
backend: BackendKind,
op: KeyValueOp,
key_template: String,
value: Option<Vec<u8>>,
ttl_seconds: Option<u64>,
},
Object {
backend: BackendKind,
op: ObjectOp,
bucket: String,
key: String,
content_type: Option<String>,
},
}
impl CompiledRendering {
pub fn backend(&self) -> BackendKind {
match self {
Self::Sql { backend, .. }
| Self::Json { backend, .. }
| Self::KeyValue { backend, .. }
| Self::Object { backend, .. } => backend.clone(),
}
}
pub fn shape_token(&self) -> &'static str {
match self {
Self::Sql { .. } => "sql",
Self::Json { .. } => "json",
Self::KeyValue { .. } => "kv",
Self::Object { .. } => "object",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyValueOp {
Get,
Set,
Delete,
Exists,
Scan,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ObjectOp {
GetObject,
PutObject,
HeadObject,
DeleteObject,
ListObjects,
GeneratePresigned,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CompileError {
UnknownMessageType { message_type: String },
UnknownField { message_type: String, field: String },
OperationNotSupported {
backend: BackendKind,
op: &'static str,
},
OperatorUnsupported {
backend: BackendKind,
op: &'static str,
},
Malformed { reason: String },
ValueOutOfRange {
backend: BackendKind,
field: String,
reason: String,
},
BackendSpecific {
backend: BackendKind,
message: String,
},
}
impl CompileError {
pub(crate) fn operation_unsupported(backend: BackendKind, op: &'static str) -> Self {
Self::OperationNotSupported { backend, op }
}
pub fn code(&self) -> &'static str {
match self {
Self::UnknownMessageType { .. } => "unknown_message_type",
Self::UnknownField { .. } => "unknown_field",
Self::OperationNotSupported { .. } => "operation_not_supported",
Self::OperatorUnsupported { .. } => "operator_unsupported",
Self::Malformed { .. } => "malformed",
Self::ValueOutOfRange { .. } => "value_out_of_range",
Self::BackendSpecific { .. } => "backend_specific",
}
}
}
impl std::fmt::Display for CompileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownMessageType { message_type } => {
write!(f, "unknown message type: {message_type}")
}
Self::UnknownField {
message_type,
field,
} => write!(f, "unknown field '{field}' on message '{message_type}'"),
Self::OperationNotSupported { backend, op } => write!(
f,
"{backend_name} does not support the {op} operation",
backend_name = backend.as_str()
),
Self::OperatorUnsupported { backend, op } => write!(
f,
"{backend_name} does not support the {op} operator",
backend_name = backend.as_str()
),
Self::Malformed { reason } => write!(f, "malformed operation: {reason}"),
Self::ValueOutOfRange {
backend,
field,
reason,
} => write!(
f,
"{backend_name} value out of range for field '{field}': {reason}",
backend_name = backend.as_str()
),
Self::BackendSpecific { backend, message } => {
write!(f, "{}: {message}", backend.as_str())
}
}
}
}
impl std::error::Error for CompileError {}
#[cfg(test)]
mod tests {
use super::*;
struct NoopCompiler;
impl Compiler for NoopCompiler {
fn kind(&self) -> BackendKind {
BackendKind::Postgres
}
}
fn manifest() -> CatalogManifest {
CatalogManifest::default()
}
#[test]
fn default_impls_reject_every_op() {
let c = NoopCompiler;
let m = manifest();
let ctx = CompileContext::new(&m);
let read = LogicalRead::message("X");
let err = c.compile_read(&read, &ctx).unwrap_err();
assert!(matches!(
err,
CompileError::OperationNotSupported { op: "read", .. }
));
assert_eq!(err.code(), "operation_not_supported");
}
#[test]
fn error_codes_are_pinned() {
assert_eq!(
CompileError::operation_unsupported(BackendKind::Redis, "search").code(),
"operation_not_supported"
);
assert_eq!(
CompileError::Malformed { reason: "x".into() }.code(),
"malformed"
);
}
#[test]
fn read_include_fails_closed_for_backend_without_lowering() {
let m = manifest();
let ctx = CompileContext::new(&m);
let read = LogicalRead::message("X").with_include("tenant");
let err = compile_with(NoopCompiler, CompileOperation::Read(&read), &ctx)
.expect_err("include must not be silently ignored");
assert!(matches!(
err,
CompileError::OperatorUnsupported { op: "include", .. }
));
assert_eq!(err.code(), "operator_unsupported");
}
#[test]
fn every_mediated_backend_has_a_real_compile_arm() {
let m = manifest();
let ctx = CompileContext::new(&m);
let mediated = mediated_backends();
assert!(
!mediated.is_empty(),
"at least Postgres is always compiled in"
);
assert!(
mediated.contains(&BackendKind::Postgres),
"Postgres has no feature gate and must always be mediated"
);
for kind in mediated {
assert!(
is_mediated_backend(&kind),
"{kind:?} in mediated_backends() but is_mediated_backend() said no"
);
let read = LogicalRead::message("X");
let result = compile_for_backend(&kind, CompileOperation::Read(&read), &ctx);
let outcome = result
.unwrap_or_else(|| panic!("{kind:?} is mediated but compile arm returned None"));
if let Err(CompileError::OperationNotSupported { op, .. }) = &outcome {
panic!("{kind:?} is mediated but rejected a trivial read as unsupported (op={op})");
}
}
}
#[test]
fn compiled_rendering_carries_backend_through() {
let r = CompiledRendering::Sql {
backend: BackendKind::Postgres,
statement: "SELECT 1".into(),
params: vec![],
};
assert_eq!(r.backend(), BackendKind::Postgres);
assert_eq!(r.shape_token(), "sql");
}
}