drift icon indicating copy to clipboard operation
drift copied to clipboard

Manager v2

Open dickermoshe opened this issue 1 year ago • 9 comments

This PR includes the following changes to the manager filter API.

Remove Value Filter for Custom Type

Until now the manage would create 2 filters for a column that has a TypeConverter. One that accepts the Custom Type and another that uses the raw value. To avoid polluting the namespace of filterset, the ability to filter on a raw value will be removed. It is extremely uncommon that a filter on this raw data would be performed, so I think it's worth it. E.G

f.category.status(Status.active)
f.category.statusValue(7) // Removed

References don't use a callback

This will help with readability:

items.filter((f)=>f.category.name("Foo"))

instead of

items.filter((f)=>f.category((f)=>f.name("Foo")))

Remove Id filter for references

Until now, references columns would create 2 columns, one for the actual column, and one for the reference object. We will remove the "Id" filter. Before:

items.filter((f)=>f.categoryId(5))

After:

items.filter((f)=>f.category.id(5))

This looks like we are performing a join for no reason, but we aren't! Under the hood no join is performed, a filter is applied to the actual category column!

Other Changes

  1. Docs for extensions have been removed until the internal manager API is more consistent. We don't want people writing code at this lower level.
  2. Mark lower level classes as internal, but still expose them for the generated code. This will change once the lower level APIs are more stable.
  3. Disable manager generation on older dart 2 code

dickermoshe avatar Apr 28 '24 15:04 dickermoshe

@simolus3 I tried debugging this myself, but I'm at a loss.

There are Book Clubs and Members. I'm filtering book clubs with 3 members. This SQL query returns 1 club, which is CORRECT!

SELECT
  "book_club"."id" AS "book_club.id",
  "book_club"."name" AS "book_club.name"
FROM
  "book_club"
  LEFT OUTER JOIN "person" "book_club__id__person__club" ON "book_club"."id" = "book_club__id__person__club"."club"
GROUP BY
  "book_club"."_rowid_"
HAVING
  COUNT(*) = ?;

HOWEVER, COUNTING DOESN'T!

SELECT
  DISTINCT COUNT(*) AS "c0"
FROM
  "book_club"
  LEFT OUTER JOIN "person" "book_club__id__person__club" ON "book_club"."id" = "book_club__id__person__club"."club"
GROUP BY
  "book_club"."_rowid_"
HAVING
  COUNT(*) = ?;

This will always return whatever the number filter was, in this case, it's returning 3! There are only 2 clubs in the database! Even accessing the raw value, gets {c0: 3} This happens even without DISTINCT

This is either a really bad bug in drift or I'm doing something wrong.

This is the test that's failing, It's mind boggling.

https://github.com/dickermoshe/drift/blob/21b7607a77bfeb50b8cb56119043a636d10fa74d/drift/test/manager/manager_referenced_filter_test.dart#L161-L173

dickermoshe avatar Apr 28 '24 17:04 dickermoshe

This is how the django ORM does counts. Can this be created using drift?

SELECT
  COUNT(*)
FROM
  (
    SELECT
      "api_product"."sku" AS "col1"
    FROM
      "api_product"
      LEFT OUTER JOIN "api_listing" ON ("api_product"."sku" = "api_listing"."product_id")
    GROUP BY
      1
    HAVING
      COUNT("api_listing"."id") = 3
  )

dickermoshe avatar Apr 28 '24 18:04 dickermoshe

@simolus3 Figured it out! As usual your awesome documentation made this easy!

dickermoshe avatar Apr 28 '24 18:04 dickermoshe

@simolus3 Counting can only really be done if there is a rowid or only 1 primary key. Even ORMs that have been developed for the past 20 years don't support a database with more than 1 primary key. We can either throw an error if they try to count (it's like that now) or ignore such tables when generating manager (makes more sense, less shoot-foot-gun issues).

What should be done?

dickermoshe avatar Apr 28 '24 21:04 dickermoshe

To support Postgres and MariaDB, where we don't have a rowid column, I think we should strive to support compound primary keys as well.

Counting can only really be done if there is a rowid or only 1 primary key.

To be sure I'm understanding this correctly, the problem is that we care about the amount of unique rows of one table in a select statement that may have additional joins? That's not really obvious from the documentation of that method, which just claims to return the amount of rows that would be returned by the whole statement. If that's the number we actually care about, what's wrong with SELECT count(*) FROM (the actual statement that would otherwise be generated)?

If we need "amount of unique rows from table x appearing in a joined result set", couldn't we add all the primary key columns to the inner result set and then do a sum + count combination? E.g. if the primary key is the columns foo, bar, baz:

SELECT sum(*) FROM (SELECT count(*) FROM (inner query) GROUP BY foo, bar, baz)

Then the inner query gives us how many rows there are for each unique combination of primary key columns and the final sum gives us the total amount. But I also don't see how that information is relevant in most queries.

simolus3 avatar Apr 29 '24 14:04 simolus3

That's real smart.

On a side note, I was looking at other ORM designs and have decided that aggregates should be done in another method and filtering should focus on filtering.

// Class where aggregates are predefined, these will be added to the filterset
class CategoryAggregateModel extends ...{
  ...
  final itemCount = ...;
  ...
}

category.annotate(CategoryAggregateModel()).filter((f)=>f.itemCount(5))

This will reduce code duplication if we ever decide to add a way to return the aggregated values.

I'm going to revert back to this design for filtering reverse references

// Category with todo with id of 5
category.filter(f.todos((f)=>f.id(5))) // Remove .exist() or .any()

Aggregates will be for a later release, they add complexity to the point where my limited knowledge of SQL is starting to show.

dickermoshe avatar May 01 '24 02:05 dickermoshe

I've followed your guidance on counting and have begun adding more and more tests, It's starting to come together nicely.

dickermoshe avatar May 01 '24 04:05 dickermoshe

Nice! Let me know if there's anything ready for me to take a more in-depth look at :)

simolus3 avatar May 01 '24 14:05 simolus3

@simolus3 The refactoring here is quite signifying. LMK if there is anything I can do to make your reviewing on this PR easier.

This PR is complete

dickermoshe avatar May 01 '24 18:05 dickermoshe

Let me know if there is anything else I can do...

dickermoshe avatar May 01 '24 22:05 dickermoshe

Yeah, this is ready for release.

dickermoshe avatar May 02 '24 21:05 dickermoshe