Closed ra0x3 closed 9 months ago
This ☝🏼 is all pseudo-code, but CC @Voxelot @deekerno @lostman for comment, as this (implementing .find()
) is gonna block #886 because we need to be able to lookup the CoinOutput
(for a given InputCoin
) by the owner: Address
field (which we currently don't support)
@ra0x3,
We would have to generate these functions ::field_name::equals()
for all types in the schema, correct?
vec![OrderEntity::amount::equals(5), OrderEntity::address::equals("0x00001")]
And they would produce SQLFragment
s directly?
@lostman
Object
and Union
with @entity
would get one of these field structs (e.g., FooId
) and each of these field structs would supports SQLFragment
for operations such as eq
, gt
, gte
, lt
, etc.I have to say that I'm not the greatest fan of using a vector for multiple parameters. I'd much rather a cleaner "object-based" syntax similar to how selection parameters are done in Prisma. However, I don't think that we can easily do that right now given the fact that Rust doesn't allow for anonymous structs; additionally, we'd want type safety wherever possible so doing something with JSON would probably not be worth the headache. In any case, I think this style is probably the best type-safe way and will work for now in order to not block the predicate support work.
Perhaps in the future, we could leverage the builder pattern to do something in a more functional style:
let found_order = OrderEntity
.find()
.filter(OrderEntity::amount::gt(5))
.filter(OrderEntity::address::equals("0x00001")
.select()?
...or something to that effect.
@deekerno
vec
syntax if we can. Entity
limited to the basics (e.g., find
in this case)
let found_order = OrderEntity.find(OrderEntity::amount::gt(5).and(OrderEntity::address::eq("0x00001"));
const
field (e.g., FooId
) would have to themselves implement the builder-ish pattern for things like eq
, gt
, etc.@deekerno @lostman thoughts? ☝🏼
I like that idea even better and we should aim for that if possible. The only hesitation I have is how we would support operator combination, if at all.
From the Prisma docs:
const result = await prisma.user.findMany({
where: {
OR: [
{
email: {
endsWith: 'prisma.io',
},
},
{ email: { endsWith: 'gmail.com' } },
],
NOT: {
email: {
endsWith: 'hotmail.com',
},
},
},
select: {
email: true,
},
})
I'd imagine that the vector style would have to make a reappearance here, but we can cross the proverbial bridge when we get to it.
@deekerno The above ☝🏼 would be written as
User.find(
User::email::ends_with("prisma.io")
.or(User::email::ends_with("gmail.com")
.and(User::email::not_ends_with("hotmail.com")
);
not_ends_with
, or ends_with
) as we want. select
, we'd have to return the full Entity
or not (given that we have to know how much space to allocate for the returned entity)Again, I'm not married to my method so feel free to push back
I don't think this is valid Rust:
OrderEntity::amount::gt(5)
^^^^^^^^^^^
type ^^^^^^
? ^^
associated function
I was thinking about something like this:
pub struct Constraint<T> {
constraint: String,
phantom: std::marker::PhantomData<T>,
}
pub trait Field<T> {
// TODO: Type needs to be convertible SQL fragment
type Type: Sized + std::fmt::Debug;
const NAME: &'static str;
fn equals(val: Self::Type) -> Constraint<T> {
Constraint {
constraint: format!("{} = '{:?}'", Self::NAME, val),
phantom: std::marker::PhantomData,
}
}
}
Then:
struct Order {
id: usize,
amount: i32,
}
struct OrderIdField;
impl Field<Order> for OrderIdField {
type Type = usize;
const NAME: &'static str = "id";
}
struct OrderAmountField;
impl Field<Order> for OrderAmountField {
type Type = i32;
const NAME: &'static str = "amount";
}
And using the vec!
syntax (for now):
pub trait Entity<'a>: Sized + PartialEq + Eq + std::fmt::Debug {
fn find(constraints: Vec<Constraint<Self>>) -> String {
let mut buf = String::new();
for c in constraints {
if !buf.is_empty() {
buf += " AND ";
}
buf += &c.constraint
}
buf
}
}
So, we'd have something like:
find(vec![OrderIdField::eq(1usize), OrderAmountField::lt(123i32)])
Of course, we can instead have a simple AST:
pub struct Constraint<T> {
constraint: String,
phantom: std::marker::PhantomData<T>,
}
impl<T> Constraint<T> {
pub fn and(self, other: impl Into<Expr<T>>) -> Expr<T> {
Expr::And(Box::new(Expr::Leaf(self)), Box::new(other.into()))
}
}
impl<T> From<Constraint<T>> for Expr<T> {
fn from(c: Constraint<T>) -> Expr<T> {
Expr::Leaf(c)
}
}
pub enum Expr<T> {
And(Box<Expr<T>>, Box<Expr<T>>),
Leaf(Constraint<T>)
}
impl<T> Expr<T> {
pub fn and(self, other: impl Into<Expr<T>>) -> Expr<T> {
Expr::And(Box::new(self), Box::new(other.into()))
}
}
An example:
let x: Expr<Block> = {
let e1: Constraint<Block> = BlockIdField::equals(block_frag.id.clone());
let e2: Constraint<Block> = BlockIdField::equals(block_frag.id.clone());
// constraint and constraint = expr
e1.and(e2)
};
let y: Expr<Block> = {
let e = BlockIdField::equals(block_frag.id.clone());
// constraint and expr = expr
// e.and(x)
// or expr and constraint = expr
x.and(e)
};
let z: Expr<Block> = {
let e1: Constraint<Block> = BlockIdField::equals(block_frag.id.clone());
let e2: Constraint<Block> = BlockIdField::equals(block_frag.id.clone());
e1.and(e2)
};
// expr and expr = expr
let v = z.and(y);
It may look a little convoluted, but it shows that these can be easily mixed and matched.
Another:
let complex = BlockIdField::equals(block_frag.id.clone())
.and(BlockConsensusField::equals(consensus.id.clone()))
.and(BlockHeaderField::equals(header.id.clone()));
@lostman
Vec
as @deekerno mentioned?
Order.find(where: Order::id::eq(5).and(Order::amount::gt(6).or(Order::amount::lt(7));
) or something like that.and()
within an .and()
@ra0x3, the AST is an internal implementation detail and pretty simple. Avoiding it wouldn't make anything simpler.
Some questions:
Option<U32>
then SomeField::eq(None)
, or SomeField::lt(Some(5))
should be supported, correct?The first would translate to WHERE some_field = null
and the second to WHERE some_field < 5
, correct?
Array
fields? What operations should we support if we have Array<U32>
?Some form of any
and all
come to mind.
let arr2 = TransactionInputsField::any(
TransactionInputsField::equals(
SizedAsciiString::new("".to_string()).unwrap(),
)
.and(TransactionInputsField::equals(
SizedAsciiString::new("xyz".to_string()).unwrap(),
)),
);
This is a little verbose but can be shortened to:
let arr2: ArrayExpr<Transaction> = {
type T = TransactionInputsField;
T::any(
T::equals(SizedAsciiString::new("".to_string()).unwrap())
.and(T::equals(SizedAsciiString::new("xyz".to_string()).unwrap())),
)
};
Some options for structuring the DSL for writing constraint expressions:
Using modules:
let c: Constraint<ScriptTransaction> = {
use script_transaction::*;
// ((gas_price < '100' OR gas_price > '200') AND gas_limit = '1000')
gas_price()
.lt(100i32)
.or(gas_price().gt(200))
.and(gas_limit().eq(1000))
};
Using associated functions:
let z = ScriptTransaction::gas_price()
.lt(100i32)
.or(ScriptTransaction::gas_price().gt(200))
.and(ScriptTransaction::gas_limit().eq(1000));
Which could be shortened with:
use ScriptTransactionResult as T;
T::gas_price()...
Having a module with these functions seems cleaner.
I still need to think about how array fields would fit into this.
ScriptTransaction.find()
? let result = ScriptTransaction.find(ScriptTransaction::gas_price()
.lt(100i32)
.or(ScriptTransaction::gas_price().gt(200))
.and(ScriptTransaction::gas_limit().eq(1000)));
@lostman
Option
Some(x)
would be the same as just x
None
would be is NULL
Vec<T>
to .find()
Context
Entity
What we currently have
impl Entity
in ourfuel_indexer_macros::decoder
module as follows:What we want
#const_fields
would be a series of tokens for each field on theEntity
as aconst
So with the following schema
You'd get the following
const
fields on theEntity
This ☝🏼 would allow us to build a
.find()
method as follows:Entity::field::equals(value)
returnsString
fragments that can be aggregated into an arbitrary singleSELECT
query and easily passed to the DB via the FFIFuture work
SQLFragment
trait to support other things such as