lakeql 0.1.9 → 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/README.md +56 -73
- package/dist/bin.js +498 -9010
- package/dist/chunk-AUL4DYT4.js +21780 -0
- package/dist/{chunk-ZVHJD6R3.js → chunk-LWF5FKHB.js} +1 -1
- package/dist/{chunk-2XZJK7EF.js → chunk-TR4JNJ47.js} +1791 -11
- package/dist/{chunk-4VJQ56HF.js → chunk-WGHR35QU.js} +3 -1
- package/dist/cloudflare.d.ts +1 -1
- package/dist/cloudflare.js +5 -5
- package/dist/{geo-backend-MY6MET3L.js → geo-backend-WYCTLPNS.js} +1 -1
- package/dist/index.d.ts +62 -3
- package/dist/index.js +4 -4
- package/dist/node.d.ts +1 -1
- package/dist/node.js +6 -6
- package/dist/{window-backend-DIMZN7EZ.js → window-backend-OECETVJ2.js} +2 -2
- package/package.json +9 -9
- package/dist/chunk-6XXXYVXT.js +0 -11741
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Lake, vectorValue, scalarVectorValue, batchFromColumns, withObjectStoreReadControls, throwIfAborted, readControlSignal, readParquetObjectBatches, readIcebergParquetDeletes } from './chunk-
|
|
2
|
-
import { stableStringify } from './chunk-
|
|
3
|
-
import { LakeqlError, jsonSafeValue, matches } from './chunk-
|
|
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
|
+
import { LakeqlError, jsonSafeValue, matches, evaluate } from './chunk-WGHR35QU.js';
|
|
4
4
|
|
|
5
5
|
// ../core/dist/in-memory.js
|
|
6
6
|
var InMemoryRowsScanner = class {
|
|
@@ -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,19 +2040,1762 @@ 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
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// src/sql.ts
|
|
2073
|
+
var textEncoder = new TextEncoder();
|
|
2074
|
+
var sqlTempOrdinal = 0;
|
|
2075
|
+
function createLake(config) {
|
|
2076
|
+
const lake = createParquetLake(config);
|
|
2077
|
+
lake.sql = (sql, options = {}) => querySql(lake, sql, options);
|
|
2078
|
+
return lake;
|
|
2079
|
+
}
|
|
2080
|
+
function querySql(lake, sql, options = {}) {
|
|
2081
|
+
return new SqlQueryResult(() => executeSql(lake, sql, options));
|
|
2082
|
+
}
|
|
2083
|
+
var SqlQueryResult = class {
|
|
2084
|
+
execute;
|
|
2085
|
+
rowsPromise;
|
|
2086
|
+
constructor(execute) {
|
|
2087
|
+
this.execute = execute;
|
|
2088
|
+
}
|
|
2089
|
+
async toArray() {
|
|
2090
|
+
this.rowsPromise ??= this.execute();
|
|
2091
|
+
return await this.rowsPromise;
|
|
2092
|
+
}
|
|
2093
|
+
async first() {
|
|
2094
|
+
return (await this.toArray())[0];
|
|
2095
|
+
}
|
|
2096
|
+
async count() {
|
|
2097
|
+
return (await this.toArray()).length;
|
|
2098
|
+
}
|
|
2099
|
+
async *rows() {
|
|
2100
|
+
for (const row of await this.toArray()) yield row;
|
|
2101
|
+
}
|
|
2102
|
+
async *batches() {
|
|
2103
|
+
const rows = await this.toArray();
|
|
2104
|
+
if (rows.length > 0) yield rows;
|
|
2105
|
+
}
|
|
2106
|
+
streamJson() {
|
|
2107
|
+
return streamText(async () => jsonStringify(await this.toArray()));
|
|
2108
|
+
}
|
|
2109
|
+
streamNdjson() {
|
|
2110
|
+
return streamText(
|
|
2111
|
+
async () => (await this.toArray()).map((row) => jsonStringify(row)).join("\n")
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
streamCsv(options = {}) {
|
|
2115
|
+
return streamText(async () => rowsToCsv(await this.toArray(), options));
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
async function executeSql(lake, sql, options) {
|
|
2119
|
+
const ast = sqlAst(sql, options);
|
|
2120
|
+
validateSourceBindings(options);
|
|
2121
|
+
const bound = bindSources(ast, options);
|
|
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);
|
|
2137
|
+
try {
|
|
2138
|
+
const resolved = await resolveScalarSubqueries(lake, materialized.ast);
|
|
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);
|
|
2146
|
+
return await builderFromAst(lake.path(resolved.source), resolved).toArray();
|
|
2147
|
+
} finally {
|
|
2148
|
+
await materialized.cleanup();
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function sqlAst(sql, options) {
|
|
2152
|
+
const trimmed = sql.trim();
|
|
2153
|
+
const sourceSql = /\bfrom\b/iu.test(trimmed) ? trimmed : addDefaultFrom(trimmed);
|
|
2154
|
+
return options.parameters === void 0 ? parseSql(sourceSql) : parseSql(sourceSql, { parameters: options.parameters });
|
|
2155
|
+
}
|
|
2156
|
+
function bindSources(ast, options) {
|
|
2157
|
+
const tables = options.tables ?? {};
|
|
2158
|
+
const icebergTables = options.icebergTables ?? {};
|
|
2159
|
+
const bindSource = (source) => {
|
|
2160
|
+
if (source === "input" && options.path !== void 0) return options.path;
|
|
2161
|
+
if (icebergTables[source] !== void 0) return source;
|
|
2162
|
+
return tables[source] ?? source;
|
|
2163
|
+
};
|
|
2164
|
+
return mapAstSources(ast, bindSource);
|
|
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
|
+
}
|
|
2177
|
+
function mapAstSources(ast, bindSource, seen = /* @__PURE__ */ new WeakSet()) {
|
|
2178
|
+
const out = { ...ast, source: bindSource(ast.source) };
|
|
2179
|
+
if (seen.has(ast)) {
|
|
2180
|
+
delete out.scalarSubqueries;
|
|
2181
|
+
return out;
|
|
2182
|
+
}
|
|
2183
|
+
seen.add(ast);
|
|
2184
|
+
if (ast.join !== void 0) out.join = { ...ast.join, source: bindSource(ast.join.source) };
|
|
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) {
|
|
2191
|
+
out.subqueryJoin = {
|
|
2192
|
+
...ast.subqueryJoin,
|
|
2193
|
+
source: bindSource(ast.subqueryJoin.source)
|
|
2194
|
+
};
|
|
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
|
+
}
|
|
2204
|
+
if (ast.cte !== void 0) {
|
|
2205
|
+
out.cte = { ...ast.cte, query: mapAstSources(ast.cte.query, bindSource, seen) };
|
|
2206
|
+
}
|
|
2207
|
+
if (ast.scalarSubqueries !== void 0) {
|
|
2208
|
+
out.scalarSubqueries = Object.fromEntries(
|
|
2209
|
+
Object.entries(ast.scalarSubqueries).map(([id, subquery]) => [
|
|
2210
|
+
id,
|
|
2211
|
+
{ ...subquery, query: mapAstSources(subquery.query, bindSource, seen) }
|
|
2212
|
+
])
|
|
2213
|
+
);
|
|
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
|
+
}
|
|
2278
|
+
return out;
|
|
2279
|
+
}
|
|
2280
|
+
function builderFromAst(builder, ast) {
|
|
2281
|
+
let next = builder;
|
|
2282
|
+
for (const [alias, expr] of Object.entries(ast.windows ?? {})) next = next.window(alias, expr);
|
|
2283
|
+
if (ast.select) next = next.select(ast.select);
|
|
2284
|
+
if (ast.projections) next = next.project(ast.projections);
|
|
2285
|
+
if (ast.where) next = next.where(ast.where);
|
|
2286
|
+
if (ast.qualify) next = next.qualify(ast.qualify);
|
|
2287
|
+
if (ast.distinct === true) next = next.distinct();
|
|
2288
|
+
if (ast.orderBy) next = next.orderBy(ast.orderBy);
|
|
2289
|
+
if (ast.offset !== void 0) next = next.offset(ast.offset);
|
|
2290
|
+
if (ast.limit !== void 0) next = next.limit(ast.limit);
|
|
2291
|
+
return next;
|
|
2292
|
+
}
|
|
2293
|
+
function hasAggregation(ast) {
|
|
2294
|
+
return ast.aggregates !== void 0 || ast.groupBy !== void 0 && ast.groupBy.length > 0 || ast.having !== void 0;
|
|
2295
|
+
}
|
|
2296
|
+
function hasWindowSemantics(ast) {
|
|
2297
|
+
return ast.windows !== void 0 && Object.keys(ast.windows).length > 0 || ast.qualify !== void 0;
|
|
2298
|
+
}
|
|
2299
|
+
async function materializeCteIfNeeded(store, lake, ast, options) {
|
|
2300
|
+
if (ast.cte === void 0) return { ast, cleanup: async () => {
|
|
2301
|
+
} };
|
|
2302
|
+
const cteRows = await executeRowsFromAst(lake, ast.cte.query, options);
|
|
2303
|
+
if (cteRows.length === 0) {
|
|
2304
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Empty CTE results are not supported yet");
|
|
2305
|
+
}
|
|
2306
|
+
const prefix = `__lakeql_sql_cte/${safeTempName(ast.cte.name)}/${nextSqlTempId()}`;
|
|
2307
|
+
const written = await writePartitionedParquet(store, prefix, {
|
|
2308
|
+
rows: cteRows,
|
|
2309
|
+
maxRowsPerFile: cteRows.length
|
|
2310
|
+
});
|
|
2311
|
+
const { cte: _cte, ...rest } = ast;
|
|
2312
|
+
const ctePath = `${prefix}/*.parquet`;
|
|
2313
|
+
return {
|
|
2314
|
+
ast: mapAstSources(rest, (source) => source === ast.cte?.name ? ctePath : source),
|
|
2315
|
+
cleanup: async () => {
|
|
2316
|
+
await Promise.all(written.files.map((file) => store.delete(file.path)));
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
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
|
+
}
|
|
2738
|
+
function nextSqlTempId() {
|
|
2739
|
+
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
2740
|
+
if (randomUuid !== void 0) return randomUuid;
|
|
2741
|
+
sqlTempOrdinal += 1;
|
|
2742
|
+
return `${Date.now().toString(36)}-${sqlTempOrdinal.toString(36)}`;
|
|
2743
|
+
}
|
|
2744
|
+
function safeTempName(name) {
|
|
2745
|
+
return name.replaceAll(/[^A-Za-z0-9_-]/gu, "_");
|
|
2746
|
+
}
|
|
2747
|
+
async function resolveScalarSubqueries(lake, ast) {
|
|
2748
|
+
if (ast.scalarSubqueries === void 0) return ast;
|
|
2749
|
+
const values = /* @__PURE__ */ new Map();
|
|
2750
|
+
for (const [id, subquery] of Object.entries(ast.scalarSubqueries)) {
|
|
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
|
+
}
|
|
2756
|
+
if (rows.length > 1) {
|
|
2757
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Scalar subquery returned more than one row");
|
|
2758
|
+
}
|
|
2759
|
+
values.set(id, rows.length === 0 ? null : rows[0]?.[subquery.column] ?? null);
|
|
2760
|
+
}
|
|
2761
|
+
const out = { ...ast };
|
|
2762
|
+
if (ast.where !== void 0) out.where = replaceScalarSubqueryExpr(ast.where, values);
|
|
2763
|
+
if (ast.having !== void 0) out.having = replaceScalarSubqueryExpr(ast.having, values);
|
|
2764
|
+
if (ast.projections !== void 0) {
|
|
2765
|
+
out.projections = Object.fromEntries(
|
|
2766
|
+
Object.entries(ast.projections).map(([alias, expr]) => [
|
|
2767
|
+
alias,
|
|
2768
|
+
replaceScalarSubqueryExpr(expr, values)
|
|
2769
|
+
])
|
|
2770
|
+
);
|
|
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
|
+
}
|
|
2796
|
+
delete out.scalarSubqueries;
|
|
2797
|
+
return out;
|
|
2798
|
+
}
|
|
2799
|
+
function replaceScalarSubqueryExpr(expr, values) {
|
|
2800
|
+
switch (expr.kind) {
|
|
2801
|
+
case "call":
|
|
2802
|
+
if (expr.fn === "__lakeql_scalar_subquery") {
|
|
2803
|
+
const id = expr.args[0];
|
|
2804
|
+
if (id?.kind !== "literal" || typeof id.value !== "string" || !values.has(id.value)) {
|
|
2805
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Invalid scalar subquery placeholder");
|
|
2806
|
+
}
|
|
2807
|
+
return { kind: "literal", value: values.get(id.value) };
|
|
2808
|
+
}
|
|
2809
|
+
return { ...expr, args: expr.args.map((arg) => replaceScalarSubqueryExpr(arg, values)) };
|
|
2810
|
+
case "compare":
|
|
2811
|
+
return {
|
|
2812
|
+
...expr,
|
|
2813
|
+
left: replaceScalarSubqueryExpr(expr.left, values),
|
|
2814
|
+
right: replaceScalarSubqueryExpr(expr.right, values)
|
|
2815
|
+
};
|
|
2816
|
+
case "between":
|
|
2817
|
+
return {
|
|
2818
|
+
...expr,
|
|
2819
|
+
target: replaceScalarSubqueryExpr(expr.target, values),
|
|
2820
|
+
low: replaceScalarSubqueryExpr(expr.low, values),
|
|
2821
|
+
high: replaceScalarSubqueryExpr(expr.high, values)
|
|
2822
|
+
};
|
|
2823
|
+
case "in":
|
|
2824
|
+
return {
|
|
2825
|
+
...expr,
|
|
2826
|
+
target: replaceScalarSubqueryExpr(expr.target, values),
|
|
2827
|
+
values: expr.values.map((value) => replaceScalarSubqueryExpr(value, values))
|
|
2828
|
+
};
|
|
2829
|
+
case "logical":
|
|
2830
|
+
return {
|
|
2831
|
+
...expr,
|
|
2832
|
+
operands: expr.operands.map((operand) => replaceScalarSubqueryExpr(operand, values))
|
|
2833
|
+
};
|
|
2834
|
+
case "not":
|
|
2835
|
+
return { ...expr, operand: replaceScalarSubqueryExpr(expr.operand, values) };
|
|
2836
|
+
case "null-check":
|
|
2837
|
+
return { ...expr, target: replaceScalarSubqueryExpr(expr.target, values) };
|
|
2838
|
+
case "like":
|
|
2839
|
+
return { ...expr, target: replaceScalarSubqueryExpr(expr.target, values) };
|
|
2840
|
+
case "arithmetic":
|
|
2841
|
+
return {
|
|
2842
|
+
...expr,
|
|
2843
|
+
left: replaceScalarSubqueryExpr(expr.left, values),
|
|
2844
|
+
right: replaceScalarSubqueryExpr(expr.right, values)
|
|
2845
|
+
};
|
|
2846
|
+
case "case":
|
|
2847
|
+
return {
|
|
2848
|
+
...expr,
|
|
2849
|
+
whens: expr.whens.map((branch) => ({
|
|
2850
|
+
when: replaceScalarSubqueryExpr(branch.when, values),
|
|
2851
|
+
value: replaceScalarSubqueryExpr(branch.value, values)
|
|
2852
|
+
})),
|
|
2853
|
+
...expr.else === void 0 ? {} : { else: replaceScalarSubqueryExpr(expr.else, values) }
|
|
2854
|
+
};
|
|
2855
|
+
case "column":
|
|
2856
|
+
case "literal":
|
|
2857
|
+
return expr;
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
async function joinRowsFromAst(lake, ast, options) {
|
|
2861
|
+
const joins = sqlJoins(ast);
|
|
2862
|
+
const firstJoin = joins[0];
|
|
2863
|
+
if (firstJoin === void 0) throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL JOIN");
|
|
2864
|
+
if (hasWindowSemantics(ast)) {
|
|
2865
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over JOIN is not supported");
|
|
2866
|
+
}
|
|
2867
|
+
if (hasAggregation(ast)) {
|
|
2868
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Aggregate SQL over JOIN is not supported yet");
|
|
2869
|
+
}
|
|
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
|
+
}
|
|
2934
|
+
}
|
|
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);
|
|
2939
|
+
if (ast.distinct === true) rows = distinctRows(rows);
|
|
2940
|
+
return offsetLimitRows(rows, ast);
|
|
2941
|
+
}
|
|
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;
|
|
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;
|
|
3073
|
+
}
|
|
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
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
return columns;
|
|
3086
|
+
}
|
|
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);
|
|
3093
|
+
}
|
|
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;
|
|
3101
|
+
}
|
|
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;
|
|
3115
|
+
}
|
|
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
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
function stripExprJoinAlias(expr, alias) {
|
|
3138
|
+
switch (expr.kind) {
|
|
3139
|
+
case "column":
|
|
3140
|
+
return { ...expr, name: stripQualifiedColumn(expr.name, alias) };
|
|
3141
|
+
case "compare":
|
|
3142
|
+
return {
|
|
3143
|
+
...expr,
|
|
3144
|
+
left: stripExprJoinAlias(expr.left, alias),
|
|
3145
|
+
right: stripExprJoinAlias(expr.right, alias)
|
|
3146
|
+
};
|
|
3147
|
+
case "between":
|
|
3148
|
+
return {
|
|
3149
|
+
...expr,
|
|
3150
|
+
target: stripExprJoinAlias(expr.target, alias),
|
|
3151
|
+
low: stripExprJoinAlias(expr.low, alias),
|
|
3152
|
+
high: stripExprJoinAlias(expr.high, alias)
|
|
3153
|
+
};
|
|
3154
|
+
case "in":
|
|
3155
|
+
return {
|
|
3156
|
+
...expr,
|
|
3157
|
+
target: stripExprJoinAlias(expr.target, alias),
|
|
3158
|
+
values: expr.values.map((value) => stripExprJoinAlias(value, alias))
|
|
3159
|
+
};
|
|
3160
|
+
case "logical":
|
|
3161
|
+
return {
|
|
3162
|
+
...expr,
|
|
3163
|
+
operands: expr.operands.map((operand) => stripExprJoinAlias(operand, alias))
|
|
3164
|
+
};
|
|
3165
|
+
case "not":
|
|
3166
|
+
return { ...expr, operand: stripExprJoinAlias(expr.operand, alias) };
|
|
3167
|
+
case "null-check":
|
|
3168
|
+
return { ...expr, target: stripExprJoinAlias(expr.target, alias) };
|
|
3169
|
+
case "like":
|
|
3170
|
+
return { ...expr, target: stripExprJoinAlias(expr.target, alias) };
|
|
3171
|
+
case "call":
|
|
3172
|
+
return { ...expr, args: expr.args.map((arg) => stripExprJoinAlias(arg, alias)) };
|
|
3173
|
+
case "arithmetic":
|
|
3174
|
+
return {
|
|
3175
|
+
...expr,
|
|
3176
|
+
left: stripExprJoinAlias(expr.left, alias),
|
|
3177
|
+
right: stripExprJoinAlias(expr.right, alias)
|
|
3178
|
+
};
|
|
3179
|
+
case "case":
|
|
3180
|
+
return {
|
|
3181
|
+
...expr,
|
|
3182
|
+
whens: expr.whens.map((branch) => ({
|
|
3183
|
+
when: stripExprJoinAlias(branch.when, alias),
|
|
3184
|
+
value: stripExprJoinAlias(branch.value, alias)
|
|
3185
|
+
})),
|
|
3186
|
+
...expr.else === void 0 ? {} : { else: stripExprJoinAlias(expr.else, alias) }
|
|
3187
|
+
};
|
|
3188
|
+
case "literal":
|
|
3189
|
+
return expr;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
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;
|
|
3197
|
+
}
|
|
3198
|
+
function collectExprColumns2(expr, columns) {
|
|
3199
|
+
switch (expr.kind) {
|
|
3200
|
+
case "column":
|
|
3201
|
+
columns.add(expr.name);
|
|
3202
|
+
return;
|
|
3203
|
+
case "compare":
|
|
3204
|
+
collectExprColumns2(expr.left, columns);
|
|
3205
|
+
collectExprColumns2(expr.right, columns);
|
|
3206
|
+
return;
|
|
3207
|
+
case "between":
|
|
3208
|
+
collectExprColumns2(expr.target, columns);
|
|
3209
|
+
collectExprColumns2(expr.low, columns);
|
|
3210
|
+
collectExprColumns2(expr.high, columns);
|
|
3211
|
+
return;
|
|
3212
|
+
case "in":
|
|
3213
|
+
collectExprColumns2(expr.target, columns);
|
|
3214
|
+
for (const value of expr.values) collectExprColumns2(value, columns);
|
|
3215
|
+
return;
|
|
3216
|
+
case "logical":
|
|
3217
|
+
for (const operand of expr.operands) collectExprColumns2(operand, columns);
|
|
3218
|
+
return;
|
|
3219
|
+
case "not":
|
|
3220
|
+
collectExprColumns2(expr.operand, columns);
|
|
3221
|
+
return;
|
|
3222
|
+
case "null-check":
|
|
3223
|
+
collectExprColumns2(expr.target, columns);
|
|
3224
|
+
return;
|
|
3225
|
+
case "like":
|
|
3226
|
+
collectExprColumns2(expr.target, columns);
|
|
3227
|
+
return;
|
|
3228
|
+
case "call":
|
|
3229
|
+
for (const arg of expr.args) collectExprColumns2(arg, columns);
|
|
3230
|
+
return;
|
|
3231
|
+
case "arithmetic":
|
|
3232
|
+
collectExprColumns2(expr.left, columns);
|
|
3233
|
+
collectExprColumns2(expr.right, columns);
|
|
3234
|
+
return;
|
|
3235
|
+
case "case":
|
|
3236
|
+
for (const branch of expr.whens) {
|
|
3237
|
+
collectExprColumns2(branch.when, columns);
|
|
3238
|
+
collectExprColumns2(branch.value, columns);
|
|
3239
|
+
}
|
|
3240
|
+
if (expr.else !== void 0) collectExprColumns2(expr.else, columns);
|
|
3241
|
+
return;
|
|
3242
|
+
case "literal":
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
function referencedJoinColumns(ast) {
|
|
3247
|
+
const columns = /* @__PURE__ */ new Set();
|
|
3248
|
+
for (const select of ast.select ?? []) {
|
|
3249
|
+
const { column } = selectColumn(select);
|
|
3250
|
+
columns.add(column);
|
|
3251
|
+
}
|
|
3252
|
+
for (const expr of Object.values(ast.projections ?? {})) collectExprColumns2(expr, columns);
|
|
3253
|
+
if (ast.where !== void 0) collectExprColumns2(ast.where, columns);
|
|
3254
|
+
for (const term of ast.orderBy ?? []) columns.add(term.column);
|
|
3255
|
+
return [...columns].filter((column) => column !== "*");
|
|
3256
|
+
}
|
|
3257
|
+
function leftJoinRightColumns(ast, rightAlias, rightRows) {
|
|
3258
|
+
const columns = /* @__PURE__ */ new Set();
|
|
3259
|
+
if (ast.select?.includes("*") ?? false) {
|
|
3260
|
+
for (const row of rightRows) {
|
|
3261
|
+
for (const column of Object.keys(row)) {
|
|
3262
|
+
if (isQualifiedBy(column, rightAlias))
|
|
3263
|
+
columns.add(stripQualifiedColumn(column, rightAlias));
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
for (const column of referencedJoinColumns(ast)) {
|
|
3268
|
+
if (isQualifiedBy(column, rightAlias)) columns.add(stripQualifiedColumn(column, rightAlias));
|
|
3269
|
+
}
|
|
3270
|
+
return [...columns];
|
|
3271
|
+
}
|
|
3272
|
+
function fillLeftJoinNulls(rows, rightAlias, rightColumns) {
|
|
3273
|
+
if (rightColumns.length === 0) return rows;
|
|
3274
|
+
return rows.map((row) => {
|
|
3275
|
+
let out;
|
|
3276
|
+
for (const column of rightColumns) {
|
|
3277
|
+
const qualified = `${rightAlias}.${column}`;
|
|
3278
|
+
if (qualified in row) continue;
|
|
3279
|
+
out ??= { ...row };
|
|
3280
|
+
out[qualified] = null;
|
|
3281
|
+
}
|
|
3282
|
+
return out ?? row;
|
|
3283
|
+
});
|
|
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
|
+
}
|
|
3312
|
+
async function subqueryJoinRowsFromAst(lake, ast, options) {
|
|
3313
|
+
const joins = sqlSubqueryJoins(ast);
|
|
3314
|
+
if (joins.length === 0) {
|
|
3315
|
+
throw new LakeqlError("LAKEQL_VALIDATION_ERROR", "Missing SQL IN subquery");
|
|
3316
|
+
}
|
|
3317
|
+
if (hasWindowSemantics(ast)) {
|
|
3318
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over IN subquery is not supported");
|
|
3319
|
+
}
|
|
3320
|
+
if (hasAggregation(ast)) {
|
|
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)));
|
|
3354
|
+
}
|
|
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, {
|
|
3411
|
+
leftKey: join.leftKey,
|
|
3412
|
+
rightKey: join.rightKey,
|
|
3413
|
+
type: join.type,
|
|
3414
|
+
maxRightRows: options.joinMaxRightRows ?? 1e5
|
|
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;
|
|
3492
|
+
}
|
|
3493
|
+
function qualifyRow(row, alias) {
|
|
3494
|
+
const out = { ...row };
|
|
3495
|
+
for (const [key, value] of Object.entries(row)) out[`${alias}.${key}`] = value;
|
|
3496
|
+
return out;
|
|
3497
|
+
}
|
|
3498
|
+
function qualifyOnlyRow(row, alias) {
|
|
3499
|
+
return Object.fromEntries(Object.entries(row).map(([key, value]) => [`${alias}.${key}`, value]));
|
|
3500
|
+
}
|
|
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);
|
|
3514
|
+
const hasWildcardProjection = ast.select?.includes("*") ?? false;
|
|
3515
|
+
return rows.map((row) => {
|
|
3516
|
+
if (hasWildcardProjection && ast.projections === void 0) return row;
|
|
3517
|
+
const out = {};
|
|
3518
|
+
if (hasWildcardProjection) {
|
|
3519
|
+
for (const [column, value] of Object.entries(row)) out[column] = value;
|
|
3520
|
+
} else {
|
|
3521
|
+
for (const select of ast.select ?? []) {
|
|
3522
|
+
const { column, alias } = selectColumn(select);
|
|
3523
|
+
out[alias] = row[column];
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
|
|
3527
|
+
out[alias] = evaluate(resolveCorrelatedScalarExpr(expr, row, lookups), row);
|
|
3528
|
+
}
|
|
3529
|
+
return out;
|
|
3530
|
+
});
|
|
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
|
+
}
|
|
3644
|
+
async function aggregateRowsFromAst(builder, ast) {
|
|
3645
|
+
if (hasWindowSemantics(ast)) {
|
|
3646
|
+
throw new LakeqlError("LAKEQL_SQL_UNSUPPORTED", "Window SQL over aggregates is not supported");
|
|
3647
|
+
}
|
|
3648
|
+
validateAggregateProjection(ast);
|
|
3649
|
+
let next = builder;
|
|
3650
|
+
if (ast.where) next = next.where(ast.where);
|
|
3651
|
+
const preProjections = aggregatePreProjections(ast);
|
|
3652
|
+
if (preProjections !== void 0) next = next.select(["*"]).project(preProjections);
|
|
3653
|
+
const aggregates = ast.aggregates ?? {};
|
|
3654
|
+
const hiddenCountAlias = hiddenAggregateAlias(ast);
|
|
3655
|
+
const aggregateSpec = Object.keys(aggregates).length > 0 ? aggregates : { [hiddenCountAlias]: { op: "count" } };
|
|
3656
|
+
let rows = await next.groupBy(ast.groupBy ?? []).aggregate(aggregateSpec, {
|
|
3657
|
+
...ast.having !== void 0 ? { having: ast.having } : {},
|
|
3658
|
+
...ast.orderBy !== void 0 ? { orderBy: ast.orderBy } : {},
|
|
3659
|
+
...ast.distinct === true ? {} : ast.limit !== void 0 ? { limit: ast.limit } : {},
|
|
3660
|
+
...ast.distinct === true ? {} : ast.offset !== void 0 ? { offset: ast.offset } : {}
|
|
3661
|
+
});
|
|
3662
|
+
rows = projectAggregateRows(rows, ast, hiddenCountAlias);
|
|
3663
|
+
if (ast.distinct === true) rows = distinctRows(rows);
|
|
3664
|
+
if (ast.distinct === true) return offsetLimitRows(rows, ast);
|
|
3665
|
+
return rows;
|
|
3666
|
+
}
|
|
3667
|
+
function validateAggregateProjection(ast) {
|
|
3668
|
+
const groupColumns = new Set(ast.groupBy ?? []);
|
|
3669
|
+
for (const select of ast.select ?? []) {
|
|
3670
|
+
const { column } = selectColumn(select);
|
|
3671
|
+
if (column === "*") continue;
|
|
3672
|
+
if (!groupColumns.has(column)) {
|
|
3673
|
+
throw new LakeqlError(
|
|
3674
|
+
"LAKEQL_SQL_UNSUPPORTED",
|
|
3675
|
+
`Aggregate SQL can only select grouped columns or aggregate expressions, not ${column}`
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
function projectAggregateRows(rows, ast, hiddenCountAlias) {
|
|
3681
|
+
const aggregates = Object.entries(ast.aggregates ?? {});
|
|
3682
|
+
const hiddenAggregates = new Set(ast.hiddenAggregates ?? []);
|
|
3683
|
+
const hasWildcardProjection = ast.select?.includes("*") ?? false;
|
|
3684
|
+
return rows.map((row) => {
|
|
3685
|
+
const out = {};
|
|
3686
|
+
if (hasWildcardProjection) {
|
|
3687
|
+
for (const [column, value] of Object.entries(row)) {
|
|
3688
|
+
if (column !== hiddenCountAlias && !hiddenAggregates.has(column)) out[column] = value;
|
|
3689
|
+
}
|
|
3690
|
+
} else if (ast.select !== void 0) {
|
|
3691
|
+
for (const select of ast.select ?? []) {
|
|
3692
|
+
const { column, alias } = selectColumn(select);
|
|
3693
|
+
out[alias] = row[column];
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
for (const [alias] of aggregates) {
|
|
3697
|
+
if (!hiddenAggregates.has(alias)) out[alias] = row[alias];
|
|
3698
|
+
}
|
|
3699
|
+
for (const [alias, expr] of Object.entries(ast.projections ?? {})) {
|
|
3700
|
+
out[alias] = aggregateProjectionValue(alias, expr, row, ast);
|
|
3701
|
+
}
|
|
3702
|
+
return out;
|
|
3703
|
+
});
|
|
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
|
+
}
|
|
3714
|
+
function hiddenAggregateAlias(ast) {
|
|
3715
|
+
const reserved = /* @__PURE__ */ new Set([...ast.select ?? [], ...Object.keys(ast.aggregates ?? {})]);
|
|
3716
|
+
let alias = "__lakeql_group_count";
|
|
3717
|
+
while (reserved.has(alias)) alias = `_${alias}`;
|
|
3718
|
+
return alias;
|
|
3719
|
+
}
|
|
3720
|
+
function selectColumn(select) {
|
|
3721
|
+
const match = /^(.+?)\s+as\s+(.+)$/iu.exec(select);
|
|
3722
|
+
if (match === null) return { column: select, alias: select };
|
|
3723
|
+
const [, column, alias] = match;
|
|
3724
|
+
return { column: column ?? select, alias: alias ?? select };
|
|
3725
|
+
}
|
|
3726
|
+
function distinctRows(rows) {
|
|
3727
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3728
|
+
const out = [];
|
|
3729
|
+
for (const row of rows) {
|
|
3730
|
+
const key = jsonStringify(row);
|
|
3731
|
+
if (seen.has(key)) continue;
|
|
3732
|
+
seen.add(key);
|
|
3733
|
+
out.push(row);
|
|
3734
|
+
}
|
|
3735
|
+
return out;
|
|
3736
|
+
}
|
|
3737
|
+
function sortRows(rows, orderBy, ast) {
|
|
3738
|
+
return [...rows].sort((a, b) => {
|
|
3739
|
+
for (const term of orderBy) {
|
|
3740
|
+
const av = sortValue(a, term.column, ast);
|
|
3741
|
+
const bv = sortValue(b, term.column, ast);
|
|
3742
|
+
if (av === bv) continue;
|
|
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;
|
|
3745
|
+
const direction = term.direction === "desc" ? -1 : 1;
|
|
3746
|
+
return (av < bv ? -1 : 1) * direction;
|
|
3747
|
+
}
|
|
3748
|
+
return 0;
|
|
3749
|
+
});
|
|
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
|
+
}
|
|
3758
|
+
function offsetLimitRows(rows, ast) {
|
|
3759
|
+
const offset = ast.offset ?? 0;
|
|
3760
|
+
if (ast.limit !== void 0) return rows.slice(offset, offset + ast.limit);
|
|
3761
|
+
return offset > 0 ? rows.slice(offset) : rows;
|
|
3762
|
+
}
|
|
3763
|
+
function rowsToCsv(rows, options) {
|
|
3764
|
+
const columns = Object.keys(rows[0] ?? {});
|
|
3765
|
+
const lines = [columns.join(",")];
|
|
3766
|
+
for (const row of rows) {
|
|
3767
|
+
lines.push(columns.map((column) => csvCell(row[column], options)).join(","));
|
|
3768
|
+
}
|
|
3769
|
+
return `${lines.join("\n")}
|
|
3770
|
+
`;
|
|
3771
|
+
}
|
|
3772
|
+
function csvCell(value, options) {
|
|
3773
|
+
if (value === null || value === void 0) return "";
|
|
3774
|
+
const raw = String(value);
|
|
3775
|
+
const text = options.preventFormulae === true && /^[=+\-@]/u.test(raw) ? `'${raw}` : raw;
|
|
3776
|
+
return /[",\n\r]/u.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
3777
|
+
}
|
|
3778
|
+
function jsonStringify(value) {
|
|
3779
|
+
return JSON.stringify(value, (_key, item) => typeof item === "bigint" ? item.toString() : item);
|
|
3780
|
+
}
|
|
3781
|
+
function streamText(read) {
|
|
3782
|
+
let done = false;
|
|
3783
|
+
return new ReadableStream({
|
|
3784
|
+
async pull(controller) {
|
|
3785
|
+
if (done) return;
|
|
3786
|
+
done = true;
|
|
3787
|
+
controller.enqueue(textEncoder.encode(await read()));
|
|
3788
|
+
controller.close();
|
|
3789
|
+
}
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
function addDefaultFrom(sql) {
|
|
3793
|
+
if (!/^\s*select\b/iu.test(sql)) {
|
|
3794
|
+
throw new LakeqlError("LAKEQL_PARSE_ERROR", "SQL must start with SELECT");
|
|
3795
|
+
}
|
|
3796
|
+
const clause = /\b(where|group\s+by|having|order\s+by|limit|offset)\b/iu.exec(sql);
|
|
3797
|
+
if (clause === null || clause.index === void 0) return `${sql} from input`;
|
|
3798
|
+
return `${sql.slice(0, clause.index)}from input ${sql.slice(clause.index)}`;
|
|
3799
|
+
}
|
|
2020
3800
|
|
|
2021
|
-
export { IcebergRestCatalog, IcebergTable, IcebergUnsupportedCatalog, InMemoryRowsScanner, InMemoryRowsStore, ObjectStoreIcebergCommitCatalog, ObjectStoreJsonCache, PACKAGE, applyIcebergDeletes, createInMemoryLake, icebergGlueCatalog, icebergNessieCatalog, icebergRestCatalog, inMemoryRowsScanner, loadIcebergTable, loadIcebergTableFromObjectStore, loadIcebergTableFromRest, loadTable, objectStoreJsonCache, planFiles2 as planFiles, scanBatches, scanPlannedIcebergRows, scanRows, vectorHashJoin };
|
|
3801
|
+
export { IcebergRestCatalog, IcebergTable, IcebergUnsupportedCatalog, InMemoryRowsScanner, InMemoryRowsStore, ObjectStoreIcebergCommitCatalog, ObjectStoreJsonCache, PACKAGE, SqlQueryResult, applyIcebergDeletes, createInMemoryLake, createLake, icebergGlueCatalog, icebergNessieCatalog, icebergRestCatalog, inMemoryRowsScanner, loadIcebergTable, loadIcebergTableFromObjectStore, loadIcebergTableFromRest, loadTable, objectStoreJsonCache, planFiles2 as planFiles, querySql, scanBatches, scanPlannedIcebergRows, scanRows, vectorHashJoin };
|