feat(utils): support for NULLS FIRST/LAST in orderByAscDesc#737
feat(utils): support for NULLS FIRST/LAST in orderByAscDesc#737benjie merged 11 commits intographile:v4from
Conversation
|
I've pondered this a bit and have concluded that I don't like the explosion of enums that this could invoke. Instead I propose that we stick with this method generating just two enum values, but what those values do is controlled by a single option: nulls: 'first' | 'last' | 'first-iff-ascending' | 'last-iff-ascending' = 'last-iff-ascending'e.g. const customOrderBy = orderByAscDesc(
'RATING_NULLS_LOW',
(helpers) => {
const { queryBuilder } = helpers;
const orderByFrag = sql.fragment`(
select avg(${sqlIdentifier}.rating)
from app_public.profile_reviews as ${sqlIdentifier}
where ${sqlIdentifier}.reviewee_profile_id = ${queryBuilder.getTableAlias()}.id
)`;
return orderByFrag;
},
{ nulls: 'first-iff-ascending' }
);would generate
You could then generate the other enums if you like by adding additional calls to Also, let's change the signature of the function so that it only accepts 3 arguments by using overloads: interface OrderByAscDescOptions {
unique?: boolean;
nulls: 'first' | 'last' | 'first-iff-ascending' | 'last-iff-ascending' = 'last-iff-ascending'
}
export function orderByAscDesc(
baseName: string,
columnOrSqlFragment: OrderBySpecIdentity,
unique?: boolean,
): MakeAddPgTableOrderByPluginOrders;
export function orderByAscDesc(
baseName: string,
columnOrSqlFragment: OrderBySpecIdentity,
options?: OrderByAscDescOptions,
): MakeAddPgTableOrderByPluginOrders;
export function orderByAscDesc(
baseName: string,
columnOrSqlFragment: OrderBySpecIdentity,
optionsOrUnique: boolean | OrderByAscDescOptions = false,
): MakeAddPgTableOrderByPluginOrders {
const options =
typeof optionsOrUnique === 'boolean'
? { unique: optionsOrUnique }
: optionsOrUnique ?? {};
const { unique, nulls } = options;(We have to accept the overloads because we cannot break the existing function signature.) Aside: for your use case you'd just call |
|
(In case you're not familiar, |
|
Hey Benjie, thanks a lot for taking the time to review my PR. I like your solution! I'll try to implement it later today and then I'll update the PR. Two questions:
uniqueOrOptions: boolean | OrderByAscDescOptions = false,and then write only one function?
- const { unique, nulls } = options;
+ const { unique = false, nulls } = options;(I'm assuming that passing in |
…; removed extra enums from orderByAscDesc;
|
Also, in response to this:
In the plugin above, it is true that I'd probably only use the For instance, I can foresee a use-case where I might want to query for profiles with the closest/farthest location, while leaving those who haven't set a location to be last (still discoverable, just penalized in search results). |
No... and I'm honestly not sure why I thought it was. I blame lack of sleep 😴
Yes, apply defaults wherever it's sensible. I don't think the
Agreed. |
|
Hahah 😄 In fact I tried to add a default Great to hear that we are agreed! Is there anything else I should do before the PR can be mergeable? (E.g. should I write tests and update the documentation myself)? Or do you prefer to handle it yourself? |
benjie
left a comment
There was a problem hiding this comment.
We should keep the default behaviour unless overridden (sorry for misleading you with the default). Let's allow nulls = undefined.
| "last-iff-ascending", | ||
| ].includes(nulls); | ||
|
|
||
| const nullsOption = isValidNullsOption ? nulls : "last-iff-ascending"; |
There was a problem hiding this comment.
If not valid; throw? (Remembering to allow for undefined, to allow default behaviour.)
| typeof uniqueOrOptions === "boolean" | ||
| ? { unique: uniqueOrOptions } | ||
| : uniqueOrOptions ?? {}; | ||
| const { unique = false, nulls = "last-iff-ascending" } = options; |
There was a problem hiding this comment.
We should try to mirror (or, preferably, invoke) the logic here:
Namely there's a graphileBuildOptions.orderByNullsLast setting that orders with nulls last always. So either factor that into the default:
const { unique = false, nulls = orderByNullsLast ? "last" : "last-iff-ascending" } = options
Or, preferably, allow nulls to be undefined and in that case go with the default (current) behaviour.
I'd appreciate help with those. Generally I leave them until the functionality is agreed upon though, because there's nothing more frustrating than throwing away tests. |
… for incorrect types;
|
Made some updates 😄 Let me know if it's fine and then I'll get started on tests / documentation. |
| this auto-generated changelog helps us to produce that list, and it may be | ||
| helpful to you also. | ||
| # [](https://github.com/graphile/graphile-engine/compare/v4.12.0-alpha.0...v) (2021-02-15) | ||
| # [4.12.0-alpha.1](https://github.com/graphile/graphile-engine/compare/v4.12.0-alpha.0...v4.12.0-alpha.1) (2021-05-12) |
There was a problem hiding this comment.
Please pull all version changes back out of the git history (this should mean CHANGELOG, lerna.json, package.json, etc are all unmodified).
|
This is ready for tests - thanks @Deckstar! |
This reverts commit 7767266.
benjie
left a comment
There was a problem hiding this comment.
Looks good; just needs tests! 👍
|
Awesome! Thanks! 😄 I'll try to write some over the next few days. |
…scDesc;
Description
Currently
orderByAscDescworks as expected, except for one detail: when ordering by descending, null values show up first. This leads to unwanted results.In my case, for example, I needed to create a plugin that ordered profiles by "top rated" — meaning the average of their reviews:
However, new profiles without any reviews were showing up first (which isn't exactly what comes to mind when thinking "top-rated" 😉 ).
This new feature offers a very quick and easy fix:
I could then make a GraphQL request for
TOP_RATED_NULLS_LAST.The new
createNullsLastandcreateNullsFirstoptions should be completely optional and have no breaking changes whatsoever. They should also be both completely type-safe and run-time safe.I have not written Jest tests for this feature as it's quite a short PR. If necessary I can do that. I am however using it in my current PostGraphile build (using the starter project as a base) and it seems to work as intended.
If the feature is acceptable in general, I can also gladly add it to the documentation.
Thanks for reading! 😄
No fixes.
Discord discussion: https://discord.com/channels/489127045289476126/498852330754801666/840304986117898240
Performance impact
Unknown (minimal hopefully?)
Security impact
Unknown (none?)
Checklist
yarn lint:fixpasses.yarn testpasses.RELEASE_NOTES.mdfile (if one exists).