From 1c40c670a56c8e11a4e5a5e2c7e70f35a989891e Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Tue, 4 Jun 2024 16:06:16 +0200 Subject: [PATCH] chore(zero): Change AST for orderBy (#1975) This changes the AST for orderBy to: ```ts type Ordering = readonly (readonly [Selector, Direction])[]; ``` This is in preparation for supporting things like ```ts q.orderBy('modified', 'desc').orderBy('id', 'asc'); ``` or written as SQL: ```sql ORDER BY modified DESC, id ASC ``` This PR does not change any semantics --- .../services/view-syncer/queries.pg-test.ts | 14 +- .../zero-cache/src/zql/deaggregation.test.ts | 2 +- packages/zero-cache/src/zql/deaggregation.ts | 8 +- packages/zero-cache/src/zql/expansion.test.ts | 25 +-- packages/zero-cache/src/zql/expansion.ts | 19 +- .../zero-cache/src/zql/invalidation.test.ts | 26 +-- packages/zero-cache/src/zql/normalize.test.ts | 91 +++++----- packages/zero-cache/src/zql/normalize.ts | 5 +- .../src/client/query-manager.test.ts | 68 ++++---- .../src/client/zero-poke-handler.test.ts | 24 +-- packages/zero-client/src/client/zero.test.ts | 4 +- .../zql/order-limit-integration.test.ts | 162 +++++++----------- packages/zero-protocol/src/ast.ts | 9 +- .../zql/ast-to-ivm/pipeline-builder.test.ts | 6 +- .../src/zql/ast-to-ivm/pipeline-builder.ts | 6 +- packages/zql/src/zql/ast/ast.test.ts | 147 +++++++--------- packages/zql/src/zql/ast/ast.ts | 8 +- packages/zql/src/zql/context/test-context.ts | 7 +- packages/zql/src/zql/context/zero-context.ts | 2 +- packages/zql/src/zql/ivm/compare.ts | 20 +-- .../zql/ivm/graph/difference-stream.test.ts | 8 +- .../ivm/graph/operators/concat-operator.ts | 2 +- .../operators/join-operator-test-util.ts | 12 +- .../zql/src/zql/ivm/source/set-source.test.ts | 62 +++---- packages/zql/src/zql/ivm/source/set-source.ts | 67 +++++--- packages/zql/src/zql/ivm/source/util.ts | 23 ++- .../zql/src/zql/ivm/view/tree-view.test.ts | 38 ++-- packages/zql/src/zql/ivm/view/tree-view.ts | 33 ++-- .../zql/src/zql/query/entity-query.test.ts | 19 +- packages/zql/src/zql/query/entity-query.ts | 61 ++++--- .../zql/src/zql/query/order-limit.test.ts | 4 +- packages/zql/src/zql/query/statement.test.ts | 22 +-- packages/zql/src/zql/query/statement.ts | 18 +- 33 files changed, 469 insertions(+), 553 deletions(-) diff --git a/packages/zero-cache/src/services/view-syncer/queries.pg-test.ts b/packages/zero-cache/src/services/view-syncer/queries.pg-test.ts index c8f2917307..7498bcd798 100644 --- a/packages/zero-cache/src/services/view-syncer/queries.pg-test.ts +++ b/packages/zero-cache/src/services/view-syncer/queries.pg-test.ts @@ -76,11 +76,8 @@ describe('view-syncer/queries', () => { [['parent', 'owner'], 'parent_owner'], ], orderBy: [ - [ - ['issues', 'id'], - ['issues', 'title'], - ], - 'desc', + [['issues', 'id'], 'desc'], + [['issues', 'title'], 'desc'], ], table: 'issues', joins: [ @@ -575,11 +572,8 @@ describe('view-syncer/queries', () => { [['parent', 'owner'], 'parent_owner'], ], orderBy: [ - [ - ['issues', 'id'], - ['issues', 'title'], - ], - 'desc', + [['issues', 'id'], 'desc'], + [['issues', 'title'], 'desc'], ], table: 'issues', joins: [ diff --git a/packages/zero-cache/src/zql/deaggregation.test.ts b/packages/zero-cache/src/zql/deaggregation.test.ts index d74ec8016c..56d8bce6cb 100644 --- a/packages/zero-cache/src/zql/deaggregation.test.ts +++ b/packages/zero-cache/src/zql/deaggregation.test.ts @@ -46,7 +46,7 @@ describe('zql/deaggregation', () => { }, ], groupBy: [['issues', 'id']], - orderBy: [[['issues', 'modified']], 'desc'], + orderBy: [[['issues', 'modified'], 'desc']], }, original: ` SELECT diff --git a/packages/zero-cache/src/zql/deaggregation.ts b/packages/zero-cache/src/zql/deaggregation.ts index df5ce55f0d..d132fa26e0 100644 --- a/packages/zero-cache/src/zql/deaggregation.ts +++ b/packages/zero-cache/src/zql/deaggregation.ts @@ -147,12 +147,12 @@ function getSupportedGroupByTable( }); if (orderBy) { - orderBy[0].forEach(s => { + for (const [selector] of orderBy) { assert( - s[0] === groupByTable, - `ORDER BY ${s} does not match GROUP BY table ${groupByTable}`, + selector[0] === groupByTable, + `ORDER BY ${selector} does not match GROUP BY table ${groupByTable}`, ); - }); + } } assertAllWheresAgainst(groupByTable, where); diff --git a/packages/zero-cache/src/zql/expansion.test.ts b/packages/zero-cache/src/zql/expansion.test.ts index 97cd2162c2..fdacdc9bdb 100644 --- a/packages/zero-cache/src/zql/expansion.test.ts +++ b/packages/zero-cache/src/zql/expansion.test.ts @@ -87,11 +87,8 @@ describe('zql/expansion', () => { ), ), orderBy: [ - [ - ['issues', 'date'], - ['issues', 'priority'], - ], - 'asc', + [['issues', 'date'], 'asc'], + [['issues', 'priority'], 'asc'], ], }, original: ` @@ -142,11 +139,8 @@ describe('zql/expansion', () => { ), ), orderBy: [ - [ - ['issues', 'date'], - ['issues', 'priority'], - ], - 'asc', + [['issues', 'date'], 'asc'], + [['issues', 'priority'], 'asc'], ], }, }, @@ -214,7 +208,7 @@ describe('zql/expansion', () => { ], }, ], - orderBy: [[['owner', 'level']], 'asc'], + orderBy: [[['owner', 'level'], 'asc']], }, original: ` SELECT @@ -283,11 +277,8 @@ describe('zql/expansion', () => { ), ), orderBy: [ - [ - ['issues', 'date'], - ['issues', 'priority'], - ], - 'asc', + [['issues', 'date'], 'asc'], + [['issues', 'priority'], 'asc'], ], }, }, @@ -465,7 +456,7 @@ describe('zql/expansion', () => { ], }, ], - orderBy: [[['owner', 'awesomeness']], 'desc'], + orderBy: [[['owner', 'awesomeness'], 'desc']], }, original: ` SELECT diff --git a/packages/zero-cache/src/zql/expansion.ts b/packages/zero-cache/src/zql/expansion.ts index c23d4b2735..d797591f77 100644 --- a/packages/zero-cache/src/zql/expansion.ts +++ b/packages/zero-cache/src/zql/expansion.ts @@ -1,4 +1,9 @@ -import type {AST, Condition, Selector} from '@rocicorp/zql/src/zql/ast/ast.js'; +import type { + AST, + Condition, + Ordering, + Selector, +} from '@rocicorp/zql/src/zql/ast/ast.js'; import {assert} from 'shared/src/asserts.js'; import type {ServerAST, SubQuery} from './server-ast.js'; @@ -235,6 +240,9 @@ export function expandSubqueries( const [from] = selector; selectors.get(from)?.push(selector) ?? selectors.set(from, [selector]); }; + const addOrderBySelector = (orderingElement: Ordering[number]) => + addSelector(orderingElement[0]); + const selected = new Set(); // Add all referenced fields / selectors. select?.forEach(([selector, alias]) => { @@ -246,7 +254,7 @@ export function expandSubqueries( getWhereColumns(where, []).forEach(selector => addSelector(selector)); joins?.forEach(({on}) => on.forEach(part => addSelector(part))); groupBy?.forEach(grouping => addSelector(grouping)); - orderBy?.[0].forEach(ordering => addSelector(ordering)); + orderBy?.forEach(addOrderBySelector); aggregate?.forEach(agg => { if (agg.field !== undefined) { addSelector(agg.field); @@ -384,6 +392,11 @@ export function reAliasAndBubbleSelections( : [from, newCol]; }; + const renameSelectorsInOrderingElement = ([ + selector, + dir, + ]: Ordering[number]): Ordering[number] => [renameSelector(selector), dir]; + // Return a modified AST with all selectors realiased (SELECT, ON, GROUP BY, ORDER BY), // and bubble up all selected aliases to the `exports` Map. const exported = new Set(); @@ -416,6 +429,6 @@ export function reAliasAndBubbleSelections( on: [renameSelector(join.on[0]), renameSelector(join.on[1])], })), groupBy: groupBy?.map(renameSelector), - orderBy: orderBy ? [orderBy[0].map(renameSelector), orderBy[1]] : undefined, + orderBy: orderBy?.map(renameSelectorsInOrderingElement), }; } diff --git a/packages/zero-cache/src/zql/invalidation.test.ts b/packages/zero-cache/src/zql/invalidation.test.ts index f8ece6433f..539d06449b 100644 --- a/packages/zero-cache/src/zql/invalidation.test.ts +++ b/packages/zero-cache/src/zql/invalidation.test.ts @@ -227,7 +227,7 @@ describe('zql/invalidation hashes filters and hashes', () => { schema: 'zero', table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], }, filters: [ { @@ -258,7 +258,7 @@ describe('zql/invalidation hashes filters and hashes', () => { schema: 'zero', table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], }, alias: 'foo', }, @@ -288,7 +288,7 @@ describe('zql/invalidation hashes filters and hashes', () => { aggregate: [ {aggregate: 'min', field: ['foo', 'priority'], alias: 'ignored'}, ], - orderBy: [[['foo', 'ignored']], 'asc'], + orderBy: [[['foo', 'ignored'], 'asc']], }, filters: [ { @@ -313,7 +313,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', aggregate: [{aggregate: 'count', alias: 'ignored'}], - orderBy: [[['foo', 'ignored']], 'asc'], + orderBy: [[['foo', 'ignored'], 'asc']], }, filters: [ { @@ -336,7 +336,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: and( cond(['foo', 'foo'], '=', 'bar'), cond(['foo', 'bar'], '=', 2), @@ -367,7 +367,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: and( cond(['foo', 'foo'], '=', 'bar'), cond(['join.alias', 'baz'], '=', 3), // Ignored @@ -399,7 +399,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: or( cond(['foo', 'foo'], '=', 'bar'), cond(['foo', 'bar'], '=', 2), @@ -456,7 +456,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: or( cond(['foo', 'foo'], '=', 'bar'), cond(['foo', 'bar'], '=', 2), @@ -500,7 +500,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: or( cond(['foo', 'foo'], '=', 'bar'), cond(['foo', 'foo'], '=', 'baz'), @@ -543,7 +543,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: and( or(cond(['foo', 'a'], '=', 1), cond(['foo', 'b'], '=', 2)), or(cond(['foo', 'c'], '=', 3), cond(['foo', 'd'], '=', 4)), @@ -665,7 +665,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: and( or(cond(['foo', 'foo'], '=', 'bar'), cond(['foo', 'bar'], '=', 1)), or(cond(['foo', 'bar'], '=', 2), cond(['foo', 'do'], '=', 'foo')), @@ -702,7 +702,7 @@ describe('zql/invalidation hashes filters and hashes', () => { ast: { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: and( cond(['foo', 'foo'], '=', 'bar'), cond(['foo', 'bar'], '=', 2), @@ -718,7 +718,7 @@ describe('zql/invalidation hashes filters and hashes', () => { { table: 'foo', select: [[['foo', 'id'], 'id']], - orderBy: [[['foo', 'id']], 'asc'], + orderBy: [[['foo', 'id'], 'asc']], where: and( or(cond(['foo', 'a'], '=', 1), cond(['foo', 'b'], '=', 2)), or(cond(['foo', 'c'], '=', 3), cond(['foo', 'd'], '=', 4)), diff --git a/packages/zero-cache/src/zql/normalize.test.ts b/packages/zero-cache/src/zql/normalize.test.ts index 1fb0714f9f..49cd8229f7 100644 --- a/packages/zero-cache/src/zql/normalize.test.ts +++ b/packages/zero-cache/src/zql/normalize.test.ts @@ -18,7 +18,7 @@ describe('zql/normalize-query-hash', () => { { table: 'issues', select: [[['issues', 'id'], 'id']], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: 'SELECT issues.id AS id FROM issues ORDER BY issues.id asc', @@ -40,7 +40,7 @@ describe('zql/normalize-query-hash', () => { alias: 'issues_alias', }, select: [[['issues_alias', 'id'], 'id']], - orderBy: [[['issues_alias', 'id']], 'asc'], + orderBy: [[['issues_alias', 'id'], 'asc']], }, ], query: @@ -59,7 +59,7 @@ describe('zql/normalize-query-hash', () => { [['clients', 'col.with.dots'], 'clientID'], [['zero.clients', 'lastMutationID'], 'lastMutationID'], ], - orderBy: [[['clients', 'col.with.dots']], 'asc'], + orderBy: [[['clients', 'col.with.dots'], 'asc']], }, ], query: @@ -74,7 +74,7 @@ describe('zql/normalize-query-hash', () => { table: 'issues', alias: 'Ishooz', select: [[['issues', 'id'], 'id']], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -89,7 +89,7 @@ describe('zql/normalize-query-hash', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -97,7 +97,7 @@ describe('zql/normalize-query-hash', () => { [['issues', 'name'], 'name'], [['issues', 'id'], 'id'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -109,7 +109,7 @@ describe('zql/normalize-query-hash', () => { { table: 'issues', aggregate: [{aggregate: 'count', alias: 'num'}], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: 'SELECT count(*) AS "count(*)" FROM issues ORDER BY issues.id asc', @@ -123,7 +123,7 @@ describe('zql/normalize-query-hash', () => { {aggregate: 'count', alias: 'num'}, {aggregate: 'max', field: ['issues', 'priority'], alias: 'maxPri'}, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -131,7 +131,7 @@ describe('zql/normalize-query-hash', () => { {aggregate: 'max', field: ['issues', 'priority'], alias: 'maxPri'}, {aggregate: 'count', alias: 'num'}, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -151,7 +151,7 @@ describe('zql/normalize-query-hash', () => { ['issues', 'id'], ['issues', 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -163,7 +163,7 @@ describe('zql/normalize-query-hash', () => { ['issues', 'name'], ['issues', 'id'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -183,11 +183,8 @@ describe('zql/normalize-query-hash', () => { ['issues', 'name'], ], orderBy: [ - [ - ['issues', 'id'], - ['issues', 'name'], - ], - 'desc', + [['issues', 'id'], 'desc'], + [['issues', 'name'], 'desc'], ], }, ], @@ -209,11 +206,8 @@ describe('zql/normalize-query-hash', () => { ], // ORDER BY expression order must be preserved. orderBy: [ - [ - ['issues', 'dueDate'], - ['issues', 'priority'], - ], - 'desc', + [['issues', 'dueDate'], 'desc'], + [['issues', 'priority'], 'desc'], ], limit: 10, }, @@ -237,11 +231,8 @@ describe('zql/normalize-query-hash', () => { ], // ORDER BY expression order must be preserved. orderBy: [ - [ - ['issues', 'priority'], - ['issues', 'dueDate'], - ], - 'desc', + [['issues', 'priority'], 'desc'], + [['issues', 'dueDate'], 'desc'], ], limit: 10, }, @@ -259,7 +250,7 @@ describe('zql/normalize-query-hash', () => { [['camelCaseTable', 'userID'], 'u'], [['camelCaseTable', 'name'], 'n'], ], - orderBy: [[['camelCaseTable', 'id']], 'asc'], + orderBy: [[['camelCaseTable', 'id'], 'asc']], }, ], query: @@ -271,7 +262,7 @@ describe('zql/normalize-query-hash', () => { { table: 'camelCaseTable', select: [[['camelCaseTable', 'userID'], 'id']], - orderBy: [[['camelCaseTable', 'userID']], 'asc'], + orderBy: [[['camelCaseTable', 'userID'], 'asc']], }, ], query: @@ -294,7 +285,7 @@ describe('zql/normalize-query-hash', () => { ], }, ], - orderBy: [[['owner', 'id']], 'asc'], + orderBy: [[['owner', 'id'], 'asc']], }, ], query: @@ -324,7 +315,7 @@ describe('zql/normalize-query-hash', () => { ], }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -346,7 +337,7 @@ describe('zql/normalize-query-hash', () => { ], }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -368,7 +359,7 @@ describe('zql/normalize-query-hash', () => { op: '=', value: {type: 'value', value: 12345}, }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -390,7 +381,7 @@ describe('zql/normalize-query-hash', () => { op: '=', value: {type: 'value', value: 1234}, }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -412,7 +403,7 @@ describe('zql/normalize-query-hash', () => { op: 'IN', value: {type: 'value', value: ['1234', '2345', '4567']}, }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -434,7 +425,7 @@ describe('zql/normalize-query-hash', () => { op: '=', value: {type: 'value', value: '1234'}, }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -480,7 +471,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -518,7 +509,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -541,7 +532,7 @@ describe('zql/normalize-query-hash', () => { op: 'AND', conditions: [], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -554,7 +545,7 @@ describe('zql/normalize-query-hash', () => { op: 'OR', conditions: [], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -587,7 +578,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -613,7 +604,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -661,7 +652,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -699,7 +690,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -759,7 +750,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -819,7 +810,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -869,7 +860,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -935,7 +926,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -991,7 +982,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: @@ -1117,7 +1108,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { // AST with different but equivalent nesting of AND's and OR's @@ -1253,7 +1244,7 @@ describe('zql/normalize-query-hash', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], query: diff --git a/packages/zero-cache/src/zql/normalize.ts b/packages/zero-cache/src/zql/normalize.ts index 832b1f478e..1b149b5345 100644 --- a/packages/zero-cache/src/zql/normalize.ts +++ b/packages/zero-cache/src/zql/normalize.ts @@ -114,9 +114,8 @@ export class Normalized { query += ` GROUP BY ${groupBy.map(x => selector(x)).join(', ')}`; } if (orderBy) { - const [names, dir] = orderBy; - query += ` ORDER BY ${names - .map(x => `${selector(x)} ${dir}`) + query += ` ORDER BY ${orderBy + .map(([x, dir]) => `${selector(x)} ${dir}`) .join(', ')}`; } if (limit !== undefined) { diff --git a/packages/zero-client/src/client/query-manager.test.ts b/packages/zero-client/src/client/query-manager.test.ts index 825dc4d679..606041fde2 100644 --- a/packages/zero-client/src/client/query-manager.test.ts +++ b/packages/zero-client/src/client/query-manager.test.ts @@ -1,20 +1,20 @@ -import {expect, test, vi} from 'vitest'; -import {QueryManager} from './query-manager.js'; import type {AST} from '@rocicorp/zql/src/zql/ast/ast.js'; -import type {ChangeDesiredQueriesMessage} from 'zero-protocol'; import { - type ScanOptions, - type ReadTransaction, - type ScanIndexOptions, - makeScanResult, - ReadonlyJSONValue, - ScanResult, + DeepReadonly, IndexKey, + ReadonlyJSONValue, ScanNoIndexOptions, - DeepReadonly, + ScanResult, + makeScanResult, + type ReadTransaction, + type ScanIndexOptions, + type ScanOptions, } from 'replicache'; import type {ReplicacheImpl} from 'replicache/src/replicache-impl.js'; +import {expect, test, vi} from 'vitest'; +import type {ChangeDesiredQueriesMessage} from 'zero-protocol'; import {toGotQueriesKey} from './keys.js'; +import {QueryManager} from './query-manager.js'; function createExperimentalWatchMock() { return vi.fn< @@ -33,7 +33,7 @@ test('add', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; queryManager.add(ast); expect(send).toBeCalledTimes(1); @@ -43,7 +43,7 @@ test('add', () => { desiredQueriesPatch: [ { op: 'put', - hash: '3o852oxdcga5g', + hash: 'vgoxbdhr8m7c', ast: { table: 'issues', alias: undefined, @@ -55,7 +55,7 @@ test('add', () => { where: undefined, joins: undefined, groupBy: undefined, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], limit: undefined, schema: undefined, } satisfies AST, @@ -78,7 +78,7 @@ test('remove', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; const remove1 = queryManager.add(ast); @@ -89,7 +89,7 @@ test('remove', () => { desiredQueriesPatch: [ { op: 'put', - hash: '3o852oxdcga5g', + hash: 'vgoxbdhr8m7c', ast: { table: 'issues', alias: undefined, @@ -102,7 +102,7 @@ test('remove', () => { joins: undefined, groupBy: undefined, schema: undefined, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], limit: undefined, } satisfies AST, }, @@ -123,7 +123,7 @@ test('remove', () => { desiredQueriesPatch: [ { op: 'del', - hash: '3o852oxdcga5g', + hash: 'vgoxbdhr8m7c', }, ], }, @@ -194,7 +194,7 @@ test('getQueriesPatch', async () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; queryManager.add(ast1); // hash 1wpmhwzkyaqrd @@ -204,13 +204,13 @@ test('getQueriesPatch', async () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'desc'], + orderBy: [[['issues', 'id'], 'desc']], }; queryManager.add(ast2); const testReadTransaction = new TestTransaction(); testReadTransaction.scanEntries = [ - ['d/client1/3o852oxdcga5g', 'unused'], + ['d/client1/vgoxbdhr8m7c', 'unused'], ['d/client1/shouldBeDeleted', 'unused'], ]; @@ -222,7 +222,7 @@ test('getQueriesPatch', async () => { }, { op: 'put', - hash: '1ld7py8mkar54', + hash: '34gh23e9vauns', ast: { table: 'issues', alias: undefined, @@ -234,7 +234,7 @@ test('getQueriesPatch', async () => { where: undefined, joins: undefined, groupBy: undefined, - orderBy: [[['issues', 'id']], 'desc'], + orderBy: [[['issues', 'id'], 'desc']], limit: undefined, schema: undefined, } satisfies AST, @@ -244,7 +244,7 @@ test('getQueriesPatch', async () => { }); test('gotCallback, query already got', async () => { - const queryHash = '3o852oxdcga5g'; + const queryHash = 'vgoxbdhr8m7c'; const experimentalWatch = createExperimentalWatchMock(); const send = vi.fn<[ChangeDesiredQueriesMessage], void>(); const queryManager = new QueryManager('client1', send, experimentalWatch); @@ -264,7 +264,7 @@ test('gotCallback, query already got', async () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; const gotCalback1 = vi.fn<[boolean], void>(); @@ -288,7 +288,7 @@ test('gotCallback, query already got', async () => { where: undefined, joins: undefined, groupBy: undefined, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], limit: undefined, schema: undefined, } satisfies AST, @@ -315,7 +315,7 @@ test('gotCallback, query already got', async () => { }); test('gotCallback, query got after add', async () => { - const queryHash = '3o852oxdcga5g'; + const queryHash = 'vgoxbdhr8m7c'; const experimentalWatch = createExperimentalWatchMock(); const send = vi.fn<[ChangeDesiredQueriesMessage], void>(); const queryManager = new QueryManager('client1', send, experimentalWatch); @@ -328,7 +328,7 @@ test('gotCallback, query got after add', async () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; const gotCalback1 = vi.fn<[boolean], void>(); @@ -352,7 +352,7 @@ test('gotCallback, query got after add', async () => { where: undefined, joins: undefined, groupBy: undefined, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], limit: undefined, schema: undefined, } satisfies AST, @@ -379,7 +379,7 @@ test('gotCallback, query got after add', async () => { }); test('gotCallback, query got after add then removed', async () => { - const queryHash = '3o852oxdcga5g'; + const queryHash = 'vgoxbdhr8m7c'; const experimentalWatch = createExperimentalWatchMock(); const send = vi.fn<[ChangeDesiredQueriesMessage], void>(); const queryManager = new QueryManager('client1', send, experimentalWatch); @@ -392,7 +392,7 @@ test('gotCallback, query got after add then removed', async () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; const gotCalback1 = vi.fn<[boolean], void>(); @@ -416,7 +416,7 @@ test('gotCallback, query got after add then removed', async () => { where: undefined, joins: undefined, groupBy: undefined, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], limit: undefined, schema: undefined, } satisfies AST, @@ -453,7 +453,7 @@ test('gotCallback, query got after add then removed', async () => { }); test('gotCallback, query got after subscription removed', async () => { - const queryHash = '3o852oxdcga5g'; + const queryHash = 'vgoxbdhr8m7c'; const experimentalWatch = createExperimentalWatchMock(); const send = vi.fn<[ChangeDesiredQueriesMessage], void>(); const queryManager = new QueryManager('client1', send, experimentalWatch); @@ -466,7 +466,7 @@ test('gotCallback, query got after subscription removed', async () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }; const gotCalback1 = vi.fn<[boolean], void>(); @@ -490,7 +490,7 @@ test('gotCallback, query got after subscription removed', async () => { where: undefined, joins: undefined, groupBy: undefined, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], limit: undefined, schema: undefined, } satisfies AST, diff --git a/packages/zero-client/src/client/zero-poke-handler.test.ts b/packages/zero-client/src/client/zero-poke-handler.test.ts index 27b2dab458..2c6ae00ccd 100644 --- a/packages/zero-client/src/client/zero-poke-handler.test.ts +++ b/packages/zero-client/src/client/zero-poke-handler.test.ts @@ -924,7 +924,7 @@ test('mergePokes with all optionals defined', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, ], @@ -939,7 +939,7 @@ test('mergePokes with all optionals defined', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, ], @@ -975,7 +975,7 @@ test('mergePokes with all optionals defined', () => { [['labels', 'id'], 'id'], [['labels', 'name'], 'name'], ], - orderBy: [[['labels', 'id']], 'asc'], + orderBy: [[['labels', 'id'], 'asc']], }, }, ], @@ -990,7 +990,7 @@ test('mergePokes with all optionals defined', () => { [['labels', 'id'], 'id'], [['labels', 'name'], 'name'], ], - orderBy: [[['labels', 'id']], 'asc'], + orderBy: [[['labels', 'id'], 'asc']], }, }, ], @@ -1066,7 +1066,7 @@ test('mergePokes with all optionals defined', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1078,7 +1078,7 @@ test('mergePokes with all optionals defined', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1106,7 +1106,7 @@ test('mergePokes with all optionals defined', () => { [['labels', 'id'], 'id'], [['labels', 'name'], 'name'], ], - orderBy: [[['labels', 'id']], 'asc'], + orderBy: [[['labels', 'id'], 'asc']], }, }, { @@ -1118,7 +1118,7 @@ test('mergePokes with all optionals defined', () => { [['labels', 'id'], 'id'], [['labels', 'name'], 'name'], ], - orderBy: [[['labels', 'id']], 'asc'], + orderBy: [[['labels', 'id'], 'asc']], }, }, { @@ -1174,7 +1174,7 @@ test('mergePokes sparse', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, ], @@ -1209,7 +1209,7 @@ test('mergePokes sparse', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, ], @@ -1262,7 +1262,7 @@ test('mergePokes sparse', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], } satisfies AST, }, { @@ -1290,7 +1290,7 @@ test('mergePokes sparse', () => { [['issues', 'id'], 'id'], [['issues', 'name'], 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { diff --git a/packages/zero-client/src/client/zero.test.ts b/packages/zero-client/src/client/zero.test.ts index 6c6f4d39b3..7c0c2ac0a5 100644 --- a/packages/zero-client/src/client/zero.test.ts +++ b/packages/zero-client/src/client/zero.test.ts @@ -453,11 +453,11 @@ suite('initConnection', () => { { ast: { aggregate: [], - orderBy: [[['e', 'id']], 'asc'], + orderBy: [[['e', 'id'], 'asc']], select: [[['e', '*'], '*']], table: 'e', } satisfies AST, - hash: '2dytaxdo1gtrn', + hash: '3v64kj3849ubl', op: 'put', }, ], diff --git a/packages/zero-client/src/client/zql/order-limit-integration.test.ts b/packages/zero-client/src/client/zql/order-limit-integration.test.ts index ef2be265c1..5ca1df8b96 100644 --- a/packages/zero-client/src/client/zql/order-limit-integration.test.ts +++ b/packages/zero-client/src/client/zql/order-limit-integration.test.ts @@ -1,20 +1,20 @@ -import {describe, expect, test} from 'vitest'; import {canonicalComparator} from '@rocicorp/zql/src/zql/context/zero-context.js'; +import {makeComparator} from '@rocicorp/zql/src/zql/ivm/compare.js'; +import {Comparator, joinSymbol} from '@rocicorp/zql/src/zql/ivm/types.js'; +import {describe, expect, test} from 'vitest'; +import {must} from '../../../../shared/src/must.js'; import { Album, Artist, + Track, + TrackArtist, bulkSet, createRandomAlbums, createRandomArtists, createRandomTracks, linkTracksToArtists, newZero, - Track, - TrackArtist, } from './integration-test-util.js'; -import {makeComparator} from '@rocicorp/zql/src/zql/ivm/compare.js'; -import {must} from '../../../../shared/src/must.js'; -import {Comparator, joinSymbol} from '@rocicorp/zql/src/zql/ivm/types.js'; describe('sorting and limiting with different query operations', async () => { const z = newZero(); @@ -100,13 +100,10 @@ describe('sorting and limiting with different query operations', async () => { expected: () => artists .sort( - makeComparator( - [ - ['artist', 'name'], - ['artist', 'id'], - ], - 'asc', - ), + makeComparator([ + [['artist', 'name'], 'asc'], + [['artist', 'id'], 'asc'], + ]), ) .slice(0, 10), }, @@ -116,13 +113,10 @@ describe('sorting and limiting with different query operations', async () => { expected: () => artists .sort( - makeComparator( - [ - ['artist', 'name'], - ['artist', 'id'], - ], - 'desc', - ), + makeComparator([ + [['artist', 'name'], 'desc'], + [['artist', 'id'], 'desc'], + ]), ) .slice(0, 10), }, @@ -137,13 +131,10 @@ describe('sorting and limiting with different query operations', async () => { tracks .filter(t => t.title > 'F') .sort( - makeComparator( - [ - ['track', 'title'], - ['tracl', 'id'], - ], - 'asc', - ), + makeComparator([ + [['track', 'title'], 'asc'], + [['tracl', 'id'], 'asc'], + ]), ) .slice(0, 3), }, @@ -157,13 +148,10 @@ describe('sorting and limiting with different query operations', async () => { albums .map(joinAlbumToArtist) .sort( - makeComparator( - [ - ['album', 'id'], - ['artist', 'id'], - ], - 'asc', - ), + makeComparator([ + [['album', 'id'], 'asc'], + [['artist', 'id'], 'asc'], + ]), ) .slice(0, 10), }, @@ -189,14 +177,11 @@ describe('sorting and limiting with different query operations', async () => { albums .map(joinAlbumToArtist) .sort( - makeComparator( - [ - ['album', 'title'], - ['album', 'id'], - ['artist', 'id'], - ], - 'asc', - ), + makeComparator([ + [['album', 'title'], 'asc'], + [['album', 'id'], 'asc'], + [['artist', 'id'], 'asc'], + ]), ) .slice(0, 10), }, @@ -208,13 +193,10 @@ describe('sorting and limiting with different query operations', async () => { .desc('album.title') .limit(10), expected: (): {artist: Artist; album: Album}[] => { - const c = makeComparator( - [ - ['album', 'title'], - ['album', 'id'], - ], - 'desc', - ); + const c = makeComparator([ + [['album', 'title'], 'desc'], + [['album', 'id'], 'desc'], + ]); return albums.map(joinAlbumToArtist).sort(c).slice(0, 10); }, }, @@ -226,14 +208,11 @@ describe('sorting and limiting with different query operations', async () => { .join(z.query.artist, 'artist', 'trackArtist.artistId', 'id') .asc('track.title', 'artist.name'), expected: () => { - const c = makeComparator( - [ - ['track', 'title'], - ['artist', 'name'], - ['track', 'id'], - ], - 'asc', - ); + const c = makeComparator([ + [['track', 'title'], 'asc'], + [['artist', 'name'], 'asc'], + [['track', 'id'], 'asc'], + ]); return tracks.flatMap(joinTrackToArtists).sort(c); }, }, @@ -245,14 +224,11 @@ describe('sorting and limiting with different query operations', async () => { .join(z.query.artist, 'artist', 'trackArtist.artistId', 'id') .desc('track.title', 'artist.name'), expected: () => { - const c = makeComparator( - [ - ['track', 'title'], - ['artist', 'name'], - ['track', 'id'], - ], - 'desc', - ); + const c = makeComparator([ + [['track', 'title'], 'desc'], + [['artist', 'name'], 'desc'], + [['track', 'id'], 'desc'], + ]); return tracks.flatMap(joinTrackToArtists).sort(c); }, }, @@ -272,22 +248,16 @@ describe('sorting and limiting with different query operations', async () => { z.query.track.groupBy('track.albumId').desc('track.title').limit(10), expected: () => groupTracksByAlbum( - makeComparator( - [ - ['track', 'title'], - ['track', 'albumId'], - ], - 'desc', - ), + makeComparator([ + [['track', 'title'], 'desc'], + [['track', 'albumId'], 'desc'], + ]), ) .sort( - makeComparator( - [ - ['track', 'title'], - ['track', 'albumId'], - ], - 'desc', - ), + makeComparator([ + [['track', 'title'], 'desc'], + [['track', 'albumId'], 'desc'], + ]), ) .slice(0, 10), }, @@ -300,16 +270,13 @@ describe('sorting and limiting with different query operations', async () => { .limit(10) .asc('track.title', 'artist.name'), expected: () => { - const c = makeComparator( - [ - ['track', 'title'], - ['artist', 'name'], - ['track', 'id'], - ['trackArtist', 'id'], - ['artist', 'id'], - ], - 'asc', - ); + const c = makeComparator([ + [['track', 'title'], 'asc'], + [['artist', 'name'], 'asc'], + [['track', 'id'], 'asc'], + [['trackArtist', 'id'], 'asc'], + [['artist', 'id'], 'asc'], + ]); return tracks.flatMap(joinTrackToArtists).sort(c).slice(0, 10); }, }, @@ -322,16 +289,13 @@ describe('sorting and limiting with different query operations', async () => { .desc('track.title', 'artist.name') .limit(10), expected: () => { - const c = makeComparator( - [ - ['track', 'title'], - ['artist', 'name'], - ['track', 'id'], - ['trackArtist', 'id'], - ['artist', 'id'], - ], - 'desc', - ); + const c = makeComparator([ + [['track', 'title'], 'desc'], + [['artist', 'name'], 'desc'], + [['track', 'id'], 'desc'], + [['trackArtist', 'id'], 'desc'], + [['artist', 'id'], 'desc'], + ]); return tracks.flatMap(joinTrackToArtists).sort(c).slice(0, 10); }, }, diff --git a/packages/zero-protocol/src/ast.ts b/packages/zero-protocol/src/ast.ts index 71021b56f2..a903b2b5d0 100644 --- a/packages/zero-protocol/src/ast.ts +++ b/packages/zero-protocol/src/ast.ts @@ -15,13 +15,12 @@ function readonly(t: v.Type): v.Type> { export const selectorSchema = readonly(v.tuple([v.string(), v.string()])); -export const orderingSchema = readonly( - v.tuple([ - readonly(v.array(selectorSchema)), - v.union(v.literal('asc'), v.literal('desc')), - ]), +const orderingElementSchema = readonly( + v.tuple([selectorSchema, v.union(v.literal('asc'), v.literal('desc'))]), ); +export const orderingSchema = readonly(v.array(orderingElementSchema)); + export const primitiveSchema = v.union( v.string(), v.number(), diff --git a/packages/zql/src/zql/ast-to-ivm/pipeline-builder.test.ts b/packages/zql/src/zql/ast-to-ivm/pipeline-builder.test.ts index 5fc424488a..791564b711 100644 --- a/packages/zql/src/zql/ast-to-ivm/pipeline-builder.test.ts +++ b/packages/zql/src/zql/ast-to-ivm/pipeline-builder.test.ts @@ -25,7 +25,7 @@ type E1 = z.infer; const context = makeTestContext(); const comparator = (l: E1, r: E1) => compareUTF8(l.id, r.id); -const ordering = [[['e1', 'id']], 'asc'] as const; +const ordering = [[['e1', 'id'], 'asc']] as const; test('A simple select', () => { const q = new EntityQuery<{e1: E1}>(context, 'e1'); const m = new Materialite(); @@ -548,7 +548,7 @@ describe('OR', () => { const m = new Materialite(); const s = m.newSetSource( comparator, - [[['items', 'id']], 'asc'], + [[['items', 'id'], 'asc']], 'items', ); @@ -560,7 +560,7 @@ describe('OR', () => { [['items', 'b'], 'b'], ], where: c.where, - orderBy: [[['items', 'id']], 'asc'], + orderBy: [[['items', 'id'], 'asc']], }; const pipeline = buildPipeline( diff --git a/packages/zql/src/zql/ast-to-ivm/pipeline-builder.ts b/packages/zql/src/zql/ast-to-ivm/pipeline-builder.ts index 06c5f660d8..ffe97f87fa 100644 --- a/packages/zql/src/zql/ast-to-ivm/pipeline-builder.ts +++ b/packages/zql/src/zql/ast-to-ivm/pipeline-builder.ts @@ -3,11 +3,11 @@ import type { AST, Aggregation, Condition, + HavingCondition, Join, - SimpleCondition, Ordering, Selector, - HavingCondition, + SimpleCondition, SimpleHavingCondition, } from '../ast/ast.js'; import {DifferenceStream, concat} from '../ivm/graph/difference-stream.js'; @@ -60,7 +60,7 @@ export function buildPipeline( ) as unknown as DifferenceStream; } // if there was no group-by then we could be aggregating the entire table - else if (ast.aggregate) { + else if (ast.aggregate && ast.aggregate.length > 0) { ret = applyFullTableAggregation( ret as DifferenceStream, ast.aggregate, diff --git a/packages/zql/src/zql/ast/ast.test.ts b/packages/zql/src/zql/ast/ast.test.ts index ab5f152e4f..6f29bb7c5e 100644 --- a/packages/zql/src/zql/ast/ast.test.ts +++ b/packages/zql/src/zql/ast/ast.test.ts @@ -26,7 +26,7 @@ describe('zql/ast', () => { op: '=', value: {type: 'value', value: 1234}, }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -41,7 +41,7 @@ describe('zql/ast', () => { op: '=', value: {type: 'value', value: 1234}, }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -83,7 +83,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -121,7 +121,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -160,7 +160,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -178,11 +178,8 @@ describe('zql/ast', () => { conditions: [], }, orderBy: [ - [ - ['issues', 'id'], - ['issues', 'name'], - ], - 'asc', + [['issues', 'id'], 'asc'], + [['issues', 'name'], 'asc'], ], }, { @@ -197,11 +194,8 @@ describe('zql/ast', () => { conditions: [], }, orderBy: [ - [ - ['issues', 'id'], - ['issues', 'name'], - ], - 'asc', + [['issues', 'id'], 'asc'], + [['issues', 'name'], 'asc'], ], }, ], @@ -212,11 +206,8 @@ describe('zql/ast', () => { [['issues', 'name'], 'n'], ], orderBy: [ - [ - ['issues', 'id'], - ['issues', 'name'], - ], - 'asc', + [['issues', 'id'], 'asc'], + [['issues', 'name'], 'asc'], ], }, }, @@ -248,7 +239,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -274,7 +265,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -301,7 +292,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -343,7 +334,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -381,7 +372,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -420,7 +411,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -474,7 +465,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -525,7 +516,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -579,7 +570,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -629,7 +620,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -680,7 +671,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -740,7 +731,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -796,7 +787,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -853,7 +844,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -973,7 +964,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { // AST with different but equivalent nesting of AND's and OR's @@ -1109,7 +1100,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -1209,7 +1200,7 @@ describe('zql/ast', () => { }, ], }, - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, ]; @@ -1222,13 +1213,13 @@ describe('zql/ast', () => { { table: 'issues', select: [[['issues', 'id'], 'alias']], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { table: 'issues', select: [[['issues', 'id'], 'alias']], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1238,14 +1229,14 @@ describe('zql/ast', () => { schema: 'zero', table: 'clients', select: [[['issues', 'id'], 'alias']], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { schema: 'zero', table: 'clients', select: [[['issues', 'id'], 'alias']], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1257,7 +1248,7 @@ describe('zql/ast', () => { [['issues', 'id'], 'id_alias'], [['issues', 'name'], 'a_name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -1265,7 +1256,7 @@ describe('zql/ast', () => { [['issues', 'name'], 'a_name'], [['issues', 'id'], 'id_alias'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -1274,7 +1265,7 @@ describe('zql/ast', () => { [['issues', 'id'], 'id_alias'], [['issues', 'name'], 'a_name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1283,13 +1274,13 @@ describe('zql/ast', () => { { table: 'issues', aggregate: [{aggregate: 'count', alias: 'num'}], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { table: 'issues', aggregate: [{aggregate: 'count', alias: 'num'}], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1305,7 +1296,7 @@ describe('zql/ast', () => { alias: 'maxPri', }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -1317,7 +1308,7 @@ describe('zql/ast', () => { }, {aggregate: 'count', alias: 'num'}, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -1330,7 +1321,7 @@ describe('zql/ast', () => { alias: 'maxPri', }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1346,7 +1337,7 @@ describe('zql/ast', () => { ['issues', 'id'], ['issues', 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -1358,7 +1349,7 @@ describe('zql/ast', () => { ['issues', 'name'], ['issues', 'id'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -1371,7 +1362,7 @@ describe('zql/ast', () => { ['issues', 'id'], ['issues', 'name'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, { @@ -1389,11 +1380,8 @@ describe('zql/ast', () => { ], // ORDER BY expression order must be preserved. orderBy: [ - [ - ['issues', 'dueDate'], - ['issues', 'priority'], - ], - 'desc', + [['issues', 'dueDate'], 'desc'], + [['issues', 'priority'], 'desc'], ], limit: 10, }, @@ -1410,11 +1398,8 @@ describe('zql/ast', () => { ], // ORDER BY expression order must be preserved. orderBy: [ - [ - ['issues', 'dueDate'], - ['issues', 'priority'], - ], - 'desc', + [['issues', 'dueDate'], 'desc'], + [['issues', 'priority'], 'desc'], ], limit: 10, }, @@ -1434,11 +1419,8 @@ describe('zql/ast', () => { ], // ORDER BY expression order must be preserved. orderBy: [ - [ - ['issues', 'priority'], - ['issues', 'dueDate'], - ], - 'desc', + [['issues', 'priority'], 'desc'], + [['issues', 'dueDate'], 'desc'], ], limit: 10, }, @@ -1455,11 +1437,8 @@ describe('zql/ast', () => { ], // ORDER BY expression order must be preserved. orderBy: [ - [ - ['issues', 'priority'], - ['issues', 'dueDate'], - ], - 'desc', + [['issues', 'priority'], 'desc'], + [['issues', 'dueDate'], 'desc'], ], limit: 10, }, @@ -1482,7 +1461,7 @@ describe('zql/ast', () => { [['issues', 'id'], 'id_alias'], [['issues', 'name'], 'b_alias'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, as: 'owner', on: [ @@ -1491,7 +1470,7 @@ describe('zql/ast', () => { ], }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, { table: 'issues', @@ -1508,7 +1487,7 @@ describe('zql/ast', () => { [['issues', 'name'], 'b_alias'], [['issues', 'id'], 'id_alias'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, as: 'owner', on: [ @@ -1517,7 +1496,7 @@ describe('zql/ast', () => { ], }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, ], normalized: { @@ -1535,7 +1514,7 @@ describe('zql/ast', () => { [['issues', 'id'], 'id_alias'], [['issues', 'name'], 'b_alias'], ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, as: 'owner', on: [ @@ -1544,7 +1523,7 @@ describe('zql/ast', () => { ], }, ], - orderBy: [[['issues', 'id']], 'asc'], + orderBy: [[['issues', 'id'], 'asc']], }, }, ...conditionCases('where'), @@ -1559,7 +1538,7 @@ describe('zql/ast', () => { type: 'left', other: { table: 'issueLabel', - orderBy: [[['issueLabel', 'id']], 'asc'], + orderBy: [[['issueLabel', 'id'], 'asc']], }, as: 'issueLabel', on: [ @@ -1571,7 +1550,7 @@ describe('zql/ast', () => { type: 'left', other: { table: 'label', - orderBy: [[['label', 'id']], 'asc'], + orderBy: [[['label', 'id'], 'asc']], }, as: 'label', on: [ @@ -1621,7 +1600,7 @@ describe('zql/ast', () => { type: 'left', other: { table: 'issueLabel', - orderBy: [[['issueLabel', 'id']], 'asc'], + orderBy: [[['issueLabel', 'id'], 'asc']], }, as: 'issueLabel', on: [ @@ -1633,7 +1612,7 @@ describe('zql/ast', () => { type: 'left', other: { table: 'label', - orderBy: [[['label', 'id']], 'asc'], + orderBy: [[['label', 'id'], 'asc']], }, as: 'label', on: [ @@ -1695,7 +1674,7 @@ describe('zql/ast', () => { type: 'left', other: { table: 'issueLabel', - orderBy: [[['issueLabel', 'id']], 'asc'], + orderBy: [[['issueLabel', 'id'], 'asc']], }, as: 'issueLabel', on: [ @@ -1707,7 +1686,7 @@ describe('zql/ast', () => { type: 'left', other: { table: 'label', - orderBy: [[['label', 'id']], 'asc'], + orderBy: [[['label', 'id'], 'asc']], }, as: 'label', on: [ diff --git a/packages/zql/src/zql/ast/ast.ts b/packages/zql/src/zql/ast/ast.ts index 2536fcf216..ebb995d5e0 100644 --- a/packages/zql/src/zql/ast/ast.ts +++ b/packages/zql/src/zql/ast/ast.ts @@ -6,11 +6,11 @@ import {defined} from 'shared/src/arrays.js'; export type Selector = readonly [table: string, column: string]; -export type Ordering = readonly [ - // TODO(mlaw): table being nullable is temporary until we fixup order-by application for joined tables - fields: readonly Selector[], +export type Ordering = readonly (readonly [ + field: Selector, direction: 'asc' | 'desc', -]; +])[]; + export type Primitive = string | number | boolean | null; export type PrimitiveArray = string[] | number[] | boolean[]; diff --git a/packages/zql/src/zql/context/test-context.ts b/packages/zql/src/zql/context/test-context.ts index 2eef428c80..e3230be1f3 100644 --- a/packages/zql/src/zql/context/test-context.ts +++ b/packages/zql/src/zql/context/test-context.ts @@ -29,7 +29,7 @@ export class TestContext implements Context { if (!this.#sources.has(name)) { const source = this.materialite.newSetSource( (l: T, r: T) => compareUTF8(l.id as string, r.id as string), - [[[name, 'id']], 'asc'], + [[[name, 'id'], 'asc']], name, ) as unknown as Source; source.seed([]); @@ -70,7 +70,7 @@ export class InfiniteSourceContext implements Context { } else { source = this.materialite.newSetSource( (l: X, r: X) => compareUTF8(l.id as string, r.id as string), - [[[name, 'id']], 'asc'], + [[[name, 'id'], 'asc']], name, ) as unknown as Source; source.seed([]); @@ -174,8 +174,7 @@ class InfiniteSource implements Source { this.#materialite.getVersion(), this.#iterable, createPullResponseMessage(message, this.#name, [ - [[this.#name, 'id']], - 'asc', + [[this.#name, 'id'], 'asc'], ]), ); break; diff --git a/packages/zql/src/zql/context/zero-context.ts b/packages/zql/src/zql/context/zero-context.ts index cdedbcfe11..a0668ad302 100644 --- a/packages/zql/src/zql/context/zero-context.ts +++ b/packages/zql/src/zql/context/zero-context.ts @@ -71,7 +71,7 @@ class ZeroSource { constructor(materialite: Materialite, name: string, addWatch: AddWatch) { this.#canonicalSource = materialite.newSetSource( canonicalComparator, - [[[name, 'id']], 'asc'], + [[[name, 'id'], 'asc']], name, ); addWatch(name, this.#handleDiff); diff --git a/packages/zql/src/zql/ivm/compare.ts b/packages/zql/src/zql/ivm/compare.ts index 6abb5859a0..64fe0f4364 100644 --- a/packages/zql/src/zql/ivm/compare.ts +++ b/packages/zql/src/zql/ivm/compare.ts @@ -1,5 +1,5 @@ import {unreachable} from 'shared/src/asserts.js'; -import type {Selector} from '../ast/ast.js'; +import type {Ordering} from '../ast/ast.js'; import {getValueFromEntity} from './source/util.js'; export function compareEntityFields(lVal: T, rVal: T) { @@ -27,23 +27,21 @@ export function compareEntityFields(lVal: T, rVal: T) { } export function makeComparator( - qualifiedColumns: readonly Selector[], - direction: 'asc' | 'desc', + orderBy: Ordering, ): (l: T, r: T) => number { const comparator = (l: T, r: T) => { - let comp = 0; - for (const qualifiedColumn of qualifiedColumns) { - comp = compareEntityFields( - getValueFromEntity(l as Record, qualifiedColumn), - getValueFromEntity(r as Record, qualifiedColumn), + for (const [selector, direction] of orderBy) { + const comp = compareEntityFields( + getValueFromEntity(l as Record, selector), + getValueFromEntity(r as Record, selector), ); if (comp !== 0) { - return comp; + return direction === 'asc' ? comp : -comp; } } - return comp; + return 0; }; - return direction === 'asc' ? comparator : (l, r) => comparator(r, l); + return comparator; } diff --git a/packages/zql/src/zql/ivm/graph/difference-stream.test.ts b/packages/zql/src/zql/ivm/graph/difference-stream.test.ts index eeb68b8325..0f0228a4ff 100644 --- a/packages/zql/src/zql/ivm/graph/difference-stream.test.ts +++ b/packages/zql/src/zql/ivm/graph/difference-stream.test.ts @@ -1,6 +1,6 @@ import {expect, test} from 'vitest'; -import {DifferenceStream} from './difference-stream.js'; import {Materialite} from '../materialite.js'; +import {DifferenceStream} from './difference-stream.js'; import {createPullMessage, createPullResponseMessage} from './message.js'; import {DebugOperator} from './operators/debug-operator.js'; @@ -130,7 +130,7 @@ test('cleaning up the only user of a stream cleans up the entire pipeline', () = const materialite = new Materialite(); const set = materialite.newSetSource( (l, r) => l.x - r.x, - [[['elem', 'x']], 'asc'], + [[['elem', 'x'], 'asc']], 'elem', ); @@ -153,7 +153,7 @@ test('cleaning up the only user of a stream cleans up the entire pipeline but st const materialite = new Materialite(); const set = materialite.newSetSource( (l, r) => l.x - r.x, - [[['elem', 'x']], 'asc'], + [[['elem', 'x'], 'asc']], 'elem', ); @@ -227,7 +227,7 @@ test('replying to a message only notifies along the requesting path', () => { x.setUpstream(s2Dbg); s3.debug(() => notified.push(6)); - const msg = createPullMessage([[], 'asc']); + const msg = createPullMessage(undefined); x.messageUpstream(msg, { commit: () => {}, diff --git a/packages/zql/src/zql/ivm/graph/operators/concat-operator.ts b/packages/zql/src/zql/ivm/graph/operators/concat-operator.ts index ac23c19ec2..e97f511761 100644 --- a/packages/zql/src/zql/ivm/graph/operators/concat-operator.ts +++ b/packages/zql/src/zql/ivm/graph/operators/concat-operator.ts @@ -113,7 +113,7 @@ export function inOrder( ) { const first = buffer[0]; const order = must(first[1].order); - const comparator = makeComparator(order[0], order[1]); + const comparator = makeComparator(order); const iterables = buffer.map(r => r[0]); return iterInOrder(iterables, (l, r) => comparator(l[0], r[0])); diff --git a/packages/zql/src/zql/ivm/graph/operators/join-operator-test-util.ts b/packages/zql/src/zql/ivm/graph/operators/join-operator-test-util.ts index 0085a44faa..1ee6c5d229 100644 --- a/packages/zql/src/zql/ivm/graph/operators/join-operator-test-util.ts +++ b/packages/zql/src/zql/ivm/graph/operators/join-operator-test-util.ts @@ -1,6 +1,6 @@ import {expect, vi} from 'vitest'; import {DifferenceStream} from '../difference-stream.js'; -import {createPullResponseMessage, PullMsg} from '../message.js'; +import {PullMsg, createPullResponseMessage} from '../message.js'; export type Track = { id: string; @@ -63,7 +63,7 @@ export function orderIsRemovedFromRequest(join: 'leftJoin' | 'join') { id: 1, hoistedConditions: [], type: 'pull', - order: [[['intentional-nonsense', 'x']], 'asc'], + order: [[['intentional-nonsense', 'x'], 'asc']], }; const listener = { commit() {}, @@ -101,7 +101,7 @@ export function orderIsRemovedFromReply(join: 'leftJoin' | 'join') { id: 1, hoistedConditions: [], type: 'pull', - order: [[['intentional-nonsense', 'x']], 'asc'], + order: [[['intentional-nonsense', 'x'], 'asc']], }; const listener = { commit() {}, @@ -109,12 +109,10 @@ export function orderIsRemovedFromReply(join: 'leftJoin' | 'join') { }; output.messageUpstream(msg, listener); const trackReply = createPullResponseMessage(msg, 'track', [ - [['track', 'id']], - 'asc', + [['track', 'id'], 'asc'], ]); const albumReply = createPullResponseMessage(msg, 'title', [ - [['title', 'id']], - 'asc', + [['title', 'id'], 'asc'], ]); trackInput.newDifference(1, [], trackReply); diff --git a/packages/zql/src/zql/ivm/source/set-source.test.ts b/packages/zql/src/zql/ivm/source/set-source.test.ts index 62d3904d2b..5fd5fa0e98 100644 --- a/packages/zql/src/zql/ivm/source/set-source.test.ts +++ b/packages/zql/src/zql/ivm/source/set-source.test.ts @@ -1,13 +1,14 @@ import fc from 'fast-check'; import {describe, expect, test} from 'vitest'; +import type {Ordering} from '../../ast/ast.js'; import type {Listener} from '../graph/difference-stream.js'; import type {PullMsg} from '../graph/message.js'; import {Materialite} from '../materialite.js'; type E = {id: number}; -const ordering = [[['test', 'id']], 'asc'] as const; -const descOrdering = [[['test', 'id']], 'desc'] as const; +const ordering: Ordering = [[['test', 'id'], 'asc']]; +const descOrdering: Ordering = [[['test', 'id'], 'desc']]; const comparator = (l: E, r: E) => l.id - r.id; const numberComparator = (l: number, r: number) => l - r; @@ -197,7 +198,7 @@ test('history requests with an alternate ordering are fulfilled by that ordering { id: 1, type: 'pull', - order: [[['e2', 'id']], 'asc'], + order: [[['e2', 'id'], 'asc']], hoistedConditions: [], }, listener, @@ -213,11 +214,8 @@ test('history requests with an alternate ordering are fulfilled by that ordering id: 2, type: 'pull', order: [ - [ - ['e2', 'x'], - ['e2', 'id'], - ], - 'asc', + [['e2', 'x'], 'asc'], + [['e2', 'id'], 'asc'], ], hoistedConditions: [], }, @@ -239,11 +237,8 @@ test('history requests with an alternate ordering are fulfilled by that ordering id: 3, type: 'pull', order: [ - [ - ['e2', 'x'], - ['e2', 'id'], - ], - 'asc', + [['e2', 'x'], 'asc'], + [['e2', 'id'], 'asc'], ], hoistedConditions: [], }, @@ -294,11 +289,8 @@ describe('history requests with hoisted filters', () => { id, type: 'pull', order: [ - [ - ['e', 'x'], - ['e', 'id'], - ], - 'asc', + [['e', 'x'], 'asc'], + [['e', 'id'], 'asc'], ], hoistedConditions: [ { @@ -391,7 +383,7 @@ describe('history requests with hoisted filters', () => { { id: 1, type: 'pull', - order: [[['e', 'id']], 'desc'], + order: [[['e', 'id'], 'desc']], hoistedConditions: [ { selector: ['e', 'id'], @@ -409,7 +401,7 @@ describe('history requests with hoisted filters', () => { { id: 1, type: 'pull', - order: [[['e', 'id']], 'desc'], + order: [[['e', 'id'], 'desc']], hoistedConditions: [ { selector: ['e', 'id'], @@ -427,7 +419,7 @@ describe('history requests with hoisted filters', () => { { id: 1, type: 'pull', - order: [[['e', 'id']], 'desc'], + order: [[['e', 'id'], 'desc']], hoistedConditions: [ { selector: ['e', 'id'], @@ -450,11 +442,8 @@ describe('history requests with hoisted filters', () => { id: 1, type: 'pull', order: [ - [ - ['e', 'x'], - ['e', 'id'], - ], - 'asc', + [['e', 'x'], 'asc'], + [['e', 'id'], 'asc'], ], hoistedConditions: [ { @@ -473,11 +462,8 @@ describe('history requests with hoisted filters', () => { id: 1, type: 'pull', order: [ - [ - ['e', 'y'], - ['e', 'id'], - ], - 'asc', + [['e', 'y'], 'asc'], + [['e', 'id'], 'asc'], ], hoistedConditions: [ { @@ -497,11 +483,8 @@ describe('history requests with hoisted filters', () => { id: 1, type: 'pull', order: [ - [ - ['e', 'x'], - ['e', 'id'], - ], - 'asc', + [['e', 'x'], 'asc'], + [['e', 'id'], 'asc'], ], hoistedConditions: [ { @@ -522,11 +505,8 @@ describe('history requests with hoisted filters', () => { id: 1, type: 'pull', order: [ - [ - ['e', 'y'], - ['e', 'id'], - ], - 'asc', + [['e', 'y'], 'asc'], + [['e', 'id'], 'asc'], ], hoistedConditions: [ { diff --git a/packages/zql/src/zql/ivm/source/set-source.ts b/packages/zql/src/zql/ivm/source/set-source.ts index 8254947205..989f71ec34 100644 --- a/packages/zql/src/zql/ivm/source/set-source.ts +++ b/packages/zql/src/zql/ivm/source/set-source.ts @@ -1,21 +1,21 @@ import {must} from 'shared/src/must.js'; +import type {ISortedMap} from 'sorted-btree-roci'; +import BTree from 'sorted-btree-roci'; import type {Ordering, Primitive, Selector} from '../../ast/ast.js'; +import {gen} from '../../util/iterables.js'; +import {makeComparator} from '../compare.js'; import {DifferenceStream} from '../graph/difference-stream.js'; import { - createPullResponseMessage, HoistedCondition, PullMsg, Request, + createPullResponseMessage, } from '../graph/message.js'; import type {MaterialiteForSourceInternal} from '../materialite.js'; import type {Entry, Multiset} from '../multiset.js'; import type {Comparator, PipelineEntity, Version} from '../types.js'; -import type {Source, SourceInternal} from './source.js'; -import type {ISortedMap} from 'sorted-btree-roci'; -import BTree from 'sorted-btree-roci'; -import {gen} from '../../util/iterables.js'; -import {makeComparator} from '../compare.js'; import {SourceHashIndex} from './source-hash-index.js'; +import type {Source, SourceInternal} from './source.js'; /** * A source that remembers what values it contains. @@ -269,7 +269,7 @@ export class SetSource implements Source { // Is there a range constraint against the ordered field? if (orderForReply !== undefined) { const range = getRange(conditionsForThisSource, orderForReply); - if (request.order === undefined || request.order[1] === 'asc') { + if (request.order === undefined || request.order[0][1] === 'asc') { this.#stream.newDifference( this._materialite.getVersion(), gen(() => @@ -284,10 +284,10 @@ export class SetSource implements Source { } const maybeKey = maybeGetKey(range.field, range.top); - if (maybeKey !== undefined && newSort.#order[0].length > 1) { + if (maybeKey !== undefined && newSort.#order.length > 1) { const entriesBelow = newSort.#tree.entries(maybeKey); let key: T | undefined; - const specialComparator = makeComparator([range.field], 'asc'); + const specialComparator = makeComparator([[range.field, 'asc']]); for (const entry of entriesBelow) { if (specialComparator(entry[0], maybeKey) > 0) { key = entry[0]; @@ -335,31 +335,45 @@ export class SetSource implements Source { return [this, this.#order]; } // only retain fields relevant to this source. - const firstField = ordering[0][0]; + const firstSelector = ordering[0][0]; - if (firstField[0] !== this.#name) { + if (firstSelector[0] !== this.#name) { return [this, this.#order]; } - const key = firstField[1]; + const key = firstSelector[1]; // this is the canonical sort. if (key === 'id') { return [this, this.#order]; } const alternateSort = this.#sorts.get(key); - const fields: Selector[] = [firstField, [this.#name, 'id']]; if (alternateSort !== undefined) { - return [alternateSort, [fields, ordering[1]]]; + const newOrdering: Ordering = [ + ordering[0], + [[this.#name, 'id'], ordering[0][1]], + ]; + return [alternateSort, newOrdering]; } // We ignore asc/desc as directionality can be achieved by reversing the order of iteration. // We do not need a separate source. // Must append id for uniqueness. - const newComparator = makeComparator(fields, 'asc'); - const source = this.withNewOrdering(newComparator, [fields, 'asc']); + const orderBy: Ordering = [ + [firstSelector, 'asc'], + [[this.#name, 'id'], 'asc'], + ]; + const newComparator = makeComparator(orderBy); + const source = this.withNewOrdering(newComparator, orderBy); this.#sorts.set(key, source); - return [source, [fields, ordering[1]]]; + const dir = ordering[0][1]; + const orderByKeepDirection: Ordering = [ + [firstSelector, dir], + [[this.#name, 'id'], dir], + ]; + + return [source, orderByKeepDirection]; + 2; } // TODO: in the future we should collapse hash and sorted indices @@ -414,7 +428,7 @@ function asEntries( message?: Request | undefined, ): Multiset { if (message?.order) { - if (message.order[1] === 'desc') { + if (message.order[0][1] === 'desc') { return gen(() => genFromBTreeEntries(m.entriesReversed())); } } @@ -457,6 +471,7 @@ function getRange(conditions: HoistedCondition[], sourceOrder: Ordering) { const sourceOrderFields = sourceOrder[0]; const firstOrderField = sourceOrderFields[0]; + // TODO: Does this work correctly with multiple conditions? for (const c of conditions) { if (c.selector[1] === firstOrderField[1]) { if (c.op === '>' || c.op === '>=' || c.op === '=') { @@ -476,38 +491,38 @@ function getRange(conditions: HoistedCondition[], sourceOrder: Ordering) { } function createEndPredicateAsc( - field: Selector, + selector: Selector, end: unknown, ): ((t: T) => boolean) | undefined { if (end === undefined) { return undefined; } - const comp = makeComparator([field], 'asc'); + const comp = makeComparator([[selector, 'asc']]); return t => { - const cmp = comp(t, {[field[1]]: end} as T); + const cmp = comp(t, {[selector[1]]: end} as T); return cmp > 0; }; } function createEndPredicateDesc( - field: Selector, + selector: Selector, end: unknown, ): ((t: T) => boolean) | undefined { if (end === undefined) { return undefined; } - const comp = makeComparator([field], 'asc'); + const comp = makeComparator([[selector, 'asc']]); return t => { - const cmp = comp(t, {[field[1]]: end} as T); + const cmp = comp(t, {[selector[1]]: end} as T); return cmp < 0; }; } -function maybeGetKey(field: Selector, value: unknown): T | undefined { +function maybeGetKey(selector: Selector, value: unknown): T | undefined { if (value === undefined) { return undefined; } return { - [field[1]]: value, + [selector[1]]: value, } as T; } diff --git a/packages/zql/src/zql/ivm/source/util.ts b/packages/zql/src/zql/ivm/source/util.ts index 1562af11ab..68a3d3366f 100644 --- a/packages/zql/src/zql/ivm/source/util.ts +++ b/packages/zql/src/zql/ivm/source/util.ts @@ -11,17 +11,22 @@ export function sourcesAreIdentical( return false; } - if (sourceAOrder[0].length !== sourceBOrder[0].length) { - return false; - } + return orderingsAreEqual(sourceAOrder, sourceBOrder); +} - if (sourceAOrder[1] !== sourceBOrder[1]) { +export function orderingsAreEqual(a: Ordering, b: Ordering) { + if (a.length !== b.length) { return false; } - - return sourceAOrder[0].every((col, i) => - selectorsAreEqual(sourceBOrder[0][i], col), - ); + for (let i = 0; i < a.length; i++) { + if (a[i][1] !== b[i][1]) { + return false; + } + if (!selectorsAreEqual(a[i][0], b[i][0])) { + return false; + } + } + return true; } export function selectorsAreEqual(l: Selector, r: Selector) { @@ -41,7 +46,7 @@ export function selectorArraysAreEqual( export function getValueFromEntity( entity: Record, qualifiedColumn: readonly [table: string | null, column: string], -) { +): unknown { if (isJoinResult(entity) && qualifiedColumn[0] !== null) { if (qualifiedColumn[1] === '*') { return (entity as Record)[qualifiedColumn[0]]; diff --git a/packages/zql/src/zql/ivm/view/tree-view.test.ts b/packages/zql/src/zql/ivm/view/tree-view.test.ts index 8c7859ff78..89066dff85 100644 --- a/packages/zql/src/zql/ivm/view/tree-view.test.ts +++ b/packages/zql/src/zql/ivm/view/tree-view.test.ts @@ -7,7 +7,7 @@ import type {Comparator} from '../types.js'; import {TreeView} from './tree-view.js'; const numberComparator = (l: number, r: number) => l - r; -const ordering = [[['x', 'id']], 'asc'] as const; +const ordering = [[['x', 'id'], 'asc']] as const; type Selected = {id: string}; test('asc and descComparator on Entities', () => { @@ -19,42 +19,24 @@ test('asc and descComparator on Entities', () => { 'x', ); const orderBy = [ - [ - ['x', 'n'], - ['x', 'id'], - ], - 'asc', + [['x', 'n'], 'asc'], + [['x', 'id'], 'asc'], ] as const; const view = new TreeView( context, s.stream, - makeComparator( - [ - ['x', 'n'], - ['x', 'id'], - ], - 'asc', - ), + makeComparator(orderBy), orderBy, ); const orderBy2 = [ - [ - ['x', 'n'], - ['x', 'id'], - ], - 'desc', + [['x', 'n'], 'desc'], + [['x', 'id'], 'desc'], ] as const; const descView = new TreeView( context, s.stream, - makeComparator( - [ - ['x', 'n'], - ['x', 'id'], - ], - 'desc', - ), + makeComparator(orderBy2), orderBy2, ); @@ -87,7 +69,7 @@ test('add & remove', () => { const {materialite} = context; const source = materialite.newSetSource<{x: number}>( (l, r) => l.x - r.x, - [[['test', 'x']], 'asc'] as const, + [[['test', 'x'], 'asc']], 'test', ); const view = new TreeView( @@ -115,7 +97,7 @@ test('replace', () => { fc.property(fc.uniqueArray(fc.integer()), arr => { const context = makeTestContext(); const {materialite} = context; - const orderBy = [[['test', 'x']], 'asc'] as const; + const orderBy = [[['test', 'x'], 'asc']] as const; const source = materialite.newSetSource<{x: number}>( (l, r) => l.x - r.x, orderBy, @@ -155,7 +137,7 @@ test('replace outside viewport', () => { type Item = {id: number; s: string}; const context = makeTestContext(); const {materialite} = context; - const orderBy = [[['test', 'x']], 'asc'] as const; + const orderBy = [[['test', 'x'], 'asc']] as const; const comparator: Comparator = (l, r) => l.id - r.id; const source = materialite.newSetSource(comparator, orderBy, 'test'); const view = new TreeView(context, source.stream, comparator, orderBy, 5); diff --git a/packages/zql/src/zql/ivm/view/tree-view.ts b/packages/zql/src/zql/ivm/view/tree-view.ts index 2afa254aa7..e350a0b6e4 100644 --- a/packages/zql/src/zql/ivm/view/tree-view.ts +++ b/packages/zql/src/zql/ivm/view/tree-view.ts @@ -3,7 +3,6 @@ import {must} from 'shared/src/must.js'; import BTree from 'sorted-btree-roci'; import type {Ordering} from '../../ast/ast.js'; import type {Context} from '../../context/context.js'; -import {fieldsMatch} from '../../query/statement.js'; import {makeComparator} from '../compare.js'; import type {DifferenceStream} from '../graph/difference-stream.js'; import {Reply, createPullMessage} from '../graph/message.js'; @@ -141,12 +140,11 @@ export class TreeView extends AbstractView { limit: number, ): IterableIterator> { const order = must(reply.order); - const fields = order?.[0] ?? []; const iterator = data[Symbol.iterator](); let i = 0; let last: T | undefined = undefined; - if (this.#order === undefined || fieldsMatch(fields, this.#order[0])) { + if (this.#order === undefined || selectorsMatch(order, this.#order)) { return { [Symbol.iterator]() { return this; @@ -170,16 +168,19 @@ export class TreeView extends AbstractView { // e.g., [modified] vs [modified, created] // in which case we process until we hit the next thing after // the source order after we limit. - if (!selectorsAreEqual(fields[0], this.#order[0][0])) { + if ( + order.length === 0 || + !selectorsAreEqual(order[0][0], this.#order[0][0]) + ) { throw new Error( - `Order must overlap on at least one field! Got: ${fields[0]} | ${ + `Order must overlap on at least one field! Got: ${order[0]?.[0]} | ${ this.#order[0][0] }`, ); } // Partial order overlap - const responseComparator = makeComparator(order[0], order[1]); + const responseComparator = makeComparator(order); return { [Symbol.iterator]() { return this; @@ -344,17 +345,27 @@ function orderingsAreCompatible( } // asc/desc differ. - if (sourceOrder[1] !== destOrder[1]) { + if (sourceOrder[0][1] !== destOrder[0][1]) { return false; } - const sourceFields = sourceOrder[0]; - const destFields = destOrder[0]; - // If at least the left most field is the same, we're compatible. - if (selectorsAreEqual(sourceFields[0], destFields[0])) { + if (selectorsAreEqual(sourceOrder[0][0], destOrder[0][0])) { return true; } return false; } + +function selectorsMatch(left: Ordering, right: Ordering) { + if (left.length !== right.length) { + return false; + } + for (let i = 0; i < left.length; i++) { + // ignore direction + if (!selectorsAreEqual(left[i][0], right[i][0])) { + return false; + } + } + return true; +} diff --git a/packages/zql/src/zql/query/entity-query.test.ts b/packages/zql/src/zql/query/entity-query.test.ts index 855a5e7456..0a85524da0 100644 --- a/packages/zql/src/zql/query/entity-query.test.ts +++ b/packages/zql/src/zql/query/entity-query.test.ts @@ -645,23 +645,20 @@ describe('ast', () => { // order methods update the ast expect(ast(q.asc('id'))).toEqual({ table: 'e1', - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], } satisfies AST); expect(ast(q.desc('id'))).toEqual({ table: 'e1', - orderBy: [[['e1', 'id']], 'desc'], + orderBy: [[['e1', 'id'], 'desc']], } satisfies AST); expect(ast(q.asc('id', 'a', 'b', 'c', 'd'))).toEqual({ table: 'e1', orderBy: [ - [ - ['e1', 'id'], - ['e1', 'a'], - ['e1', 'b'], - ['e1', 'c'], - ['e1', 'd'], - ], - 'asc', + [['e1', 'id'], 'asc'], + [['e1', 'a'], 'asc'], + [['e1', 'b'], 'asc'], + [['e1', 'c'], 'asc'], + [['e1', 'd'], 'asc'], ], } satisfies AST); }); @@ -1297,7 +1294,7 @@ describe('all references to columns are always qualified', () => { test: 'order by', q: q.asc('a'), expected: { - orderBy: [[['e1', 'a']], 'asc'], + orderBy: [[['e1', 'a'], 'asc']], table: 'e1', }, }, diff --git a/packages/zql/src/zql/query/entity-query.ts b/packages/zql/src/zql/query/entity-query.ts index 91088ba90a..19257bf220 100644 --- a/packages/zql/src/zql/query/entity-query.ts +++ b/packages/zql/src/zql/query/entity-query.ts @@ -1,19 +1,19 @@ import {must} from 'shared/src/must.js'; import type { AST, + Selector as ASTSelector, Aggregation, Condition, EqualityOps, + HavingCondition, InOps, LikeOps, OrderOps, - SetOps, - SimpleOperator, - Selector as ASTSelector, + Ordering, Primitive, PrimitiveArray, - HavingCondition, - Ordering, + SetOps, + SimpleOperator, } from '../ast/ast.js'; import type {Context} from '../context/context.js'; import {Misuse} from '../error/misuse.js'; @@ -265,21 +265,23 @@ export class EntityQuery { ): EntityQuery[]> { const seen = new Set(this.#ast.select?.map(s => s[1])); const aggregate: Aggregation[] = []; - const select = [...(this.#ast.select ?? [])]; + const ast = this.#ast; + const select = ast.select ? [...ast.select] : []; + for (const more of x) { if (!isAggregate(more)) { if (seen.has(more)) { continue; } seen.add(more); - select.push([qualifySelector(this.#ast, more), more]); + select.push([qualifySelector(ast, more), more]); continue; } aggregate.push({ field: more.field !== undefined - ? qualifySelector(this.#ast, more.field) + ? qualifySelector(ast, more.field) : undefined, alias: more.alias, aggregate: more.aggregate, @@ -290,8 +292,8 @@ export class EntityQuery { this.#context, this.#name, { - ...this.#ast, - select: [...select], + ...ast, + select, aggregate, }, ); @@ -484,14 +486,14 @@ export class EntityQuery { asc(...x: SimpleSelector[]) { return new EntityQuery(this.#context, this.#name, { ...this.#ast, - orderBy: [x.map(x => qualifySelector(this.#ast, x)), 'asc'], + orderBy: x.map(x => [qualifySelector(this.#ast, x), 'asc']), }); } desc(...x: SimpleSelector[]) { return new EntityQuery(this.#context, this.#name, { ...this.#ast, - orderBy: [x.map(x => qualifySelector(this.#ast, x)), 'desc'], + orderBy: x.map(x => [qualifySelector(this.#ast, x), 'desc']), }); } @@ -721,22 +723,30 @@ export function makeOrderingDeterministic( ast: AST, order: Ordering | undefined, ): Ordering { - if (order === undefined) { - order = [[[ast.table, 'id']], 'asc']; + const requiredOrderFields = + getRequiredOrderFieldsForDeterministicOrdering(ast); + + if (order) { + for (const [selector] of order) { + const key = selector.join('.'); + requiredOrderFields.delete(key); + } + } else { + requiredOrderFields.delete(`${ast.table}.id`); + order = [[[ast.table, 'id'], 'asc']]; } - const required = getRequiredOrderFieldsForDeterministicOrdering(ast); - const selectors = [...order[0]]; - for (const selector of selectors) { - const key = selector.join('.'); - required.delete(key); + if (requiredOrderFields.size === 0) { + return order; } - for (const selector of required.values()) { - selectors.push(selector); + const defaultDirection = order[0][1]; + const newOrder = [...order]; + for (const selector of requiredOrderFields.values()) { + newOrder.push([selector, defaultDirection]); } - return [selectors, order[1]]; + return newOrder; } function getRequiredOrderFieldsForDeterministicOrdering( @@ -755,8 +765,11 @@ function getRequiredOrderFieldsForDeterministicOrdering( // TODO(mlaw): this'll change with compound primary keys ret.set(ast.table + '.id', [ast.table, 'id']); - for (const join of ast.joins || []) { - ret.set(join.as + '.id', [join.as, 'id']); + const {joins} = ast; + if (joins) { + for (const join of joins) { + ret.set(join.as + '.id', [join.as, 'id']); + } } return ret; diff --git a/packages/zql/src/zql/query/order-limit.test.ts b/packages/zql/src/zql/query/order-limit.test.ts index 3cf3e19872..4e4bb73721 100644 --- a/packages/zql/src/zql/query/order-limit.test.ts +++ b/packages/zql/src/zql/query/order-limit.test.ts @@ -1,12 +1,12 @@ import {beforeEach, describe, expect, test} from 'vitest'; import { + TestContext, makeInfiniteSourceContext, makeTestContext, - TestContext, } from '../context/test-context.js'; import type {Source} from '../ivm/source/source.js'; -import {EntityQuery, exp, or} from './entity-query.js'; import * as agg from './agg.js'; +import {EntityQuery, exp, or} from './entity-query.js'; describe('a limited window is correctly maintained over differences', () => { type E = { diff --git a/packages/zql/src/zql/query/statement.test.ts b/packages/zql/src/zql/query/statement.test.ts index 8549b5b18c..c58ec7570e 100644 --- a/packages/zql/src/zql/query/statement.test.ts +++ b/packages/zql/src/zql/query/statement.test.ts @@ -1,9 +1,9 @@ +import {assert} from 'shared/src/asserts.js'; import {expect, test} from 'vitest'; import {z} from 'zod'; import {makeTestContext} from '../context/test-context.js'; import {makeComparator} from '../ivm/compare.js'; import {EntityQuery, astForTesting as ast} from './entity-query.js'; -import {assert} from 'shared/src/asserts.js'; const e1 = z.object({ id: z.string(), @@ -63,7 +63,6 @@ test('sorted materialization', async () => { id: 'c', n: 1, }); - await Promise.resolve(); expect(await ascStatement.exec()).toEqual([ {id: 'c', n: 1}, @@ -97,7 +96,6 @@ test('sorting is stable via suffixing the primary key to the order', async () => id: 'c', n: 1, }); - await Promise.resolve(); expect(await ascStatement.exec()).toEqual([ {id: 'a', n: 1}, {id: 'b', n: 1}, @@ -138,8 +136,10 @@ test('makeComparator', () => { function check(values1: unknown[], values2: unknown[], expected: number) { expect( makeComparator>( - Array.from({length: values1.length}).map((_, i) => ['x', 'field' + i]), - 'asc', + Array.from({length: values1.length}).map((_, i) => [ + ['x', 'field' + i], + 'asc', + ]), )(makeObject(values1), makeObject(values2)), ).toBe(expected); } @@ -188,7 +188,7 @@ test('ensure we get callbacks when subscribing and unsubscribing', async () => { type: 'added', ast: { ...ast(q), - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], }, }); @@ -200,7 +200,7 @@ test('ensure we get callbacks when subscribing and unsubscribing', async () => { type: 'removed', ast: { ...ast(q), - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], }, }, ]); @@ -223,7 +223,7 @@ test('preloaded resolves to true when subscription is got', async () => { type: 'added', ast: { ...ast(q), - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], }, }); @@ -251,7 +251,7 @@ test('preloaded resolves to true when subscription is got', async () => { type: 'removed', ast: { ...ast(q), - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], }, }, ]); @@ -274,7 +274,7 @@ test('preloaded resolves to false if preload is cleanedup before query is ever g type: 'added', ast: { ...ast(q), - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], }, }); @@ -292,7 +292,7 @@ test('preloaded resolves to false if preload is cleanedup before query is ever g type: 'removed', ast: { ...ast(q), - orderBy: [[['e1', 'id']], 'asc'], + orderBy: [[['e1', 'id'], 'asc']], }, }, ]); diff --git a/packages/zql/src/zql/query/statement.ts b/packages/zql/src/zql/query/statement.ts index ca61e4b21a..989d023188 100644 --- a/packages/zql/src/zql/query/statement.ts +++ b/packages/zql/src/zql/query/statement.ts @@ -4,7 +4,7 @@ import { buildPipeline, pullUsedSources, } from '../ast-to-ivm/pipeline-builder.js'; -import type {AST, Ordering, Selector} from '../ast/ast.js'; +import type {AST, Ordering} from '../ast/ast.js'; import type {Context} from '../context/context.js'; import {makeComparator} from '../ivm/compare.js'; import type {DifferenceStream} from '../ivm/graph/difference-stream.js'; @@ -130,6 +130,7 @@ export class Statement implements IStatement { async function createMaterialization(ast: AST, context: Context) { const {orderBy, limit} = ast; assert(orderBy); + assert(orderBy.length > 0); const usedSources = pullUsedSources(ast, new Set()); const promises: PromiseLike[] = []; @@ -161,23 +162,10 @@ async function createMaterialization(ast: AST, context: Context) { pipeline as unknown as DifferenceStream< Return extends [] ? Return[number] : never >, - makeComparator>(orderBy[0], orderBy[1]), + makeComparator>(orderBy), orderBy, limit, ) as unknown as View; view.pullHistoricalData(); return view; } - -export function fieldsMatch( - left: readonly Selector[], - right: readonly Selector[], -) { - return ( - left.length === right.length && - left.every( - (leftItem, i) => - leftItem[0] === right[i][0] && leftItem[1] === right[i][1], - ) - ); -}