lakeql 0.2.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +470 -169
- package/dist/{chunk-32PE6IK4.js → chunk-AUL4DYT4.js} +1076 -99
- package/dist/{chunk-3WUY56UD.js → chunk-TR4JNJ47.js} +1274 -172
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js +2 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +2 -2
- package/dist/node.d.ts +1 -1
- package/dist/node.js +3 -3
- package/package.json +6 -6
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Lake, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes, createParquetLake, parseSql, writePartitionedParquet, broadcastJoin } from './chunk-
|
|
2
|
-
import { stableStringify } from './chunk-LWF5FKHB.js';
|
|
1
|
+
import { Lake, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes, createParquetLake, parseSql, writePartitionedParquet, broadcastJoin, crossJoin } from './chunk-AUL4DYT4.js';
|
|
2
|
+
import { stableStringify, collectExprColumns } from './chunk-LWF5FKHB.js';
|
|
3
3
|
import { LakeqlError, jsonSafeValue, matches, evaluate } from './chunk-WGHR35QU.js';
|
|
4
4
|
|
|
5
5
|
// ../core/dist/in-memory.js
|
|
@@ -984,7 +984,7 @@ async function* scanPlannedIcebergRows(options) {
|
|
|
984
984
|
throwIfAborted(signal);
|
|
985
985
|
const deletes = await decodedDeletesForFile(file, options, signal);
|
|
986
986
|
throwIfAborted(signal);
|
|
987
|
-
const data = await options.readDataFile(file);
|
|
987
|
+
const data = await options.readDataFile(file, deletes);
|
|
988
988
|
throwIfAborted(signal);
|
|
989
989
|
let rowOffset = 0;
|
|
990
990
|
for await (const batch of rowBatches(data)) {
|
|
@@ -1876,15 +1876,51 @@ function projectedIds(fields, select) {
|
|
|
1876
1876
|
function partitionMayMatch(expr, partition) {
|
|
1877
1877
|
if (!expr)
|
|
1878
1878
|
return true;
|
|
1879
|
+
const value = partitionEval(expr, partition);
|
|
1880
|
+
return value !== false;
|
|
1881
|
+
}
|
|
1882
|
+
function partitionEval(expr, partition) {
|
|
1883
|
+
switch (expr.kind) {
|
|
1884
|
+
case "literal":
|
|
1885
|
+
case "column":
|
|
1886
|
+
return "unknown";
|
|
1887
|
+
case "compare":
|
|
1888
|
+
case "in":
|
|
1889
|
+
case "between":
|
|
1890
|
+
case "null-check":
|
|
1891
|
+
case "like":
|
|
1892
|
+
case "call":
|
|
1893
|
+
case "arithmetic":
|
|
1894
|
+
case "case":
|
|
1895
|
+
return expressionIsPartitionOnly(expr, partition) ? matchesPartition(expr, partition) : "unknown";
|
|
1896
|
+
case "not": {
|
|
1897
|
+
const value = partitionEval(expr.operand, partition);
|
|
1898
|
+
return value === "unknown" ? "unknown" : !value;
|
|
1899
|
+
}
|
|
1900
|
+
case "logical": {
|
|
1901
|
+
const values = expr.operands.map((operand) => partitionEval(operand, partition));
|
|
1902
|
+
if (expr.op === "and") {
|
|
1903
|
+
if (values.some((value) => value === false))
|
|
1904
|
+
return false;
|
|
1905
|
+
return values.every((value) => value === true) ? true : "unknown";
|
|
1906
|
+
}
|
|
1907
|
+
if (values.some((value) => value === true))
|
|
1908
|
+
return true;
|
|
1909
|
+
return values.every((value) => value === false) ? false : "unknown";
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
function expressionIsPartitionOnly(expr, partition) {
|
|
1879
1914
|
const columns = /* @__PURE__ */ new Set();
|
|
1880
1915
|
collectColumns(expr, columns);
|
|
1881
|
-
|
|
1882
|
-
|
|
1916
|
+
return columns.size > 0 && [...columns].every((column) => column in partition);
|
|
1917
|
+
}
|
|
1918
|
+
function matchesPartition(expr, partition) {
|
|
1883
1919
|
try {
|
|
1884
1920
|
return matches(expr, partition);
|
|
1885
1921
|
} catch (cause) {
|
|
1886
1922
|
if (cause instanceof LakeqlError && cause.code === "LAKEQL_TYPE_ERROR")
|
|
1887
|
-
return
|
|
1923
|
+
return "unknown";
|
|
1888
1924
|
throw cause;
|
|
1889
1925
|
}
|
|
1890
1926
|
}
|
|
@@ -1982,13 +2018,14 @@ async function* scanBatches(plan, options = {}) {
|
|
|
1982
2018
|
for await (const batch of scanPlannedIcebergRows({
|
|
1983
2019
|
plan: plan.plan,
|
|
1984
2020
|
...options,
|
|
1985
|
-
readDataFile: async (file) => projectIcebergParquetBatches(
|
|
2021
|
+
readDataFile: async (file, deletes) => projectIcebergParquetBatches(
|
|
1986
2022
|
plan.store,
|
|
1987
2023
|
plan.table,
|
|
1988
2024
|
file.path,
|
|
1989
2025
|
file.partition,
|
|
1990
2026
|
file.snapshotId,
|
|
1991
2027
|
plan.options,
|
|
2028
|
+
deletes,
|
|
1992
2029
|
options
|
|
1993
2030
|
),
|
|
1994
2031
|
readDeleteFile: async (deleteFile) => readIcebergParquetDeletes(plan.store, deleteFile)
|
|
@@ -2003,20 +2040,34 @@ async function* scanRows(plan, options = {}) {
|
|
|
2003
2040
|
}
|
|
2004
2041
|
}
|
|
2005
2042
|
}
|
|
2006
|
-
async function* projectIcebergParquetBatches(store, table, path, partition, snapshotId, planOptions, scanOptions) {
|
|
2043
|
+
async function* projectIcebergParquetBatches(store, table, path, partition, snapshotId, planOptions, deletes, scanOptions) {
|
|
2007
2044
|
const readOptions = {};
|
|
2008
2045
|
if (scanOptions.batchSize !== void 0) readOptions.batchSize = scanOptions.batchSize;
|
|
2046
|
+
const retainedColumns = retainedFilterColumns(deletes, planOptions);
|
|
2009
2047
|
for await (const batch of readParquetObjectBatches(store, path, readOptions)) {
|
|
2010
2048
|
yield {
|
|
2011
2049
|
rowOffset: batch.rowOffset,
|
|
2012
2050
|
rows: batch.rows.map((row) => {
|
|
2051
|
+
const sourceRow = { ...partition, ...row };
|
|
2013
2052
|
const projectOptions = { snapshotId };
|
|
2014
2053
|
if (planOptions.select !== void 0) projectOptions.select = planOptions.select;
|
|
2015
|
-
|
|
2054
|
+
const projected = table.projectRow(sourceRow, projectOptions);
|
|
2055
|
+
for (const column of retainedColumns) {
|
|
2056
|
+
if (column in sourceRow && !(column in projected)) projected[column] = sourceRow[column];
|
|
2057
|
+
}
|
|
2058
|
+
return projected;
|
|
2016
2059
|
})
|
|
2017
2060
|
};
|
|
2018
2061
|
}
|
|
2019
2062
|
}
|
|
2063
|
+
function retainedFilterColumns(deletes, planOptions) {
|
|
2064
|
+
const columns = /* @__PURE__ */ new Set();
|
|
2065
|
+
if (planOptions.select === void 0) collectExprColumns(planOptions.where, columns);
|
|
2066
|
+
for (const deletion of deletes.equalityDeletes ?? []) {
|
|
2067
|
+
for (const column of deletion.columns) columns.add(column);
|
|
2068
|
+
}
|
|
2069
|
+
return columns;
|
|
2070
|
+
}
|
|
2020
2071
|
|
|
2021
2072
|
// src/sql.ts
|
|
2022
2073
|
var textEncoder = new TextEncoder();
|
|
@@ -2066,14 +2117,32 @@ var SqlQueryResult = class {
|
|
|
2066
2117
|
};
|
|
2067
2118
|
async function executeSql(lake, sql, options) {
|
|
2068
2119
|
const ast = sqlAst(sql, options);
|
|
2120
|
+
validateSourceBindings(options);
|
|
2069
2121
|
const bound = bindSources(ast, options);
|
|
2070
|
-
const
|
|
2122
|
+
const pushed = pushdownIcebergPredicates(bound, options);
|
|
2123
|
+
const materializedIceberg = await materializeIcebergTablesIfNeeded(
|
|
2124
|
+
lake.store,
|
|
2125
|
+
pushed.ast,
|
|
2126
|
+
options,
|
|
2127
|
+
pushed
|
|
2128
|
+
);
|
|
2129
|
+
try {
|
|
2130
|
+
return await executeRowsFromAst(lake, materializedIceberg.ast, options);
|
|
2131
|
+
} finally {
|
|
2132
|
+
await materializedIceberg.cleanup();
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
async function executeRowsFromAst(lake, ast, options) {
|
|
2136
|
+
const materialized = await materializeCteIfNeeded(lake.store, lake, ast, options);
|
|
2071
2137
|
try {
|
|
2072
2138
|
const resolved = await resolveScalarSubqueries(lake, materialized.ast);
|
|
2073
|
-
if (resolved.
|
|
2074
|
-
return subqueryJoinRowsFromAst(lake, resolved, options);
|
|
2075
|
-
if (resolved.
|
|
2076
|
-
if (hasAggregation(resolved))
|
|
2139
|
+
if (sqlSubqueryJoins(resolved).length > 0)
|
|
2140
|
+
return await subqueryJoinRowsFromAst(lake, resolved, options);
|
|
2141
|
+
if (sqlJoins(resolved).length > 0) return await joinRowsFromAst(lake, resolved, options);
|
|
2142
|
+
if (hasAggregation(resolved))
|
|
2143
|
+
return await aggregateRowsFromAst(lake.path(resolved.source), resolved);
|
|
2144
|
+
if (hasCorrelatedScalarSubqueries(resolved))
|
|
2145
|
+
return await correlatedScalarRowsFromAst(lake, resolved);
|
|
2077
2146
|
return await builderFromAst(lake.path(resolved.source), resolved).toArray();
|
|
2078
2147
|
} finally {
|
|
2079
2148
|
await materialized.cleanup();
|
|
@@ -2086,12 +2155,25 @@ function sqlAst(sql, options) {
|
|
|
2086
2155
|
}
|
|
2087
2156
|
function bindSources(ast, options) {
|
|
2088
2157
|
const tables = options.tables ?? {};
|
|
2158
|
+
const icebergTables = options.icebergTables ?? {};
|
|
2089
2159
|
const bindSource = (source) => {
|
|
2090
2160
|
if (source === "input" && options.path !== void 0) return options.path;
|
|
2161
|
+
if (icebergTables[source] !== void 0) return source;
|
|
2091
2162
|
return tables[source] ?? source;
|
|
2092
2163
|
};
|
|
2093
2164
|
return mapAstSources(ast, bindSource);
|
|
2094
2165
|
}
|
|
2166
|
+
function validateSourceBindings(options) {
|
|
2167
|
+
const tables = options.tables ?? {};
|
|
2168
|
+
const icebergTables = options.icebergTables ?? {};
|
|
2169
|
+
for (const name of Object.keys(icebergTables)) {
|
|
2170
|
+
if (tables[name] === void 0) continue;
|
|
2171
|
+
throw new LakeqlError(
|
|
2172
|
+
"LAKEQL_VALIDATION_ERROR",
|
|
2173
|
+
`SQL source "${name}" is bound as both Parquet and Iceberg`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2095
2177
|
function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2096
2178
|
const out = { ...ast, source: bindSource(ast.source) };
|
|
2097
2179
|
if (seen.has(ast)) {
|
|
@@ -2100,12 +2182,25 @@ function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
|
|
|
2100
2182
|
}
|
|
2101
2183
|
seen.add(ast);
|
|
2102
2184
|
if (ast.join !== void 0) out.join = { ...ast.join, source: bindSource(ast.join.source) };
|
|
2103
|
-
if (ast.
|
|
2185
|
+
if (ast.joins !== void 0) {
|
|
2186
|
+
out.joins = ast.joins.map((join) => ({ ...join, source: bindSource(join.source) }));
|
|
2187
|
+
const firstJoin = out.joins[0];
|
|
2188
|
+
if (firstJoin !== void 0) out.join = firstJoin;
|
|
2189
|
+
}
|
|
2190
|
+
if (ast.subqueryJoin !== void 0 && ast.subqueryJoins === void 0) {
|
|
2104
2191
|
out.subqueryJoin = {
|
|
2105
2192
|
...ast.subqueryJoin,
|
|
2106
2193
|
source: bindSource(ast.subqueryJoin.source)
|
|
2107
2194
|
};
|
|
2108
2195
|
}
|
|
2196
|
+
if (ast.subqueryJoins !== void 0) {
|
|
2197
|
+
out.subqueryJoins = ast.subqueryJoins.map((join) => ({
|
|
2198
|
+
...join,
|
|
2199
|
+
source: bindSource(join.source)
|
|
2200
|
+
}));
|
|
2201
|
+
const firstSubqueryJoin = out.subqueryJoins[0];
|
|
2202
|
+
if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
|
|
2203
|
+
}
|
|
2109
2204
|
if (ast.cte !== void 0) {
|
|
2110
2205
|
out.cte = { ...ast.cte, query: mapAstSources(ast.cte.query, bindSource, seen) };
|
|
2111
2206
|
}
|
|
@@ -2117,6 +2212,69 @@ function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
|
|
|
2117
2212
|
])
|
|
2118
2213
|
);
|
|
2119
2214
|
}
|
|
2215
|
+
if (ast.correlatedScalarSubqueries !== void 0) {
|
|
2216
|
+
out.correlatedScalarSubqueries = Object.fromEntries(
|
|
2217
|
+
Object.entries(ast.correlatedScalarSubqueries).map(([id, subquery]) => [
|
|
2218
|
+
id,
|
|
2219
|
+
{ ...subquery, source: bindSource(subquery.source) }
|
|
2220
|
+
])
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
return out;
|
|
2224
|
+
}
|
|
2225
|
+
async function mapAstSourcesAsync(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2226
|
+
const out = { ...ast, source: await bindSource(ast.source) };
|
|
2227
|
+
if (seen.has(ast)) {
|
|
2228
|
+
delete out.scalarSubqueries;
|
|
2229
|
+
return out;
|
|
2230
|
+
}
|
|
2231
|
+
seen.add(ast);
|
|
2232
|
+
if (ast.join !== void 0) {
|
|
2233
|
+
out.join = { ...ast.join, source: await bindSource(ast.join.source) };
|
|
2234
|
+
}
|
|
2235
|
+
if (ast.joins !== void 0) {
|
|
2236
|
+
out.joins = await Promise.all(
|
|
2237
|
+
ast.joins.map(async (join) => ({ ...join, source: await bindSource(join.source) }))
|
|
2238
|
+
);
|
|
2239
|
+
const firstJoin = out.joins[0];
|
|
2240
|
+
if (firstJoin !== void 0) out.join = firstJoin;
|
|
2241
|
+
}
|
|
2242
|
+
if (ast.subqueryJoin !== void 0 && ast.subqueryJoins === void 0) {
|
|
2243
|
+
out.subqueryJoin = {
|
|
2244
|
+
...ast.subqueryJoin,
|
|
2245
|
+
source: await bindSource(ast.subqueryJoin.source)
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
if (ast.subqueryJoins !== void 0) {
|
|
2249
|
+
out.subqueryJoins = await Promise.all(
|
|
2250
|
+
ast.subqueryJoins.map(async (join) => ({ ...join, source: await bindSource(join.source) }))
|
|
2251
|
+
);
|
|
2252
|
+
const firstSubqueryJoin = out.subqueryJoins[0];
|
|
2253
|
+
if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
|
|
2254
|
+
}
|
|
2255
|
+
if (ast.cte !== void 0) {
|
|
2256
|
+
out.cte = { ...ast.cte, query: await mapAstSourcesAsync(ast.cte.query, bindSource, seen) };
|
|
2257
|
+
}
|
|
2258
|
+
if (ast.scalarSubqueries !== void 0) {
|
|
2259
|
+
out.scalarSubqueries = Object.fromEntries(
|
|
2260
|
+
await Promise.all(
|
|
2261
|
+
Object.entries(ast.scalarSubqueries).map(async ([id, subquery]) => [
|
|
2262
|
+
id,
|
|
2263
|
+
{ ...subquery, query: await mapAstSourcesAsync(subquery.query, bindSource, seen) }
|
|
2264
|
+
])
|
|
2265
|
+
)
|
|
2266
|
+
);
|
|
2267
|
+
}
|
|
2268
|
+
if (ast.correlatedScalarSubqueries !== void 0) {
|
|
2269
|
+
out.correlatedScalarSubqueries = Object.fromEntries(
|
|
2270
|
+
await Promise.all(
|
|
2271
|
+
Object.entries(ast.correlatedScalarSubqueries).map(async ([id, subquery]) => [
|
|
2272
|
+
id,
|
|
2273
|
+
{ ...subquery, source: await bindSource(subquery.source) }
|
|
2274
|
+
])
|
|
2275
|
+
)
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2120
2278
|
return out;
|
|
2121
2279
|
}
|
|
2122
2280
|
function builderFromAst(builder, ast) {
|
|
@@ -2138,19 +2296,10 @@ function hasAggregation(ast) {
|
|
|
2138
2296
|
function hasWindowSemantics(ast) {
|
|
2139
2297
|
return ast.windows !== void 0 && Object.keys(ast.windows).length > 0 || ast.qualify !== void 0;
|
|
2140
2298
|
}
|
|
2141
|
-
async function materializeCteIfNeeded(store, lake, ast) {
|
|
2299
|
+
async function materializeCteIfNeeded(store, lake, ast, options) {
|
|
2142
2300
|
if (ast.cte === void 0) return { ast, cleanup: async () => {
|
|
2143
2301
|
} };
|
|
2144
|
-
|
|
2145
|
-
throw new LakeqlError(
|
|
2146
|
-
"LAKEQL_SQL_UNSUPPORTED",
|
|
2147
|
-
"CTEs are only supported as the outer FROM source"
|
|
2148
|
-
);
|
|
2149
|
-
}
|
|
2150
|
-
if (ast.join !== void 0 || ast.subqueryJoin !== void 0) {
|
|
2151
|
-
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "CTEs inside JOINs are not supported yet");
|
|
2152
|
-
}
|
|
2153
|
-
const cteRows = hasAggregation(ast.cte.query) ? await aggregateRowsFromAst(lake.path(ast.cte.query.source), ast.cte.query) : await builderFromAst(lake.path(ast.cte.query.source), ast.cte.query).toArray();
|
|
2302
|
+
const cteRows = await executeRowsFromAst(lake, ast.cte.query, options);
|
|
2154
2303
|
if (cteRows.length === 0) {
|
|
2155
2304
|
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Empty CTE results are not supported yet");
|
|
2156
2305
|
}
|
|
@@ -2160,13 +2309,432 @@ async function materializeCteIfNeeded(store, lake, ast) {
|
|
|
2160
2309
|
maxRowsPerFile: cteRows.length
|
|
2161
2310
|
});
|
|
2162
2311
|
const { cte: _cte, ...rest } = ast;
|
|
2312
|
+
const ctePath = `${prefix}/*.parquet`;
|
|
2163
2313
|
return {
|
|
2164
|
-
ast:
|
|
2314
|
+
ast: mapAstSources(rest, (source) => source === ast.cte?.name ? ctePath : source),
|
|
2165
2315
|
cleanup: async () => {
|
|
2166
2316
|
await Promise.all(written.files.map((file) => store.delete(file.path)));
|
|
2167
2317
|
}
|
|
2168
2318
|
};
|
|
2169
2319
|
}
|
|
2320
|
+
async function materializeIcebergTablesIfNeeded(store, ast, options, pushed = emptyIcebergPushdownResult(ast)) {
|
|
2321
|
+
const icebergTables = options.icebergTables ?? {};
|
|
2322
|
+
if (Object.keys(icebergTables).length === 0 && pushed.sources.size === 0) {
|
|
2323
|
+
return { ast, cleanup: async () => {
|
|
2324
|
+
} };
|
|
2325
|
+
}
|
|
2326
|
+
const materialized = /* @__PURE__ */ new Map();
|
|
2327
|
+
const requiredColumns = collectSourceColumns(ast);
|
|
2328
|
+
const materializeSource = async (source) => {
|
|
2329
|
+
const icebergTable = pushed.sources.get(source) ?? icebergTables[source];
|
|
2330
|
+
if (icebergTable === void 0) return source;
|
|
2331
|
+
const cached = materialized.get(source);
|
|
2332
|
+
if (cached !== void 0) return cached.path;
|
|
2333
|
+
const table = await loadTable({
|
|
2334
|
+
format: "iceberg",
|
|
2335
|
+
store,
|
|
2336
|
+
metadataPath: icebergTable.metadataPath
|
|
2337
|
+
});
|
|
2338
|
+
if (table.format !== "iceberg") {
|
|
2339
|
+
throw new LakeqlError("LAKEQL_VALIDATION_ERROR", `SQL source "${source}" is not Iceberg`);
|
|
2340
|
+
}
|
|
2341
|
+
const planOptions = icebergPlanOptions(icebergTable);
|
|
2342
|
+
const sourceColumns = icebergRequiredColumns(requiredColumns, pushed.predicates, source);
|
|
2343
|
+
const selectedColumns = icebergSelectableColumns(table.table, planOptions, sourceColumns);
|
|
2344
|
+
if (selectedColumns !== void 0) planOptions.select = selectedColumns;
|
|
2345
|
+
const predicate = pushed.predicates.get(source);
|
|
2346
|
+
if (predicate !== void 0) planOptions.where = predicate;
|
|
2347
|
+
const plan = planFiles2(table, planOptions);
|
|
2348
|
+
const rows = [];
|
|
2349
|
+
for await (const row of scanRows(plan)) rows.push(row);
|
|
2350
|
+
if (rows.length === 0) {
|
|
2351
|
+
throw new LakeqlError(
|
|
2352
|
+
"LAKEQL_SQL_UNSUPPORTED",
|
|
2353
|
+
`Iceberg SQL source "${source}" produced no rows`
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
const prefix = `__lakeql_sql_iceberg/${safeTempName(source)}/${nextSqlTempId()}`;
|
|
2357
|
+
const written = await writePartitionedParquet(store, prefix, {
|
|
2358
|
+
rows,
|
|
2359
|
+
maxRowsPerFile: icebergTable.maxRowsPerFile ?? rows.length
|
|
2360
|
+
});
|
|
2361
|
+
const result = {
|
|
2362
|
+
path: `${prefix}/*.parquet`,
|
|
2363
|
+
files: written.files.map((file) => file.path)
|
|
2364
|
+
};
|
|
2365
|
+
materialized.set(source, result);
|
|
2366
|
+
return result.path;
|
|
2367
|
+
};
|
|
2368
|
+
const mapped = await mapAstSourcesAsync(ast, materializeSource);
|
|
2369
|
+
return {
|
|
2370
|
+
ast: mapped,
|
|
2371
|
+
cleanup: async () => {
|
|
2372
|
+
await Promise.all(
|
|
2373
|
+
[...materialized.values()].flatMap((table) => table.files).map((path) => store.delete(path))
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
function icebergRequiredColumns(requiredColumns, predicates, source) {
|
|
2379
|
+
const columns = new Set(requiredColumns.get(source) ?? []);
|
|
2380
|
+
const predicate = predicates.get(source);
|
|
2381
|
+
if (predicate !== void 0) collectExprColumns2(predicate, columns);
|
|
2382
|
+
return columns.size === 0 ? void 0 : columns;
|
|
2383
|
+
}
|
|
2384
|
+
function icebergSelectableColumns(table, planOptions, columns) {
|
|
2385
|
+
if (columns === void 0) return void 0;
|
|
2386
|
+
const snapshot = table.snapshot(planOptions);
|
|
2387
|
+
const fields = new Set(table.schema(snapshot["schema-id"]).map((field) => field.name));
|
|
2388
|
+
for (const column of columns) {
|
|
2389
|
+
if (!fields.has(column)) return void 0;
|
|
2390
|
+
}
|
|
2391
|
+
return [...columns];
|
|
2392
|
+
}
|
|
2393
|
+
function emptyIcebergPushdownResult(ast) {
|
|
2394
|
+
return { ast, predicates: /* @__PURE__ */ new Map(), sources: /* @__PURE__ */ new Map() };
|
|
2395
|
+
}
|
|
2396
|
+
function pushdownIcebergPredicates(ast, options) {
|
|
2397
|
+
const icebergTables = options.icebergTables ?? {};
|
|
2398
|
+
if (Object.keys(icebergTables).length === 0) return emptyIcebergPushdownResult(ast);
|
|
2399
|
+
const context = {
|
|
2400
|
+
icebergTables,
|
|
2401
|
+
predicates: /* @__PURE__ */ new Map(),
|
|
2402
|
+
sources: /* @__PURE__ */ new Map(),
|
|
2403
|
+
seen: /* @__PURE__ */ new WeakSet(),
|
|
2404
|
+
nextSourceId: 0
|
|
2405
|
+
};
|
|
2406
|
+
return {
|
|
2407
|
+
ast: pushdownIcebergPredicatesFromAst(ast, context),
|
|
2408
|
+
predicates: context.predicates,
|
|
2409
|
+
sources: context.sources
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
function pushdownIcebergPredicatesFromAst(ast, context) {
|
|
2413
|
+
if (context.seen.has(ast)) return ast;
|
|
2414
|
+
context.seen.add(ast);
|
|
2415
|
+
const out = {
|
|
2416
|
+
...ast,
|
|
2417
|
+
source: icebergOccurrenceSource(ast.source, context)
|
|
2418
|
+
};
|
|
2419
|
+
if (ast.join !== void 0) {
|
|
2420
|
+
out.join = { ...ast.join, source: icebergOccurrenceSource(ast.join.source, context) };
|
|
2421
|
+
}
|
|
2422
|
+
if (ast.joins !== void 0) {
|
|
2423
|
+
out.joins = ast.joins.map((join) => ({
|
|
2424
|
+
...join,
|
|
2425
|
+
source: icebergOccurrenceSource(join.source, context)
|
|
2426
|
+
}));
|
|
2427
|
+
const firstJoin = out.joins[0];
|
|
2428
|
+
if (firstJoin !== void 0) out.join = firstJoin;
|
|
2429
|
+
}
|
|
2430
|
+
if (ast.subqueryJoin !== void 0 && ast.subqueryJoins === void 0) {
|
|
2431
|
+
out.subqueryJoin = {
|
|
2432
|
+
...ast.subqueryJoin,
|
|
2433
|
+
source: icebergOccurrenceSource(ast.subqueryJoin.source, context)
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
if (ast.subqueryJoins !== void 0) {
|
|
2437
|
+
out.subqueryJoins = ast.subqueryJoins.map((join) => ({
|
|
2438
|
+
...join,
|
|
2439
|
+
source: icebergOccurrenceSource(join.source, context)
|
|
2440
|
+
}));
|
|
2441
|
+
const firstSubqueryJoin = out.subqueryJoins[0];
|
|
2442
|
+
if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
|
|
2443
|
+
}
|
|
2444
|
+
const aliases = sourceAliases(out);
|
|
2445
|
+
if (ast.where !== void 0) {
|
|
2446
|
+
const remaining = [];
|
|
2447
|
+
for (const predicate of splitAndPredicates(ast.where)) {
|
|
2448
|
+
const source = predicateSource(predicate, aliases);
|
|
2449
|
+
if (source !== void 0 && context.sources.has(source)) {
|
|
2450
|
+
addPushdownPredicate(
|
|
2451
|
+
context.predicates,
|
|
2452
|
+
source,
|
|
2453
|
+
stripExprSource(predicate, aliases, source)
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
remaining.push(predicate);
|
|
2457
|
+
}
|
|
2458
|
+
const where = combineAndPredicates(remaining);
|
|
2459
|
+
if (where === void 0) delete out.where;
|
|
2460
|
+
else out.where = where;
|
|
2461
|
+
}
|
|
2462
|
+
const pushedSubqueryJoins = sqlSubqueryJoins(ast).map((join, index) => {
|
|
2463
|
+
const outJoin = sqlSubqueryJoins(out)[index];
|
|
2464
|
+
if (join.where === void 0 || outJoin === void 0) return outJoin ?? join;
|
|
2465
|
+
const source = outJoin.source;
|
|
2466
|
+
if (source !== void 0 && context.sources.has(source)) {
|
|
2467
|
+
const subqueryAliases = /* @__PURE__ */ new Map([[source, source]]);
|
|
2468
|
+
const remaining = [];
|
|
2469
|
+
for (const predicate of splitAndPredicates(join.where)) {
|
|
2470
|
+
const predicateTable = predicateSource(predicate, subqueryAliases);
|
|
2471
|
+
if (predicateTable === source) {
|
|
2472
|
+
addPushdownPredicate(
|
|
2473
|
+
context.predicates,
|
|
2474
|
+
source,
|
|
2475
|
+
stripExprSource(predicate, subqueryAliases, source)
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
remaining.push(predicate);
|
|
2479
|
+
}
|
|
2480
|
+
const where = combineAndPredicates(remaining);
|
|
2481
|
+
const next = { ...outJoin };
|
|
2482
|
+
if (where === void 0) delete next.where;
|
|
2483
|
+
else next.where = where;
|
|
2484
|
+
return next;
|
|
2485
|
+
}
|
|
2486
|
+
return outJoin;
|
|
2487
|
+
});
|
|
2488
|
+
if (pushedSubqueryJoins.length > 0) {
|
|
2489
|
+
out.subqueryJoins = pushedSubqueryJoins;
|
|
2490
|
+
const firstSubqueryJoin = pushedSubqueryJoins[0];
|
|
2491
|
+
if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
|
|
2492
|
+
}
|
|
2493
|
+
if (ast.cte !== void 0) {
|
|
2494
|
+
out.cte = {
|
|
2495
|
+
...ast.cte,
|
|
2496
|
+
query: pushdownIcebergPredicatesFromAst(ast.cte.query, context)
|
|
2497
|
+
};
|
|
2498
|
+
}
|
|
2499
|
+
if (ast.scalarSubqueries !== void 0) {
|
|
2500
|
+
out.scalarSubqueries = Object.fromEntries(
|
|
2501
|
+
Object.entries(ast.scalarSubqueries).map(([id, subquery]) => [
|
|
2502
|
+
id,
|
|
2503
|
+
{
|
|
2504
|
+
...subquery,
|
|
2505
|
+
query: pushdownIcebergPredicatesFromAst(subquery.query, context)
|
|
2506
|
+
}
|
|
2507
|
+
])
|
|
2508
|
+
);
|
|
2509
|
+
}
|
|
2510
|
+
if (ast.correlatedScalarSubqueries !== void 0) {
|
|
2511
|
+
out.correlatedScalarSubqueries = Object.fromEntries(
|
|
2512
|
+
Object.entries(ast.correlatedScalarSubqueries).map(([id, subquery]) => [
|
|
2513
|
+
id,
|
|
2514
|
+
{ ...subquery, source: icebergOccurrenceSource(subquery.source, context) }
|
|
2515
|
+
])
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
return out;
|
|
2519
|
+
}
|
|
2520
|
+
function icebergOccurrenceSource(source, context) {
|
|
2521
|
+
const table = context.icebergTables[source];
|
|
2522
|
+
if (table === void 0) return source;
|
|
2523
|
+
const occurrence = `__lakeql_sql_iceberg_source_${context.nextSourceId}_${safeTempName(source)}`;
|
|
2524
|
+
context.nextSourceId += 1;
|
|
2525
|
+
context.sources.set(occurrence, table);
|
|
2526
|
+
return occurrence;
|
|
2527
|
+
}
|
|
2528
|
+
function splitAndPredicates(expr) {
|
|
2529
|
+
if (expr.kind !== "logical" || expr.op !== "and") return [expr];
|
|
2530
|
+
return expr.operands.flatMap(splitAndPredicates);
|
|
2531
|
+
}
|
|
2532
|
+
function combineAndPredicates(predicates) {
|
|
2533
|
+
if (predicates.length === 0) return void 0;
|
|
2534
|
+
if (predicates.length === 1) return predicates[0];
|
|
2535
|
+
return { kind: "logical", op: "and", operands: [...predicates] };
|
|
2536
|
+
}
|
|
2537
|
+
function addPushdownPredicate(predicates, source, predicate) {
|
|
2538
|
+
const existing = predicates.get(source);
|
|
2539
|
+
predicates.set(
|
|
2540
|
+
source,
|
|
2541
|
+
existing === void 0 ? predicate : { kind: "logical", op: "and", operands: [existing, predicate] }
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
function predicateSource(expr, aliases) {
|
|
2545
|
+
const columns = /* @__PURE__ */ new Set();
|
|
2546
|
+
collectExprColumns2(expr, columns);
|
|
2547
|
+
let source;
|
|
2548
|
+
for (const column of columns) {
|
|
2549
|
+
const columnSource = columnSourceName(column, aliases);
|
|
2550
|
+
if (columnSource === void 0) return void 0;
|
|
2551
|
+
if (source === void 0) source = columnSource;
|
|
2552
|
+
else if (source !== columnSource) return void 0;
|
|
2553
|
+
}
|
|
2554
|
+
return source;
|
|
2555
|
+
}
|
|
2556
|
+
function columnSourceName(column, aliases) {
|
|
2557
|
+
const dot = column.indexOf(".");
|
|
2558
|
+
if (dot > 0) return aliases.get(column.slice(0, dot));
|
|
2559
|
+
if (aliases.size !== 1) return void 0;
|
|
2560
|
+
return aliases.values().next().value;
|
|
2561
|
+
}
|
|
2562
|
+
function stripExprSource(expr, aliases, source) {
|
|
2563
|
+
switch (expr.kind) {
|
|
2564
|
+
case "column":
|
|
2565
|
+
return { ...expr, name: stripSourceColumn(expr.name, aliases, source) };
|
|
2566
|
+
case "compare":
|
|
2567
|
+
return {
|
|
2568
|
+
...expr,
|
|
2569
|
+
left: stripExprSource(expr.left, aliases, source),
|
|
2570
|
+
right: stripExprSource(expr.right, aliases, source)
|
|
2571
|
+
};
|
|
2572
|
+
case "between":
|
|
2573
|
+
return {
|
|
2574
|
+
...expr,
|
|
2575
|
+
target: stripExprSource(expr.target, aliases, source),
|
|
2576
|
+
low: stripExprSource(expr.low, aliases, source),
|
|
2577
|
+
high: stripExprSource(expr.high, aliases, source)
|
|
2578
|
+
};
|
|
2579
|
+
case "in":
|
|
2580
|
+
return {
|
|
2581
|
+
...expr,
|
|
2582
|
+
target: stripExprSource(expr.target, aliases, source),
|
|
2583
|
+
values: expr.values.map((value) => stripExprSource(value, aliases, source))
|
|
2584
|
+
};
|
|
2585
|
+
case "logical":
|
|
2586
|
+
return {
|
|
2587
|
+
...expr,
|
|
2588
|
+
operands: expr.operands.map((operand) => stripExprSource(operand, aliases, source))
|
|
2589
|
+
};
|
|
2590
|
+
case "not":
|
|
2591
|
+
return { ...expr, operand: stripExprSource(expr.operand, aliases, source) };
|
|
2592
|
+
case "null-check":
|
|
2593
|
+
return { ...expr, target: stripExprSource(expr.target, aliases, source) };
|
|
2594
|
+
case "like":
|
|
2595
|
+
return { ...expr, target: stripExprSource(expr.target, aliases, source) };
|
|
2596
|
+
case "call":
|
|
2597
|
+
return { ...expr, args: expr.args.map((arg) => stripExprSource(arg, aliases, source)) };
|
|
2598
|
+
case "arithmetic":
|
|
2599
|
+
return {
|
|
2600
|
+
...expr,
|
|
2601
|
+
left: stripExprSource(expr.left, aliases, source),
|
|
2602
|
+
right: stripExprSource(expr.right, aliases, source)
|
|
2603
|
+
};
|
|
2604
|
+
case "case":
|
|
2605
|
+
return {
|
|
2606
|
+
...expr,
|
|
2607
|
+
whens: expr.whens.map((branch) => ({
|
|
2608
|
+
when: stripExprSource(branch.when, aliases, source),
|
|
2609
|
+
value: stripExprSource(branch.value, aliases, source)
|
|
2610
|
+
})),
|
|
2611
|
+
...expr.else === void 0 ? {} : { else: stripExprSource(expr.else, aliases, source) }
|
|
2612
|
+
};
|
|
2613
|
+
case "literal":
|
|
2614
|
+
return expr;
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
function stripSourceColumn(column, aliases, source) {
|
|
2618
|
+
const dot = column.indexOf(".");
|
|
2619
|
+
if (dot <= 0) return column;
|
|
2620
|
+
const qualifier = column.slice(0, dot);
|
|
2621
|
+
return aliases.get(qualifier) === source ? column.slice(dot + 1) : column;
|
|
2622
|
+
}
|
|
2623
|
+
function collectSourceColumns(ast) {
|
|
2624
|
+
const columns = /* @__PURE__ */ new Map();
|
|
2625
|
+
collectSourceColumnsFromAst(ast, columns);
|
|
2626
|
+
return columns;
|
|
2627
|
+
}
|
|
2628
|
+
function collectSourceColumnsFromAst(ast, columns, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2629
|
+
if (seen.has(ast)) return;
|
|
2630
|
+
seen.add(ast);
|
|
2631
|
+
const aliases = sourceAliases(ast);
|
|
2632
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2633
|
+
for (const select of ast.select ?? []) referenced.add(select);
|
|
2634
|
+
for (const expr of Object.values(ast.projections ?? {})) collectExprColumns2(expr, referenced);
|
|
2635
|
+
for (const aggregate of Object.values(ast.aggregates ?? {})) {
|
|
2636
|
+
if (aggregate.column !== void 0) referenced.add(aggregate.column);
|
|
2637
|
+
if (aggregate.expr !== void 0) collectExprColumns2(aggregate.expr, referenced);
|
|
2638
|
+
}
|
|
2639
|
+
for (const column of ast.groupBy ?? []) referenced.add(column);
|
|
2640
|
+
for (const term of ast.orderBy ?? []) referenced.add(term.column);
|
|
2641
|
+
if (ast.where !== void 0) collectExprColumns2(ast.where, referenced);
|
|
2642
|
+
if (ast.having !== void 0) collectExprColumns2(ast.having, referenced);
|
|
2643
|
+
if (ast.qualify !== void 0) collectExprColumns2(ast.qualify, referenced);
|
|
2644
|
+
for (const window of Object.values(ast.windows ?? {})) {
|
|
2645
|
+
for (const expr of window.args) collectExprColumns2(expr, referenced);
|
|
2646
|
+
for (const expr of window.over.partitionBy) collectExprColumns2(expr, referenced);
|
|
2647
|
+
for (const term of window.over.orderBy) collectExprColumns2(term.expr, referenced);
|
|
2648
|
+
if (window.over.frame?.start.offset !== void 0)
|
|
2649
|
+
collectExprColumns2(window.over.frame.start.offset, referenced);
|
|
2650
|
+
if (window.over.frame?.end.offset !== void 0)
|
|
2651
|
+
collectExprColumns2(window.over.frame.end.offset, referenced);
|
|
2652
|
+
if (window.filter !== void 0) collectExprColumns2(window.filter, referenced);
|
|
2653
|
+
}
|
|
2654
|
+
for (const join of sqlJoins(ast)) {
|
|
2655
|
+
for (const column of join.leftKey) referenced.add(column);
|
|
2656
|
+
for (const column of join.rightKey) referenced.add(column);
|
|
2657
|
+
if (join.predicate !== void 0) collectExprColumns2(join.predicate, referenced);
|
|
2658
|
+
}
|
|
2659
|
+
for (const subqueryJoin of sqlSubqueryJoins(ast)) {
|
|
2660
|
+
for (const column of subqueryJoin.leftKey) referenced.add(column);
|
|
2661
|
+
for (const column of subqueryJoin.rightKey) referenced.add(column);
|
|
2662
|
+
for (const column of subqueryJoin.groupBy ?? []) referenced.add(column);
|
|
2663
|
+
for (const aggregate of Object.values(subqueryJoin.aggregates ?? {})) {
|
|
2664
|
+
if (aggregate.column !== void 0) referenced.add(aggregate.column);
|
|
2665
|
+
if (aggregate.expr !== void 0) collectExprColumns2(aggregate.expr, referenced);
|
|
2666
|
+
}
|
|
2667
|
+
if (subqueryJoin.where !== void 0) collectExprColumns2(subqueryJoin.where, referenced);
|
|
2668
|
+
if (subqueryJoin.having !== void 0) collectExprColumns2(subqueryJoin.having, referenced);
|
|
2669
|
+
if (subqueryJoin.predicate !== void 0)
|
|
2670
|
+
collectExprColumns2(subqueryJoin.predicate, referenced);
|
|
2671
|
+
for (const term of subqueryJoin.orderBy ?? []) referenced.add(term.column);
|
|
2672
|
+
}
|
|
2673
|
+
for (const subquery of Object.values(ast.correlatedScalarSubqueries ?? {})) {
|
|
2674
|
+
for (const column of subquery.leftKey) referenced.add(column);
|
|
2675
|
+
for (const column of subquery.rightKey) addColumn(columns, subquery.source, column);
|
|
2676
|
+
for (const column of subquery.groupBy) addColumn(columns, subquery.source, column);
|
|
2677
|
+
for (const aggregate of Object.values(subquery.aggregates)) {
|
|
2678
|
+
if (aggregate.column !== void 0) addColumn(columns, subquery.source, aggregate.column);
|
|
2679
|
+
if (aggregate.expr !== void 0)
|
|
2680
|
+
collectExprColumnsForSource(aggregate.expr, columns, subquery.source);
|
|
2681
|
+
}
|
|
2682
|
+
if (subquery.where !== void 0)
|
|
2683
|
+
collectExprColumnsForSource(subquery.where, columns, subquery.source);
|
|
2684
|
+
if (subquery.having !== void 0)
|
|
2685
|
+
collectExprColumnsForSource(subquery.having, columns, subquery.source);
|
|
2686
|
+
}
|
|
2687
|
+
for (const column of referenced) addSourceColumn(columns, aliases, column);
|
|
2688
|
+
if (ast.cte !== void 0) collectSourceColumnsFromAst(ast.cte.query, columns, seen);
|
|
2689
|
+
for (const subquery of Object.values(ast.scalarSubqueries ?? {})) {
|
|
2690
|
+
collectSourceColumnsFromAst(subquery.query, columns, seen);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
function sourceAliases(ast) {
|
|
2694
|
+
const aliases = /* @__PURE__ */ new Map([[ast.source, ast.source]]);
|
|
2695
|
+
const joins = sqlJoins(ast);
|
|
2696
|
+
const firstJoin = joins[0];
|
|
2697
|
+
if (firstJoin !== void 0) aliases.set(firstJoin.leftAlias, ast.source);
|
|
2698
|
+
for (const join of joins) {
|
|
2699
|
+
aliases.set(join.source, join.source);
|
|
2700
|
+
aliases.set(join.alias, join.source);
|
|
2701
|
+
}
|
|
2702
|
+
return aliases;
|
|
2703
|
+
}
|
|
2704
|
+
function addSourceColumn(columns, aliases, column) {
|
|
2705
|
+
if (column === "*" || column.endsWith(".*")) return;
|
|
2706
|
+
const dot = column.indexOf(".");
|
|
2707
|
+
if (dot > 0) {
|
|
2708
|
+
const source2 = aliases.get(column.slice(0, dot));
|
|
2709
|
+
if (source2 === void 0) return;
|
|
2710
|
+
addColumn(columns, source2, column.slice(dot + 1));
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
if (aliases.size !== 1) return;
|
|
2714
|
+
const source = aliases.values().next().value;
|
|
2715
|
+
if (source !== void 0) addColumn(columns, source, column);
|
|
2716
|
+
}
|
|
2717
|
+
function collectExprColumnsForSource(expr, columns, source) {
|
|
2718
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2719
|
+
collectExprColumns2(expr, referenced);
|
|
2720
|
+
for (const column of referenced) addColumn(columns, source, column);
|
|
2721
|
+
}
|
|
2722
|
+
function addColumn(columns, source, column) {
|
|
2723
|
+
let sourceColumns = columns.get(source);
|
|
2724
|
+
if (sourceColumns === void 0) {
|
|
2725
|
+
sourceColumns = /* @__PURE__ */ new Set();
|
|
2726
|
+
columns.set(source, sourceColumns);
|
|
2727
|
+
}
|
|
2728
|
+
sourceColumns.add(column);
|
|
2729
|
+
}
|
|
2730
|
+
function icebergPlanOptions(table) {
|
|
2731
|
+
const options = {};
|
|
2732
|
+
if (table.snapshotId !== void 0) options.snapshotId = table.snapshotId;
|
|
2733
|
+
if (table.asOfTimestampMs !== void 0) options.asOfTimestampMs = table.asOfTimestampMs;
|
|
2734
|
+
if (table.ref !== void 0) options.ref = table.ref;
|
|
2735
|
+
if (table.readMode !== void 0) options.readMode = table.readMode;
|
|
2736
|
+
return options;
|
|
2737
|
+
}
|
|
2170
2738
|
function nextSqlTempId() {
|
|
2171
2739
|
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
2172
2740
|
if (randomUuid !== void 0) return randomUuid;
|
|
@@ -2181,6 +2749,10 @@ async function resolveScalarSubqueries(lake, ast) {
|
|
|
2181
2749
|
const values = /* @__PURE__ */ new Map();
|
|
2182
2750
|
for (const [id, subquery] of Object.entries(ast.scalarSubqueries)) {
|
|
2183
2751
|
const rows = hasAggregation(subquery.query) ? await aggregateRowsFromAst(lake.path(subquery.query.source), subquery.query) : await builderFromAst(lake.path(subquery.query.source), subquery.query).toArray();
|
|
2752
|
+
if (subquery.mode === "exists") {
|
|
2753
|
+
values.set(id, rows.length > 0);
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2184
2756
|
if (rows.length > 1) {
|
|
2185
2757
|
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Scalar subquery returned more than one row");
|
|
2186
2758
|
}
|
|
@@ -2197,6 +2769,30 @@ async function resolveScalarSubqueries(lake, ast) {
|
|
|
2197
2769
|
])
|
|
2198
2770
|
);
|
|
2199
2771
|
}
|
|
2772
|
+
if (ast.joins !== void 0) {
|
|
2773
|
+
out.joins = ast.joins.map(
|
|
2774
|
+
(join) => join.predicate === void 0 ? join : { ...join, predicate: replaceScalarSubqueryExpr(join.predicate, values) }
|
|
2775
|
+
);
|
|
2776
|
+
const firstJoin = out.joins[0];
|
|
2777
|
+
if (firstJoin !== void 0) out.join = firstJoin;
|
|
2778
|
+
} else if (ast.join?.predicate !== void 0) {
|
|
2779
|
+
out.join = { ...ast.join, predicate: replaceScalarSubqueryExpr(ast.join.predicate, values) };
|
|
2780
|
+
}
|
|
2781
|
+
if (ast.subqueryJoins !== void 0) {
|
|
2782
|
+
out.subqueryJoins = ast.subqueryJoins.map((join) => ({
|
|
2783
|
+
...join,
|
|
2784
|
+
...join.where === void 0 ? {} : { where: replaceScalarSubqueryExpr(join.where, values) },
|
|
2785
|
+
...join.predicate === void 0 ? {} : { predicate: replaceScalarSubqueryExpr(join.predicate, values) }
|
|
2786
|
+
}));
|
|
2787
|
+
const firstSubqueryJoin = out.subqueryJoins[0];
|
|
2788
|
+
if (firstSubqueryJoin !== void 0) out.subqueryJoin = firstSubqueryJoin;
|
|
2789
|
+
} else if (ast.subqueryJoin?.where !== void 0 || ast.subqueryJoin?.predicate !== void 0) {
|
|
2790
|
+
out.subqueryJoin = {
|
|
2791
|
+
...ast.subqueryJoin,
|
|
2792
|
+
...ast.subqueryJoin.where === void 0 ? {} : { where: replaceScalarSubqueryExpr(ast.subqueryJoin.where, values) },
|
|
2793
|
+
...ast.subqueryJoin.predicate === void 0 ? {} : { predicate: replaceScalarSubqueryExpr(ast.subqueryJoin.predicate, values) }
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2200
2796
|
delete out.scalarSubqueries;
|
|
2201
2797
|
return out;
|
|
2202
2798
|
}
|
|
@@ -2262,221 +2858,402 @@ function replaceScalarSubqueryExpr(expr, values) {
|
|
|
2262
2858
|
}
|
|
2263
2859
|
}
|
|
2264
2860
|
async function joinRowsFromAst(lake, ast, options) {
|
|
2265
|
-
|
|
2266
|
-
const
|
|
2861
|
+
const joins = sqlJoins(ast);
|
|
2862
|
+
const firstJoin = joins[0];
|
|
2863
|
+
if (firstJoin === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
|
|
2267
2864
|
if (hasWindowSemantics(ast)) {
|
|
2268
2865
|
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over JOIN is not supported");
|
|
2269
2866
|
}
|
|
2270
2867
|
if (hasAggregation(ast)) {
|
|
2271
2868
|
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Aggregate SQL over JOIN is not supported yet");
|
|
2272
2869
|
}
|
|
2273
|
-
const
|
|
2274
|
-
const
|
|
2275
|
-
let
|
|
2276
|
-
|
|
2277
|
-
if (
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2870
|
+
const pushedFilters = pushdownJoinFilters(ast, joins);
|
|
2871
|
+
const readColumns = joinReadColumns(ast, joins);
|
|
2872
|
+
let leftRows = await rowsForJoinAlias(lake.path(ast.source), readColumns, firstJoin.leftAlias);
|
|
2873
|
+
const leftFilter = pushedFilters.filters.get(firstJoin.leftAlias);
|
|
2874
|
+
if (leftFilter !== void 0) leftRows = leftRows.filter((row) => matches(leftFilter, row));
|
|
2875
|
+
let rows = leftRows.map((row) => qualifyRow(row, firstJoin.leftAlias));
|
|
2876
|
+
for (const join of joins) {
|
|
2877
|
+
let rawRightRows = await rowsForJoinAlias(lake.path(join.source), readColumns, join.alias);
|
|
2878
|
+
const rightFilter = pushedFilters.filters.get(join.alias);
|
|
2879
|
+
if (rightFilter !== void 0)
|
|
2880
|
+
rawRightRows = rawRightRows.filter((row) => matches(rightFilter, row));
|
|
2881
|
+
const rightRows = rawRightRows.map((row) => qualifyOnlyRow(row, join.alias));
|
|
2882
|
+
if (join.predicate !== void 0) {
|
|
2883
|
+
rows = predicateJoinRows(rows, rightRows, ast, join, options);
|
|
2884
|
+
} else if (join.type === "right") {
|
|
2885
|
+
const leftRows2 = rows;
|
|
2886
|
+
rows = await broadcastJoin(rightRows, leftRows2, {
|
|
2887
|
+
leftKey: join.rightKey,
|
|
2888
|
+
rightKey: join.leftKey,
|
|
2889
|
+
type: "left",
|
|
2890
|
+
rightPrefix: `${join.leftAlias}.`,
|
|
2891
|
+
maxRightRows: options.joinMaxRightRows ?? 1e5
|
|
2892
|
+
});
|
|
2893
|
+
rows = fillRightJoinNulls(rows, ast, join.alias, leftRows2);
|
|
2894
|
+
} else if (join.type === "full") {
|
|
2895
|
+
const leftRows2 = rows;
|
|
2896
|
+
const leftJoined = await broadcastJoin(leftRows2, rightRows, {
|
|
2897
|
+
leftKey: join.leftKey,
|
|
2898
|
+
rightKey: join.rightKey,
|
|
2899
|
+
type: "left",
|
|
2900
|
+
rightPrefix: `${join.alias}.`,
|
|
2901
|
+
maxRightRows: options.joinMaxRightRows ?? 1e5
|
|
2902
|
+
});
|
|
2903
|
+
const unmatchedRight = await broadcastJoin(rightRows, leftRows2, {
|
|
2904
|
+
leftKey: join.rightKey,
|
|
2905
|
+
rightKey: join.leftKey,
|
|
2906
|
+
type: "anti",
|
|
2907
|
+
rightPrefix: `${join.leftAlias}.`,
|
|
2908
|
+
maxRightRows: options.joinMaxRightRows ?? 1e5
|
|
2909
|
+
});
|
|
2910
|
+
rows = [
|
|
2911
|
+
...fillLeftJoinNulls(
|
|
2912
|
+
leftJoined,
|
|
2913
|
+
join.alias,
|
|
2914
|
+
leftJoinRightColumns(ast, join.alias, rightRows)
|
|
2915
|
+
),
|
|
2916
|
+
...fillRightJoinNulls(unmatchedRight, ast, join.alias, leftRows2)
|
|
2917
|
+
];
|
|
2918
|
+
} else {
|
|
2919
|
+
rows = join.type === "cross" ? await crossJoin(rows, rightRows, {
|
|
2920
|
+
rightPrefix: `${join.alias}.`,
|
|
2921
|
+
maxRightRows: options.joinMaxRightRows ?? 1e5,
|
|
2922
|
+
maxOutputRows: options.joinMaxOutputRows ?? 1e5
|
|
2923
|
+
}) : await broadcastJoin(rows, rightRows, {
|
|
2924
|
+
leftKey: join.leftKey,
|
|
2925
|
+
rightKey: join.rightKey,
|
|
2926
|
+
type: join.type,
|
|
2927
|
+
rightPrefix: `${join.alias}.`,
|
|
2928
|
+
maxRightRows: options.joinMaxRightRows ?? 1e5
|
|
2929
|
+
});
|
|
2930
|
+
}
|
|
2931
|
+
if (join.type === "left") {
|
|
2932
|
+
rows = fillLeftJoinNulls(rows, join.alias, leftJoinRightColumns(ast, join.alias, rightRows));
|
|
2933
|
+
}
|
|
2292
2934
|
}
|
|
2293
|
-
if (
|
|
2294
|
-
rows = rows.filter((row) => matches(
|
|
2295
|
-
if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy);
|
|
2296
|
-
rows = projectRows(rows, ast);
|
|
2935
|
+
if (pushedFilters.where !== void 0)
|
|
2936
|
+
rows = rows.filter((row) => matches(pushedFilters.where, row));
|
|
2937
|
+
if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
|
|
2938
|
+
rows = await projectRows(lake, rows, ast);
|
|
2297
2939
|
if (ast.distinct === true) rows = distinctRows(rows);
|
|
2298
2940
|
return offsetLimitRows(rows, ast);
|
|
2299
2941
|
}
|
|
2300
|
-
function
|
|
2301
|
-
if (
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2942
|
+
function predicateJoinRows(leftRows, rightRows, ast, join, options) {
|
|
2943
|
+
if (join.predicate === void 0) return leftRows;
|
|
2944
|
+
const maxRightRows = options.joinMaxRightRows ?? 1e5;
|
|
2945
|
+
if (rightRows.length > maxRightRows) {
|
|
2946
|
+
throw new LakeqlError(
|
|
2947
|
+
"LAKEQL_BUDGET_EXCEEDED",
|
|
2948
|
+
`Predicate join exceeded maxRightRows (${rightRows.length} > ${maxRightRows})`,
|
|
2949
|
+
{ metric: "maxRightRows", limit: maxRightRows, actual: rightRows.length }
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const maxOutputRows = options.joinMaxOutputRows ?? 1e5;
|
|
2953
|
+
const matchedLeft = /* @__PURE__ */ new Set();
|
|
2954
|
+
const matchedRight = /* @__PURE__ */ new Set();
|
|
2955
|
+
const out = [];
|
|
2956
|
+
let candidateRows = 0;
|
|
2957
|
+
for (const [leftIndex, leftRow] of leftRows.entries()) {
|
|
2958
|
+
for (const [rightIndex, rightRow] of rightRows.entries()) {
|
|
2959
|
+
candidateRows += 1;
|
|
2960
|
+
if (candidateRows > maxOutputRows) {
|
|
2961
|
+
throw new LakeqlError(
|
|
2962
|
+
"LAKEQL_BUDGET_EXCEEDED",
|
|
2963
|
+
`Predicate join exceeded maxOutputRows (${candidateRows} > ${maxOutputRows})`,
|
|
2964
|
+
{ metric: "maxOutputRows", limit: maxOutputRows, actual: candidateRows }
|
|
2965
|
+
);
|
|
2966
|
+
}
|
|
2967
|
+
const joined = mergePredicateJoinRows(leftRow, rightRow, `${join.alias}.`);
|
|
2968
|
+
if (!matches(join.predicate, joined)) continue;
|
|
2969
|
+
matchedLeft.add(leftIndex);
|
|
2970
|
+
matchedRight.add(rightIndex);
|
|
2971
|
+
out.push(joined);
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
if (join.type === "left" || join.type === "full") {
|
|
2975
|
+
const unmatchedLeft = leftRows.filter((_row, index) => !matchedLeft.has(index));
|
|
2976
|
+
out.push(
|
|
2977
|
+
...fillLeftJoinNulls(
|
|
2978
|
+
unmatchedLeft,
|
|
2979
|
+
join.alias,
|
|
2980
|
+
leftJoinRightColumns(ast, join.alias, rightRows)
|
|
2981
|
+
)
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
if (join.type === "right" || join.type === "full") {
|
|
2985
|
+
const unmatchedRight = rightRows.filter((_row, index) => !matchedRight.has(index));
|
|
2986
|
+
out.push(...fillRightJoinNulls(unmatchedRight, ast, join.alias, leftRows));
|
|
2987
|
+
}
|
|
2988
|
+
return out;
|
|
2989
|
+
}
|
|
2990
|
+
function mergePredicateJoinRows(left, right, rightPrefix) {
|
|
2991
|
+
const out = { ...left };
|
|
2992
|
+
for (const [key, value] of Object.entries(right)) {
|
|
2993
|
+
const outKey = key in out ? `${rightPrefix}${key}` : key;
|
|
2994
|
+
out[outKey] = value;
|
|
2995
|
+
}
|
|
2996
|
+
return out;
|
|
2997
|
+
}
|
|
2998
|
+
function sqlJoins(ast) {
|
|
2999
|
+
if (ast.joins !== void 0) return ast.joins;
|
|
3000
|
+
return ast.join === void 0 ? [] : [ast.join];
|
|
3001
|
+
}
|
|
3002
|
+
function sqlSubqueryJoins(ast) {
|
|
3003
|
+
if (ast.subqueryJoins !== void 0) return ast.subqueryJoins;
|
|
3004
|
+
return ast.subqueryJoin === void 0 ? [] : [ast.subqueryJoin];
|
|
3005
|
+
}
|
|
3006
|
+
function pushdownJoinFilters(ast, joins) {
|
|
3007
|
+
const filters = /* @__PURE__ */ new Map();
|
|
3008
|
+
if (ast.where === void 0) {
|
|
3009
|
+
return { filters, ...ast.where === void 0 ? {} : { where: ast.where } };
|
|
3010
|
+
}
|
|
3011
|
+
const removableAliases = nonNullableJoinAliases(joins);
|
|
3012
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3013
|
+
const firstJoin = joins[0];
|
|
3014
|
+
if (firstJoin !== void 0) aliases.add(firstJoin.leftAlias);
|
|
3015
|
+
for (const join of joins) aliases.add(join.alias);
|
|
3016
|
+
const remaining = [];
|
|
3017
|
+
for (const predicate of splitAndPredicates(ast.where)) {
|
|
3018
|
+
const alias = predicateJoinAlias(predicate, aliases);
|
|
3019
|
+
if (alias === void 0) {
|
|
3020
|
+
remaining.push(predicate);
|
|
3021
|
+
continue;
|
|
2313
3022
|
}
|
|
3023
|
+
addJoinFilter(filters, alias, stripExprJoinAlias(predicate, alias));
|
|
3024
|
+
if (!removableAliases.has(alias)) remaining.push(predicate);
|
|
3025
|
+
}
|
|
3026
|
+
const where = combineAndPredicates(remaining);
|
|
3027
|
+
return { filters, ...where === void 0 ? {} : { where } };
|
|
3028
|
+
}
|
|
3029
|
+
function nonNullableJoinAliases(joins) {
|
|
3030
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3031
|
+
const firstJoin = joins[0];
|
|
3032
|
+
if (firstJoin === void 0) return aliases;
|
|
3033
|
+
aliases.add(firstJoin.leftAlias);
|
|
3034
|
+
for (const join of joins) {
|
|
3035
|
+
switch (join.type) {
|
|
3036
|
+
case "inner":
|
|
3037
|
+
case "cross":
|
|
3038
|
+
case "left":
|
|
3039
|
+
if (join.type !== "left") aliases.add(join.alias);
|
|
3040
|
+
break;
|
|
3041
|
+
case "right":
|
|
3042
|
+
aliases.clear();
|
|
3043
|
+
aliases.add(join.alias);
|
|
3044
|
+
break;
|
|
3045
|
+
case "full":
|
|
3046
|
+
aliases.clear();
|
|
3047
|
+
break;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
return aliases;
|
|
3051
|
+
}
|
|
3052
|
+
async function rowsForJoinAlias(builder, readColumns, alias) {
|
|
3053
|
+
const columns = readColumns?.get(alias);
|
|
3054
|
+
if (columns === void 0 || columns.size === 0) return await builder.toArray();
|
|
3055
|
+
return await builder.select([...columns]).toArray();
|
|
3056
|
+
}
|
|
3057
|
+
function joinReadColumns(ast, joins) {
|
|
3058
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3059
|
+
const firstJoin = joins[0];
|
|
3060
|
+
if (firstJoin !== void 0) aliases.add(firstJoin.leftAlias);
|
|
3061
|
+
for (const join of joins) aliases.add(join.alias);
|
|
3062
|
+
const columns = /* @__PURE__ */ new Map();
|
|
3063
|
+
for (const select of ast.select ?? []) {
|
|
3064
|
+
const { column } = selectColumn(select);
|
|
3065
|
+
if (!addJoinReadColumn(columns, aliases, column)) return void 0;
|
|
3066
|
+
}
|
|
3067
|
+
for (const expr of Object.values(ast.projections ?? {})) {
|
|
3068
|
+
if (!addJoinReadExpr(columns, aliases, expr)) return void 0;
|
|
3069
|
+
}
|
|
3070
|
+
if (ast.where !== void 0 && !addJoinReadExpr(columns, aliases, ast.where)) return void 0;
|
|
3071
|
+
for (const term of ast.orderBy ?? []) {
|
|
3072
|
+
if (!addJoinOrderByReadColumns(columns, aliases, term.column, ast)) return void 0;
|
|
2314
3073
|
}
|
|
2315
|
-
const
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
3074
|
+
for (const join of joins) {
|
|
3075
|
+
for (const column of join.leftKey) {
|
|
3076
|
+
if (!addJoinReadColumn(columns, aliases, column)) return void 0;
|
|
3077
|
+
}
|
|
3078
|
+
for (const column of join.rightKey) {
|
|
3079
|
+
if (!addJoinReadColumn(columns, aliases, column)) return void 0;
|
|
3080
|
+
}
|
|
3081
|
+
if (join.predicate !== void 0 && !addJoinReadExpr(columns, aliases, join.predicate)) {
|
|
3082
|
+
return void 0;
|
|
3083
|
+
}
|
|
2325
3084
|
}
|
|
2326
|
-
return
|
|
3085
|
+
return columns;
|
|
2327
3086
|
}
|
|
2328
|
-
function
|
|
2329
|
-
|
|
2330
|
-
if (
|
|
2331
|
-
|
|
3087
|
+
function addJoinOrderByReadColumns(columns, aliases, column, ast) {
|
|
3088
|
+
const projection = ast.projections?.[column];
|
|
3089
|
+
if (projection !== void 0) return addJoinReadExpr(columns, aliases, projection);
|
|
3090
|
+
const select = ast.select?.find((value) => selectColumn(value).alias === column);
|
|
3091
|
+
if (select !== void 0) return addJoinReadColumn(columns, aliases, selectColumn(select).column);
|
|
3092
|
+
return addJoinReadColumn(columns, aliases, column);
|
|
2332
3093
|
}
|
|
2333
|
-
function
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
3094
|
+
function addJoinReadExpr(columns, aliases, expr) {
|
|
3095
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
3096
|
+
collectExprColumns2(expr, referenced);
|
|
3097
|
+
for (const column of referenced) {
|
|
3098
|
+
if (!addJoinReadColumn(columns, aliases, column)) return false;
|
|
3099
|
+
}
|
|
3100
|
+
return true;
|
|
2337
3101
|
}
|
|
2338
|
-
function
|
|
2339
|
-
|
|
3102
|
+
function addJoinReadColumn(columns, aliases, column) {
|
|
3103
|
+
if (column === "*" || column.endsWith(".*")) return false;
|
|
3104
|
+
const dot = column.indexOf(".");
|
|
3105
|
+
if (dot <= 0) return false;
|
|
3106
|
+
const alias = column.slice(0, dot);
|
|
3107
|
+
if (!aliases.has(alias)) return false;
|
|
3108
|
+
let aliasColumns = columns.get(alias);
|
|
3109
|
+
if (aliasColumns === void 0) {
|
|
3110
|
+
aliasColumns = /* @__PURE__ */ new Set();
|
|
3111
|
+
columns.set(alias, aliasColumns);
|
|
3112
|
+
}
|
|
3113
|
+
aliasColumns.add(column.slice(dot + 1));
|
|
3114
|
+
return true;
|
|
2340
3115
|
}
|
|
2341
|
-
function
|
|
2342
|
-
|
|
3116
|
+
function predicateJoinAlias(expr, aliases) {
|
|
3117
|
+
const columns = /* @__PURE__ */ new Set();
|
|
3118
|
+
collectExprColumns2(expr, columns);
|
|
3119
|
+
let alias;
|
|
3120
|
+
for (const column of columns) {
|
|
3121
|
+
const dot = column.indexOf(".");
|
|
3122
|
+
if (dot <= 0) return void 0;
|
|
3123
|
+
const columnAlias = column.slice(0, dot);
|
|
3124
|
+
if (!aliases.has(columnAlias)) return void 0;
|
|
3125
|
+
if (alias === void 0) alias = columnAlias;
|
|
3126
|
+
else if (alias !== columnAlias) return void 0;
|
|
3127
|
+
}
|
|
3128
|
+
return alias;
|
|
3129
|
+
}
|
|
3130
|
+
function addJoinFilter(filters, alias, predicate) {
|
|
3131
|
+
const existing = filters.get(alias);
|
|
3132
|
+
filters.set(
|
|
3133
|
+
alias,
|
|
3134
|
+
existing === void 0 ? predicate : { kind: "logical", op: "and", operands: [existing, predicate] }
|
|
3135
|
+
);
|
|
2343
3136
|
}
|
|
2344
|
-
function
|
|
3137
|
+
function stripExprJoinAlias(expr, alias) {
|
|
2345
3138
|
switch (expr.kind) {
|
|
2346
3139
|
case "column":
|
|
2347
3140
|
return { ...expr, name: stripQualifiedColumn(expr.name, alias) };
|
|
2348
3141
|
case "compare":
|
|
2349
3142
|
return {
|
|
2350
3143
|
...expr,
|
|
2351
|
-
left:
|
|
2352
|
-
right:
|
|
3144
|
+
left: stripExprJoinAlias(expr.left, alias),
|
|
3145
|
+
right: stripExprJoinAlias(expr.right, alias)
|
|
2353
3146
|
};
|
|
2354
3147
|
case "between":
|
|
2355
3148
|
return {
|
|
2356
3149
|
...expr,
|
|
2357
|
-
target:
|
|
2358
|
-
low:
|
|
2359
|
-
high:
|
|
3150
|
+
target: stripExprJoinAlias(expr.target, alias),
|
|
3151
|
+
low: stripExprJoinAlias(expr.low, alias),
|
|
3152
|
+
high: stripExprJoinAlias(expr.high, alias)
|
|
2360
3153
|
};
|
|
2361
3154
|
case "in":
|
|
2362
3155
|
return {
|
|
2363
3156
|
...expr,
|
|
2364
|
-
target:
|
|
2365
|
-
values: expr.values.map((value) =>
|
|
3157
|
+
target: stripExprJoinAlias(expr.target, alias),
|
|
3158
|
+
values: expr.values.map((value) => stripExprJoinAlias(value, alias))
|
|
2366
3159
|
};
|
|
2367
3160
|
case "logical":
|
|
2368
3161
|
return {
|
|
2369
3162
|
...expr,
|
|
2370
|
-
operands: expr.operands.map((operand) =>
|
|
3163
|
+
operands: expr.operands.map((operand) => stripExprJoinAlias(operand, alias))
|
|
2371
3164
|
};
|
|
2372
3165
|
case "not":
|
|
2373
|
-
return { ...expr, operand:
|
|
3166
|
+
return { ...expr, operand: stripExprJoinAlias(expr.operand, alias) };
|
|
2374
3167
|
case "null-check":
|
|
2375
|
-
return { ...expr, target:
|
|
3168
|
+
return { ...expr, target: stripExprJoinAlias(expr.target, alias) };
|
|
2376
3169
|
case "like":
|
|
2377
|
-
return { ...expr, target:
|
|
3170
|
+
return { ...expr, target: stripExprJoinAlias(expr.target, alias) };
|
|
2378
3171
|
case "call":
|
|
2379
|
-
return { ...expr, args: expr.args.map((arg) =>
|
|
3172
|
+
return { ...expr, args: expr.args.map((arg) => stripExprJoinAlias(arg, alias)) };
|
|
2380
3173
|
case "arithmetic":
|
|
2381
3174
|
return {
|
|
2382
3175
|
...expr,
|
|
2383
|
-
left:
|
|
2384
|
-
right:
|
|
3176
|
+
left: stripExprJoinAlias(expr.left, alias),
|
|
3177
|
+
right: stripExprJoinAlias(expr.right, alias)
|
|
2385
3178
|
};
|
|
2386
3179
|
case "case":
|
|
2387
3180
|
return {
|
|
2388
3181
|
...expr,
|
|
2389
3182
|
whens: expr.whens.map((branch) => ({
|
|
2390
|
-
when:
|
|
2391
|
-
value:
|
|
3183
|
+
when: stripExprJoinAlias(branch.when, alias),
|
|
3184
|
+
value: stripExprJoinAlias(branch.value, alias)
|
|
2392
3185
|
})),
|
|
2393
|
-
...expr.else === void 0 ? {} : { else:
|
|
3186
|
+
...expr.else === void 0 ? {} : { else: stripExprJoinAlias(expr.else, alias) }
|
|
2394
3187
|
};
|
|
2395
3188
|
case "literal":
|
|
2396
3189
|
return expr;
|
|
2397
3190
|
}
|
|
2398
3191
|
}
|
|
2399
|
-
function
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
3192
|
+
function isQualifiedBy(column, alias) {
|
|
3193
|
+
return column.startsWith(`${alias}.`) && column.length > alias.length + 1;
|
|
3194
|
+
}
|
|
3195
|
+
function stripQualifiedColumn(column, alias) {
|
|
3196
|
+
return isQualifiedBy(column, alias) ? column.slice(alias.length + 1) : column;
|
|
2403
3197
|
}
|
|
2404
|
-
function
|
|
3198
|
+
function collectExprColumns2(expr, columns) {
|
|
2405
3199
|
switch (expr.kind) {
|
|
2406
3200
|
case "column":
|
|
2407
3201
|
columns.add(expr.name);
|
|
2408
3202
|
return;
|
|
2409
3203
|
case "compare":
|
|
2410
|
-
|
|
2411
|
-
|
|
3204
|
+
collectExprColumns2(expr.left, columns);
|
|
3205
|
+
collectExprColumns2(expr.right, columns);
|
|
2412
3206
|
return;
|
|
2413
3207
|
case "between":
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
3208
|
+
collectExprColumns2(expr.target, columns);
|
|
3209
|
+
collectExprColumns2(expr.low, columns);
|
|
3210
|
+
collectExprColumns2(expr.high, columns);
|
|
2417
3211
|
return;
|
|
2418
3212
|
case "in":
|
|
2419
|
-
|
|
2420
|
-
for (const value of expr.values)
|
|
3213
|
+
collectExprColumns2(expr.target, columns);
|
|
3214
|
+
for (const value of expr.values) collectExprColumns2(value, columns);
|
|
2421
3215
|
return;
|
|
2422
3216
|
case "logical":
|
|
2423
|
-
for (const operand of expr.operands)
|
|
3217
|
+
for (const operand of expr.operands) collectExprColumns2(operand, columns);
|
|
2424
3218
|
return;
|
|
2425
3219
|
case "not":
|
|
2426
|
-
|
|
3220
|
+
collectExprColumns2(expr.operand, columns);
|
|
2427
3221
|
return;
|
|
2428
3222
|
case "null-check":
|
|
2429
|
-
|
|
3223
|
+
collectExprColumns2(expr.target, columns);
|
|
2430
3224
|
return;
|
|
2431
3225
|
case "like":
|
|
2432
|
-
|
|
3226
|
+
collectExprColumns2(expr.target, columns);
|
|
2433
3227
|
return;
|
|
2434
3228
|
case "call":
|
|
2435
|
-
for (const arg of expr.args)
|
|
3229
|
+
for (const arg of expr.args) collectExprColumns2(arg, columns);
|
|
2436
3230
|
return;
|
|
2437
3231
|
case "arithmetic":
|
|
2438
|
-
|
|
2439
|
-
|
|
3232
|
+
collectExprColumns2(expr.left, columns);
|
|
3233
|
+
collectExprColumns2(expr.right, columns);
|
|
2440
3234
|
return;
|
|
2441
3235
|
case "case":
|
|
2442
3236
|
for (const branch of expr.whens) {
|
|
2443
|
-
|
|
2444
|
-
|
|
3237
|
+
collectExprColumns2(branch.when, columns);
|
|
3238
|
+
collectExprColumns2(branch.value, columns);
|
|
2445
3239
|
}
|
|
2446
|
-
if (expr.else !== void 0)
|
|
3240
|
+
if (expr.else !== void 0) collectExprColumns2(expr.else, columns);
|
|
2447
3241
|
return;
|
|
2448
3242
|
case "literal":
|
|
2449
3243
|
return;
|
|
2450
3244
|
}
|
|
2451
3245
|
}
|
|
2452
|
-
function canProjectJoinSides(ast, leftAlias, rightAlias) {
|
|
2453
|
-
if (ast.select?.includes("*") ?? false) return false;
|
|
2454
|
-
return referencedJoinColumns(ast).every(
|
|
2455
|
-
(column) => isQualifiedBy(column, leftAlias) || isQualifiedBy(column, rightAlias)
|
|
2456
|
-
);
|
|
2457
|
-
}
|
|
2458
3246
|
function referencedJoinColumns(ast) {
|
|
2459
3247
|
const columns = /* @__PURE__ */ new Set();
|
|
2460
3248
|
for (const select of ast.select ?? []) {
|
|
2461
3249
|
const { column } = selectColumn(select);
|
|
2462
3250
|
columns.add(column);
|
|
2463
3251
|
}
|
|
2464
|
-
for (const expr of Object.values(ast.projections ?? {}))
|
|
2465
|
-
if (ast.where !== void 0)
|
|
3252
|
+
for (const expr of Object.values(ast.projections ?? {})) collectExprColumns2(expr, columns);
|
|
3253
|
+
if (ast.where !== void 0) collectExprColumns2(ast.where, columns);
|
|
2466
3254
|
for (const term of ast.orderBy ?? []) columns.add(term.column);
|
|
2467
3255
|
return [...columns].filter((column) => column !== "*");
|
|
2468
3256
|
}
|
|
2469
|
-
function joinSideColumns(ast, alias, joinKey2) {
|
|
2470
|
-
const columns = new Set(
|
|
2471
|
-
(Array.isArray(joinKey2) ? joinKey2 : [joinKey2]).map(
|
|
2472
|
-
(column) => stripQualifiedColumn(column, alias)
|
|
2473
|
-
)
|
|
2474
|
-
);
|
|
2475
|
-
for (const column of referencedJoinColumns(ast)) {
|
|
2476
|
-
if (isQualifiedBy(column, alias)) columns.add(stripQualifiedColumn(column, alias));
|
|
2477
|
-
}
|
|
2478
|
-
return [...columns];
|
|
2479
|
-
}
|
|
2480
3257
|
function leftJoinRightColumns(ast, rightAlias, rightRows) {
|
|
2481
3258
|
const columns = /* @__PURE__ */ new Set();
|
|
2482
3259
|
if (ast.select?.includes("*") ?? false) {
|
|
@@ -2505,33 +3282,213 @@ function fillLeftJoinNulls(rows, rightAlias, rightColumns) {
|
|
|
2505
3282
|
return out ?? row;
|
|
2506
3283
|
});
|
|
2507
3284
|
}
|
|
3285
|
+
function fillRightJoinNulls(rows, ast, preservedAlias, leftRows) {
|
|
3286
|
+
const columns = rightJoinLeftColumns(ast, preservedAlias, leftRows);
|
|
3287
|
+
if (columns.length === 0) return rows;
|
|
3288
|
+
return rows.map((row) => {
|
|
3289
|
+
let out;
|
|
3290
|
+
for (const column of columns) {
|
|
3291
|
+
if (column in row) continue;
|
|
3292
|
+
out ??= { ...row };
|
|
3293
|
+
out[column] = null;
|
|
3294
|
+
}
|
|
3295
|
+
return out ?? row;
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
function rightJoinLeftColumns(ast, preservedAlias, leftRows) {
|
|
3299
|
+
const columns = /* @__PURE__ */ new Set();
|
|
3300
|
+
if (ast.select?.includes("*") ?? false) {
|
|
3301
|
+
for (const row of leftRows) {
|
|
3302
|
+
for (const column of Object.keys(row)) {
|
|
3303
|
+
if (!isQualifiedBy(column, preservedAlias)) columns.add(column);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
for (const column of referencedJoinColumns(ast)) {
|
|
3308
|
+
if (!isQualifiedBy(column, preservedAlias)) columns.add(column);
|
|
3309
|
+
}
|
|
3310
|
+
return [...columns];
|
|
3311
|
+
}
|
|
2508
3312
|
async function subqueryJoinRowsFromAst(lake, ast, options) {
|
|
2509
|
-
|
|
3313
|
+
const joins = sqlSubqueryJoins(ast);
|
|
3314
|
+
if (joins.length === 0) {
|
|
2510
3315
|
throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL IN subquery");
|
|
2511
3316
|
}
|
|
2512
3317
|
if (hasWindowSemantics(ast)) {
|
|
2513
3318
|
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over IN subquery is not supported");
|
|
2514
3319
|
}
|
|
2515
3320
|
if (hasAggregation(ast)) {
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
3321
|
+
return await aggregateSubqueryJoinRowsFromAst(lake, ast, joins, options);
|
|
3322
|
+
}
|
|
3323
|
+
let rows = await lake.path(ast.source).toArray();
|
|
3324
|
+
for (const join of joins) {
|
|
3325
|
+
rows = await applySubqueryJoinRows(lake, rows, join, options);
|
|
3326
|
+
}
|
|
3327
|
+
if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
|
|
3328
|
+
if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
|
|
3329
|
+
rows = await projectRows(lake, rows, ast);
|
|
3330
|
+
if (ast.distinct === true) rows = distinctRows(rows);
|
|
3331
|
+
return offsetLimitRows(rows, ast);
|
|
3332
|
+
}
|
|
3333
|
+
async function aggregateSubqueryJoinRowsFromAst(lake, ast, joins, options) {
|
|
3334
|
+
let rows = await lake.path(ast.source).toArray();
|
|
3335
|
+
for (const join of joins) rows = await applySubqueryJoinRows(lake, rows, join, options);
|
|
3336
|
+
if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
|
|
3337
|
+
const aggregateAst = subqueryJoinAggregateAst(ast);
|
|
3338
|
+
if (rows.length === 0) {
|
|
3339
|
+
if ((ast.groupBy?.length ?? 0) > 0) return [];
|
|
3340
|
+
return emptyAggregateRowsFromAst(aggregateAst);
|
|
3341
|
+
}
|
|
3342
|
+
const prefix = `__lakeql_sql_subquery/${safeTempName(ast.source)}/${nextSqlTempId()}`;
|
|
3343
|
+
const written = await writePartitionedParquet(lake.store, prefix, {
|
|
3344
|
+
rows,
|
|
3345
|
+
maxRowsPerFile: rows.length
|
|
3346
|
+
});
|
|
3347
|
+
try {
|
|
3348
|
+
return await aggregateRowsFromAst(lake.path(`${prefix}/*.parquet`), {
|
|
3349
|
+
...aggregateAst,
|
|
3350
|
+
source: `${prefix}/*.parquet`
|
|
3351
|
+
});
|
|
3352
|
+
} finally {
|
|
3353
|
+
await Promise.all(written.files.map((file) => lake.store.delete(file.path)));
|
|
2520
3354
|
}
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
3355
|
+
}
|
|
3356
|
+
function subqueryJoinAggregateAst(ast) {
|
|
3357
|
+
const {
|
|
3358
|
+
subqueryJoin: _subqueryJoin,
|
|
3359
|
+
subqueryJoins: _subqueryJoins,
|
|
3360
|
+
where: _where,
|
|
3361
|
+
...rest
|
|
3362
|
+
} = ast;
|
|
3363
|
+
return rest;
|
|
3364
|
+
}
|
|
3365
|
+
function emptyAggregateRowsFromAst(ast) {
|
|
3366
|
+
validateAggregateProjection(ast);
|
|
3367
|
+
const hiddenCountAlias = hiddenAggregateAlias(ast);
|
|
3368
|
+
const aggregateSpec = ast.aggregates === void 0 || Object.keys(ast.aggregates).length === 0 ? { [hiddenCountAlias]: { op: "count" } } : ast.aggregates;
|
|
3369
|
+
let rows = [emptyAggregateRow(aggregateSpec)];
|
|
3370
|
+
if (ast.having !== void 0) rows = rows.filter((row) => matches(ast.having, row));
|
|
3371
|
+
rows = projectAggregateRows(rows, ast, hiddenCountAlias);
|
|
3372
|
+
if (ast.distinct === true) rows = distinctRows(rows);
|
|
3373
|
+
if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
|
|
3374
|
+
const offset = ast.offset ?? 0;
|
|
3375
|
+
if (ast.limit !== void 0) rows = rows.slice(offset, offset + ast.limit);
|
|
3376
|
+
else if (offset > 0) rows = rows.slice(offset);
|
|
3377
|
+
return rows;
|
|
3378
|
+
}
|
|
3379
|
+
function emptyAggregateRow(spec) {
|
|
3380
|
+
return Object.fromEntries(
|
|
3381
|
+
Object.entries(spec).map(([alias, aggregate]) => [alias, emptyAggregateValue(aggregate.op)])
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
function emptyAggregateValue(op) {
|
|
3385
|
+
switch (op) {
|
|
3386
|
+
case "count":
|
|
3387
|
+
case "count_distinct":
|
|
3388
|
+
case "approx_count_distinct":
|
|
3389
|
+
return 0;
|
|
3390
|
+
case "sum":
|
|
3391
|
+
case "avg":
|
|
3392
|
+
case "var_samp":
|
|
3393
|
+
case "var_pop":
|
|
3394
|
+
case "stddev_samp":
|
|
3395
|
+
case "stddev_pop":
|
|
3396
|
+
case "median":
|
|
3397
|
+
case "quantile":
|
|
3398
|
+
case "min":
|
|
3399
|
+
case "max":
|
|
3400
|
+
case "mode":
|
|
3401
|
+
case "first":
|
|
3402
|
+
case "last":
|
|
3403
|
+
case "any":
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
async function applySubqueryJoinRows(lake, leftRows, join, options) {
|
|
3408
|
+
let rightRows = await subqueryJoinRightRows(lake, join);
|
|
3409
|
+
rightRows = offsetLimitSubqueryRows(rightRows, join);
|
|
3410
|
+
return join.predicate === void 0 ? await broadcastJoin(leftRows, rightRows, {
|
|
2525
3411
|
leftKey: join.leftKey,
|
|
2526
3412
|
rightKey: join.rightKey,
|
|
2527
3413
|
type: join.type,
|
|
2528
3414
|
maxRightRows: options.joinMaxRightRows ?? 1e5
|
|
2529
|
-
});
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
3415
|
+
}) : predicateSubqueryJoinRows(leftRows, rightRows, join, options);
|
|
3416
|
+
}
|
|
3417
|
+
async function subqueryJoinRightRows(lake, join) {
|
|
3418
|
+
if (subqueryJoinHasAggregation(join)) {
|
|
3419
|
+
return await aggregateRowsFromAst(lake.path(join.source), {
|
|
3420
|
+
source: join.source,
|
|
3421
|
+
select: join.rightKey,
|
|
3422
|
+
...join.where === void 0 ? {} : { where: join.where },
|
|
3423
|
+
...join.groupBy === void 0 ? {} : { groupBy: join.groupBy },
|
|
3424
|
+
...join.aggregates === void 0 ? {} : { aggregates: join.aggregates },
|
|
3425
|
+
...join.hiddenAggregates === void 0 ? {} : { hiddenAggregates: join.hiddenAggregates },
|
|
3426
|
+
...join.having === void 0 ? {} : { having: join.having },
|
|
3427
|
+
...join.orderBy === void 0 ? {} : { orderBy: join.orderBy }
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
let rows = await lake.path(join.source).toArray();
|
|
3431
|
+
if (join.where !== void 0) rows = rows.filter((row) => matches(join.where, row));
|
|
3432
|
+
if (join.orderBy !== void 0) rows = sortRows(rows, join.orderBy);
|
|
3433
|
+
return rows;
|
|
3434
|
+
}
|
|
3435
|
+
function subqueryJoinHasAggregation(join) {
|
|
3436
|
+
return join.aggregates !== void 0 || join.having !== void 0 || join.groupBy !== void 0 && join.groupBy.length > 0;
|
|
3437
|
+
}
|
|
3438
|
+
function predicateSubqueryJoinRows(leftRows, rightRows, join, options) {
|
|
3439
|
+
if (join.predicate === void 0) return leftRows;
|
|
3440
|
+
const maxRightRows = options.joinMaxRightRows ?? 1e5;
|
|
3441
|
+
if (rightRows.length > maxRightRows) {
|
|
3442
|
+
throw new LakeqlError(
|
|
3443
|
+
"LAKEQL_BUDGET_EXCEEDED",
|
|
3444
|
+
`Predicate subquery join exceeded maxRightRows (${rightRows.length} > ${maxRightRows})`,
|
|
3445
|
+
{ metric: "maxRightRows", limit: maxRightRows, actual: rightRows.length }
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
const maxOutputRows = options.joinMaxOutputRows ?? 1e5;
|
|
3449
|
+
const out = [];
|
|
3450
|
+
const leftAlias = join.leftAlias ?? "";
|
|
3451
|
+
const rightAlias = join.alias ?? join.source;
|
|
3452
|
+
let candidateRows = 0;
|
|
3453
|
+
for (const leftRow of leftRows) {
|
|
3454
|
+
let matched = false;
|
|
3455
|
+
for (const rightRow of rightRows) {
|
|
3456
|
+
candidateRows += 1;
|
|
3457
|
+
if (candidateRows > maxOutputRows) {
|
|
3458
|
+
throw new LakeqlError(
|
|
3459
|
+
"LAKEQL_BUDGET_EXCEEDED",
|
|
3460
|
+
`Predicate subquery join exceeded maxOutputRows (${candidateRows} > ${maxOutputRows})`,
|
|
3461
|
+
{ metric: "maxOutputRows", limit: maxOutputRows, actual: candidateRows }
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
if (!subqueryKeysMatch(leftRow, rightRow, join)) continue;
|
|
3465
|
+
const row = {
|
|
3466
|
+
...leftRow,
|
|
3467
|
+
...leftAlias.length === 0 ? {} : qualifyOnlyRow(leftRow, leftAlias),
|
|
3468
|
+
...qualifyOnlyRow(rightRow, rightAlias)
|
|
3469
|
+
};
|
|
3470
|
+
if (!matches(join.predicate, row)) continue;
|
|
3471
|
+
matched = true;
|
|
3472
|
+
break;
|
|
3473
|
+
}
|
|
3474
|
+
if (join.type === "semi" && matched || join.type === "anti" && !matched) out.push(leftRow);
|
|
3475
|
+
}
|
|
3476
|
+
return out;
|
|
3477
|
+
}
|
|
3478
|
+
function subqueryKeysMatch(leftRow, rightRow, join) {
|
|
3479
|
+
for (const [index, leftKey] of join.leftKey.entries()) {
|
|
3480
|
+
const rightKey = join.rightKey[index];
|
|
3481
|
+
if (rightKey === void 0) return false;
|
|
3482
|
+
const left = leftRow[leftKey];
|
|
3483
|
+
const right = rightRow[rightKey];
|
|
3484
|
+
if (left === null || right === null || left !== right) return false;
|
|
3485
|
+
}
|
|
3486
|
+
return true;
|
|
3487
|
+
}
|
|
3488
|
+
function offsetLimitSubqueryRows(rows, subquery) {
|
|
3489
|
+
const offset = subquery.offset ?? 0;
|
|
3490
|
+
if (subquery.limit !== void 0) return rows.slice(offset, offset + subquery.limit);
|
|
3491
|
+
return offset > 0 ? rows.slice(offset) : rows;
|
|
2535
3492
|
}
|
|
2536
3493
|
function qualifyRow(row, alias) {
|
|
2537
3494
|
const out = { ...row };
|
|
@@ -2541,7 +3498,19 @@ function qualifyRow(row, alias) {
|
|
|
2541
3498
|
function qualifyOnlyRow(row, alias) {
|
|
2542
3499
|
return Object.fromEntries(Object.entries(row).map(([key, value]) => [`${alias}.${key}`, value]));
|
|
2543
3500
|
}
|
|
2544
|
-
function
|
|
3501
|
+
async function correlatedScalarRowsFromAst(lake, ast) {
|
|
3502
|
+
let rows = await lake.path(ast.source).toArray();
|
|
3503
|
+
if (ast.where !== void 0) rows = rows.filter((row) => matches(ast.where, row));
|
|
3504
|
+
if (ast.orderBy !== void 0) rows = sortRows(rows, ast.orderBy, ast);
|
|
3505
|
+
rows = await projectRows(lake, rows, ast);
|
|
3506
|
+
if (ast.distinct === true) rows = distinctRows(rows);
|
|
3507
|
+
return offsetLimitRows(rows, ast);
|
|
3508
|
+
}
|
|
3509
|
+
function hasCorrelatedScalarSubqueries(ast) {
|
|
3510
|
+
return ast.correlatedScalarSubqueries !== void 0 && Object.keys(ast.correlatedScalarSubqueries).length > 0;
|
|
3511
|
+
}
|
|
3512
|
+
async function projectRows(lake, rows, ast) {
|
|
3513
|
+
const lookups = await correlatedScalarLookups(lake, ast);
|
|
2545
3514
|
const hasWildcardProjection = ast.select?.includes("*") ?? false;
|
|
2546
3515
|
return rows.map((row) => {
|
|
2547
3516
|
if (hasWildcardProjection && ast.projections === void 0) return row;
|
|
@@ -2555,11 +3524,123 @@ function projectRows(rows, ast) {
|
|
|
2555
3524
|
}
|
|
2556
3525
|
}
|
|
2557
3526
|
for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
|
|
2558
|
-
out[alias] = evaluate(expr, row);
|
|
3527
|
+
out[alias] = evaluate(resolveCorrelatedScalarExpr(expr, row, lookups), row);
|
|
2559
3528
|
}
|
|
2560
3529
|
return out;
|
|
2561
3530
|
});
|
|
2562
3531
|
}
|
|
3532
|
+
async function correlatedScalarLookups(lake, ast) {
|
|
3533
|
+
const lookups = /* @__PURE__ */ new Map();
|
|
3534
|
+
for (const [id, subquery] of Object.entries(ast.correlatedScalarSubqueries ?? {})) {
|
|
3535
|
+
const rightRows = await aggregateRowsFromAst(lake.path(subquery.source), {
|
|
3536
|
+
source: subquery.source,
|
|
3537
|
+
select: subquery.rightKey,
|
|
3538
|
+
groupBy: subquery.groupBy,
|
|
3539
|
+
aggregates: subquery.aggregates,
|
|
3540
|
+
...subquery.hiddenAggregates === void 0 ? {} : { hiddenAggregates: subquery.hiddenAggregates },
|
|
3541
|
+
...subquery.where === void 0 ? {} : { where: subquery.where },
|
|
3542
|
+
...subquery.having === void 0 ? {} : { having: subquery.having }
|
|
3543
|
+
});
|
|
3544
|
+
lookups.set(id, {
|
|
3545
|
+
subquery,
|
|
3546
|
+
rows: new Map(rightRows.map((row) => [correlatedScalarRightKey(row, subquery), row]))
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
return lookups;
|
|
3550
|
+
}
|
|
3551
|
+
function resolveCorrelatedScalarExpr(expr, row, lookups) {
|
|
3552
|
+
switch (expr.kind) {
|
|
3553
|
+
case "call":
|
|
3554
|
+
if (expr.fn === "__lakeql_correlated_scalar_subquery") {
|
|
3555
|
+
return {
|
|
3556
|
+
kind: "literal",
|
|
3557
|
+
value: correlatedScalarValue(expr, row, lookups)
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
return {
|
|
3561
|
+
...expr,
|
|
3562
|
+
args: expr.args.map((arg) => resolveCorrelatedScalarExpr(arg, row, lookups))
|
|
3563
|
+
};
|
|
3564
|
+
case "compare":
|
|
3565
|
+
return {
|
|
3566
|
+
...expr,
|
|
3567
|
+
left: resolveCorrelatedScalarExpr(expr.left, row, lookups),
|
|
3568
|
+
right: resolveCorrelatedScalarExpr(expr.right, row, lookups)
|
|
3569
|
+
};
|
|
3570
|
+
case "arithmetic":
|
|
3571
|
+
return {
|
|
3572
|
+
...expr,
|
|
3573
|
+
left: resolveCorrelatedScalarExpr(expr.left, row, lookups),
|
|
3574
|
+
right: resolveCorrelatedScalarExpr(expr.right, row, lookups)
|
|
3575
|
+
};
|
|
3576
|
+
case "case":
|
|
3577
|
+
return {
|
|
3578
|
+
...expr,
|
|
3579
|
+
whens: expr.whens.map((branch) => ({
|
|
3580
|
+
when: resolveCorrelatedScalarExpr(branch.when, row, lookups),
|
|
3581
|
+
value: resolveCorrelatedScalarExpr(branch.value, row, lookups)
|
|
3582
|
+
})),
|
|
3583
|
+
...expr.else === void 0 ? {} : { else: resolveCorrelatedScalarExpr(expr.else, row, lookups) }
|
|
3584
|
+
};
|
|
3585
|
+
case "logical":
|
|
3586
|
+
return {
|
|
3587
|
+
...expr,
|
|
3588
|
+
operands: expr.operands.map(
|
|
3589
|
+
(operand) => resolveCorrelatedScalarExpr(operand, row, lookups)
|
|
3590
|
+
)
|
|
3591
|
+
};
|
|
3592
|
+
case "not":
|
|
3593
|
+
return { ...expr, operand: resolveCorrelatedScalarExpr(expr.operand, row, lookups) };
|
|
3594
|
+
case "between":
|
|
3595
|
+
return {
|
|
3596
|
+
...expr,
|
|
3597
|
+
target: resolveCorrelatedScalarExpr(expr.target, row, lookups),
|
|
3598
|
+
low: resolveCorrelatedScalarExpr(expr.low, row, lookups),
|
|
3599
|
+
high: resolveCorrelatedScalarExpr(expr.high, row, lookups)
|
|
3600
|
+
};
|
|
3601
|
+
case "in":
|
|
3602
|
+
return {
|
|
3603
|
+
...expr,
|
|
3604
|
+
target: resolveCorrelatedScalarExpr(expr.target, row, lookups),
|
|
3605
|
+
values: expr.values.map((value) => resolveCorrelatedScalarExpr(value, row, lookups))
|
|
3606
|
+
};
|
|
3607
|
+
case "like":
|
|
3608
|
+
return { ...expr, target: resolveCorrelatedScalarExpr(expr.target, row, lookups) };
|
|
3609
|
+
case "null-check":
|
|
3610
|
+
return { ...expr, target: resolveCorrelatedScalarExpr(expr.target, row, lookups) };
|
|
3611
|
+
case "column":
|
|
3612
|
+
case "literal":
|
|
3613
|
+
return expr;
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
function correlatedScalarValue(expr, row, lookups) {
|
|
3617
|
+
const id = expr.args[0];
|
|
3618
|
+
if (id?.kind !== "literal" || typeof id.value !== "string") {
|
|
3619
|
+
throw new LakeqlError(
|
|
3620
|
+
"LAKEQL_SQL_UNSUPPORTED",
|
|
3621
|
+
"Invalid correlated scalar subquery placeholder"
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
const lookup = lookups.get(id.value);
|
|
3625
|
+
if (lookup === void 0) {
|
|
3626
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Missing correlated scalar subquery metadata");
|
|
3627
|
+
}
|
|
3628
|
+
const key = correlatedScalarLeftKey(row, lookup.subquery);
|
|
3629
|
+
if (key === void 0) return null;
|
|
3630
|
+
return lookup.rows.get(key)?.[lookup.subquery.column] ?? null;
|
|
3631
|
+
}
|
|
3632
|
+
function correlatedScalarLeftKey(row, subquery) {
|
|
3633
|
+
const values = [];
|
|
3634
|
+
for (const key of subquery.leftKey) {
|
|
3635
|
+
const value = row[key];
|
|
3636
|
+
if (value === null || value === void 0) return void 0;
|
|
3637
|
+
values.push(value);
|
|
3638
|
+
}
|
|
3639
|
+
return JSON.stringify(values);
|
|
3640
|
+
}
|
|
3641
|
+
function correlatedScalarRightKey(row, subquery) {
|
|
3642
|
+
return JSON.stringify(subquery.rightKey.map((key) => row[key]));
|
|
3643
|
+
}
|
|
2563
3644
|
async function aggregateRowsFromAst(builder, ast) {
|
|
2564
3645
|
if (hasWindowSemantics(ast)) {
|
|
2565
3646
|
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over aggregates is not supported");
|
|
@@ -2567,6 +3648,8 @@ async function aggregateRowsFromAst(builder, ast) {
|
|
|
2567
3648
|
validateAggregateProjection(ast);
|
|
2568
3649
|
let next = builder;
|
|
2569
3650
|
if (ast.where) next = next.where(ast.where);
|
|
3651
|
+
const preProjections = aggregatePreProjections(ast);
|
|
3652
|
+
if (preProjections !== void 0) next = next.select(["*"]).project(preProjections);
|
|
2570
3653
|
const aggregates = ast.aggregates ?? {};
|
|
2571
3654
|
const hiddenCountAlias = hiddenAggregateAlias(ast);
|
|
2572
3655
|
const aggregateSpec = Object.keys(aggregates).length > 0 ? aggregates : { [hiddenCountAlias]: { op: "count" } };
|
|
@@ -2596,12 +3679,13 @@ function validateAggregateProjection(ast) {
|
|
|
2596
3679
|
}
|
|
2597
3680
|
function projectAggregateRows(rows, ast, hiddenCountAlias) {
|
|
2598
3681
|
const aggregates = Object.entries(ast.aggregates ?? {});
|
|
3682
|
+
const hiddenAggregates = new Set(ast.hiddenAggregates ?? []);
|
|
2599
3683
|
const hasWildcardProjection = ast.select?.includes("*") ?? false;
|
|
2600
3684
|
return rows.map((row) => {
|
|
2601
3685
|
const out = {};
|
|
2602
3686
|
if (hasWildcardProjection) {
|
|
2603
3687
|
for (const [column, value] of Object.entries(row)) {
|
|
2604
|
-
if (column !== hiddenCountAlias) out[column] = value;
|
|
3688
|
+
if (column !== hiddenCountAlias && !hiddenAggregates.has(column)) out[column] = value;
|
|
2605
3689
|
}
|
|
2606
3690
|
} else if (ast.select !== void 0) {
|
|
2607
3691
|
for (const select of ast.select ?? []) {
|
|
@@ -2609,13 +3693,24 @@ function projectAggregateRows(rows, ast, hiddenCountAlias) {
|
|
|
2609
3693
|
out[alias] = row[column];
|
|
2610
3694
|
}
|
|
2611
3695
|
}
|
|
2612
|
-
for (const [alias] of aggregates)
|
|
3696
|
+
for (const [alias] of aggregates) {
|
|
3697
|
+
if (!hiddenAggregates.has(alias)) out[alias] = row[alias];
|
|
3698
|
+
}
|
|
2613
3699
|
for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
|
|
2614
|
-
out[alias] =
|
|
3700
|
+
out[alias] = aggregateProjectionValue(alias, expr, row, ast);
|
|
2615
3701
|
}
|
|
2616
3702
|
return out;
|
|
2617
3703
|
});
|
|
2618
3704
|
}
|
|
3705
|
+
function aggregatePreProjections(ast) {
|
|
3706
|
+
const groupBy = new Set(ast.groupBy ?? []);
|
|
3707
|
+
const entries = Object.entries(ast.projections ?? {}).filter(([alias]) => groupBy.has(alias));
|
|
3708
|
+
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
3709
|
+
}
|
|
3710
|
+
function aggregateProjectionValue(alias, expr, row, ast) {
|
|
3711
|
+
if (ast.groupBy?.includes(alias) && alias in row) return row[alias];
|
|
3712
|
+
return evaluate(expr, row);
|
|
3713
|
+
}
|
|
2619
3714
|
function hiddenAggregateAlias(ast) {
|
|
2620
3715
|
const reserved = /* @__PURE__ */ new Set([...ast.select ?? [], ...Object.keys(ast.aggregates ?? {})]);
|
|
2621
3716
|
let alias = "__lakeql_group_count";
|
|
@@ -2639,20 +3734,27 @@ function distinctRows(rows) {
|
|
|
2639
3734
|
}
|
|
2640
3735
|
return out;
|
|
2641
3736
|
}
|
|
2642
|
-
function sortRows(rows, orderBy) {
|
|
3737
|
+
function sortRows(rows, orderBy, ast) {
|
|
2643
3738
|
return [...rows].sort((a, b) => {
|
|
2644
3739
|
for (const term of orderBy) {
|
|
2645
|
-
const av = a
|
|
2646
|
-
const bv = b
|
|
3740
|
+
const av = sortValue(a, term.column, ast);
|
|
3741
|
+
const bv = sortValue(b, term.column, ast);
|
|
2647
3742
|
if (av === bv) continue;
|
|
2648
|
-
if (av === null) return term.nulls === "first" ? -1 : 1;
|
|
2649
|
-
if (bv === null) return term.nulls === "first" ? 1 : -1;
|
|
3743
|
+
if (av === null || av === void 0) return term.nulls === "first" ? -1 : 1;
|
|
3744
|
+
if (bv === null || bv === void 0) return term.nulls === "first" ? 1 : -1;
|
|
2650
3745
|
const direction = term.direction === "desc" ? -1 : 1;
|
|
2651
3746
|
return (av < bv ? -1 : 1) * direction;
|
|
2652
3747
|
}
|
|
2653
3748
|
return 0;
|
|
2654
3749
|
});
|
|
2655
3750
|
}
|
|
3751
|
+
function sortValue(row, column, ast) {
|
|
3752
|
+
const expr = ast?.projections?.[column];
|
|
3753
|
+
if (expr !== void 0) return evaluate(expr, row) ?? null;
|
|
3754
|
+
const select = ast?.select?.find((value) => selectColumn(value).alias === column);
|
|
3755
|
+
if (select !== void 0) return row[selectColumn(select).column] ?? null;
|
|
3756
|
+
return row[column] ?? null;
|
|
3757
|
+
}
|
|
2656
3758
|
function offsetLimitRows(rows, ast) {
|
|
2657
3759
|
const offset = ast.offset ?? 0;
|
|
2658
3760
|
if (ast.limit !== void 0) return rows.slice(offset, offset + ast.limit);
|