1use std::collections::BTreeSet;
4
5use arrow::datatypes::DataType;
6use serde_json::{Value, json};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum RelationIdType {
14 Int32,
16 Int64,
18 Utf8,
20 LargeUtf8,
22 Utf8View,
24}
25
26impl RelationIdType {
27 pub fn from_data_type(data_type: &DataType) -> Option<Self> {
29 match data_type {
30 DataType::Int32 => Some(Self::Int32),
31 DataType::Int64 => Some(Self::Int64),
32 DataType::Utf8 => Some(Self::Utf8),
33 DataType::LargeUtf8 => Some(Self::LargeUtf8),
34 DataType::Utf8View => Some(Self::Utf8View),
35 _ => None,
36 }
37 }
38}
39
40pub const RELATION_ID_TYPES: &[RelationIdType] = &[
42 RelationIdType::Int32,
43 RelationIdType::Int64,
44 RelationIdType::Utf8,
45 RelationIdType::LargeUtf8,
46 RelationIdType::Utf8View,
47];
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub enum VectorElementType {
52 Float32,
54 Float64,
56 Int8,
58 UInt8,
60}
61
62impl VectorElementType {
63 pub fn from_data_type(data_type: &DataType) -> Option<Self> {
65 match data_type {
66 DataType::Float32 => Some(Self::Float32),
67 DataType::Float64 => Some(Self::Float64),
68 DataType::Int8 => Some(Self::Int8),
69 DataType::UInt8 => Some(Self::UInt8),
70 _ => None,
71 }
72 }
73
74 pub const fn as_str(self) -> &'static str {
76 match self {
77 Self::Float32 => "Float32",
78 Self::Float64 => "Float64",
79 Self::Int8 => "Int8",
80 Self::UInt8 => "UInt8",
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, PartialEq)]
88pub enum GpuOptionKind {
89 RelationBinding,
91 Integer {
93 minimum: u64,
95 maximum: u64,
97 default: Option<u64>,
99 },
100 NonnegativeNumber {
102 default: f64,
104 },
105 PositiveNumber {
107 default: f64,
109 },
110 UnitInterval {
112 default: f64,
114 },
115 Fraction {
117 default: f64,
119 },
120 Boolean {
122 default: Option<bool>,
124 },
125 Enum {
127 values: &'static [&'static str],
129 default: Option<&'static str>,
131 },
132 Text {
135 max_chars: usize,
137 },
138}
139
140impl GpuOptionKind {
141 pub(crate) const fn default_u64(self) -> Option<u64> {
142 match self {
143 Self::Integer { default, .. } => default,
144 _ => None,
145 }
146 }
147
148 pub(crate) const fn default_f64(self) -> Option<f64> {
149 match self {
150 Self::NonnegativeNumber { default }
151 | Self::PositiveNumber { default }
152 | Self::UnitInterval { default }
153 | Self::Fraction { default } => Some(default),
154 _ => None,
155 }
156 }
157
158 pub(crate) const fn default_bool(self) -> Option<bool> {
159 match self {
160 Self::Boolean { default } => default,
161 _ => None,
162 }
163 }
164
165 pub(crate) const fn default_str(self) -> Option<&'static str> {
166 match self {
167 Self::Enum { default, .. } => default,
168 _ => None,
169 }
170 }
171}
172
173#[derive(Debug, Clone, Copy, PartialEq)]
177pub struct GpuOptionDefinition {
178 pub name: &'static str,
180 pub required: bool,
183 pub kind: GpuOptionKind,
185 pub description: &'static str,
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub struct GpuOutputFieldDefinition {
192 pub name: &'static str,
194 pub data_type: &'static str,
196 pub nullable: bool,
198 pub repeats: Option<&'static str>,
200 pub description: &'static str,
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct GpuOptionValidationError {
209 code: &'static str,
210 message: String,
211 argument: Option<&'static str>,
212}
213
214impl GpuOptionValidationError {
215 pub fn code(&self) -> &'static str {
217 self.code
218 }
219
220 pub fn argument(&self) -> Option<&'static str> {
222 self.argument
223 }
224
225 pub(crate) fn for_argument(mut self, argument: &'static str) -> Self {
226 self.argument = Some(argument);
227 self
228 }
229
230 pub fn message(&self) -> &str {
232 &self.message
233 }
234
235 pub(crate) fn new(code: &'static str, message: impl Into<String>) -> Self {
236 Self { code, message: message.into(), argument: None }
237 }
238}
239
240pub(crate) fn dense_relation_binding_schema(elements: &[VectorElementType]) -> Value {
241 let elements_label = elements.iter().map(|element| element.as_str()).collect::<Vec<_>>().join(", ");
242 let list_types = elements
243 .iter()
244 .map(|element| {
245 format!(
246 "FixedSizeList<{}, D>, List<{}>, or LargeList<{}>",
247 element.as_str(),
248 element.as_str(),
249 element.as_str()
250 )
251 })
252 .collect::<Vec<_>>()
253 .join("; ");
254 json!({
255 "type": "object", "additionalProperties": false, "required": ["id", "vector"],
256 "properties": {
257 "id": {"type": "string", "minLength": 1,
258 "description": "Logical row ID column; a null ID fails the query. IDs may repeat; result ordinals disambiguate physical rows."},
259 "vector": {"description": "Choose exactly one dense-vector binding shape.", "oneOf": [
260 {"type": "object", "additionalProperties": false, "required": ["columns"], "properties": {
261 "columns": {"type": "array", "minItems": 1, "uniqueItems": true,
262 "description": format!("Ordered, unique {elements_label} feature columns without null values; order defines vector dimensions."),
263 "items": {"type": "string", "minLength": 1}}}},
264 {"type": "object", "additionalProperties": false, "required": ["column"], "properties": {
265 "column": {"type": "string", "minLength": 1, "description": format!("One {list_types} column without null rows or null elements.")}}}
266 ]}
267 }
268 })
269}
270
271pub(crate) fn validate_dense_relation_binding_value(
272 family: &str,
273 option: &GpuOptionDefinition,
274 value: &Value,
275 allow_label: bool,
276) -> Result<(), GpuOptionValidationError> {
277 let invalid = || {
278 GpuOptionValidationError::new("missing_or_invalid_option", format!("invalid {family} option '{}'", option.name))
279 };
280 let binding = value.as_object().ok_or_else(invalid)?;
281 if let Some(unknown) =
282 binding.keys().find(|key| !matches!(key.as_str(), "id" | "vector") && !(allow_label && key.as_str() == "label"))
283 {
284 return Err(GpuOptionValidationError::new(
285 "unknown_option",
286 format!("unknown {family} option '{unknown}' in {}", option.name),
287 ));
288 }
289 if binding.get("id").and_then(Value::as_str).filter(|value| !value.is_empty()).is_none() {
290 return Err(GpuOptionValidationError::new(
291 "missing_or_invalid_option",
292 format!("{family} option '{}.id' must be a non-empty string", option.name),
293 ));
294 }
295 if allow_label && binding.get("label").and_then(Value::as_str) != Some("label") {
296 return Err(GpuOptionValidationError::new(
297 "missing_or_invalid_option",
298 format!("{family} option '{}.label' must be 'label'", option.name),
299 ));
300 }
301 let vector = binding.get("vector").and_then(Value::as_object).ok_or_else(|| {
302 GpuOptionValidationError::new(
303 "missing_or_invalid_option",
304 format!("{family} option '{}.vector' must be an object", option.name),
305 )
306 })?;
307 if let Some(unknown) = vector.keys().find(|key| !matches!(key.as_str(), "columns" | "column")) {
308 return Err(GpuOptionValidationError::new(
309 "unknown_option",
310 format!("unknown {family} option '{unknown}' in {}.vector", option.name),
311 ));
312 }
313 match (vector.get("columns"), vector.get("column")) {
314 (Some(columns), None) => {
315 let columns = columns.as_array().filter(|columns| !columns.is_empty()).ok_or_else(|| {
316 GpuOptionValidationError::new(
317 "invalid_vector_binding",
318 format!("{family} option '{}.vector.columns' must be a non-empty string array", option.name),
319 )
320 })?;
321 let mut unique = BTreeSet::new();
322 for column in columns {
323 let column = column.as_str().filter(|column| !column.is_empty()).ok_or_else(|| {
324 GpuOptionValidationError::new(
325 "invalid_vector_binding",
326 format!("{family} option '{}.vector.columns' entries must be non-empty strings", option.name),
327 )
328 })?;
329 if !unique.insert(column) {
330 return Err(GpuOptionValidationError::new(
331 "duplicate_vector_column",
332 format!("{family} option '{}.vector.columns' contains duplicate field '{column}'", option.name),
333 ));
334 }
335 }
336 },
337 (None, Some(column)) => {
338 if column.as_str().filter(|column| !column.is_empty()).is_none() {
339 return Err(GpuOptionValidationError::new(
340 "invalid_vector_binding",
341 format!("{family} option '{}.vector.column' must be a non-empty string", option.name),
342 ));
343 }
344 },
345 _ => {
346 return Err(GpuOptionValidationError::new(
347 "invalid_vector_binding",
348 format!("{family} option '{}.vector' must contain exactly one of columns or column", option.name),
349 ));
350 },
351 }
352 Ok(())
353}
354
355pub(crate) fn option_schema(kind: GpuOptionKind, relation_schema: &Value) -> Value {
356 match kind {
357 GpuOptionKind::RelationBinding => relation_schema.clone(),
358 GpuOptionKind::Integer { minimum, maximum, default } => {
359 let mut value = json!({"type": "integer", "minimum": minimum, "maximum": maximum});
360 if let Some(default) = default {
361 value["default"] = json!(default);
362 }
363 value
364 },
365 GpuOptionKind::NonnegativeNumber { default } => json!({"type": "number", "minimum": 0.0, "default": default}),
366 GpuOptionKind::PositiveNumber { default } => {
367 json!({"type": "number", "exclusiveMinimum": 0, "default": default})
368 },
369 GpuOptionKind::UnitInterval { default } => {
370 json!({"type": "number", "minimum": 0, "maximum": 1, "default": default})
371 },
372 GpuOptionKind::Fraction { default } => {
373 json!({"type": "number", "exclusiveMinimum": 0, "maximum": 1, "default": default})
374 },
375 GpuOptionKind::Boolean { default } => {
376 let mut value = json!({"type": "boolean"});
377 if let Some(default) = default {
378 value["default"] = json!(default);
379 }
380 value
381 },
382 GpuOptionKind::Enum { values, default } => {
383 let mut value = json!({"type": "string", "enum": values});
384 if let Some(default) = default {
385 value["default"] = json!(default);
386 }
387 value
388 },
389 GpuOptionKind::Text { max_chars } => json!({"type": "string", "minLength": 1, "maxLength": max_chars}),
390 }
391}
392
393pub(crate) fn canonicalize_options(
400 options: &[GpuOptionDefinition],
401 raw: &str,
402 family: &str,
403 validate_relation: impl Fn(&GpuOptionDefinition, &Value) -> Result<(), GpuOptionValidationError>,
404) -> Result<serde_json::Map<String, Value>, GpuOptionValidationError> {
405 let parsed: Value = serde_json::from_str(raw).map_err(|_| {
406 GpuOptionValidationError::new("invalid_options_json", format!("{family} options must be valid JSON"))
407 })?;
408 let mut object = parsed.as_object().cloned().ok_or_else(|| {
409 GpuOptionValidationError::new("options_not_object", format!("{family} options must be a JSON object"))
410 })?;
411 if let Some(unknown) = object.keys().find(|key| !options.iter().any(|option| option.name == key.as_str())) {
412 return Err(GpuOptionValidationError::new("unknown_option", format!("unknown {family} option '{unknown}'")));
413 }
414 for option in options {
415 let Some(value) = object.get_mut(option.name) else {
416 if option.required {
417 return Err(GpuOptionValidationError::new(
418 "missing_or_invalid_option",
419 format!("{family} option '{}' is required", option.name),
420 )
421 .for_argument(option.name));
422 }
423 let default = match option.kind {
424 GpuOptionKind::Integer { default: Some(value), .. } => json!(value),
425 GpuOptionKind::NonnegativeNumber { default }
426 | GpuOptionKind::PositiveNumber { default }
427 | GpuOptionKind::UnitInterval { default }
428 | GpuOptionKind::Fraction { default } => json!(default),
429 GpuOptionKind::Boolean { default: Some(value) } => json!(value),
430 GpuOptionKind::Enum { default: Some(value), .. } => json!(value),
431 _ => continue,
432 };
433 object.insert(option.name.to_string(), default);
434 continue;
435 };
436 validate_option_value(option, value, family, &validate_relation)
437 .map_err(|error| error.for_argument(option.name))?;
438 }
439 Ok(object)
440}
441
442pub(crate) fn validate_option_value(
443 option: &GpuOptionDefinition,
444 value: &Value,
445 family: &str,
446 validate_relation: &impl Fn(&GpuOptionDefinition, &Value) -> Result<(), GpuOptionValidationError>,
447) -> Result<(), GpuOptionValidationError> {
448 let invalid = || {
449 GpuOptionValidationError::new("missing_or_invalid_option", format!("invalid {family} option '{}'", option.name))
450 };
451 match option.kind {
452 GpuOptionKind::RelationBinding => validate_relation(option, value)?,
453 GpuOptionKind::Integer { minimum, maximum, .. } => {
454 if value.as_u64().filter(|value| *value >= minimum && *value <= maximum).is_none() {
455 return Err(invalid());
456 }
457 },
458 GpuOptionKind::NonnegativeNumber { .. } => {
459 if value.as_f64().filter(|value| value.is_finite() && *value >= 0.0).is_none() {
460 return Err(invalid());
461 }
462 },
463 GpuOptionKind::PositiveNumber { .. } => {
464 if value.as_f64().filter(|value| value.is_finite() && *value > 0.0).is_none() {
465 return Err(invalid());
466 }
467 },
468 GpuOptionKind::UnitInterval { .. } => {
469 if value.as_f64().filter(|value| value.is_finite() && (0.0..=1.0).contains(value)).is_none() {
470 return Err(invalid());
471 }
472 },
473 GpuOptionKind::Fraction { .. } => {
474 if value.as_f64().filter(|value| value.is_finite() && *value > 0.0 && *value <= 1.0).is_none() {
475 return Err(invalid());
476 }
477 },
478 GpuOptionKind::Boolean { .. } => {
479 if !value.is_boolean() {
480 return Err(invalid());
481 }
482 },
483 GpuOptionKind::Enum { values, .. } => {
484 if value.as_str().is_none_or(|value| !values.contains(&value)) {
485 return Err(invalid());
486 }
487 },
488 GpuOptionKind::Text { max_chars } => {
489 if value.as_str().is_none_or(|value| value.is_empty() || value.chars().count() > max_chars) {
490 return Err(invalid());
491 }
492 },
493 }
494 Ok(())
495}
496
497#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
499pub enum DenseVectorBinding {
500 Columns(Vec<String>),
502 VectorColumn(String),
504}
505
506impl DenseVectorBinding {
507 pub(crate) fn columns(columns: impl IntoIterator<Item = impl Into<String>>) -> Self {
508 Self::Columns(columns.into_iter().map(Into::into).collect())
509 }
510
511 pub(crate) fn vector_column(column: impl Into<String>) -> Self {
512 Self::VectorColumn(column.into())
513 }
514
515 pub(crate) fn display_label(&self) -> String {
516 match self {
517 Self::Columns(columns) => columns.join("|"),
518 Self::VectorColumn(column) => format!("[{column}]"),
519 }
520 }
521}