refactor(sql): promote the single-row limit onto ISqlDialect

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
Joseph Doherty
2026-07-24 13:57:25 -04:00
parent 984cc875e8
commit 3d7e1226d2
4 changed files with 72 additions and 17 deletions
@@ -41,8 +41,7 @@ public static class SqlGroupPlanner
/// up front, so reaching one here means a caller built a definition by hand with a broken invariant.
/// </exception>
/// <exception cref="NotSupportedException">
/// A tag carries <see cref="SqlTagModel.Query"/> (design §5.4, deferred to P3), or a
/// <c>topByTimestamp</c> selector is planned for a provider whose row-limit syntax is not yet modelled.
/// A tag carries <see cref="SqlTagModel.Query"/> (design §5.4, deferred to P3).
/// <b>Deliberately loud rather than skipped:</b> silently dropping a tag would leave an authored node
/// permanently unfed with nothing in the log, and the parser makes this branch unreachable in practice.
/// </exception>
@@ -157,7 +156,10 @@ public static class SqlGroupPlanner
/// <summary>
/// Emits <c>SELECT &lt;cols&gt; FROM &lt;table&gt; WHERE &lt;whereColumn&gt; = @w</c>, or — for a
/// <c>topByTimestamp</c> selector — <c>SELECT TOP 1 &lt;cols&gt; FROM &lt;table&gt; ORDER BY &lt;ts&gt; DESC</c>.
/// <c>topByTimestamp</c> selector — the single-row form
/// <c>SELECT &lt;limit&gt; &lt;cols&gt; FROM &lt;table&gt; ORDER BY &lt;ts&gt; DESC &lt;limit&gt;</c>, whose
/// row limit comes from <see cref="ISqlDialect.SingleRowLimitPrefix"/> /
/// <see cref="ISqlDialect.SingleRowLimitSuffix"/> (T-SQL fills the prefix: <c>SELECT TOP 1 …</c>).
/// <para>The SELECT list is each member's <c>columnName</c> (distinct, in first-appearance order),
/// followed by each distinct member <c>timestampColumn</c> — so members of one row may carry different
/// timestamp columns without splitting the group. The where-column itself is <b>not</b> selected: every
@@ -191,18 +193,13 @@ public static class SqlGroupPlanner
if (!Blank(first.RowSelectorTopByTimestamp))
{
// "TOP 1" is T-SQL. Other providers spell the row limit differently (LIMIT 1, FETCH FIRST,
// ROWNUM), and ISqlDialect does not model it yet because v1 constructs SqlServer only — so refuse
// loudly rather than emit a statement that would not parse. Promote a row-limit member onto
// ISqlDialect when P2 adds Postgres/ODBC.
if (dialect.Provider != SqlProvider.SqlServer)
throw new NotSupportedException(
$"The wide-row 'topByTimestamp' row selector needs a provider-specific row limit; " +
$"{dialect.Provider} is not modelled yet (v1 constructs SqlServer only).");
// The row limit is dialect syntax and sits at opposite ends of the statement per provider
// (T-SQL "SELECT TOP 1 …", SQLite/Postgres/MySQL "… LIMIT 1"), so both ends are emitted
// unconditionally and the dialect decides which one it fills. See ISqlDialect.SingleRowLimitPrefix.
var sql = string.Concat(
"SELECT TOP 1 ", columns, " FROM ", table,
" ORDER BY ", dialect.QuoteIdentifier(first.RowSelectorTopByTimestamp!), " DESC");
"SELECT ", dialect.SingleRowLimitPrefix, columns, " FROM ", table,
" ORDER BY ", dialect.QuoteIdentifier(first.RowSelectorTopByTimestamp!), " DESC",
dialect.SingleRowLimitSuffix);
return new SqlQueryPlan(SqlTagModel.WideRow, groupKey, sql, [], [], members, selected);
}