-
Notifications
You must be signed in to change notification settings - Fork 279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Expression Builder API feedback #494
Comments
Hey 👋 Thank you for taking the time to write this. We went with such abbreviations because:
We thought about
|
Thanks @igalklebanov! I think I agree with not adding qb.where(({ $, and }) => and([
$('first_name', '=', 'Jennifer'),
$('last_name', '!=', 'Tremblay'),
$('updated_at', '>', lastMonth)
]))
db.updateTable('person')
.set(({ $ }) => ({
age: $('age', '+', 1)
})) I'm not necessarily arguing for that, but it would seem to be the logical extension of the current design. I.e., some of the downsides with In the above example, |
If we renamed We need separate binary expression and a comparison expression. If we only had
Another reason for the abbreviated names was that people won't find the methods by searching no matter what we name them. They should be discovered from examples. The API documentation is filled with examples (which people don't seem to find). The new site will have those examples in a more accessible/searchable place. |
We could replace I kind of like the idea of replacing both with |
Hmm…I think the obvious candidate would be qb.where(({ e, and }) => and([
e('first_name', '=', 'Jennifer'),
e('last_name', '!=', 'Tremblay'),
e('updated_at', '>', lastMonth)
]))
db.updateTable('person')
.set(({ e }) => ({
age: e('age', '+', 1)
})) Or possibly qb.where(({ xp, and }) => and([
xp('first_name', '=', 'Jennifer'),
xp('last_name', '!=', 'Tremblay'),
xp('updated_at', '>', lastMonth)
]))
db.updateTable('person')
.set(({ xp }) => ({
age: xp('age', '+', 1)
})) Could also do just qb.where(({ x, and }) => and([
x('first_name', '=', 'Jennifer'),
x('last_name', '!=', 'Tremblay'),
x('updated_at', '>', lastMonth)
]))
db.updateTable('person')
.set(({ x }) => ({
age: x('age', '+', 1)
})) |
Another direction would be qb.where(({ is, and }) => and([
is('first_name', '=', 'Jennifer'),
is('last_name', '!=', 'Tremblay'),
is('updated_at', '>', lastMonth)
]))
db.updateTable('person')
.set(({ is }) => ({
age: is('age', '+', 1)
})) I think that’s my favorite by far: it’s actually a full word, while still being shorter than the current names. If we don’t like |
I suspect an objection to So here’s one more option: qb.where(({ get, and }) => and([
get('first_name', '=', 'Jennifer'),
get('last_name', '!=', 'Tremblay'),
get('updated_at', '>', lastMonth),
get('not', 'is_deleted')
]))
db.updateTable('person')
.set(({ get }) => ({
age: get('age', '+', 1),
backwards: get('-', 'age')
})) Of course, we can mix and match between all these options (eg, unifying I’m also not sure about the compiler performance hit of unifying |
Left field idea: eb.b('ref', '+', 5) // `b` for binary.
eb.b01('ref', '=', 'moshe') // cmpr replacement. `b` for binary, `01` for SqlBool hint :) |
Hmm, I think I'd rather try to unify |
Another one: eb('ref', '+', 5)
eb('ref', '=', 'moshe') |
Love that! Although it does mean that every other property ( |
Yeah that could work. interface EB {
(): {}
eb: Omit<EB, 'eb'>
fn: () => {}
...
}
({ eb, fn }) => ... |
I'm loving this! But why not: interface EB {
(): {}
eb: EB // no omit here
fn: () => {}
...
} To me, it makes sense that the Combining that with unifying qb.where(({ eb, and }) => and([
eb('first_name', '=', 'Jennifer'),
eb('last_name', '!=', 'Tremblay'),
eb('updated_at', '>', lastMonth),
eb('not', 'is_deleted')
]))
db.updateTable('person')
.set(eb => ({
age: eb('age', '+', 1),
backwards: eb('-', 'age')
})) |
This seems awesome! But let's really think this through. Deprecating the functions we just added to replace another set of deprecated functions is a bit ugly 😅 Let's make sure this one sticks or we end up changing the API again. |
Totally agree: if we make changes, we should be reasonably confident we're making all the changes we'd want to make, and doing more than just bikeshedding on function names. I've been thinking a bit more deeply about this today, and will have a proposal soon. In the meantime, I'm curious: how are custom (dialect-specific) binary operators supposed to work today with Also, it seems like the return type of By my read, if we were gonna merge // This is the exact type that `cmpr` has today, except it constrains OP to _only_ known
// comparison operators, rather than `ComparisonOperatorExpression`.
function bxp<
O extends SqlBool = SqlBool,
RE extends ReferenceExpression<DB, TB> = ReferenceExpression<DB, TB>
>(
lhs: RE,
op: ComparisonOperator,
rhs: OperandValueExpressionOrList<DB, TB, RE>
): ExpressionWrapper<O>;
// This is the exact type that `bxp` has today, except that it also replaces
// BinaryOperatorExpression with just BinaryOperator, and simplifies the return type
// because ComparisonOperator is now handled above.
function bxp<
RE extends ReferenceExpression<DB, TB>,
>(
lhs: RE,
op: BinaryOperator,
rhs: OperandValueExpression<DB, TB, RE>
): ExpressionWrapper<ExtractTypeFromReferenceExpression<DB, TB, RE>>
// Finally, we handle the custom operator case as just string, and let the caller
// customize the return type with a new RES parameter.
function bxp<
RE extends ReferenceExpression<DB, TB>,
RES extends unknown = ExtractTypeFromReferenceExpression<DB, TB, RE>
>(
lhs: RE,
op: string,
rhs: OperandValueExpression<DB, TB, RE>
): ExpressionWrapper<RES> Does that seem right to both of you? I just wanna check that I'm not misunderstanding something fundamental before I go down a longer rabit hole of thinking more about how we might unify some of these functions. Btw, I’ve never made overloads with different type parameters before — didn’t even realize that was legal — so I’m not sure how well TS will resolve different call sites against overloads like this, or how the compiler performance compares with unifying all of them through some fancy conditional types. But, again, I just wanted to start by checking my conceptual understanding of how custom operators should work, before we get into these details |
@ethanresnick have you tried this in a playground? I suspect it might break autocompletion which is a big no-no. |
@igalklebanov I haven't, but I will. Again, though, I'm less interested in "should this be overloads or conditional types" and more interested in: am I understanding correctly that the current setup for custom operators — namely, making the operator be an It also seems a little weird to be that |
Quick update: I did test the above set of overloads in the playground, and autocomplete works! However, if I also try to add overloads for what is currently the That’s too bad, because I was hoping this qb.where(eb => eb('and', [
eb('first_name', '=', 'Jennifer'),
eb('last_name', '!=', 'Tremblay'),
eb('updated_at', '>', lastMonth),
eb('not', 'is_deleted')
])) But it doesn’t seem like that’s currently possible while still having great autocomplete. The rule would’ve been:
|
Let's not try to combine |
Ok. In that case, I think my only outstanding questions are the ones I posed above about custom operators: how are they passed, and are they supported only for binary operators? |
@ethanresnick I'm working on these changes here #565 Thanks again for the discussion here! I think the next versions is a big improvement. |
We already support most operators. I think it's fine to require sql`custom_op` in the rare cases its needed. We can also add operators as we go. |
Wow, thanks for working on this @koskimas! It's awesome to see it landed 😁 🚀 As far as custom operators go, I don't use any custom operators myself, so I don't have a strong opinion, but using the
So, I guess I could go either way. I do think though, that the inferred return type for expressions with custom operators seems a bit off, as I mentioned in the comment linked above. |
Thanks again for all the great work you're doing here! I just started using the new(ish) ExpressionBuilder API, and wanted to give a bit of feedback, which I hope will be helpful.
In general, I think the new design makes a lot of sense, and the new names apply better in the various contexts where expressions can be used than the old ones did. My only suggestion has to do with the abbreviated method names
cmpr
andbxp
. Two things struck me about these:Using abbreviations felt a bit out of place with the rest of the kysely API, which generally seems favors explicitness and readability over maximum concision (e.g.,
selectFrom
rather thanfrom
, or something even shorter). To me, the three letters saved by usingcmpr
as opposed tocompare
doesn't seem worth it, given thatcmpr
is not an especially familiar abbreviation.However, if the expression builder API is going to use abbreviations — and I can see a case for that in this API — then I think it might be worth adding abbreviated methods for specific, common binary comparisons, like
eq
,neq
,gt
,gte
, etc. Having methods like that is, to me, both more intuitive (becauseeq
is a much more common/familiar abbreviation thancmpr
) and more concise (since the operator can be omitted).So, I guess the above suggest three possible APIs:
Personally, I think I prefer (1), for maximum clarity without too much verbosity. But, if maximal concision is the goal, then (3) makes more sense to me than two.
The text was updated successfully, but these errors were encountered: