kitcn 0.33 stops long filters from reading the whole table

2 hours ago

kitcn 0.33 cuts read cost. Queries that filter on a long list of values, or pair an in list and a limit with row-level security, used to read far more documents than they returned. Now they stay on the index. Two paging orders change too, and both migrations come at the end.

Ask about this presentation

Chapters

  1. 0:00kitcn 0.33
  2. 0:20A limit of 3 reads 6
  3. 0:38Limit counts visible rows
  4. 0:54Past 64 values
  5. 1:15Pipeline filters use indexes
  6. 1:33Aggregate writes fold
  7. 1:52SSR auth fix
  8. 2:09Breaking: wide filter page order
  9. 2:26Breaking: flatMap page order
  10. 2:42Upgrade
Show transcript
  1. kitcn 0.33 cuts read cost. Queries that filter on a long list of values, or pair an in list and a limit with row-level security, used to read far more documents than they returned. Now they stay on the index. Two paging orders change too, and both migrations come at the end.

    Slide text
    kitcn 0.33.0

    Long filters stop reading the whole table

    Fewer documents read by the ORM, two paging orders that change, and an SSR auth fix.

  2. Take a findMany with an in list and a limit of three. With row-level security on, or beside a filter like contains, it read every row it matched. That was 500 documents on a 500-row table. Now it reads 6, at any table size, with the same rows in the same order.

    Slide text
    kitcn 0.33.0 /ORM/Fixed

    A limit of 3 now reads 6 documents, at any table size

    RLS on, or beside a filter like contains findMany({ where: { ownerId: { in: [a, b] }, }, limit: 3, })
    500-row table
    500before
    6now
    200-row table
    200before
    6now
  3. The limit is also counted against rows the caller can see. Put 80 rows an RLS policy hides in front of the matches. The three visible rows come after them. A limit of three still returns all three, and reads 86 documents instead of 200.

    Slide text
    kitcn 0.33.0 /ORM/Fixed

    With RLS, limit counts the rows a caller can see

    80 rows hidden by a policy 3 visible matches
    Documents read for limit: 3
    86 200
    Still returns three rows.
  4. A filter with more than 64 values could fall back to reading the full table. Now indexed in, notIn, ne and same-field OR filters stay bounded at any length. Up to 64 ranges are merged into one stream. A wider list is read one range after another, so its pages come back in index order.

    Slide text
    kitcn 0.33.0 /ORM/Fixed

    Filters with more than 64 values stay on the index

    in notIn ne same-field OR
    Up to 64 ranges
    One merged stream
    More than 64 ranges
    One range after another, in index order
  5. The select pipeline gets the same treatment. A where on a union source is compiled against your indexes. And a flatMap stage's where rides an index that extends the relation's foreign key, so only posts with more than ten likes are read. A callback where now runs once, instead of twice.

    Slide text
    kitcn 0.33.0 /ORM/Changed

    A where inside a select() pipeline now uses your indexes

    index("by_author_likes").on(t.authorId, t.numLikes); await ctx.orm.query.users .select() .flatMap("posts", { includeParent: false, where: { numLikes: { gt: 10 } } }) .paginate({ cursor: null, limit: 20 });

    Only the by_author_likes range for that author is read.

  6. Aggregates get cheaper to maintain. Inside one statement, kitcn reuses bucket reads and folds shared updates. A 40-row key migration used to make 80 bucket writes. Now it makes two, plus two extrema writes and the forty membership rows. Your hooks still see the statement's own writes.

    Slide text
    kitcn 0.33.0 /ORM/Aggregates

    Bulk writes on an aggregateIndex table fold shared updates

    A 40-row key migration
    plus 40 membership rows, one per changed document
    Bucket writes
    80before
    2now
    Extrema writes
    80before
    2now
  7. On the React side, a server-rendered page could lose its signed-in cRPC results while hydrating, when Convex confirmed the token the client already held. Now that confirmation keeps them. Signing out, a rejected token or a different user still resets them.

    Slide text
    kitcn 0.33.0 /React/Fixed

    Signed-in cRPC queries keep their results when an SSR page hydrates

    Server render
    Authenticated queries prefetched
    →
    Hydrate
    Client already holds the token
    →
    Convex confirms
    The same token
    →
    Cleared Kept
    Results stay on screen

    Sign-out, a rejected token or a new user still reset them.

  8. Now the two breaking changes. Paging a filter with more than 64 values follows index order, grouped by value, rather than creation order. For newest first, add an orderBy on createdAt and keep maxScan, which that sort still needs past 64 values.

    Slide text
    kitcn 0.33.0 /Breaking/Pagination

    Pages over 64 values now follow index order

    Grouped by the filtered value, rather than creation order.

    For newest first, add orderBy and keep maxScan.

    Migration const page = await db.query.users .withIndex("by_status") .findMany({ where: { status: { in: manyStatuses } }, orderBy: { createdAt: "desc" }, cursor: null, limit: 20, maxScan: 500, });
  9. Second, flatMap. When your schema has an index that extends the relation's foreign key, a stage where reads through it. Children then arrive in that index's order, ahead of creation time. Page cursors already out for those queries don't carry over.

    Slide text
    kitcn 0.33.0 /Breaking/Pipelines

    flatMap pages can arrive in a new order

    When the schema declares an index that extends the relation's foreign key.

    index("by_author_likes")
    Children arrive in numLikes order, ahead of creation time
    cursor
    Cursors already out for these queries don't carry over
  10. So, bump kitcn to 0.33.0. First check any filter over 64 values you page newest first, and any flatMap where you page with live cursors. One limit: a where that filters through a relation keeps its old read cost. That's kitcn 0.33, where long filters stop reading the whole table.

    Slide text
    kitcn 0.33.0 /Upgrade

    Upgrade to kitcn 0.33.0

    Check first
    Filters over 64 values you page newest first
    Check first
    flatMap wheres you page with live cursors
    Unchanged
    A where through a relation keeps its read cost

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️