What it measures: whether a caller who wants
#384's DISTINCT ON
can have it without waiting for dbplyr,
how far into dbplyr's internals that reaches,
and what the verb can already express as it stands.
Whether the clause is worth wanting for speed is
2026-08-09-distinct-on-cost/.
When and on what: 2026-08-09, Linux x86_64, R 4.5.3,
duckdb 1.5.5 from CRAN, dbplyr 2.6.0, dplyr 1.2.1.
override.R rendered to override.md with
reprex::reprex(input = "override.R", venue = "gh").
What it supports: the distinct(.keep_all = TRUE) bullet in
usage/integrations/.
.keep_all = TRUE keeps one row per group, and dbplyr can be told
which:
window_order(desc(x))abovedistinct()rendersROW_NUMBER() OVER (PARTITION BY grp ORDER BY x DESC), and keeps 3, 6, 9 of the nine rows;window_order(x)keeps 1, 4, 7.arrange(desc(x))in the same position warns ("ORDER BY is ignored in subqueries without LIMIT") and renders the same SQL, ordering the window the same way. The warning is generic; here nothing was ignored.window_order()is the spelling that says it without one.
So the gap #384 names is DISTINCT ON as a clause, not the ability
to determine the pick. A pipeline that says nothing gets
ORDER BY on the first column, which is arbitrary — and, per the cost
experiment, exactly as fast as DISTINCT ON with no ordering at all.
The wrapper renders the pipeline it is given, wraps the result in
SELECT DISTINCT ON (…) * FROM (…) ORDER BY …,
and hands that back as a lazy table.
Everything it needs is exported: remote_con(), sql_render(),
translate_sql_() (which turns desc(x) into "x" DESC),
ident(), escape() and sql().
Nothing comes from :::, so no dbplyr release can break it by
rearranging its private surface — the risk
#1982 carries for
n_distinct().
The opt-in is an argument, not a setting.
Registered as distinct.tbl_duckdb_connection, the method takes
.order_by and hands straight back to dbplyr through NextMethod()
whenever it is absent — so registering it changes nothing until a call
asks, .keep_all = FALSE never reaches it, and an explicit
.order_by = NULL behaves like saying nothing.
An earlier draft gated it on a global option instead; the argument is
better on both counts.
It is per call rather than per session, and it cannot change an
existing pipeline's answer silently, because the calls it fires on are
exactly the calls that stated which row they meant.
Asked, it complies: .order_by = desc(x) keeps 3, 6, 9 and
.order_by = x keeps 1, 4, 7, and the result composes like any other
lazy table.
The wrapper starts a new tbl() over rendered SQL, so it reads nothing
dbplyr was tracking — including a window_order() the caller already
wrote. The ordering has to be stated again, in .order_by, and a
pipeline carrying both says it twice.
Which is the argument for the clause landing in dbplyr rather than in a wrapper (tidyverse/dbplyr#1620): dbplyr builds the query and already knows the window ordering the pipeline asked for.