The largest user experience improvement for Rust users of SurrealDB 3.0 is the surrealdb-types crate, which was created to have a shared public value type system for SurrealDB.
This crate was separated from the SurrealDB core to decouple types and type conversions from core database logic, and to allow other crates to make use of types on their own without needing the entire SurrealDB database along with it.
The SurrealValue trait
The main difference between SurrealDB 3.0 and previous versions for Rust users is the existence of a SurrealValue trait that can be derived automatically. Deriving this trait is all that is needed to use a Rust type for serialization and deserialization.
The SurrealValue trait can be implemented manually via three methods: one to indicate the matching SurrealDB type, a second to convert into a SurrealDB Value, and a third to convert out of a SurrealDB Value.
This crate includes a kind! macro which allows a SurrealQL type to be used directly instead of its Rust equivalent.
This macro is especially useful when working with types like literals which are similar to enums but can specify exact possible values in a way that Rust would require deriving TryFrom to work. In this case, the SurrealValue trait can be implemented manually and the kind! macro used for its kind_of() method.
This is technically possible without the macro, but requires a lot more boilerplate. Here is the output when using cargo expand to show the generated code for the example above.
The following example shows the kind! macro used for a Rust enum that manually implements SurrealValue, along with examples of its use from the Rust side to the SurrealDB side, and vice versa.
fnfrom_value(value: Value)->Result<Self,Error> where Self: Sized, { letValue::Object(o)=value else{ returnErr(Error::thrown("Should have been an object".to_string())); }; letSome(Value::String(status))=o.get("status")else{ returnErr(Error::thrown( "Error trying to get 'status' field".to_string(), )); }; matchstatus.as_str(){ "Good"=>Ok(Response::Good), status @ "GoodWithNotification"=>{ Ok(Response::GoodWithNotification(status.to_string())) } "Error"=>{ letSome(Value::Datetime(at))=o.get("at")else{ returnErr(Error::thrown("Error trying to get 'at' field".to_string())); }; letSome(Value::String(reason))=o.get("reason")else{ returnErr(Error::thrown( "Error trying to get 'reason' field".to_string(), )); }; Ok(Response::Error(MyError{ at: at.clone(), reason: reason.clone(), })) } _=>Err(Error::thrown("No status field for some reason".to_string())), } }
// Turning DB results into Rust enum letmutstatuses=db.query(" { status: 'Good' }; { status: 'GoodWithNotification', notification: 'We need things to make us go. We need help.' }; { status: 'Error', at: d'1914-07-28', reason: 'General conflagration'}; ").await.unwrap();
// Turn Rust enum into Values, // use them in the CONTENT clause // and then print the result letgood=Response::Good; letgood_but=Response::GoodWithNotification("Keep it up!".into()); leterror=Response::Error(MyError{ at: Datetime::now(), reason: "Error: can't think of interesting error message".into(), });
// Insert it into a query to create a record letres=db .query("CREATE ONLY person CONTENT $person") .bind(("person",as_person)) .await .unwrap() .take::<Value>(0) .unwrap();
A Value can be manually constructed using any of the various structs and enums contained within it. This is particularly useful when constructing a complex ID made up of a table name and an array for the key.
The SurrealValue trait can be customised using the surreal attribute. These are used in a similar way to Serde attributes, though created in order to interact with SurrealQL in particular and thus often somewhat different.
The currently available attributes are as follows.
surreal(default)
The surreal(default) attribute is used to default to certain values when these values are not present when deserializing. The Default trait is required to use this.
letmuthas_two_fields=db .query("CREATE user SET num = 10, other_num = 20") .await .unwrap();
letmuthas_one_field=db.query("CREATE user SET num = 5").await.unwrap();
println!( "Regular deserialization from DB result: {}", has_two_fields .take::<Option<UserData>>(0) .unwrap() .unwrap() .into_value() .to_sql() );
println!( "Deserialization using DB result plus default value: {}", has_one_field .take::<Option<UserDataDefault>>(0) .unwrap() .unwrap() .into_value() .to_sql() ) }
Regular deserialization from DB result: { num: 10, other_num: 20 } Deserialization using DB result plus default value: { num: 5, other_num: 20 }
surreal(rename)
The surreal(rename) attribute is used to provide a different name for a field on the SurrealDB side than the one used in the Rust code.
Before attribute: { Debug: 'User1' } After uppercase: { DEBUG: 'User1' } After lowercase: { debug: 'User1' }
surreal(tuple)
As SurrealQL does not have a tuple type, this attribute can be used to interface in which a Rust tuple struct is treated as an array (instead of a single value) and vice versa.
Without tuple attribute: 555 With tuple attribute: [555]
surreal(untagged)
The surreal(untagged) attribute removes the tag from the variant of an enum. This is similar to using VALUE in SurrealQL to show only the value and not the field name of a record.
Before untagged: { Debug: 'User1' } After untagged: 'User1'
surreal(tag)
The surreal(tag) attribute can be used to give a tag to a variant. This will create a structure in which the new tag value is the field name, and the variant its value.
Before content: { Debug: 'User1' } After content: { log_level: 'Debug', user: 'User1' }
surreal(value)
This attribute can be used on the fields of an enum marked with surreal(untagged) to give it a substitute value. The value that follows this attribute can be a NONE, NULL, bool, string, int, or float.