Skip to content

Commit b459240

Browse files
committed
PR comments
1 parent d3a2daa commit b459240

File tree

7 files changed

+191
-32
lines changed

7 files changed

+191
-32
lines changed

apps/backend/scripts/run-bulldozer-studio.ts

Lines changed: 139 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,112 @@ function keyPathSqlLiteral(pathSegments: string[]): string {
101101
return `ARRAY[${pathSegments.map((segment) => quoteSqlJsonbLiteral(segment)).join(", ")}]::jsonb[]`;
102102
}
103103

104+
function splitSqlStatements(sqlScript: string): string[] {
105+
const statements: string[] = [];
106+
let statementStart = 0;
107+
let index = 0;
108+
let inSingleQuote = false;
109+
let inDoubleQuote = false;
110+
let inLineComment = false;
111+
let blockCommentDepth = 0;
112+
let dollarQuoteTag: null | string = null;
113+
while (index < sqlScript.length) {
114+
const current = sqlScript[index];
115+
const next = sqlScript[index + 1];
116+
117+
if (inLineComment) {
118+
if (current === "\n") inLineComment = false;
119+
index++;
120+
continue;
121+
}
122+
if (blockCommentDepth > 0) {
123+
if (current === "/" && next === "*") {
124+
blockCommentDepth++;
125+
index += 2;
126+
continue;
127+
}
128+
if (current === "*" && next === "/") {
129+
blockCommentDepth--;
130+
index += 2;
131+
continue;
132+
}
133+
index++;
134+
continue;
135+
}
136+
if (dollarQuoteTag !== null) {
137+
if (sqlScript.startsWith(dollarQuoteTag, index)) {
138+
index += dollarQuoteTag.length;
139+
dollarQuoteTag = null;
140+
} else {
141+
index++;
142+
}
143+
continue;
144+
}
145+
if (inSingleQuote) {
146+
if (current === "'") {
147+
if (next === "'") {
148+
index += 2;
149+
continue;
150+
}
151+
inSingleQuote = false;
152+
}
153+
index++;
154+
continue;
155+
}
156+
if (inDoubleQuote) {
157+
if (current === "\"") inDoubleQuote = false;
158+
index++;
159+
continue;
160+
}
161+
162+
if (current === "-" && next === "-") {
163+
inLineComment = true;
164+
index += 2;
165+
continue;
166+
}
167+
if (current === "/" && next === "*") {
168+
blockCommentDepth = 1;
169+
index += 2;
170+
continue;
171+
}
172+
if (current === "'") {
173+
inSingleQuote = true;
174+
index++;
175+
continue;
176+
}
177+
if (current === "\"") {
178+
inDoubleQuote = true;
179+
index++;
180+
continue;
181+
}
182+
if (current === "$") {
183+
let tagEnd = index + 1;
184+
while (tagEnd < sqlScript.length && /[a-zA-Z0-9_]/.test(sqlScript[tagEnd] ?? "")) {
185+
tagEnd++;
186+
}
187+
if (sqlScript[tagEnd] === "$") {
188+
dollarQuoteTag = sqlScript.slice(index, tagEnd + 1);
189+
index = tagEnd + 1;
190+
continue;
191+
}
192+
}
193+
if (current === ";") {
194+
const statement = sqlScript.slice(statementStart, index).trim();
195+
if (statement.length > 0) {
196+
statements.push(statement);
197+
}
198+
statementStart = index + 1;
199+
}
200+
index++;
201+
}
202+
203+
const trailingStatement = sqlScript.slice(statementStart).trim();
204+
if (trailingStatement.length > 0) {
205+
statements.push(trailingStatement);
206+
}
207+
return statements;
208+
}
209+
104210
function tableIdToString(tableId: unknown): string {
105211
if (typeof tableId === "string") return tableId;
106212
return JSON.stringify(tableId);
@@ -134,10 +240,14 @@ const schemaObject: Record<string, unknown> = exampleFungibleLedgerSchema;
134240
const registry = createTableRegistry(schemaObject);
135241

136242
async function executeStatements(statements: SqlStatement[]): Promise<void> {
243+
const sqlScript = toExecutableSqlStatements(statements);
244+
const executableStatements = splitSqlStatements(sqlScript);
137245
await retryTransaction(globalPrismaClient, async (tx) => {
138246
await tx.$executeRawUnsafe(`SET LOCAL jit = off`);
139247
await tx.$executeRawUnsafe(`SELECT pg_advisory_xact_lock(${BULLDOZER_LOCK_ID})`);
140-
await tx.$executeRawUnsafe(toExecutableSqlStatements(statements));
248+
for (const statement of executableStatements) {
249+
await tx.$executeRawUnsafe(statement);
250+
}
141251
});
142252
}
143253

@@ -229,17 +339,17 @@ async function computeStudioLayout(tables: Array<Awaited<ReturnType<typeof getTa
229339
}),
230340
});
231341

