Skip to main content

helix_db/
query_generator.rs

1use serde::{Deserialize, Serialize};
2use std::collections::BTreeMap;
3use std::path::{Path, PathBuf};
4
5/// Last query-bundle wire-format version before row-binding steps were added.
6pub const LEGACY_QUERY_BUNDLE_VERSION_V4: u32 = 4;
7
8/// Current wire-format version for generated query bundles.
9pub const QUERY_BUNDLE_VERSION: u32 = 5;
10
11/// Query-bundle versions this SDK can deserialize.
12pub const SUPPORTED_QUERY_BUNDLE_VERSIONS: &[u32] =
13    &[LEGACY_QUERY_BUNDLE_VERSION_V4, QUERY_BUNDLE_VERSION];
14
15/// Declared shape of a registered query parameter.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub enum QueryParamType {
18    /// Boolean parameter.
19    Bool,
20    /// 64-bit signed integer parameter.
21    I64,
22    /// 64-bit floating point parameter.
23    F64,
24    /// 32-bit floating point parameter.
25    F32,
26    /// UTF-8 string parameter.
27    String,
28    /// RFC3339 datetime parameter normalized to UTC.
29    DateTime,
30    /// Raw bytes parameter.
31    Bytes,
32    /// Any nested `PropertyValue` payload.
33    Value,
34    /// Object/map payload.
35    Object,
36    /// Array payload whose elements have the given shape.
37    Array(Box<QueryParamType>),
38}
39
40/// Declared parameter for a registered query.
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct QueryParameter {
43    /// Parameter name.
44    pub name: String,
45    /// Parameter shape.
46    pub ty: QueryParamType,
47}
48
49/// Versioned payload written to `queries.json`.
50#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51pub struct QueryBundle {
52    /// Wire-format version.
53    pub version: u32,
54    /// Read-only query routes by route name.
55    pub read_routes: BTreeMap<String, crate::ReadBatch>,
56    /// Write-capable query routes by route name.
57    pub write_routes: BTreeMap<String, crate::WriteBatch>,
58    /// Registered read-route parameter metadata.
59    pub read_parameters: BTreeMap<String, Vec<QueryParameter>>,
60    /// Registered write-route parameter metadata.
61    pub write_parameters: BTreeMap<String, Vec<QueryParameter>>,
62}
63
64impl Default for QueryBundle {
65    fn default() -> Self {
66        Self {
67            version: QUERY_BUNDLE_VERSION,
68            read_routes: BTreeMap::new(),
69            write_routes: BTreeMap::new(),
70            read_parameters: BTreeMap::new(),
71            write_parameters: BTreeMap::new(),
72        }
73    }
74}
75
76/// Registered read-query function.
77pub struct RegisteredReadQuery {
78    /// Route name.
79    pub name: &'static str,
80    /// Function that constructs the route AST.
81    pub build: fn() -> crate::ReadBatch,
82    /// Function that constructs declared parameter metadata.
83    pub parameters: fn() -> Vec<QueryParameter>,
84}
85
86/// Registered write-query function.
87pub struct RegisteredWriteQuery {
88    /// Route name.
89    pub name: &'static str,
90    /// Function that constructs the route AST.
91    pub build: fn() -> crate::WriteBatch,
92    /// Function that constructs declared parameter metadata.
93    pub parameters: fn() -> Vec<QueryParameter>,
94}
95
96inventory::collect!(RegisteredReadQuery);
97inventory::collect!(RegisteredWriteQuery);
98
99/// Errors returned while generating or loading query bundles.
100#[derive(Debug)]
101pub enum GenerateError {
102    /// More than one query registered the same route name.
103    DuplicateQueryName(String),
104    /// Failed to read or write bundle file.
105    Io(std::io::Error),
106    /// Failed to serialize or deserialize the bundle.
107    Json(sonic_rs::Error),
108    /// Bundle version is unsupported.
109    UnsupportedVersion {
110        /// Version read from payload.
111        found: u32,
112        /// Version required by this crate.
113        expected: u32,
114    },
115}
116
117impl std::fmt::Display for GenerateError {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        match self {
120            Self::DuplicateQueryName(name) => {
121                write!(f, "duplicate generated query name: {name}")
122            }
123            Self::Io(err) => write!(f, "io error: {err}"),
124            Self::Json(err) => write!(f, "json error: {err}"),
125            Self::UnsupportedVersion { found, expected } => {
126                write!(
127                    f,
128                    "unsupported query bundle version {found} (expected {expected})"
129                )
130            }
131        }
132    }
133}
134
135impl std::error::Error for GenerateError {}
136
137impl From<std::io::Error> for GenerateError {
138    fn from(value: std::io::Error) -> Self {
139        Self::Io(value)
140    }
141}
142
143impl From<sonic_rs::Error> for GenerateError {
144    fn from(value: sonic_rs::Error) -> Self {
145        Self::Json(value)
146    }
147}
148
149/// Build the in-memory query bundle from all `#[register]` registrations.
150pub fn build_query_bundle() -> Result<QueryBundle, GenerateError> {
151    let mut bundle = QueryBundle::default();
152
153    for registered in inventory::iter::<RegisteredReadQuery> {
154        if bundle.read_routes.contains_key(registered.name)
155            || bundle.write_routes.contains_key(registered.name)
156        {
157            return Err(GenerateError::DuplicateQueryName(
158                registered.name.to_string(),
159            ));
160        }
161
162        bundle
163            .read_routes
164            .insert(registered.name.to_string(), (registered.build)());
165        bundle
166            .read_parameters
167            .insert(registered.name.to_string(), (registered.parameters)());
168    }
169
170    for registered in inventory::iter::<RegisteredWriteQuery> {
171        if bundle.read_routes.contains_key(registered.name)
172            || bundle.write_routes.contains_key(registered.name)
173        {
174            return Err(GenerateError::DuplicateQueryName(
175                registered.name.to_string(),
176            ));
177        }
178
179        bundle
180            .write_routes
181            .insert(registered.name.to_string(), (registered.build)());
182        bundle
183            .write_parameters
184            .insert(registered.name.to_string(), (registered.parameters)());
185    }
186
187    Ok(bundle)
188}
189
190/// Serialize a query bundle to JSON bytes.
191pub fn serialize_query_bundle(bundle: &QueryBundle) -> Result<Vec<u8>, GenerateError> {
192    Ok(sonic_rs::to_vec_pretty(bundle)?)
193}
194
195/// Deserialize a query bundle from JSON bytes.
196pub fn deserialize_query_bundle(bytes: &[u8]) -> Result<QueryBundle, GenerateError> {
197    let bundle: QueryBundle = sonic_rs::from_slice(bytes)?;
198
199    if !SUPPORTED_QUERY_BUNDLE_VERSIONS.contains(&bundle.version) {
200        return Err(GenerateError::UnsupportedVersion {
201            found: bundle.version,
202            expected: QUERY_BUNDLE_VERSION,
203        });
204    }
205
206    Ok(bundle)
207}
208
209/// Write a query bundle to a file.
210pub fn write_query_bundle_to_path<P: AsRef<Path>>(
211    bundle: &QueryBundle,
212    path: P,
213) -> Result<(), GenerateError> {
214    let bytes = serialize_query_bundle(bundle)?;
215    std::fs::write(path, bytes)?;
216    Ok(())
217}
218
219/// Read a query bundle from a file.
220pub fn read_query_bundle_from_path<P: AsRef<Path>>(path: P) -> Result<QueryBundle, GenerateError> {
221    let bytes = std::fs::read(path)?;
222    deserialize_query_bundle(&bytes)
223}
224
225/// Generate `queries.json` in the current working directory.
226pub fn generate() -> Result<PathBuf, GenerateError> {
227    generate_to_path("queries.json")
228}
229
230/// Generate a query bundle and write it to the requested output path.
231pub fn generate_to_path<P: AsRef<Path>>(path: P) -> Result<PathBuf, GenerateError> {
232    let path = path.as_ref();
233    let bundle = build_query_bundle()?;
234    write_query_bundle_to_path(&bundle, path)?;
235    Ok(path.to_path_buf())
236}