1use std::collections::{BTreeMap, BTreeSet};
2
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
5use quote::{format_ident, quote};
6use sha2::{Digest, Sha256};
7use syn::{
8 parse::Parser, parse_macro_input, punctuated::Punctuated, spanned::Spanned, Attribute, Data,
9 DeriveInput, Expr, ExprLit, Fields, FnArg, GenericArgument, Item, ItemFn, ItemMod, Lit, Meta,
10 PathArguments, ReturnType, Token, Type,
11};
12
13#[proc_macro_derive(RadixType, attributes(radix_type, radix_field))]
14pub fn derive_radix_type(input: TokenStream) -> TokenStream {
15 match expand_radix_type(parse_macro_input!(input as DeriveInput)) {
16 Ok(tokens) => tokens.into(),
17 Err(error) => error.into_compile_error().into(),
18 }
19}
20
21#[proc_macro_attribute]
22pub fn radixdb_plugin(attribute: TokenStream, item: TokenStream) -> TokenStream {
23 let args = match parse_options(attribute.into()) {
24 Ok(args) => args,
25 Err(error) => return error.into_compile_error().into(),
26 };
27 match expand_plugin(args, parse_macro_input!(item as ItemMod)) {
28 Ok(tokens) => tokens.into(),
29 Err(error) => error.into_compile_error().into(),
30 }
31}
32
33macro_rules! passthrough_attribute {
34 ($name:ident) => {
35 #[proc_macro_attribute]
36 pub fn $name(_attribute: TokenStream, item: TokenStream) -> TokenStream {
37 item
38 }
39 };
40}
41
42passthrough_attribute!(radixdb_scalar);
43passthrough_attribute!(radixdb_batch);
44passthrough_attribute!(radixdb_operator);
45passthrough_attribute!(radixdb_operator_class);
46passthrough_attribute!(radixdb_planner_support);
47
48macro_rules! rejected_attribute {
49 ($name:ident, $kind:literal) => {
50 #[proc_macro_attribute]
51 pub fn $name(_attribute: TokenStream, _item: TokenStream) -> TokenStream {
52 syn::Error::new(
53 Span::call_site(),
54 concat!(
55 $kind,
56 " plugin functions are outside the RadixDB 1.2 authoring scope"
57 ),
58 )
59 .into_compile_error()
60 .into()
61 }
62 };
63}
64
65rejected_attribute!(radixdb_aggregate, "aggregate");
66rejected_attribute!(radixdb_window, "window");
67rejected_attribute!(radixdb_tvf, "table-valued");
68
69#[derive(Default, Clone)]
70struct Options {
71 values: BTreeMap<String, Expr>,
72 flags: BTreeSet<String>,
73}
74
75impl Options {
76 fn required_string(&self, name: &str, span: Span) -> syn::Result<String> {
77 self.string(name)?
78 .ok_or_else(|| syn::Error::new(span, format!("missing required `{name}`")))
79 }
80
81 fn string(&self, name: &str) -> syn::Result<Option<String>> {
82 self.values
83 .get(name)
84 .map(|value| match value {
85 Expr::Lit(ExprLit {
86 lit: Lit::Str(value),
87 ..
88 }) => Ok(value.value()),
89 _ => Err(syn::Error::new(value.span(), "expected string literal")),
90 })
91 .transpose()
92 }
93
94 fn required_u32(&self, name: &str, span: Span) -> syn::Result<u32> {
95 self.u32(name)?
96 .ok_or_else(|| syn::Error::new(span, format!("missing required `{name}`")))
97 }
98
99 fn u32(&self, name: &str) -> syn::Result<Option<u32>> {
100 self.values
101 .get(name)
102 .map(|value| match value {
103 Expr::Lit(ExprLit {
104 lit: Lit::Int(value),
105 ..
106 }) => value.base10_parse(),
107 _ => Err(syn::Error::new(value.span(), "expected integer literal")),
108 })
109 .transpose()
110 }
111
112 fn path(&self, name: &str) -> syn::Result<Option<syn::Path>> {
113 self.values
114 .get(name)
115 .map(|value| match value {
116 Expr::Path(value) => Ok(value.path.clone()),
117 _ => Err(syn::Error::new(value.span(), "expected callback path")),
118 })
119 .transpose()
120 }
121
122 fn reject_unknown(&self, allowed_values: &[&str], allowed_flags: &[&str]) -> syn::Result<()> {
123 for name in self.values.keys() {
124 if !allowed_values.contains(&name.as_str()) {
125 return Err(syn::Error::new(
126 Span::call_site(),
127 format!("unknown RadixDB attribute option `{name}`"),
128 ));
129 }
130 }
131 for name in &self.flags {
132 if !allowed_flags.contains(&name.as_str()) {
133 return Err(syn::Error::new(
134 Span::call_site(),
135 format!("unknown RadixDB attribute flag `{name}`"),
136 ));
137 }
138 }
139 Ok(())
140 }
141}
142
143fn parse_options(tokens: TokenStream2) -> syn::Result<Options> {
144 let metas = Punctuated::<Meta, Token![,]>::parse_terminated.parse2(tokens)?;
145 let mut result = Options::default();
146 for meta in metas {
147 match meta {
148 Meta::Path(path) => {
149 let name = single_ident(&path)?;
150 if !result.flags.insert(name.clone()) {
151 return Err(syn::Error::new(path.span(), format!("duplicate `{name}`")));
152 }
153 }
154 Meta::NameValue(value) => {
155 let name = single_ident(&value.path)?;
156 if result.values.insert(name.clone(), value.value).is_some() {
157 return Err(syn::Error::new(
158 value.path.span(),
159 format!("duplicate `{name}`"),
160 ));
161 }
162 }
163 Meta::List(list) => {
164 return Err(syn::Error::new(
165 list.span(),
166 "nested RadixDB attribute options are not supported",
167 ));
168 }
169 }
170 }
171 Ok(result)
172}
173
174fn options_from_attribute(attribute: &Attribute) -> syn::Result<Options> {
175 let Meta::List(list) = &attribute.meta else {
176 return Err(syn::Error::new(
177 attribute.span(),
178 "expected attribute arguments",
179 ));
180 };
181 parse_options(list.tokens.clone())
182}
183
184fn single_ident(path: &syn::Path) -> syn::Result<String> {
185 if path.segments.len() != 1 {
186 return Err(syn::Error::new(
187 path.span(),
188 "expected a single option name",
189 ));
190 }
191 Ok(path.segments[0].ident.to_string())
192}
193
194fn attribute<'a>(attributes: &'a [Attribute], name: &str) -> Option<&'a Attribute> {
195 attributes
196 .iter()
197 .find(|attribute| attribute.path().is_ident(name))
198}
199
200fn expand_radix_type(input: DeriveInput) -> syn::Result<TokenStream2> {
201 let ident = &input.ident;
202 if !input.generics.params.is_empty() {
203 return Err(syn::Error::new(
204 input.generics.span(),
205 "RadixType does not support generic external types",
206 ));
207 }
208 let type_attribute = attribute(&input.attrs, "radix_type")
209 .ok_or_else(|| syn::Error::new(input.span(), "missing #[radix_type(...)]"))?;
210 let options = options_from_attribute(type_attribute)?;
211 options.reject_unknown(
212 &[
213 "id",
214 "name",
215 "codec",
216 "semantic_revision",
217 "storage",
218 "max_bytes",
219 "equality",
220 "hash",
221 "ordering",
222 "manual",
223 ],
224 &[],
225 )?;
226 let local_id = options.required_string("id", input.span())?;
227 let display_name = options.required_string("name", input.span())?;
228 validate_local_id(&local_id, input.span())?;
229 validate_sql_name(&display_name, input.span())?;
230 let codec_version = options.required_u32("codec", input.span())?;
231 let semantic_revision = options.required_u32("semantic_revision", input.span())?;
232 if codec_version == 0 || semantic_revision == 0 {
233 return Err(syn::Error::new(
234 input.span(),
235 "codec and semantic_revision must be non-zero",
236 ));
237 }
238 let storage = options.required_string("storage", input.span())?;
239 let max_bytes = options.required_u32("max_bytes", input.span())?;
240 if max_bytes == 0 || max_bytes > 16 * 1024 * 1024 {
241 return Err(syn::Error::new(
242 input.span(),
243 "max_bytes must be in 1..=16777216",
244 ));
245 }
246 let storage_kind = match storage.as_str() {
247 "fixed" => quote!(::radixdb_plugin::__private::abi::RADIX_EXTERNAL_STORAGE_FIXED),
248 "variable" => quote!(::radixdb_plugin::__private::abi::RADIX_EXTERNAL_STORAGE_VARIABLE),
249 _ => {
250 return Err(syn::Error::new(
251 input.span(),
252 "storage must be \"fixed\" or \"variable\"",
253 ));
254 }
255 };
256 let fixed_bytes = if storage == "fixed" { max_bytes } else { 0 };
257 let equality = options.path("equality")?;
258 let hash = options.path("hash")?;
259 let ordering = options.path("ordering")?;
260 if hash.is_some() && equality.is_none() {
261 return Err(syn::Error::new(
262 input.span(),
263 "hash callback requires an explicit equality callback",
264 ));
265 }
266 let manual = options.path("manual")?;
267
268 let Data::Struct(data) = &input.data else {
269 return Err(syn::Error::new(input.span(), "RadixType requires a struct"));
270 };
271 let Fields::Named(fields) = &data.fields else {
272 return Err(syn::Error::new(
273 data.fields.span(),
274 "RadixType requires named fields",
275 ));
276 };
277
278 let (encode_body, decode_body, corpus_body, schema, inferred_fixed_bytes) = if let Some(codec) =
279 manual
280 {
281 if fields
282 .named
283 .iter()
284 .any(|field| attribute(&field.attrs, "radix_field").is_some())
285 {
286 return Err(syn::Error::new(
287 fields.span(),
288 "manual codec types must not declare radix_field codecs",
289 ));
290 }
291 (
292 quote!(<#codec as ::radixdb_plugin::ManualCodec<Self>>::encode(self, output)),
293 quote!(<#codec as ::radixdb_plugin::ManualCodec<Self>>::decode(input)),
294 quote!(<#codec as ::radixdb_plugin::ManualCodec<Self>>::corpus()),
295 format!("manual:{}", quote!(#codec)),
296 None,
297 )
298 } else {
299 let mut encode = Vec::new();
300 let mut decode = Vec::new();
301 let mut names = Vec::new();
302 let mut edge_extensions = Vec::new();
303 let mut schema_parts = Vec::new();
304 let mut encoded_width = Some(0_u32);
305 for field in &fields.named {
306 let field_ident = field.ident.as_ref().expect("named field");
307 let field_type = &field.ty;
308 names.push(field_ident.clone());
309 let field_attribute = attribute(&field.attrs, "radix_field").ok_or_else(|| {
310 syn::Error::new(
311 field.span(),
312 "every derived field requires #[radix_field(...)]",
313 )
314 })?;
315 let field_options = options_from_attribute(field_attribute)?;
316 field_options.reject_unknown(&["codec", "max_items", "max_bytes"], &[])?;
317 let codec = field_options.required_string("codec", field.span())?;
318 let generated =
319 generate_field_codec(field_ident, &field.ty, &codec, &field_options, field.span())?;
320 encode.push(generated.encode);
321 decode.push(generated.decode);
322 edge_extensions.push(generated.edge);
323 encoded_width = match (encoded_width, generated.fixed_width) {
324 (Some(total), Some(width)) => Some(total.checked_add(width).ok_or_else(|| {
325 syn::Error::new(field.span(), "derived fixed width exceeds u32")
326 })?),
327 _ => None,
328 };
329 schema_parts.push(format!(
330 "{}:{}:{}",
331 field_ident,
332 quote!(#field_type),
333 generated.schema
334 ));
335 }
336 let defaults = names.iter().zip(fields.named.iter()).map(|(name, field)| {
337 let ty = &field.ty;
338 quote!(#name: <#ty as ::std::default::Default>::default())
339 });
340 (
341 quote!({ #(#encode)* Ok(()) }),
342 quote!({ #(let #names = #decode;)* Ok(Self { #(#names),* }) }),
343 quote!({
344 let mut corpus = vec![Self { #(#defaults),* }];
345 #(#edge_extensions)*
346 corpus
347 }),
348 schema_parts.join(";"),
349 encoded_width,
350 )
351 };
352 if storage == "fixed" && inferred_fixed_bytes.is_some_and(|encoded| encoded != max_bytes) {
353 return Err(syn::Error::new(
354 input.span(),
355 format!(
356 "fixed type max_bytes must equal derived encoded width {}",
357 inferred_fixed_bytes.unwrap()
358 ),
359 ));
360 }
361
362 let mut capabilities = 0u64;
363 if equality.is_some() {
364 capabilities |= 1 << 0;
365 }
366 if hash.is_some() {
367 capabilities |= 1 << 1;
368 }
369 if ordering.is_some() {
370 capabilities |= 1 << 2;
371 }
372 let fingerprint = digest32(format!(
377 "radixdb.type.codec.v1\0{id}\0{codec_version}\0{storage}\0{max_bytes}\0{schema}",
378 id = local_id,
379 ));
380 let fingerprint_tokens = bytes_tokens(&fingerprint);
381 let encode_wrapper =
382 format_ident!("__radixdb_type_{}_encode", ident.to_string().to_lowercase());
383 let decode_wrapper =
384 format_ident!("__radixdb_type_{}_decode", ident.to_string().to_lowercase());
385 let equal_wrapper = format_ident!("__radixdb_type_{}_equal", ident.to_string().to_lowercase());
386 let hash_wrapper = format_ident!("__radixdb_type_{}_hash", ident.to_string().to_lowercase());
387 let compare_wrapper = format_ident!(
388 "__radixdb_type_{}_compare",
389 ident.to_string().to_lowercase()
390 );
391
392 let equality_impl = if let Some(callback) = &equality {
393 quote! {
394 fn semantic_equal(left: &Self, right: &Self) -> Option<bool> {
395 let callback: fn(&Self, &Self) -> bool = #callback;
396 Some(callback(left, right))
397 }
398 }
399 } else {
400 quote!()
401 };
402 let hash_impl = if let Some(callback) = &hash {
403 quote! {
404 fn semantic_hash(
405 value: &Self,
406 sink: &mut ::radixdb_plugin::HashSink<'_>,
407 ) -> Option<::radixdb_plugin::PluginResult<()>> {
408 let callback: fn(&Self, &mut ::radixdb_plugin::HashSink<'_>)
409 -> ::radixdb_plugin::PluginResult<()> = #callback;
410 Some(callback(value, sink))
411 }
412 }
413 } else {
414 quote!()
415 };
416 let ordering_impl = if let Some(callback) = &ordering {
417 quote! {
418 fn semantic_compare(left: &Self, right: &Self) -> Option<::std::cmp::Ordering> {
419 let callback: fn(&Self, &Self) -> ::std::cmp::Ordering = #callback;
420 Some(callback(left, right))
421 }
422 }
423 } else {
424 quote!()
425 };
426 let equal_items = if equality.is_some() {
427 quote! {
428 unsafe extern "C" fn #equal_wrapper(
429 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
430 left: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
431 right: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
432 output: *mut u8,
433 ) -> u32 {
434 unsafe { ::radixdb_plugin::__private::run_equal::<#ident>(context, left, right, output) }
435 }
436 }
437 } else {
438 quote!()
439 };
440 let hash_items = if hash.is_some() {
441 quote! {
442 unsafe extern "C" fn #hash_wrapper(
443 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
444 value: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
445 sink: *const ::radixdb_plugin::__private::abi::RadixAbiHashSinkV1,
446 ) -> u32 {
447 unsafe { ::radixdb_plugin::__private::run_hash::<#ident>(context, value, sink) }
448 }
449 }
450 } else {
451 quote!()
452 };
453 let ordering_items = if ordering.is_some() {
454 quote! {
455 unsafe extern "C" fn #compare_wrapper(
456 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
457 left: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
458 right: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
459 output: *mut i8,
460 ) -> u32 {
461 unsafe { ::radixdb_plugin::__private::run_compare::<#ident>(context, left, right, output) }
462 }
463 }
464 } else {
465 quote!()
466 };
467 let equal_const = equality
468 .as_ref()
469 .map(|_| quote!(Some(#equal_wrapper)))
470 .unwrap_or(quote!(None));
471 let hash_const = hash
472 .as_ref()
473 .map(|_| quote!(Some(#hash_wrapper)))
474 .unwrap_or(quote!(None));
475 let ordering_const = ordering
476 .as_ref()
477 .map(|_| quote!(Some(#compare_wrapper)))
478 .unwrap_or(quote!(None));
479
480 Ok(quote! {
481 unsafe extern "C" fn #encode_wrapper(
482 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
483 input: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
484 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
485 ) -> u32 {
486 unsafe { ::radixdb_plugin::__private::run_codec_encode::<#ident>(context, input, output) }
487 }
488
489 unsafe extern "C" fn #decode_wrapper(
490 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
491 input: ::radixdb_plugin::__private::abi::RadixAbiSliceV1,
492 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
493 ) -> u32 {
494 unsafe { ::radixdb_plugin::__private::run_codec_decode::<#ident>(context, input, output) }
495 }
496
497 #equal_items
498 #hash_items
499 #ordering_items
500
501 impl ::radixdb_plugin::RadixType for #ident {
502 const LOCAL_ID: &'static str = #local_id;
503 const DISPLAY_NAME: &'static str = #display_name;
504 const CODEC_VERSION: u32 = #codec_version;
505 const SEMANTIC_REVISION: u32 = #semantic_revision;
506 const STORAGE_KIND: u16 = #storage_kind;
507 const FIXED_BYTES: u32 = #fixed_bytes;
508 const MAX_BYTES: u32 = #max_bytes;
509 const CAPABILITIES: u64 = #capabilities;
510 const CODEC_FINGERPRINT: [u8; 32] = [#(#fingerprint_tokens),*];
511 const ABI_ENCODE: Option<::radixdb_plugin::__private::abi::RadixAbiCodecFnV1> =
512 Some(#encode_wrapper);
513 const ABI_DECODE: Option<::radixdb_plugin::__private::abi::RadixAbiParseFnV1> =
514 Some(#decode_wrapper);
515 const ABI_EQUALITY: Option<::radixdb_plugin::__private::abi::RadixAbiEqualFnV1> =
516 #equal_const;
517 const ABI_HASH: Option<::radixdb_plugin::__private::abi::RadixAbiHashFnV1> =
518 #hash_const;
519 const ABI_ORDERING: Option<::radixdb_plugin::__private::abi::RadixAbiCompareFnV1> =
520 #ordering_const;
521
522 fn encode(&self, output: &mut ::radixdb_plugin::CodecWriter)
523 -> ::radixdb_plugin::PluginResult<()>
524 {
525 #encode_body
526 }
527
528 fn decode(input: &mut ::radixdb_plugin::CodecReader<'_>)
529 -> ::radixdb_plugin::PluginResult<Self>
530 {
531 #decode_body
532 }
533
534 #[allow(clippy::needless_update)]
535 fn test_corpus() -> Vec<Self> {
536 #corpus_body
537 }
538
539 #equality_impl
540 #hash_impl
541 #ordering_impl
542 }
543 })
544}
545
546struct GeneratedField {
547 encode: TokenStream2,
548 decode: TokenStream2,
549 edge: TokenStream2,
550 schema: String,
551 fixed_width: Option<u32>,
552}
553
554fn generate_field_codec(
555 field: &Ident,
556 ty: &Type,
557 codec: &str,
558 options: &Options,
559 span: Span,
560) -> syn::Result<GeneratedField> {
561 if let Some(inner) = vec_inner(ty) {
562 let max_items = options
563 .u32("max_items")?
564 .ok_or_else(|| syn::Error::new(span, "bounded sequence requires max_items"))?;
565 let max_bytes = options
566 .u32("max_bytes")?
567 .ok_or_else(|| syn::Error::new(span, "bounded sequence requires max_bytes"))?;
568 validate_primitive_codec(inner, codec, span)?;
569 return Ok(GeneratedField {
570 encode: quote! {
571 ::radixdb_plugin::__private::encode_sequence(
572 &self.#field,
573 #max_items as usize,
574 #max_bytes as usize,
575 output,
576 )?;
577 },
578 decode: quote!(::radixdb_plugin::__private::decode_sequence::<#inner>(
579 input,
580 #max_items as usize,
581 #max_bytes as usize,
582 )?),
583 edge: quote! {
584 for value in <#inner as ::radixdb_plugin::__private::CanonicalField>::edge_values() {
585 corpus.push(Self {
586 #field: vec![value.clone()],
587 ..Self::default()
588 });
589 corpus.push(Self {
590 #field: vec![value; #max_items as usize],
591 ..Self::default()
592 });
593 }
594 },
595 schema: format!("{codec}[max_items={max_items},max_bytes={max_bytes}]"),
596 fixed_width: None,
597 });
598 }
599 if options.values.contains_key("max_items") || options.values.contains_key("max_bytes") {
600 return Err(syn::Error::new(
601 span,
602 "max_items/max_bytes are valid only for bounded sequences",
603 ));
604 }
605 if let Type::Array(array) = ty {
606 validate_primitive_codec(&array.elem, codec, span)?;
607 let length = match &array.len {
608 Expr::Lit(ExprLit {
609 lit: Lit::Int(length),
610 ..
611 }) => length.base10_parse::<u32>()?,
612 _ => {
613 return Err(syn::Error::new(
614 array.len.span(),
615 "fixed array length must be an integer literal",
616 ));
617 }
618 };
619 let element_width = primitive_codec_width(&array.elem).ok_or_else(|| {
620 syn::Error::new(array.elem.span(), "unsupported fixed-array element type")
621 })?;
622 return Ok(GeneratedField {
623 encode: quote!(::radixdb_plugin::__private::CanonicalField::encode_field(
624 &self.#field,
625 output,
626 )?;),
627 decode: quote!(<#ty as ::radixdb_plugin::__private::CanonicalField>::decode_field(input)?),
628 edge: quote! {
629 for value in <#ty as ::radixdb_plugin::__private::CanonicalField>::edge_values() {
630 corpus.push(Self {
631 #field: value,
632 ..Self::default()
633 });
634 }
635 },
636 schema: format!("{codec}[{}]", quote!(#array.len)),
637 fixed_width: Some(length.checked_mul(element_width).ok_or_else(|| {
638 syn::Error::new(array.len.span(), "fixed array width exceeds u32")
639 })?),
640 });
641 }
642 validate_primitive_codec(ty, codec, span)?;
643 Ok(GeneratedField {
644 encode: quote!(::radixdb_plugin::__private::CanonicalField::encode_field(
645 &self.#field,
646 output,
647 )?;),
648 decode: quote!(<#ty as ::radixdb_plugin::__private::CanonicalField>::decode_field(input)?),
649 edge: quote! {
650 for value in <#ty as ::radixdb_plugin::__private::CanonicalField>::edge_values() {
651 corpus.push(Self {
652 #field: value,
653 ..Self::default()
654 });
655 }
656 },
657 schema: codec.to_string(),
658 fixed_width: primitive_codec_width(ty),
659 })
660}
661
662fn primitive_codec_width(ty: &Type) -> Option<u32> {
663 match terminal_type_ident(ty)?.as_str() {
664 "i8" | "u8" | "bool" => Some(1),
665 "i16" | "u16" => Some(2),
666 "i32" | "u32" | "f32" => Some(4),
667 "i64" | "u64" | "f64" => Some(8),
668 _ => None,
669 }
670}
671
672fn validate_primitive_codec(ty: &Type, codec: &str, span: Span) -> syn::Result<()> {
673 let expected = match terminal_type_ident(ty).as_deref() {
674 Some("i8") => "i8-le",
675 Some("i16") => "i16-le",
676 Some("i32") => "i32-le",
677 Some("i64") => "i64-le",
678 Some("u8") => "u8-le",
679 Some("u16") => "u16-le",
680 Some("u32") => "u32-le",
681 Some("u64") => "u64-le",
682 Some("f32") => "f32-le",
683 Some("f64") => "f64-le",
684 Some("bool") => "bool-u8",
685 _ => {
686 return Err(syn::Error::new(
687 span,
688 "unsupported field type; use a primitive, fixed array, bounded Vec, or manual codec",
689 ));
690 }
691 };
692 if codec != expected {
693 return Err(syn::Error::new(
694 span,
695 format!("field type requires codec \"{expected}\""),
696 ));
697 }
698 Ok(())
699}
700
701fn expand_plugin(args: Options, mut module: ItemMod) -> syn::Result<TokenStream2> {
702 args.reject_unknown(&["id", "name", "version"], &[])?;
703 let package_id = args.required_string("id", module.span())?;
704 let package_name = args.required_string("name", module.span())?;
705 let package_version = args.required_string("version", module.span())?;
706 let uuid = uuid::Uuid::parse_str(&package_id)
707 .map_err(|_| syn::Error::new(module.span(), "plugin id must be a canonical UUID"))?;
708 if uuid.to_string() != package_id {
709 return Err(syn::Error::new(
710 module.span(),
711 "plugin id must use canonical lowercase UUID spelling",
712 ));
713 }
714 validate_package_name(&package_name, module.span())?;
715 let version = semver::Version::parse(&package_version)
716 .map_err(|_| syn::Error::new(module.span(), "plugin version must be SemVer"))?;
717 if version.to_string() != package_version || !version.build.is_empty() {
718 return Err(syn::Error::new(
719 module.span(),
720 "plugin version must be canonical SemVer without build metadata",
721 ));
722 }
723 let Some((_, items)) = &mut module.content else {
724 return Err(syn::Error::new(
725 module.span(),
726 "radixdb_plugin requires an inline module",
727 ));
728 };
729
730 let mut type_specs = Vec::new();
731 let mut scalar_specs = Vec::new();
732 let mut batch_specs = BTreeMap::new();
733 let mut operator_specs = Vec::new();
734 let mut opclass_specs = Vec::new();
735 let mut planner_specs = Vec::new();
736 let mut local_ids = BTreeSet::new();
737 for item in items.iter() {
738 match item {
739 Item::Struct(item) => {
740 if let Some(attribute) = attribute(&item.attrs, "radix_type") {
741 let options = options_from_attribute(attribute)?;
742 let local_id = options.required_string("id", item.span())?;
743 admit_local_id(&mut local_ids, &local_id, item.span())?;
744 type_specs.push((item.ident.clone(), local_id, options));
745 }
746 }
747 Item::Fn(function) => {
748 if let Some(attribute) = attribute(&function.attrs, "radixdb_scalar") {
749 let options = options_from_attribute(attribute)?;
750 let local_id = options.required_string("id", function.span())?;
751 admit_local_id(&mut local_ids, &local_id, function.span())?;
752 scalar_specs.push((function.clone(), local_id, options));
753 }
754 if let Some(attribute) = attribute(&function.attrs, "radixdb_batch") {
755 let options = options_from_attribute(attribute)?;
756 let scalar = options.required_string("for_scalar", function.span())?;
757 if batch_specs
758 .insert(scalar.clone(), (function.clone(), options))
759 .is_some()
760 {
761 return Err(syn::Error::new(
762 function.span(),
763 format!("duplicate batch adapter for `{scalar}`"),
764 ));
765 }
766 }
767 if let Some(attribute) = attribute(&function.attrs, "radixdb_operator") {
768 let options = options_from_attribute(attribute)?;
769 let local_id = options.required_string("id", function.span())?;
770 admit_local_id(&mut local_ids, &local_id, function.span())?;
771 operator_specs.push((function.clone(), local_id, options));
772 }
773 if let Some(attribute) = attribute(&function.attrs, "radixdb_operator_class") {
774 let options = options_from_attribute(attribute)?;
775 let local_id = options.required_string("id", function.span())?;
776 admit_local_id(&mut local_ids, &local_id, function.span())?;
777 opclass_specs.push((function.clone(), local_id, options));
778 }
779 if let Some(attribute) = attribute(&function.attrs, "radixdb_planner_support") {
780 let options = options_from_attribute(attribute)?;
781 let local_id = options.required_string("id", function.span())?;
782 admit_local_id(&mut local_ids, &local_id, function.span())?;
783 planner_specs.push((function.clone(), local_id, options));
784 }
785 }
786 _ => {}
787 }
788 }
789
790 let package_uuid = *uuid.as_bytes();
791 let type_map: BTreeMap<String, ([u8; 16], u32)> = type_specs
792 .iter()
793 .map(|(ident, local_id, options)| {
794 Ok((
795 ident.to_string(),
796 (
797 object_id(package_uuid, local_id),
798 options.required_u32("codec", ident.span())?,
799 ),
800 ))
801 })
802 .collect::<syn::Result<_>>()?;
803 let mut type_names = BTreeSet::new();
804 for (ident, _, options) in &type_specs {
805 let name = options.required_string("name", ident.span())?;
806 if !type_names.insert(name.clone()) {
807 return Err(syn::Error::new(
808 ident.span(),
809 format!("duplicate external type SQL name `{name}`"),
810 ));
811 }
812 }
813 let mut overloads = BTreeSet::new();
814 for (function, _, options) in &scalar_specs {
815 let name = options.required_string("name", function.span())?;
816 let arguments = function_arguments(function)?;
817 let signature = format!(
818 "{}({})",
819 name,
820 arguments
821 .iter()
822 .map(|(_, ty)| quote!(#ty).to_string())
823 .collect::<Vec<_>>()
824 .join(",")
825 );
826 if !overloads.insert(signature.clone()) {
827 return Err(syn::Error::new(
828 function.span(),
829 format!("duplicate scalar overload `{signature}`"),
830 ));
831 }
832 }
833 let mut operator_overloads = BTreeSet::new();
834 for (function, _, options) in &operator_specs {
835 let symbol = options.required_string("symbol", function.span())?;
836 let left = parse_type_option(options, "left", function.span())?;
837 let right = parse_type_option(options, "right", function.span())?;
838 let signature = format!("{}({},{})", symbol, quote!(#left), quote!(#right));
839 if !operator_overloads.insert(signature.clone()) {
840 return Err(syn::Error::new(
841 function.span(),
842 format!("duplicate operator overload `{signature}`"),
843 ));
844 }
845 }
846 let function_ids: BTreeMap<String, [u8; 16]> = scalar_specs
847 .iter()
848 .map(|(_, local_id, _)| (local_id.clone(), object_id(package_uuid, local_id)))
849 .chain(scalar_specs.iter().map(|(function, local_id, _)| {
850 (
851 function.sig.ident.to_string(),
852 object_id(package_uuid, local_id),
853 )
854 }))
855 .collect();
856 let operator_ids: BTreeMap<(String, String, String), [u8; 16]> = operator_specs
857 .iter()
858 .map(|(function, local_id, options)| {
859 let symbol = options.required_string("symbol", function.span())?;
860 let left = parse_type_option(options, "left", function.span())?;
861 let right = parse_type_option(options, "right", function.span())?;
862 Ok((
863 (
864 symbol,
865 quote!(#left).to_string(),
866 quote!(#right).to_string(),
867 ),
868 object_id(package_uuid, local_id),
869 ))
870 })
871 .collect::<syn::Result<_>>()?;
872 let opclass_ids: BTreeMap<String, [u8; 16]> = opclass_specs
873 .iter()
874 .map(|(function, local_id, _)| {
875 (
876 function.sig.ident.to_string(),
877 object_id(package_uuid, local_id),
878 )
879 })
880 .chain(
881 opclass_specs
882 .iter()
883 .map(|(_, local_id, _)| (local_id.clone(), object_id(package_uuid, local_id))),
884 )
885 .collect();
886
887 let type_descriptors = type_specs
888 .iter()
889 .map(|(ident, local_id, _)| generate_type_descriptor(ident, package_uuid, local_id));
890 let has_batch = !batch_specs.is_empty();
891 let mut generated_functions = Vec::new();
892 let mut function_descriptors = Vec::new();
893 for (function, local_id, options) in &scalar_specs {
894 let batch = batch_specs
895 .remove(local_id)
896 .or_else(|| batch_specs.remove(&function.sig.ident.to_string()));
897 let generated = generate_scalar_descriptor(
898 function,
899 local_id,
900 options,
901 batch.as_ref(),
902 package_uuid,
903 &type_map,
904 )?;
905 generated_functions.push(generated.items);
906 function_descriptors.push(generated.descriptor);
907 }
908 if let Some((name, (function, _))) = batch_specs.into_iter().next() {
909 return Err(syn::Error::new(
910 function.span(),
911 format!("batch adapter references unknown scalar `{name}`"),
912 ));
913 }
914 let operator_descriptors = operator_specs
915 .iter()
916 .map(|(function, local_id, options)| {
917 generate_operator_descriptor(
918 function,
919 local_id,
920 options,
921 package_uuid,
922 &type_map,
923 &function_ids,
924 )
925 })
926 .collect::<syn::Result<Vec<_>>>()?;
927 let mut opclass_items = Vec::new();
928 let mut opclass_descriptors = Vec::new();
929 for (function, local_id, options) in &opclass_specs {
930 let generated = generate_opclass_descriptor(
931 function,
932 local_id,
933 options,
934 package_uuid,
935 &type_map,
936 &operator_ids,
937 )?;
938 opclass_items.push(generated.items);
939 opclass_descriptors.push(generated.descriptor);
940 }
941 let mut planner_items = Vec::new();
942 let mut planner_descriptors = Vec::new();
943 for (function, local_id, options) in &planner_specs {
944 let generated = generate_planner_descriptor(
945 function,
946 local_id,
947 options,
948 package_uuid,
949 &function_ids,
950 &opclass_ids,
951 )?;
952 planner_items.push(generated.items);
953 planner_descriptors.push(generated.descriptor);
954 }
955
956 let package_caps: u64 = (if !type_specs.is_empty() { 1 } else { 0 })
957 | (if !scalar_specs.is_empty() { 1 << 1 } else { 0 })
958 | (if has_batch { 1 << 2 } else { 0 })
959 | (if !operator_specs.is_empty() {
960 1 << 3
961 } else {
962 0
963 })
964 | (if !opclass_specs.is_empty() { 1 << 4 } else { 0 })
965 | (if !planner_specs.is_empty() { 1 << 5 } else { 0 });
966
967 let source_contract = quote!(#(#items)*).to_string();
968 let descriptor_fingerprint = digest32(format!(
969 "radixdb.package.v1\0{}\0{}\0{}\0{}",
970 package_id,
971 package_name,
972 local_ids.iter().cloned().collect::<Vec<_>>().join("\0"),
973 source_contract,
974 ));
975 let fingerprint_tokens = bytes_tokens(&descriptor_fingerprint);
976 let package_id_tokens = bytes_tokens(&package_uuid);
977 let type_count = type_specs.len() as u32;
978 let function_count = scalar_specs.len() as u32;
979 let operator_count = operator_specs.len() as u32;
980 let opclass_count = opclass_specs.len() as u32;
981 let planner_count = planner_specs.len() as u32;
982
983 let generated = quote! {
984 #(#generated_functions)*
985 #(#opclass_items)*
986 #(#planner_items)*
987
988 static __RADIXDB_TYPES: [::radixdb_plugin::__private::abi::RadixAbiExternalTypeDescriptorV1; #type_count as usize] = [
989 #(#type_descriptors),*
990 ];
991 static __RADIXDB_FUNCTIONS: [::radixdb_plugin::__private::abi::RadixAbiScalarFunctionDescriptorV1; #function_count as usize] = [
992 #(#function_descriptors),*
993 ];
994 static __RADIXDB_OPERATORS: [::radixdb_plugin::__private::abi::RadixAbiOperatorDescriptorV1; #operator_count as usize] = [
995 #(#operator_descriptors),*
996 ];
997 static __RADIXDB_OPERATOR_CLASSES: [::radixdb_plugin::__private::abi::RadixAbiOperatorClassDescriptorV1; #opclass_count as usize] = [
998 #(#opclass_descriptors),*
999 ];
1000 static __RADIXDB_PLANNER_SUPPORT: [::radixdb_plugin::__private::abi::RadixAbiPlannerSupportDescriptorV1; #planner_count as usize] = [
1001 #(#planner_descriptors),*
1002 ];
1003 static __RADIXDB_PACKAGE: ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 =
1004 ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 {
1005 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1006 ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1
1007 >(#package_caps),
1008 package_id: [#(#package_id_tokens),*],
1009 package_name: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#package_name.as_bytes()),
1010 package_version: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#package_version.as_bytes()),
1011 abi_min_minor: ::radixdb_plugin::__private::abi::RADIX_ABI_MINOR,
1012 abi_max_minor: ::radixdb_plugin::__private::abi::RADIX_ABI_MINOR,
1013 reserved: 0,
1014 descriptor_fingerprint: [#(#fingerprint_tokens),*],
1015 type_count: #type_count,
1016 reserved_types: 0,
1017 types: __RADIXDB_TYPES.as_ptr(),
1018 function_count: #function_count,
1019 reserved_functions: 0,
1020 functions: __RADIXDB_FUNCTIONS.as_ptr(),
1021 operator_count: #operator_count,
1022 reserved_operators: 0,
1023 operators: __RADIXDB_OPERATORS.as_ptr(),
1024 operator_class_count: #opclass_count,
1025 reserved_operator_classes: 0,
1026 operator_classes: __RADIXDB_OPERATOR_CLASSES.as_ptr(),
1027 planner_support_count: #planner_count,
1028 reserved_planner_support: 0,
1029 planner_support: __RADIXDB_PLANNER_SUPPORT.as_ptr(),
1030 };
1031
1032 #[doc(hidden)]
1033 pub fn __radixdb_descriptor() -> &'static ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 {
1034 &__RADIXDB_PACKAGE
1035 }
1036 };
1037 let original_items = items.clone();
1038 let attrs = &module.attrs;
1039 let visibility = &module.vis;
1040 let module_ident = &module.ident;
1041 Ok(quote! {
1042 #(#attrs)* #visibility mod #module_ident {
1043 #(#original_items)*
1044 #generated
1045 }
1046
1047 #[unsafe(no_mangle)]
1048 pub unsafe extern "C" fn radixdb_plugin_entry_v1(
1049 host: *const ::radixdb_plugin::__private::abi::RadixHostApiV1,
1050 status: *mut u32,
1051 ) -> *const ::radixdb_plugin::__private::abi::RadixPluginDescriptorV1 {
1052 let outcome = ::std::panic::catch_unwind(|| {
1053 let host = unsafe { host.as_ref() }.ok_or(())?;
1054 if host.header.abi_major != ::radixdb_plugin::__private::abi::RADIX_ABI_MAJOR
1055 || host.header.abi_minor < ::radixdb_plugin::__private::abi::RADIX_ABI_MINOR
1056 {
1057 return Err(());
1058 }
1059 Ok(#module_ident::__radixdb_descriptor() as *const _)
1060 });
1061 match outcome {
1062 Ok(Ok(descriptor)) => {
1063 if let Some(status) = unsafe { status.as_mut() } {
1064 *status = ::radixdb_plugin::__private::abi::RADIX_STATUS_OK;
1065 }
1066 descriptor
1067 }
1068 Ok(Err(())) => {
1069 if let Some(status) = unsafe { status.as_mut() } {
1070 *status = ::radixdb_plugin::__private::abi::RADIX_STATUS_UNSUPPORTED_ABI;
1071 }
1072 ::std::ptr::null()
1073 }
1074 Err(_) => {
1075 if let Some(status) = unsafe { status.as_mut() } {
1076 *status = ::radixdb_plugin::__private::abi::RADIX_STATUS_PANIC;
1077 }
1078 ::std::ptr::null()
1079 }
1080 }
1081 }
1082 })
1083}
1084
1085fn generate_type_descriptor(ident: &Ident, package_id: [u8; 16], local_id: &str) -> TokenStream2 {
1086 let object = bytes_tokens(&object_id(package_id, local_id));
1087 quote! {
1088 ::radixdb_plugin::__private::abi::RadixAbiExternalTypeDescriptorV1 {
1089 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1090 ::radixdb_plugin::__private::abi::RadixAbiExternalTypeDescriptorV1
1091 >(0),
1092 object_id: [#(#object),*],
1093 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(
1094 <#ident as ::radixdb_plugin::RadixType>::LOCAL_ID.as_bytes()
1095 ),
1096 display_name: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(
1097 <#ident as ::radixdb_plugin::RadixType>::DISPLAY_NAME.as_bytes()
1098 ),
1099 codec_version: <#ident as ::radixdb_plugin::RadixType>::CODEC_VERSION,
1100 semantic_revision: <#ident as ::radixdb_plugin::RadixType>::SEMANTIC_REVISION,
1101 storage_kind: <#ident as ::radixdb_plugin::RadixType>::STORAGE_KIND,
1102 reserved_u16: 0,
1103 fixed_bytes: <#ident as ::radixdb_plugin::RadixType>::FIXED_BYTES,
1104 max_bytes: <#ident as ::radixdb_plugin::RadixType>::MAX_BYTES,
1105 reserved_u32: 0,
1106 capabilities: <#ident as ::radixdb_plugin::RadixType>::CAPABILITIES,
1107 codec_fingerprint: <#ident as ::radixdb_plugin::RadixType>::CODEC_FINGERPRINT,
1108 encode: <#ident as ::radixdb_plugin::RadixType>::ABI_ENCODE,
1109 decode: <#ident as ::radixdb_plugin::RadixType>::ABI_DECODE,
1110 equality: <#ident as ::radixdb_plugin::RadixType>::ABI_EQUALITY,
1111 hash: <#ident as ::radixdb_plugin::RadixType>::ABI_HASH,
1112 ordering: <#ident as ::radixdb_plugin::RadixType>::ABI_ORDERING,
1113 text_input: None,
1114 text_output: None,
1115 binary_input: None,
1116 binary_output: None,
1117 }
1118 }
1119}
1120
1121struct GeneratedDescriptor {
1122 items: TokenStream2,
1123 descriptor: TokenStream2,
1124}
1125
1126fn generate_scalar_descriptor(
1127 function: &ItemFn,
1128 local_id: &str,
1129 options: &Options,
1130 batch: Option<&(ItemFn, Options)>,
1131 package_id: [u8; 16],
1132 type_map: &BTreeMap<String, ([u8; 16], u32)>,
1133) -> syn::Result<GeneratedDescriptor> {
1134 options.reject_unknown(
1135 &[
1136 "id",
1137 "name",
1138 "semantic_revision",
1139 "cost",
1140 "cancellation",
1141 "max_output_bytes",
1142 ],
1143 &["immutable", "stable", "volatile", "strict", "parallel_safe"],
1144 )?;
1145 let name = options.required_string("name", function.span())?;
1146 validate_sql_name(&name, function.span())?;
1147 let semantic_revision = options.required_u32("semantic_revision", function.span())?;
1148 let cost = options.required_u32("cost", function.span())?;
1149 if semantic_revision == 0 || cost == 0 {
1150 return Err(syn::Error::new(
1151 function.span(),
1152 "semantic_revision and cost must be non-zero",
1153 ));
1154 }
1155 let volatility_flags = ["immutable", "stable", "volatile"]
1156 .into_iter()
1157 .filter(|flag| options.flags.contains(*flag))
1158 .collect::<Vec<_>>();
1159 if volatility_flags.len() != 1 {
1160 return Err(syn::Error::new(
1161 function.span(),
1162 "scalar requires exactly one of immutable, stable, volatile",
1163 ));
1164 }
1165 let volatility = match volatility_flags[0] {
1166 "immutable" => quote!(::radixdb_plugin::__private::abi::RADIX_VOLATILITY_IMMUTABLE),
1167 "stable" => quote!(::radixdb_plugin::__private::abi::RADIX_VOLATILITY_STABLE),
1168 _ => quote!(::radixdb_plugin::__private::abi::RADIX_VOLATILITY_VOLATILE),
1169 };
1170 let cancellation = options.required_string("cancellation", function.span())?;
1171 if cancellation != "bounded" {
1172 return Err(syn::Error::new(
1173 function.span(),
1174 "RadixDB 1.2 scalar cancellation must be \"bounded\"",
1175 ));
1176 }
1177 let arguments = function_arguments(function)?;
1178 let result = plugin_result_type(&function.sig.output)?;
1179 let argument_refs = arguments
1180 .iter()
1181 .map(|(_, ty)| type_ref_tokens(ty, type_map))
1182 .collect::<syn::Result<Vec<_>>>()?;
1183 let result_ref = type_ref_tokens(&result, type_map)?;
1184 let argument_count = arguments.len() as u32;
1185 let function_ident = &function.sig.ident;
1186 let wrapper = format_ident!("__radixdb_scalar_{}", function_ident);
1187 let argument_static = format_ident!(
1188 "__RADIXDB_ARGS_{}",
1189 function_ident.to_string().to_uppercase()
1190 );
1191 let decoded = arguments.iter().enumerate().map(|(index, (ident, ty))| {
1192 quote!(let #ident: #ty = <#ty as ::radixdb_plugin::ValueType>::decode_abi(&__arguments[#index])?;)
1193 });
1194 let call_arguments = arguments.iter().map(|(ident, _)| ident);
1195 let strict = options.flags.contains("strict");
1196 let parallel_safe = options.flags.contains("parallel_safe");
1197 let max_output = options
1198 .u32("max_output_bytes")?
1199 .map(|value| quote!(#value))
1200 .unwrap_or_else(|| quote!(<#result as ::radixdb_plugin::ValueType>::MAX_OUTPUT_BYTES));
1201
1202 let (batch_items, batch_pointer) = if let Some((batch, batch_options)) = batch {
1203 let generated = generate_batch_wrapper(batch, batch_options, argument_count, &result)?;
1204 let wrapper = generated.wrapper;
1205 (generated.items, quote!(Some(#wrapper)))
1206 } else {
1207 (quote!(), quote!(None))
1208 };
1209 let object = bytes_tokens(&object_id(package_id, local_id));
1210 Ok(GeneratedDescriptor {
1211 items: quote! {
1212 static #argument_static: [::radixdb_plugin::__private::abi::RadixAbiTypeRefV1; #argument_count as usize] = [
1213 #(#argument_refs),*
1214 ];
1215 unsafe extern "C" fn #wrapper(
1216 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1217 arguments: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
1218 argument_count: u32,
1219 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1220 ) -> u32 {
1221 unsafe {
1222 ::radixdb_plugin::__private::run_scalar(
1223 context,
1224 arguments,
1225 argument_count,
1226 output,
1227 #argument_count,
1228 #strict,
1229 |_context, __arguments, __output| {
1230 #(#decoded)*
1231 let __result: #result = #function_ident(#(#call_arguments),*)?;
1232 __output.push(__result)
1233 },
1234 )
1235 }
1236 }
1237 #batch_items
1238 },
1239 descriptor: quote! {
1240 ::radixdb_plugin::__private::abi::RadixAbiScalarFunctionDescriptorV1 {
1241 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1242 ::radixdb_plugin::__private::abi::RadixAbiScalarFunctionDescriptorV1
1243 >(0),
1244 object_id: [#(#object),*],
1245 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1246 display_name: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#name.as_bytes()),
1247 semantic_revision: #semantic_revision,
1248 argument_count: #argument_count,
1249 arguments: #argument_static.as_ptr(),
1250 result: #result_ref,
1251 volatility: #volatility,
1252 cancellation: ::radixdb_plugin::__private::abi::RADIX_CANCELLATION_BOUNDED,
1253 strict: #strict as u8,
1254 parallel_safe: #parallel_safe as u8,
1255 reserved_u16: 0,
1256 cost: #cost,
1257 max_output_bytes: #max_output,
1258 scalar: Some(#wrapper),
1259 batch: #batch_pointer,
1260 }
1261 },
1262 })
1263}
1264
1265struct GeneratedBatch {
1266 items: TokenStream2,
1267 wrapper: Ident,
1268}
1269
1270fn generate_batch_wrapper(
1271 function: &ItemFn,
1272 options: &Options,
1273 expected_columns: u32,
1274 result: &Type,
1275) -> syn::Result<GeneratedBatch> {
1276 options.reject_unknown(&["for_scalar", "rows_per_cancel_check"], &[])?;
1277 let rows = options.required_u32("rows_per_cancel_check", function.span())?;
1278 if rows == 0 {
1279 return Err(syn::Error::new(
1280 function.span(),
1281 "rows_per_cancel_check must be non-zero",
1282 ));
1283 }
1284 let inputs = function
1285 .sig
1286 .inputs
1287 .iter()
1288 .take(expected_columns as usize)
1289 .enumerate()
1290 .map(|(index, argument)| {
1291 let FnArg::Typed(argument) = argument else {
1292 return Err(syn::Error::new(
1293 argument.span(),
1294 "methods are not supported",
1295 ));
1296 };
1297 let inner = generic_inner(&argument.ty, "ColumnView").ok_or_else(|| {
1298 syn::Error::new(argument.ty.span(), "batch inputs must be ColumnView<'_, T>")
1299 })?;
1300 let name = format_ident!("__column_{index}");
1301 Ok((name, inner))
1302 })
1303 .collect::<syn::Result<Vec<_>>>()?;
1304 if function.sig.inputs.len() != expected_columns as usize + 2 {
1305 return Err(syn::Error::new(
1306 function.sig.inputs.span(),
1307 "batch signature must contain scalar columns, output builder, and call context",
1308 ));
1309 }
1310 let function_ident = &function.sig.ident;
1311 let wrapper = format_ident!("__radixdb_batch_{}", function_ident);
1312 let columns = inputs
1313 .iter()
1314 .enumerate()
1315 .map(|(index, (name, ty))| quote!(let #name = __input.column::<#ty>(#index)?;));
1316 let names = inputs.iter().map(|(name, _)| name);
1317 Ok(GeneratedBatch {
1318 items: quote! {
1319 unsafe extern "C" fn #wrapper(
1320 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1321 input: *const ::radixdb_plugin::__private::abi::RadixAbiBatchViewV1,
1322 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1323 ) -> u32 {
1324 unsafe {
1325 ::radixdb_plugin::__private::run_batch(
1326 context,
1327 input,
1328 output,
1329 #expected_columns,
1330 |__context, __input, __raw_output| {
1331 #(#columns)*
1332 let mut __output = ::radixdb_plugin::__private::column_builder::<#result>(__raw_output);
1333 #function_ident(#(#names),*, &mut __output, __context)
1334 },
1335 )
1336 }
1337 }
1338 },
1339 wrapper,
1340 })
1341}
1342
1343fn generate_operator_descriptor(
1344 function: &ItemFn,
1345 local_id: &str,
1346 options: &Options,
1347 package_id: [u8; 16],
1348 type_map: &BTreeMap<String, ([u8; 16], u32)>,
1349 function_ids: &BTreeMap<String, [u8; 16]>,
1350) -> syn::Result<TokenStream2> {
1351 options.reject_unknown(
1352 &[
1353 "id",
1354 "symbol",
1355 "semantic_revision",
1356 "function",
1357 "left",
1358 "right",
1359 "result",
1360 ],
1361 &[],
1362 )?;
1363 let symbol = options.required_string("symbol", function.span())?;
1364 let semantic_revision = options.required_u32("semantic_revision", function.span())?;
1365 if semantic_revision == 0 {
1366 return Err(syn::Error::new(
1367 function.span(),
1368 "semantic_revision must be non-zero",
1369 ));
1370 }
1371 let target = options.required_string("function", function.span())?;
1372 let function_id = function_ids.get(&target).ok_or_else(|| {
1373 syn::Error::new(
1374 function.span(),
1375 format!("unknown scalar function `{target}`"),
1376 )
1377 })?;
1378 let left = parse_type_option(options, "left", function.span())?;
1379 let right = parse_type_option(options, "right", function.span())?;
1380 let result = parse_type_option(options, "result", function.span())?;
1381 let left_ref = type_ref_tokens(&left, type_map)?;
1382 let right_ref = type_ref_tokens(&right, type_map)?;
1383 let result_ref = type_ref_tokens(&result, type_map)?;
1384 let object = bytes_tokens(&object_id(package_id, local_id));
1385 let function_object = bytes_tokens(function_id);
1386 Ok(quote! {
1387 ::radixdb_plugin::__private::abi::RadixAbiOperatorDescriptorV1 {
1388 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1389 ::radixdb_plugin::__private::abi::RadixAbiOperatorDescriptorV1
1390 >(0),
1391 object_id: [#(#object),*],
1392 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1393 symbol: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#symbol.as_bytes()),
1394 semantic_revision: #semantic_revision,
1395 reserved: 0,
1396 left: #left_ref,
1397 right: #right_ref,
1398 result: #result_ref,
1399 function_id: [#(#function_object),*],
1400 }
1401 })
1402}
1403
1404fn generate_opclass_descriptor(
1405 function: &ItemFn,
1406 local_id: &str,
1407 options: &Options,
1408 package_id: [u8; 16],
1409 type_map: &BTreeMap<String, ([u8; 16], u32)>,
1410 operator_ids: &BTreeMap<(String, String, String), [u8; 16]>,
1411) -> syn::Result<GeneratedDescriptor> {
1412 options.reject_unknown(
1413 &[
1414 "id",
1415 "semantic_revision",
1416 "access_method",
1417 "input",
1418 "key",
1419 "key_codec_revision",
1420 ],
1421 &[],
1422 )?;
1423 let semantic_revision = options.required_u32("semantic_revision", function.span())?;
1424 let key_codec_revision = options.required_u32("key_codec_revision", function.span())?;
1425 if semantic_revision == 0 || key_codec_revision == 0 {
1426 return Err(syn::Error::new(
1427 function.span(),
1428 "semantic_revision and key_codec_revision must be non-zero",
1429 ));
1430 }
1431 let method_name = options.required_string("access_method", function.span())?;
1432 let (method, required_strategies): (TokenStream2, &[(u16, &str)]) = match method_name.as_str() {
1433 "btree" => (
1434 quote!(::radixdb_plugin::__private::abi::RADIX_ACCESS_METHOD_BTREE),
1435 &[(1, "<"), (2, "<="), (3, "="), (4, ">="), (5, ">")],
1436 ),
1437 "hash" => (
1438 quote!(::radixdb_plugin::__private::abi::RADIX_ACCESS_METHOD_HASH),
1439 &[(1, "=")],
1440 ),
1441 "bitmap" => (
1442 quote!(::radixdb_plugin::__private::abi::RADIX_ACCESS_METHOD_BITMAP),
1443 &[(1, "=")],
1444 ),
1445 "hnsw" => {
1446 return Err(syn::Error::new(
1447 function.span(),
1448 "external HNSW operator classes require planner support outside v1.2",
1449 ));
1450 }
1451 _ => {
1452 return Err(syn::Error::new(
1453 function.span(),
1454 "operator class access_method must be core-owned btree/hash/bitmap/hnsw",
1455 ));
1456 }
1457 };
1458 let input = parse_type_option(options, "input", function.span())?;
1459 let key = parse_type_option(options, "key", function.span())?;
1460 let input_ref = type_ref_tokens(&input, type_map)?;
1461 let key_ref = type_ref_tokens(&key, type_map)?;
1462 let function_ident = &function.sig.ident;
1463 let wrapper = format_ident!("__radixdb_key_{}", function_ident);
1464 let law_test = format_ident!("__radixdb_operator_class_laws_{}", function_ident);
1465 let strategy_table = format_ident!(
1466 "__RADIXDB_STRATEGIES_{}",
1467 function_ident.to_string().to_uppercase()
1468 );
1469 let input_key = quote!(#input).to_string();
1470 let strategies = required_strategies
1471 .iter()
1472 .map(|(slot, symbol)| {
1473 let key = ((*symbol).to_owned(), input_key.clone(), input_key.clone());
1474 let object_id = operator_ids.get(&key).ok_or_else(|| {
1475 syn::Error::new(
1476 function.span(),
1477 format!("{method_name} operator class requires `{symbol}` over `{input_key}`"),
1478 )
1479 })?;
1480 let object_id = bytes_tokens(object_id);
1481 Ok(quote! {
1482 ::radixdb_plugin::__private::abi::RadixAbiBindingEntryV1 {
1483 slot: #slot,
1484 flags: 0,
1485 object_id: [#(#object_id),*],
1486 }
1487 })
1488 })
1489 .collect::<syn::Result<Vec<_>>>()?;
1490 let strategy_count = strategies.len() as u32;
1491 let object = bytes_tokens(&object_id(package_id, local_id));
1492 let fingerprint = bytes_tokens(&digest32(format!(
1493 "radixdb.opclass.v1\0{local_id}\0{semantic_revision}\0{key_codec_revision}\0{}",
1494 quote!(#function)
1495 )));
1496 let law_check = match method_name.as_str() {
1497 "btree" => quote!(
1498 ::radixdb_plugin::testing::check_btree_operator_class::<#input, #key>(#function_ident)
1499 ),
1500 "hash" => quote!(
1501 ::radixdb_plugin::testing::check_hash_operator_class::<#input>()
1502 ),
1503 "bitmap" => quote!(
1504 ::radixdb_plugin::testing::check_bitmap_operator_class::<#input, #key>(#function_ident)
1505 ),
1506 "hnsw" => quote!(Ok(())),
1507 _ => unreachable!("access method was validated above"),
1508 };
1509 Ok(GeneratedDescriptor {
1510 items: quote! {
1511 static #strategy_table: [
1512 ::radixdb_plugin::__private::abi::RadixAbiBindingEntryV1;
1513 #strategy_count as usize
1514 ] = [#(#strategies),*];
1515
1516 unsafe extern "C" fn #wrapper(
1517 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1518 value: *const ::radixdb_plugin::__private::abi::RadixAbiValueV1,
1519 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1520 ) -> u32 {
1521 let callback: fn(#input) -> ::radixdb_plugin::PluginResult<#key> = #function_ident;
1522 unsafe {
1523 ::radixdb_plugin::__private::run_key_encoder::<#input, #key>(
1524 context, value, output, callback
1525 )
1526 }
1527 }
1528
1529 #[cfg(test)]
1530 #[test]
1531 fn #law_test() {
1532 #law_check.expect("operator-class law check failed");
1533 ::radixdb_plugin::testing::check_operator_class_strategies::<#input>(
1534 &__RADIXDB_PACKAGE,
1535 #local_id,
1536 )
1537 .expect("operator-class strategy law check failed");
1538 }
1539 },
1540 descriptor: quote! {
1541 ::radixdb_plugin::__private::abi::RadixAbiOperatorClassDescriptorV1 {
1542 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1543 ::radixdb_plugin::__private::abi::RadixAbiOperatorClassDescriptorV1
1544 >(0),
1545 object_id: [#(#object),*],
1546 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1547 semantic_revision: #semantic_revision,
1548 access_method: #method,
1549 reserved_u16: 0,
1550 input_type: #input_ref,
1551 key_type: #key_ref,
1552 key_codec_revision: #key_codec_revision,
1553 strategy_count: #strategy_count,
1554 strategies: #strategy_table.as_ptr(),
1555 support_count: 0,
1556 reserved_u32: 0,
1557 supports: ::std::ptr::null(),
1558 fingerprint: [#(#fingerprint),*],
1559 encode_key: Some(#wrapper),
1560 }
1561 },
1562 })
1563}
1564
1565fn generate_planner_descriptor(
1566 function: &ItemFn,
1567 local_id: &str,
1568 options: &Options,
1569 package_id: [u8; 16],
1570 function_ids: &BTreeMap<String, [u8; 16]>,
1571 opclass_ids: &BTreeMap<String, [u8; 16]>,
1572) -> syn::Result<GeneratedDescriptor> {
1573 options.reject_unknown(
1574 &[
1575 "id",
1576 "name",
1577 "semantic_revision",
1578 "for_function",
1579 "operator_class",
1580 "max_spans",
1581 "max_output_bytes",
1582 ],
1583 &["exact", "always_recheck"],
1584 )?;
1585 let _name = options.required_string("name", function.span())?;
1586 let semantic_revision = options.required_u32("semantic_revision", function.span())?;
1587 let max_spans = options.required_u32("max_spans", function.span())?;
1588 let max_output_bytes = options.required_u32("max_output_bytes", function.span())?;
1589 let policies = ["exact", "always_recheck"]
1590 .into_iter()
1591 .filter(|flag| options.flags.contains(*flag))
1592 .collect::<Vec<_>>();
1593 if policies.len() != 1 {
1594 return Err(syn::Error::new(
1595 function.span(),
1596 "planner support requires exactly one of exact or always_recheck",
1597 ));
1598 }
1599 if semantic_revision == 0 || max_spans == 0 || max_spans > 4096 || max_output_bytes == 0 {
1600 return Err(syn::Error::new(
1601 function.span(),
1602 "semantic_revision and planner bounds must be non-zero and max_spans <= 4096",
1603 ));
1604 }
1605 let target = options.required_string("for_function", function.span())?;
1606 let target_function = function_ids.get(&target).ok_or_else(|| {
1607 syn::Error::new(
1608 function.span(),
1609 format!("unknown target function `{target}`"),
1610 )
1611 })?;
1612 let opclass = options.required_string("operator_class", function.span())?;
1613 let target_opclass = opclass_ids.get(&opclass).ok_or_else(|| {
1614 syn::Error::new(
1615 function.span(),
1616 format!("unknown operator class `{opclass}`"),
1617 )
1618 })?;
1619 let recheck = if policies[0] == "exact" {
1620 quote!(::radixdb_plugin::__private::abi::RADIX_RECHECK_EXACT)
1621 } else {
1622 quote!(::radixdb_plugin::__private::abi::RADIX_RECHECK_ALWAYS)
1623 };
1624 let function_ident = &function.sig.ident;
1625 let wrapper = format_ident!("__radixdb_planner_{}", function_ident);
1626 let object = bytes_tokens(&object_id(package_id, local_id));
1627 let target_function = bytes_tokens(target_function);
1628 let target_opclass = bytes_tokens(target_opclass);
1629 let fingerprint = bytes_tokens(&digest32(format!(
1630 "radixdb.planner.v1\0{local_id}\0{semantic_revision}\0{max_spans}\0{max_output_bytes}\0{}",
1631 policies[0]
1632 )));
1633 Ok(GeneratedDescriptor {
1634 items: quote! {
1635 unsafe extern "C" fn #wrapper(
1636 context: *const ::radixdb_plugin::__private::abi::RadixAbiCallContextV1,
1637 predicate: ::radixdb_plugin::__private::abi::RadixAbiSliceV1,
1638 output: *const ::radixdb_plugin::__private::abi::RadixAbiResultBuilderV1,
1639 ) -> u32 {
1640 unsafe {
1641 ::radixdb_plugin::__private::run_planner(
1642 context,
1643 predicate,
1644 output,
1645 |predicate, output| #function_ident(predicate, output),
1646 )
1647 }
1648 }
1649 },
1650 descriptor: quote! {
1651 ::radixdb_plugin::__private::abi::RadixAbiPlannerSupportDescriptorV1 {
1652 header: ::radixdb_plugin::__private::abi::RadixAbiHeaderV1::new::<
1653 ::radixdb_plugin::__private::abi::RadixAbiPlannerSupportDescriptorV1
1654 >(0),
1655 object_id: [#(#object),*],
1656 local_id: ::radixdb_plugin::__private::abi::RadixAbiSliceV1::from_static(#local_id.as_bytes()),
1657 semantic_revision: #semantic_revision,
1658 max_spans: #max_spans,
1659 max_output_bytes: #max_output_bytes,
1660 recheck_policy: #recheck,
1661 reserved_u16: 0,
1662 target_function_id: [#(#target_function),*],
1663 target_operator_class_id: [#(#target_opclass),*],
1664 fingerprint: [#(#fingerprint),*],
1665 callback: Some(#wrapper),
1666 }
1667 },
1668 })
1669}
1670
1671fn function_arguments(function: &ItemFn) -> syn::Result<Vec<(Ident, Type)>> {
1672 function
1673 .sig
1674 .inputs
1675 .iter()
1676 .map(|argument| {
1677 let FnArg::Typed(argument) = argument else {
1678 return Err(syn::Error::new(
1679 argument.span(),
1680 "plugin methods are not supported",
1681 ));
1682 };
1683 let syn::Pat::Ident(ident) = argument.pat.as_ref() else {
1684 return Err(syn::Error::new(
1685 argument.pat.span(),
1686 "plugin arguments require simple names",
1687 ));
1688 };
1689 if matches!(argument.ty.as_ref(), Type::Reference(_)) {
1690 return Err(syn::Error::new(
1691 argument.ty.span(),
1692 "scalar arguments must be owned typed values",
1693 ));
1694 }
1695 Ok((ident.ident.clone(), (*argument.ty).clone()))
1696 })
1697 .collect()
1698}
1699
1700fn plugin_result_type(output: &ReturnType) -> syn::Result<Type> {
1701 let ReturnType::Type(_, ty) = output else {
1702 return Err(syn::Error::new(
1703 output.span(),
1704 "plugin function must return PluginResult<T>",
1705 ));
1706 };
1707 generic_inner(ty, "PluginResult")
1708 .ok_or_else(|| syn::Error::new(ty.span(), "plugin function must return PluginResult<T>"))
1709}
1710
1711fn generic_inner(ty: &Type, expected: &str) -> Option<Type> {
1712 let Type::Path(path) = ty else {
1713 return None;
1714 };
1715 let segment = path.path.segments.last()?;
1716 if segment.ident != expected {
1717 return None;
1718 }
1719 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1720 return None;
1721 };
1722 arguments.args.iter().find_map(|argument| match argument {
1723 GenericArgument::Type(ty) => Some(ty.clone()),
1724 _ => None,
1725 })
1726}
1727
1728fn vec_inner(ty: &Type) -> Option<&Type> {
1729 let Type::Path(path) = ty else {
1730 return None;
1731 };
1732 let segment = path.path.segments.last()?;
1733 if segment.ident != "Vec" {
1734 return None;
1735 }
1736 let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
1737 return None;
1738 };
1739 arguments.args.iter().find_map(|argument| match argument {
1740 GenericArgument::Type(ty) => Some(ty),
1741 _ => None,
1742 })
1743}
1744
1745fn parse_type_option(options: &Options, name: &str, span: Span) -> syn::Result<Type> {
1746 let expression = options
1747 .values
1748 .get(name)
1749 .ok_or_else(|| syn::Error::new(span, format!("missing required `{name}`")))?;
1750 match expression {
1751 Expr::Path(path) => Ok(Type::Path(syn::TypePath {
1752 qself: None,
1753 path: path.path.clone(),
1754 })),
1755 _ => Err(syn::Error::new(
1756 expression.span(),
1757 "expected Rust type path",
1758 )),
1759 }
1760}
1761
1762fn type_ref_tokens(
1763 ty: &Type,
1764 external: &BTreeMap<String, ([u8; 16], u32)>,
1765) -> syn::Result<TokenStream2> {
1766 let ty = generic_inner(ty, "Option").unwrap_or_else(|| ty.clone());
1767 let ident = terminal_type_ident(&ty)
1768 .ok_or_else(|| syn::Error::new(ty.span(), "unsupported plugin signature type"))?;
1769 let builtin = match ident.as_str() {
1770 "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" => Some(1_u16),
1771 "f32" | "f64" => Some(2_u16),
1772 "BoundedText" => Some(3_u16),
1773 "bool" => Some(4_u16),
1774 "BoundedBytes" => Some(11_u16),
1775 _ => None,
1776 };
1777 if let Some(tag) = builtin {
1778 return Ok(quote!(::radixdb_plugin::__private::abi::RadixAbiTypeRefV1::builtin(#tag)));
1779 }
1780 let (object_id, codec) = external.get(&ident).ok_or_else(|| {
1781 syn::Error::new(
1782 ty.span(),
1783 "signature type is neither a supported bounded built-in nor a local RadixType",
1784 )
1785 })?;
1786 let object = bytes_tokens(object_id);
1787 Ok(
1788 quote!(::radixdb_plugin::__private::abi::RadixAbiTypeRefV1::external(
1789 [#(#object),*], #codec
1790 )),
1791 )
1792}
1793
1794fn terminal_type_ident(ty: &Type) -> Option<String> {
1795 let Type::Path(path) = ty else {
1796 return None;
1797 };
1798 path.path
1799 .segments
1800 .last()
1801 .map(|segment| segment.ident.to_string())
1802}
1803
1804fn admit_local_id(ids: &mut BTreeSet<String>, value: &str, span: Span) -> syn::Result<()> {
1805 validate_local_id(value, span)?;
1806 if !ids.insert(value.to_string()) {
1807 return Err(syn::Error::new(
1808 span,
1809 format!("duplicate stable local id `{value}`"),
1810 ));
1811 }
1812 Ok(())
1813}
1814
1815fn validate_local_id(value: &str, span: Span) -> syn::Result<()> {
1816 if value.is_empty()
1817 || value.len() > 255
1818 || !value
1819 .bytes()
1820 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
1821 {
1822 return Err(syn::Error::new(
1823 span,
1824 "local id must match [a-z0-9_]{1,255}",
1825 ));
1826 }
1827 Ok(())
1828}
1829
1830fn validate_package_name(value: &str, span: Span) -> syn::Result<()> {
1831 if value.is_empty()
1832 || value.len() > 128
1833 || !value.bytes().all(|byte| {
1834 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
1835 })
1836 {
1837 return Err(syn::Error::new(
1838 span,
1839 "package name must match [a-z0-9_-]{1,128}",
1840 ));
1841 }
1842 Ok(())
1843}
1844
1845fn validate_sql_name(value: &str, span: Span) -> syn::Result<()> {
1846 if value.is_empty() || value.len() > 255 || value.contains('\0') {
1847 return Err(syn::Error::new(span, "SQL name is empty or too large"));
1848 }
1849 Ok(())
1850}
1851
1852fn object_id(package_id: [u8; 16], local_id: &str) -> [u8; 16] {
1853 let mut digest = Sha256::new();
1854 digest.update(b"radixdb.plugin.object.v1\0");
1855 digest.update(package_id);
1856 digest.update((local_id.len() as u32).to_le_bytes());
1857 digest.update(local_id.as_bytes());
1858 digest.finalize()[..16].try_into().unwrap()
1859}
1860
1861fn digest32(value: String) -> [u8; 32] {
1862 Sha256::digest(value.as_bytes()).into()
1863}
1864
1865fn bytes_tokens<const N: usize>(bytes: &[u8; N]) -> Vec<syn::LitInt> {
1866 bytes
1867 .iter()
1868 .map(|byte| syn::LitInt::new(&byte.to_string(), Span::call_site()))
1869 .collect()
1870}
1871
1872#[cfg(test)]
1873mod tests {
1874 use super::object_id;
1875
1876 #[test]
1877 fn sql_rename_cannot_change_object_identity() {
1878 let package = [0x5a; 16];
1879 let before_sql_rename = object_id(package, "distance");
1880 let after_sql_rename = object_id(package, "distance");
1881 assert_eq!(before_sql_rename, after_sql_rename);
1882 assert_ne!(before_sql_rename, object_id(package, "distance_v2"));
1883 assert_ne!(before_sql_rename, object_id([0xa5; 16], "distance"));
1884 }
1885}