232-
const positions: Record<string, { x: number, y: number }> = {};
342+
const positions = new Map<string, { x: number, y: number }>();
233343
for (const child of layout.children ?? []) {
234344
if (typeof child.id !== "string") continue;
235-
positions[child.id] = {
345+
positions.set(child.id, {
236346
x: Number(child.x ?? 0),
237347
y: Number(child.y ?? 0),
238-
};
348+
});
239349
}
240350

241351
return {
242-
positions,
352+
positions: Object.fromEntries(positions),
243353
sceneWidth: Number(Reflect.get(layout, "width") ?? 600),
244354
sceneHeight: Number(Reflect.get(layout, "height") ?? 600),
245355
};
@@ -1532,13 +1642,24 @@ function getStudioPageHtml(): string {
15321642
info.className = "detail-section";
15331643
const kv = document.createElement("div");
15341644
kv.className = "kv";
1535-
kv.innerHTML = ""
1536-
+ "<div class='kv-key'>name</div><div class='mono'>" + table.name + "</div>"
1537-
+ "<div class='kv-key'>tableId</div><div class='mono'>" + table.tableId + "</div>"
1538-
+ "<div class='kv-key'>operator</div><div class='mono'>" + table.operator + "</div>"
1539-
+ "<div class='kv-key'>initialized</div><div>" + String(table.initialized) + "</div>"
1540-
+ "<div class='kv-key'>dependencies</div><div class='mono'>" + (table.dependencies.length === 0 ? "(none)" : table.dependencies.join(", ")) + "</div>"
1541-
+ "<div class='kv-key'>rows(all groups)</div><div class='mono'>" + String(details.totalRows) + "</div>";
1645+
const appendInfoRow = (label: string, value: string, isMonospace = false) => {
1646+
const keyCell = document.createElement("div");
1647+
keyCell.className = "kv-key";
1648+
keyCell.textContent = label;
1649+
const valueCell = document.createElement("div");
1650+
if (isMonospace) {
1651+
valueCell.className = "mono";
1652+
}
1653+
valueCell.textContent = value;
1654+
kv.appendChild(keyCell);
1655+
kv.appendChild(valueCell);
1656+
};
1657+
appendInfoRow("name", table.name, true);
1658+
appendInfoRow("tableId", table.tableId, true);
1659+
appendInfoRow("operator", table.operator, true);
1660+
appendInfoRow("initialized", String(table.initialized));
1661+
appendInfoRow("dependencies", table.dependencies.length === 0 ? "(none)" : table.dependencies.join(", "), true);
1662+
appendInfoRow("rows(all groups)", String(details.totalRows), true);
15421663
info.appendChild(kv);
15431664
detailsPane.appendChild(info);
15441665
@@ -2038,6 +2159,12 @@ async function handleRequest(request: http.IncomingMessage, response: http.Serve
20382159
if (method === "POST" && pathname === "/api/raw/delete") {
20392160
const body = requireRecord(await readJsonBody(request), "raw delete body must be an object.");
20402161
const pathSegments = requireStringArray(Reflect.get(body, "pathSegments"), "pathSegments must be a string[]");
2162+
if (
2163+
pathSegments.length === 0
2164+
|| (pathSegments.length === 1 && pathSegments[0] === "table")
2165+
) {
2166+
throw new StackAssertionError("Deleting reserved root paths is not allowed.");
2167+
}
20412168
await retryTransaction(globalPrismaClient, async (tx) => {
20422169
await tx.$executeRawUnsafe(`
20432170
DELETE FROM "BulldozerStorageEngine"

apps/backend/scripts/run-cron-jobs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ async function main() {
2525

2626
for (const endpoint of endpoints) {
2727
runAsynchronously(async () => {
28-
await wait(30_000); // Wait a few seconds to make sure the server is fully started
28+
await wait(30_000); // Wait 30 seconds to make sure the server is fully started
2929
while (true) {
3030
const runResult = await Result.fromPromise(run(endpoint));
3131
if (runResult.status === "error") {

apps/backend/src/lib/bulldozer/db/example-schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const exampleFungibleLedgerSchema = (() => {
104104
const accountEntriesWithCounterparty = declareFilterTable({
105105
tableId: "bulldozer-example-ledger-account-entries-with-counterparty",
106106
fromTable: entriesByAccount,
107-
filter: predicate(`("rowData"->'counterparty') IS NOT NULL`),
107+
filter: predicate(`("rowData"->>'counterparty') IS NOT NULL`),
108108
});
109109
const accountEntriesSortedByAmount = declareSortTable({
110110
tableId: "bulldozer-example-ledger-account-entries-sorted-by-amount",

apps/backend/src/lib/bulldozer/db/index.perf.test.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const LOAD_COUNT_QUERY_MAX_MS = 5_000;
2727
const LOAD_POINT_MUTATION_MAX_MS = 400;
2828
const LOAD_SET_ROW_AVG_ITERATIONS = 10;
2929
const LOAD_SET_ROW_AVG_MAX_MS = 50;
30+
const LOAD_ONLINE_MUTATION_ITERATIONS = 5;
3031
const LOAD_ONLINE_MUTATION_MAX_MS = 50;
3132
const LOAD_SUBSET_ITERATION_MAX_MS = 50;
3233
const LOAD_SUBSET_ITERATION_ROW_COUNT = 1_000;
@@ -44,7 +45,7 @@ const LOAD_CONCAT_TABLE_INIT_MAX_MS = 10_000;
4445
const LOAD_CONCAT_TABLE_COUNT_QUERY_MAX_MS = 8_000;
4546
const LOAD_SORT_TABLE_INIT_MAX_MS = 90_000;
4647
const LOAD_SORT_TABLE_COUNT_QUERY_MAX_MS = 8_000;
47-
const LOAD_LFOLD_TABLE_INIT_MAX_MS = 120_000;
48+
const LOAD_LFOLD_TABLE_INIT_MAX_MS = 130_000;
4849
const LOAD_LFOLD_TABLE_COUNT_QUERY_MAX_MS = 12_000;
4950
const LOAD_LEFT_JOIN_TABLE_INIT_MAX_MS = 90_000;
5051
const LOAD_LEFT_JOIN_TABLE_COUNT_QUERY_MAX_MS = 8_000;
@@ -519,18 +520,30 @@ describe.sequential("bulldozer db performance (real postgres)", () => {
519520
const setRowAverageMs = setRowIterationTimes.reduce((acc, value) => acc + value, 0) / setRowIterationTimes.length;
520521
logLine(`[bulldozer-perf] load setRow average (${LOAD_SET_ROW_AVG_ITERATIONS} iterations): ${setRowAverageMs.toFixed(1)} ms`);
521522
expect(setRowAverageMs).toBeLessThanOrEqual(LOAD_SET_ROW_AVG_MAX_MS);
522-
const onlineInsert = await measureMs("load online setRow insert (stored table)", async () => {
523-
await runStatements(table.setRow("perf-online-row", expr(jsonbLiteral({ team: "beta", value: 111 }))));
524-
});
525-
expect(onlineInsert.elapsedMs).toBeLessThanOrEqual(LOAD_ONLINE_MUTATION_MAX_MS);
526-
const onlineUpdate = await measureMs("load online setRow update (stored table)", async () => {
527-
await runStatements(table.setRow("perf-online-row", expr(jsonbLiteral({ team: "beta", value: 222 }))));
528-
});
529-
expect(onlineUpdate.elapsedMs).toBeLessThanOrEqual(LOAD_ONLINE_MUTATION_MAX_MS);
530-
const onlineDelete = await measureMs("load online deleteRow (stored table)", async () => {
531-
await runStatements(table.deleteRow("perf-online-row"));
532-
});
533-
expect(onlineDelete.elapsedMs).toBeLessThanOrEqual(LOAD_ONLINE_MUTATION_MAX_MS);
523+
const onlineInsertTimes: number[] = [];
524+
const onlineUpdateTimes: number[] = [];
525+
const onlineDeleteTimes: number[] = [];
526+
for (let i = 0; i < LOAD_ONLINE_MUTATION_ITERATIONS; i++) {
527+
const rowIdentifier = `perf-online-row-${i}`;
528+
const insertStartedAt = performance.now();
529+
await runStatements(table.setRow(rowIdentifier, expr(jsonbLiteral({ team: "beta", value: 111 + i }))));
530+
onlineInsertTimes.push(performance.now() - insertStartedAt);
531+
const updateStartedAt = performance.now();
532+
await runStatements(table.setRow(rowIdentifier, expr(jsonbLiteral({ team: "beta", value: 211 + i }))));
533+
onlineUpdateTimes.push(performance.now() - updateStartedAt);
534+
const deleteStartedAt = performance.now();
535+
await runStatements(table.deleteRow(rowIdentifier));
536+
onlineDeleteTimes.push(performance.now() - deleteStartedAt);
537+
}
538+
const onlineInsertAvgMs = onlineInsertTimes.reduce((acc, value) => acc + value, 0) / onlineInsertTimes.length;
539+
const onlineUpdateAvgMs = onlineUpdateTimes.reduce((acc, value) => acc + value, 0) / onlineUpdateTimes.length;
540+
const onlineDeleteAvgMs = onlineDeleteTimes.reduce((acc, value) => acc + value, 0) / onlineDeleteTimes.length;
541+
logLine(`[bulldozer-perf] load online setRow insert average (${LOAD_ONLINE_MUTATION_ITERATIONS} iterations): ${onlineInsertAvgMs.toFixed(1)} ms`);
542+
logLine(`[bulldozer-perf] load online setRow update average (${LOAD_ONLINE_MUTATION_ITERATIONS} iterations): ${onlineUpdateAvgMs.toFixed(1)} ms`);
543+
logLine(`[bulldozer-perf] load online deleteRow average (${LOAD_ONLINE_MUTATION_ITERATIONS} iterations): ${onlineDeleteAvgMs.toFixed(1)} ms`);
544+
expect(onlineInsertAvgMs).toBeLessThanOrEqual(LOAD_ONLINE_MUTATION_MAX_MS);
545+
expect(onlineUpdateAvgMs).toBeLessThanOrEqual(LOAD_ONLINE_MUTATION_MAX_MS);
546+
expect(onlineDeleteAvgMs).toBeLessThanOrEqual(LOAD_ONLINE_MUTATION_MAX_MS);
534547

535548
const pointDelete = await measureMs("load point delete (deleteRow existing)", async () => {
536549
await runStatements(table.deleteRow(`seed-${Math.floor(loadRowCount / 2) - 1}`));
@@ -1020,7 +1033,7 @@ describe.sequential("bulldozer db performance (real postgres)", () => {
10201033
`;
10211034
expect(isInitializedRows[0].initialized).toBe(false);
10221035

1023-
logLine(`[bulldozer-perf] load thresholds(ms): prefill<=${LOAD_PREFILL_MAX_MS}, baseCount<=${LOAD_COUNT_QUERY_MAX_MS}, setRowAvg<=${LOAD_SET_ROW_AVG_MAX_MS} over ${LOAD_SET_ROW_AVG_ITERATIONS}, pointDelete<=${LOAD_POINT_MUTATION_MAX_MS}, onlineMutation<=${LOAD_ONLINE_MUTATION_MAX_MS}, subsetIteration<=${LOAD_SUBSET_ITERATION_MAX_MS} for ${LOAD_SUBSET_ITERATION_ROW_COUNT} rows, derivedInit<=${LOAD_DERIVED_INIT_MAX_MS}, filterInit<=${LOAD_FILTER_TABLE_INIT_MAX_MS}, sortInit<=${LOAD_SORT_TABLE_INIT_MAX_MS}, lfoldInit<=${LOAD_LFOLD_TABLE_INIT_MAX_MS}, leftJoinInit<=${LOAD_LEFT_JOIN_TABLE_INIT_MAX_MS}, concatInit<=${LOAD_CONCAT_TABLE_INIT_MAX_MS}, limitInit<=${LOAD_LIMIT_TABLE_INIT_MAX_MS}, expandingInit<=${LOAD_EXPANDING_INIT_MAX_MS}, derivedCount<=${LOAD_DERIVED_COUNT_QUERY_MAX_MS}, filterCount<=${LOAD_FILTER_TABLE_COUNT_QUERY_MAX_MS}, lfoldCount<=${LOAD_LFOLD_TABLE_COUNT_QUERY_MAX_MS}, leftJoinCount<=${LOAD_LEFT_JOIN_TABLE_COUNT_QUERY_MAX_MS}, concatCount<=${LOAD_CONCAT_TABLE_COUNT_QUERY_MAX_MS}, limitCount<=${LOAD_LIMIT_TABLE_COUNT_QUERY_MAX_MS}, expandingCount<=${LOAD_EXPANDING_COUNT_QUERY_MAX_MS}, filteredQuery<=${LOAD_FILTERED_QUERY_MAX_MS}, tableDelete<=${LOAD_TABLE_DELETE_MAX_MS}`);
1036+
logLine(`[bulldozer-perf] load thresholds(ms): prefill<=${LOAD_PREFILL_MAX_MS}, baseCount<=${LOAD_COUNT_QUERY_MAX_MS}, setRowAvg<=${LOAD_SET_ROW_AVG_MAX_MS} over ${LOAD_SET_ROW_AVG_ITERATIONS}, pointDelete<=${LOAD_POINT_MUTATION_MAX_MS}, onlineMutationAvg<=${LOAD_ONLINE_MUTATION_MAX_MS} over ${LOAD_ONLINE_MUTATION_ITERATIONS}, subsetIteration<=${LOAD_SUBSET_ITERATION_MAX_MS} for ${LOAD_SUBSET_ITERATION_ROW_COUNT} rows, derivedInit<=${LOAD_DERIVED_INIT_MAX_MS}, filterInit<=${LOAD_FILTER_TABLE_INIT_MAX_MS}, sortInit<=${LOAD_SORT_TABLE_INIT_MAX_MS}, lfoldInit<=${LOAD_LFOLD_TABLE_INIT_MAX_MS}, leftJoinInit<=${LOAD_LEFT_JOIN_TABLE_INIT_MAX_MS}, concatInit<=${LOAD_CONCAT_TABLE_INIT_MAX_MS}, limitInit<=${LOAD_LIMIT_TABLE_INIT_MAX_MS}, expandingInit<=${LOAD_EXPANDING_INIT_MAX_MS}, derivedCount<=${LOAD_DERIVED_COUNT_QUERY_MAX_MS}, filterCount<=${LOAD_FILTER_TABLE_COUNT_QUERY_MAX_MS}, lfoldCount<=${LOAD_LFOLD_TABLE_COUNT_QUERY_MAX_MS}, leftJoinCount<=${LOAD_LEFT_JOIN_TABLE_COUNT_QUERY_MAX_MS}, concatCount<=${LOAD_CONCAT_TABLE_COUNT_QUERY_MAX_MS}, limitCount<=${LOAD_LIMIT_TABLE_COUNT_QUERY_MAX_MS}, expandingCount<=${LOAD_EXPANDING_COUNT_QUERY_MAX_MS}, filteredQuery<=${LOAD_FILTERED_QUERY_MAX_MS}, tableDelete<=${LOAD_TABLE_DELETE_MAX_MS}`);
10241037
}, 300_000);
10251038
});
10261039

apps/backend/src/lib/bulldozer/db/index.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,7 +1112,7 @@ describe.sequential("declareStoredTable (real postgres)", () => {
11121112
expect(groupsAfterReinit.map((row) => row.groupkey).sort(stringCompare)).toEqual(["alpha", "beta"]);
11131113
});
11141114

1115-
test("groupBy listGroups returns all groups when the range is inclusive", async () => {
1115+
test("groupBy listGroups applies group-key ranges", async () => {
11161116
const { fromTable, groupedTable } = createGroupedTable();
11171117
await runStatements(fromTable.init());
11181118
await runStatements(fromTable.setRow("u1", expr(`'{"team":"alpha","value":1}'::jsonb`)));
@@ -1126,7 +1126,7 @@ describe.sequential("declareStoredTable (real postgres)", () => {
11261126
startInclusive: true,
11271127
endInclusive: true,
11281128
}));
1129-
expect(inclusive.map((row) => row.groupkey).sort(stringCompare)).toEqual(["alpha", "beta", "gamma"]);
1129+
expect(inclusive.map((row) => row.groupkey).sort(stringCompare)).toEqual(["beta", "gamma"]);
11301130

11311131
const exclusive = await readRows(groupedTable.listGroups({
11321132
start: expr(`to_jsonb('beta'::text)`),

apps/backend/src/lib/bulldozer/db/tables/group-by-table.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export function declareGroupByTable<
2424
const getGroupKeyPath = (groupKey: SqlExpression<Json>) => getStorageEnginePath(options.tableId, ["groups", groupKey]);
2525
const getGroupRowsPath = (groupKey: SqlExpression<Json>) => getStorageEnginePath(options.tableId, ["groups", groupKey, "rows"]);
2626
const getGroupRowPath = (groupKey: SqlExpression<Json>, rowIdentifier: SqlExpression<Json>) => getStorageEnginePath(options.tableId, ["groups", groupKey, "rows", rowIdentifier]);
27+
const compareGroupKeys = (a: SqlExpression<GK>, b: SqlExpression<GK>) => sqlExpression`
28+
((${a}) > (${b}))::int - ((${a}) < (${b}))::int
29+
`;
2730
const isInitializedExpression = sqlExpression`
2831
EXISTS (
2932
SELECT 1 FROM "BulldozerStorageEngine"
@@ -182,7 +185,7 @@ export function declareGroupByTable<
182185
fromTableId: tableIdToDebugString(options.fromTable.tableId),
183186
groupBySql: options.groupBy.sql,
184187
},
185-
compareGroupKeys: (a, b) => sqlExpression` 0 `,
188+
compareGroupKeys,
186189
compareSortKeys: (a, b) => sqlExpression` 0 `,
187190
init: () => {
188191
const fromTableAllRowsTableName = `from_table_all_rows_${generateSecureRandomString()}`;
@@ -278,7 +281,21 @@ export function declareGroupByTable<
278281
WHERE "groupRowsPath"."keyPathParent" = "groupPath"."keyPath"
279282
AND "groupRowsPath"."keyPath"[cardinality("groupRowsPath"."keyPath")] = to_jsonb('rows'::text)
280283
)
281-
AND ${singleNullSortKeyRangePredicate({ start, end, startInclusive, endInclusive })}
284+
AND ${
285+
start === "start"
286+
? sqlExpression`1 = 1`
287+
: startInclusive
288+
? sqlExpression`${compareGroupKeys(sqlExpression`"groupPath"."keyPath"[cardinality("groupPath"."keyPath")]`, start)} >= 0`
289+
: sqlExpression`${compareGroupKeys(sqlExpression`"groupPath"."keyPath"[cardinality("groupPath"."keyPath")]`, start)} > 0`
290+
}
291+
AND ${
292+
end === "end"
293+
? sqlExpression`1 = 1`
294+
: endInclusive
295+
? sqlExpression`${compareGroupKeys(sqlExpression`"groupPath"."keyPath"[cardinality("groupPath"."keyPath")]`, end)} <= 0`
296+
: sqlExpression`${compareGroupKeys(sqlExpression`"groupPath"."keyPath"[cardinality("groupPath"."keyPath")]`, end)} < 0`
297+
}
298+
ORDER BY "groupPath"."keyPath"[cardinality("groupPath"."keyPath")] ASC
282299
`,
283300
listRowsInGroup: ({ groupKey, start, end, startInclusive, endInclusive }) => groupKey ? sqlQuery`
284301
SELECT

apps/backend/src/lib/bulldozer/db/tables/left-join-table.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ export function declareLeftJoinTable<
439439
FROM "BulldozerStorageEngine" AS "row"
440440
WHERE "row"."keyPathParent" = ${getGroupRowsPath(groupKey)}::jsonb[]
441441
AND ${singleNullSortKeyRangePredicate({ start, end, startInclusive, endInclusive })}
442+
ORDER BY rowIdentifier ASC
442443
` : sqlQuery`
443444
SELECT
444445
"groupPath"."keyPath"[cardinality("groupPath"."keyPath")] AS groupKey,
@@ -453,6 +454,7 @@ export function declareLeftJoinTable<
453454
WHERE "groupPath"."keyPathParent" = ${groupsPath}::jsonb[]
454455
AND "groupRowsPath"."keyPath"[cardinality("groupRowsPath"."keyPath")] = to_jsonb('rows'::text)
455456
AND ${singleNullSortKeyRangePredicate({ start, end, startInclusive, endInclusive })}
457+
ORDER BY groupKey ASC, rowIdentifier ASC
456458
`,
457459
registerRowChangeTrigger: (trigger) => {
458460
const id = generateSecureRandomString();

0 commit comments

Comments
 (0)