-
-
Notifications
You must be signed in to change notification settings - Fork 473
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
Feat: Add type-safe count
, avg
, sum
, max
and min
aggregate functions
#1487
Feat: Add type-safe count
, avg
, sum
, max
and min
aggregate functions
#1487
Conversation
Implement `count`, `avg`, `sum`, `max` and `min` to PG dialect
Implement `count`, `avg`, `sum`, `max` and `min` to MySQL dialect and all of the previously mentioned and `total` to SQLite dialect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really impressive work! You can tell this was a lot of work.
Although I'm going through the code wondering why do you think the BuiltInFunction
class was necessary?
Also, it seems like all the implementations for each dialect are exactly the same. Do you think we could put it in a common place?
Please keep in mind that if we implement the built-in functions as suggested, none of the changes in the dialect, query-builders, and sql file would be necessary. We should avoid complexity as much as we can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's how the count()
function might be implemented a bit clearer:
class Count extends SQL<number> {
constructor(value: SQLWrapper) {
super([sql`cast(count(${value}) as int)`]);
}
filterWhere(value: SQLWrapper) {
this.queryChunks.push(sql` filter (where ${value})`);
return this;
}
}
function count(value: SQLWrapper) {
return new Count(value);
}
For other operators, if they don't require extra chaining methods, you can use the same approach as with filter operators (sql/expressions/conditions.ts
)
Just for clarification, we just discussed this case, we shouldn't do any casting at the database level: constructor(value: SQLWrapper) {
super([sql`count(${value})`]);
this.decoder = Number;
return return this as SQL<number>
} Avoiding casting at db level would prevent possible issues down the road with function composition, ie: |
Adding to @Angelelz's comment, having (in this case) interface count extends SQL {
filterWhere: (where: SQL) => SQL;
}
const count = (value: SQL | AnyColumn) => {
interface Count extends SQL {
filterWhere: (where: SQL) => SQL;
}
const count = sql`count(${value})`;
(count as Count)["filterWhere"] = (where: SQL) => {
return count.append(sql` filter (where ${where})`);
};
return count as Count;
};
// This works
count(users.name).filterWhere(eq(users.name, "user")); @dankochetov Would like to know your thoughts on how I should reimplement this. |
Where is the circular dependency? This file should only be importing from |
Not to familiar with this circular dependency error and therefore not sure why it's there in the first place, but if I import |
Can you push your changes so I can take a look? |
Whilst trying to do an example for this error, now I'm not getting it. It seems like a joke but I'm being serious here. With that said, it seems a reimplementation is very much possible without any "hacks", without disorganizing the code and, most importantly, without introducing greater complexity like I've done so far. |
After discussing the implementation of these functions with @Angelelz, I've made the following changes:
With all these changes in place and using the same example as I did previously, the current implementation looks as follows: await db.select({
scores: count(),
noReplays: countDistinct(scores.setById),
avg: avg(scores.score).mapWith(Number), // `avg` and `sum` return a string by default in case the user desires greater control over the floating point value's precision
total: sum(scores.score),
highest: max(scores.score),
lowest: min(scores.score),
oldest: min(scores.submittedAt), // This will be returned as a Date instead of a number
}).from(scores); |
count
, avg
, sum
, max
and min
aggregate functions
❤️ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me! Thank you for your work. Just making sure that generic parameter is necessary, I saw it in several places. 👍🏻
@@ -141,7 +142,7 @@ export class PgDeleteBase< | |||
returning( | |||
fields: SelectedFieldsFlat = this.config.table[Table.Symbol.Columns], | |||
): PgDeleteReturning<this, TDynamic, any> { | |||
this.config.returning = orderSelectedFields(fields); | |||
this.config.returning = orderSelectedFields<PgColumn>(fields); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be left over code from your initial implementation. Or is it necessary to make the compiler happy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of both. Yes, it's leftover from the previous implementation, but decided to leave it as it has nothing to do with the BuiltInFunction
class. This makes it consistent with the MySQL dialect, as MySqlColumn
was specified there but not in PG or SQLite for some reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Angelelz Let me know if you think this should stay or not.
Just throwing it out here: In order to use aggregate functions in a select statement, either all the selected expressions need to be aggregate functions, or there needs to be a group by statement in the query. For instance following is illegal |
Although a good idea on paper, this would likely require many changes to existing types as well as having to add new ones, and even then, there can be scenarios in which catching that sort of thing at a type level is simply not possible, so it would be a ton of effort with very little reward as it may not handle certain cases. This is out this PR's scope. |
This seems like a good fit for a linter. Definitely out of the scope for this PR. |
Will be added to Check for group by will be added to eslint package, that @Angelelz created and I guess we will release it tomorrow
|
Question: Why were these added as |
This PR implements type-safety for the following aggregate functions to all dialects:
count
avg
sum
max
min
The following is valid Drizzle syntax:
This PR also opens the doors for contributors to add other (built-in) functions via the
BuiltInFunction
class that sort of acts like a wrapper for thesql
operator. So something like:Can be implemented as:
Just to name a very simple example.