Oyelowo / surreal-orm

Powerful & expressive ORM/query-builder/static checker for raw queries/Fully Automated migration tooling , designed to offer an intuitive API, strict type-checking, novel features, & full specification support. It provides a fresh perspective in data management. Currently supports SurrealDB engine. RDMSs(PG, MYSQL etc) and others coming soon
82 stars 1 forks source link

Implement select query macro with auto-inferred return type #69

Open Oyelowo opened 1 month ago

Oyelowo commented 1 month ago

The API might look like this (Generated by a procedural macro):

use surrealdb::{Surreal};
use surreal_query_builder::sql;
use serde::{Deserialize, Serialize};
use serde_json;

// Query
// "SELECT *, name, age, address.coordinates AS coordinates FROM person
// WHERE ->knows->person->(knows WHERE influencer = $influencer) TIMEOUT 5s
// "
//

struct SelectionMeta {
    query_string: String,
    selected_field_type_metadata: String, // Tokenstream
    variables_interpolated: String, // Tokenstream
}

struct Coordinates;
// SELECT * FROM person WHERE ->knows->person->(knows WHERE influencer = true) TIMEOUT 5s;
select_infer!(PersonSelect, "SELECT * person", {
    person: Person
});

#[derive(Serialize, Deserialize)]
struct PersonSelect {
    #[serde(flatten)]
    person: Person,
}

select_infer!(User, "SELECT *, name, age, address.coordinates AS coordinates FROM person
WHERE ->knows->person->(knows WHERE influencer = $influencer) TIMEOUT 5s
", {
    coorinates: Coordinates
})

// Return Type
#[derive(Serialize, Deserialize)]
struct User {
    #[serde(flatten)]
    person: Person, // Require this from the dev
    name: serde_json::Value,
    age: serde_json::Value,
    coordinates: Coordinates,
}

trait ToValue {
    fn to_value(self) -> sql::Value;
}

impl<T: Into<sql::Value>> ToValue for T {
    fn to_value(self) -> sql::Value {
        let value: sql::Value = self.into();
        value
        // serde_json::Value::String(self.into().to_string())
    }
}

struct Variables<'a> {
    influencer: &'a dyn ToValue,
}

struct SomeSelection {
    name: String,
    age: u8,
    coordinates: u8,
}

impl User {
    pub fn select_variables<'a>(db: Surreal<>, variables: Variables<'a>) ->  User {
        let data: User = query(
            "SELECT *, name, age, count(), array::sum(address.coordinates) AS coordinates FROM person
            WHERE ->knows->person->(knows WHERE influencer = $influencer) TIMEOUT 5s",
            variables
        ).bind("influencer", variables.influencer)
       .await
      .unwrap()
      .take(0)
     .unwrap();

    data

        ;

    }
}

fn test_it(){
    let variables = Variables {
        influencer: &true,
    };
    User::select(variables);
}
Oyelowo commented 1 month ago

After weighing the cost and benefits of this, I am not yet convinced that it is worth it, considering that the current APIs cover probably 99% of use-cases.

Oyelowo commented 1 month ago

Query "SELECT *, name, age, address.coordinates AS coordinates FROM person WHERE ->knows->person->(knows WHERE influencer = $influencer) TIMEOUT 5s "

PROS

CONS

Oyelowo commented 1 month ago

With the new pick macro implemented, we can use this to infer nested fields. and prompt user to provide the trait for each nested level field.

The nice thing about this pattern is that, if the user provides the wrong picker trait, it wont compile. So, we still get compile-time check.

Also, we will have to implement a pattern matching engine for all the surreal functions and casting to mapping the wrapped data into their respective types.

This will generate the type at the outer level.

struct Info {
    age: u8,
    class: String
}

struct User {
    name: String,
    info: Info,
}

select name, info.age
type nameField = pick!(User as UserPicker, [name])
type InfoField = pick!(User as UserPicker, [info]);
type ageField = pick!(InfoField as <Prompt the user to import the trait for this and must be `InfoPicker`>, [age]);

returnType {
    name: nameField,
   age: ageField
 }

2. We can also implement an inline type inference that uses generics for all fields and then uses a builder pattern for each field to infer their return type. The downside of this is that, we cant use the type declaration outside the function and even though, it might be able to infer the return type, it wont generate proper type that can be used outside:

// original
struct User {
    name: String,
    age: u8
}

// duplicate:
struct User<T : Into<serde_json::Value>, U: Into<serde_json::Value> {
name: T,
age: U
Oyelowo commented 1 month ago

For reference/link fields, i'll create double/triple types for each. One for the link container or id, and the other for the wrapped foreign object which will provide the foreign fields. I'll come up with a convention for it, maybe some prefixes e.g friends: LinkMany<Friend> or Vec<SurrealSimpleId<Friend>> and __link_many___friends: Friend