@malloy-publisher/server 0.0.202 → 0.0.204
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/app/api-doc.yaml +25 -3
- package/dist/app/assets/{EnvironmentPage-CNQYDaxR.js → EnvironmentPage-CX06cjOF.js} +1 -1
- package/dist/app/assets/HomePage-CNFt_eUU.js +1 -0
- package/dist/app/assets/{MainPage-B0kNpkxT.js → MainPage-nUJ9YatG.js} +1 -1
- package/dist/app/assets/{PackagePage-yAh0TrOV.js → MaterializationsPage-B5goxVXW.js} +1 -1
- package/dist/app/assets/{ModelPage-DcVElc9L.js → ModelPage-Ba7Xh4lL.js} +1 -1
- package/dist/app/assets/PackagePage-BaEVdEAG.js +1 -0
- package/dist/app/assets/{RouteError-DknUbx_s.js → RouteError-BShQjZio.js} +1 -1
- package/dist/app/assets/{WorkbookPage-CCqc8otA.js → WorkbookPage-CBn6ZjJW.js} +1 -1
- package/dist/app/assets/{core-B3A61KGJ.es-iOUZ6RJL.js → core-DECXYL4E.es-OaRfXwuQ.js} +1 -1
- package/dist/app/assets/{index-W0bOLKGl.js → index-BLfPC1gy.js} +2 -2
- package/dist/app/assets/index-DqiJ0bWp.js +455 -0
- package/dist/app/assets/index-Dy3YhAZQ.js +1812 -0
- package/dist/app/assets/index.umd-DAN9K8yC.js +2469 -0
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +392 -67
- package/dist/server.mjs +418 -153
- package/package.json +11 -11
- package/src/ducklake_version.spec.ts +43 -0
- package/src/ducklake_version.ts +26 -0
- package/src/errors.ts +18 -1
- package/src/package_load/package_load_pool.ts +0 -5
- package/src/package_load/package_load_worker.ts +41 -99
- package/src/package_load/protocol.ts +1 -7
- package/src/service/annotations.spec.ts +118 -0
- package/src/service/annotations.ts +91 -0
- package/src/service/authorize.spec.ts +132 -0
- package/src/service/authorize.ts +241 -0
- package/src/service/authorize_integration.spec.ts +838 -0
- package/src/service/connection.ts +1 -1
- package/src/service/environment.ts +4 -4
- package/src/service/environment_store.ts +14 -2
- package/src/service/filter.spec.ts +14 -3
- package/src/service/filter.ts +5 -1
- package/src/service/filter_bypass.spec.ts +418 -0
- package/src/service/given.ts +37 -12
- package/src/service/givens_integration.spec.ts +34 -7
- package/src/service/materialization_service.ts +25 -20
- package/src/service/materialized_table_gc.spec.ts +6 -5
- package/src/service/materialized_table_gc.ts +2 -50
- package/src/service/model.spec.ts +203 -8
- package/src/service/model.ts +305 -155
- package/src/service/package_worker_path.spec.ts +113 -0
- package/src/service/quoting.ts +0 -20
- package/src/service/restricted_mode.spec.ts +299 -0
- package/src/service/source_extraction.ts +226 -0
- package/src/storage/StorageManager.ts +73 -0
- package/dist/app/assets/HomePage-DBFTIoD8.js +0 -1
- package/dist/app/assets/index-F_o127LC.js +0 -454
- package/dist/app/assets/index-QeX_e740.js +0 -1803
- package/dist/app/assets/index.umd-CEDRw4TK.js +0 -1145
package/dist/server.mjs
CHANGED
|
@@ -147328,6 +147328,8 @@ function internalErrorToHttpError(error) {
|
|
|
147328
147328
|
return httpError(400, error.message);
|
|
147329
147329
|
} else if (error instanceof FrozenConfigError) {
|
|
147330
147330
|
return httpError(403, error.message);
|
|
147331
|
+
} else if (error instanceof AccessDeniedError) {
|
|
147332
|
+
return httpError(403, error.message);
|
|
147331
147333
|
} else if (error instanceof EnvironmentNotFoundError) {
|
|
147332
147334
|
return httpError(404, error.message);
|
|
147333
147335
|
} else if (error instanceof PackageNotFoundError) {
|
|
@@ -147369,7 +147371,7 @@ function httpError(code, message) {
|
|
|
147369
147371
|
}
|
|
147370
147372
|
};
|
|
147371
147373
|
}
|
|
147372
|
-
var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, ModelCompilationError, FrozenConfigError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
|
|
147374
|
+
var NotImplementedError, BadRequestError, EnvironmentNotFoundError, PackageNotFoundError, ModelNotFoundError, ConnectionNotFoundError, ConnectionError, ConnectionAuthError, ModelCompilationError, FrozenConfigError, AccessDeniedError, MaterializationNotFoundError, MaterializationConflictError, InvalidStateTransitionError, ServiceUnavailableError, PayloadTooLargeError, QueryTimeoutError;
|
|
147373
147375
|
var init_errors = __esm(() => {
|
|
147374
147376
|
init_constants();
|
|
147375
147377
|
NotImplementedError = class NotImplementedError extends Error {
|
|
@@ -147422,6 +147424,12 @@ var init_errors = __esm(() => {
|
|
|
147422
147424
|
super(message);
|
|
147423
147425
|
}
|
|
147424
147426
|
};
|
|
147427
|
+
AccessDeniedError = class AccessDeniedError extends Error {
|
|
147428
|
+
constructor(message) {
|
|
147429
|
+
super(message);
|
|
147430
|
+
this.name = "AccessDeniedError";
|
|
147431
|
+
}
|
|
147432
|
+
};
|
|
147425
147433
|
MaterializationNotFoundError = class MaterializationNotFoundError extends Error {
|
|
147426
147434
|
constructor(message) {
|
|
147427
147435
|
super(message);
|
|
@@ -199146,9 +199154,6 @@ function buildFetchOptions(options) {
|
|
|
199146
199154
|
if (options.refreshTimestamp !== undefined) {
|
|
199147
199155
|
out.refreshTimestamp = options.refreshTimestamp;
|
|
199148
199156
|
}
|
|
199149
|
-
if (options.modelAnnotation !== undefined) {
|
|
199150
|
-
out.modelAnnotation = options.modelAnnotation;
|
|
199151
|
-
}
|
|
199152
199157
|
return out;
|
|
199153
199158
|
}
|
|
199154
199159
|
function adaptResult(result) {
|
|
@@ -221830,7 +221835,7 @@ function buildEnvironmentMalloyConfig(connections = [], environmentPath = "", is
|
|
|
221830
221835
|
...azureDuckDBCache.values()
|
|
221831
221836
|
];
|
|
221832
221837
|
const closeResults = await Promise.allSettled([
|
|
221833
|
-
malloyConfig.
|
|
221838
|
+
malloyConfig.shutdown("close"),
|
|
221834
221839
|
...wrapperPromises.map(async (promise) => {
|
|
221835
221840
|
const connection = await promise;
|
|
221836
221841
|
await connection.close();
|
|
@@ -229646,10 +229651,23 @@ function registerHealthEndpoints(app) {
|
|
|
229646
229651
|
// src/service/environment_store.ts
|
|
229647
229652
|
init_logger();
|
|
229648
229653
|
|
|
229654
|
+
// src/storage/StorageManager.ts
|
|
229655
|
+
import * as crypto3 from "crypto";
|
|
229656
|
+
|
|
229657
|
+
// src/ducklake_version.ts
|
|
229658
|
+
var SUPPORTED_CATALOG_VERSIONS = [
|
|
229659
|
+
"0.1",
|
|
229660
|
+
"0.2",
|
|
229661
|
+
"0.3-dev1",
|
|
229662
|
+
"0.3"
|
|
229663
|
+
];
|
|
229664
|
+
function isCatalogVersionSupported(version) {
|
|
229665
|
+
return SUPPORTED_CATALOG_VERSIONS.includes(version);
|
|
229666
|
+
}
|
|
229667
|
+
|
|
229649
229668
|
// src/storage/StorageManager.ts
|
|
229650
229669
|
init_errors();
|
|
229651
229670
|
init_logger();
|
|
229652
|
-
import * as crypto3 from "crypto";
|
|
229653
229671
|
|
|
229654
229672
|
// src/storage/duckdb/DuckDBConnection.ts
|
|
229655
229673
|
import duckdb from "duckdb";
|
|
@@ -230673,6 +230691,30 @@ function catalogNameForConfig(c) {
|
|
|
230673
230691
|
const hash = crypto3.createHash("sha256").update(configKey(c)).digest("hex").slice(0, 8);
|
|
230674
230692
|
return `manifest_lake_${hash}`;
|
|
230675
230693
|
}
|
|
230694
|
+
async function readDuckLakeCatalogVersion(connection, catalogUrl, catalogName) {
|
|
230695
|
+
if (!catalogUrl.startsWith("postgres:")) {
|
|
230696
|
+
return;
|
|
230697
|
+
}
|
|
230698
|
+
const pgConnString = catalogUrl.slice("postgres:".length);
|
|
230699
|
+
const tempDb = `${catalogName}_preflight`;
|
|
230700
|
+
const escaped = escapeSQL2(pgConnString);
|
|
230701
|
+
try {
|
|
230702
|
+
await connection.run(`ATTACH '${escaped}' AS ${tempDb} (TYPE postgres, READ_ONLY);`);
|
|
230703
|
+
const rows = await connection.all(`SELECT value FROM ${tempDb}.ducklake_metadata WHERE key = 'version' LIMIT 1;`);
|
|
230704
|
+
const value = rows[0]?.value;
|
|
230705
|
+
return typeof value === "string" ? value : undefined;
|
|
230706
|
+
} catch (error) {
|
|
230707
|
+
logger.warn("DuckLake catalog version preflight failed; falling back to ATTACH", {
|
|
230708
|
+
catalogName,
|
|
230709
|
+
error: redactPgSecrets(error instanceof Error ? error.message : String(error))
|
|
230710
|
+
});
|
|
230711
|
+
return;
|
|
230712
|
+
} finally {
|
|
230713
|
+
try {
|
|
230714
|
+
await connection.run(`DETACH ${tempDb};`);
|
|
230715
|
+
} catch {}
|
|
230716
|
+
}
|
|
230717
|
+
}
|
|
230676
230718
|
|
|
230677
230719
|
class StorageManager {
|
|
230678
230720
|
connection = null;
|
|
@@ -230751,6 +230793,13 @@ class StorageManager {
|
|
|
230751
230793
|
if (isCloudStorage) {
|
|
230752
230794
|
await connection.run("INSTALL httpfs; LOAD httpfs;");
|
|
230753
230795
|
}
|
|
230796
|
+
if (isPostgres) {
|
|
230797
|
+
const catalogVersion = await readDuckLakeCatalogVersion(connection, catalogUrl, catalogName);
|
|
230798
|
+
if (catalogVersion && !isCatalogVersionSupported(catalogVersion)) {
|
|
230799
|
+
const supportedMax = SUPPORTED_CATALOG_VERSIONS[SUPPORTED_CATALOG_VERSIONS.length - 1];
|
|
230800
|
+
throw new ConnectionAuthError(`DuckLake catalog version ${catalogVersion} is newer than this Publisher's extension supports (max ${supportedMax}). Upgrade the Publisher image or downgrade the catalog.`);
|
|
230801
|
+
}
|
|
230802
|
+
}
|
|
230754
230803
|
let attachCmd = `ATTACH 'ducklake:${escapedCatalogUrl}' AS ${catalogName}`;
|
|
230755
230804
|
const attachOpts = [
|
|
230756
230805
|
`DATA_PATH '${escapedDataPath}'`,
|
|
@@ -230861,9 +230910,9 @@ import {
|
|
|
230861
230910
|
init_package_load_pool();
|
|
230862
230911
|
var import_api4 = __toESM(require_src(), 1);
|
|
230863
230912
|
import {
|
|
230913
|
+
Annotations as Annotations2,
|
|
230864
230914
|
API,
|
|
230865
230915
|
FixedConnectionMap,
|
|
230866
|
-
isSourceDef,
|
|
230867
230916
|
MalloyConfig as MalloyConfig2,
|
|
230868
230917
|
MalloyError as MalloyError2,
|
|
230869
230918
|
modelDefToModelInfo,
|
|
@@ -231081,7 +231130,8 @@ function injectFilterRefinement(query, filterClause) {
|
|
|
231081
231130
|
if (!filterClause) {
|
|
231082
231131
|
return query;
|
|
231083
231132
|
}
|
|
231084
|
-
return `${query.trimEnd()}
|
|
231133
|
+
return `${query.trimEnd()}
|
|
231134
|
+
+ {where: ${filterClause}}`;
|
|
231085
231135
|
}
|
|
231086
231136
|
|
|
231087
231137
|
class FilterValidationError extends Error {
|
|
@@ -231120,6 +231170,149 @@ function tokenize(input) {
|
|
|
231120
231170
|
return tokens;
|
|
231121
231171
|
}
|
|
231122
231172
|
|
|
231173
|
+
// src/service/authorize.ts
|
|
231174
|
+
init_errors();
|
|
231175
|
+
var SOURCE_PREFIX = "#(authorize)";
|
|
231176
|
+
var FILE_PREFIX = "##(authorize)";
|
|
231177
|
+
function buildAuthorizeProbe(exprs) {
|
|
231178
|
+
const selects = exprs.map((expr, i) => `__auth_${i} is (${expr})`).join(`
|
|
231179
|
+
`);
|
|
231180
|
+
return `run: duckdb.sql("SELECT 1 AS __authorize_probe_row") -> {
|
|
231181
|
+
select:
|
|
231182
|
+
${selects}
|
|
231183
|
+
limit: 1
|
|
231184
|
+
}`;
|
|
231185
|
+
}
|
|
231186
|
+
function isProbeTrue(cell) {
|
|
231187
|
+
return cell === true || cell === 1 || cell === "true";
|
|
231188
|
+
}
|
|
231189
|
+
async function evaluateAuthorize(executor, exprs, givens) {
|
|
231190
|
+
for (const expr of exprs) {
|
|
231191
|
+
try {
|
|
231192
|
+
const result = await executor.loadQuery(buildAuthorizeProbe([expr])).run({ rowLimit: 1, givens });
|
|
231193
|
+
const row = result?.data?.value?.[0];
|
|
231194
|
+
if (row && isProbeTrue(row.__auth_0)) {
|
|
231195
|
+
return true;
|
|
231196
|
+
}
|
|
231197
|
+
} catch {
|
|
231198
|
+
continue;
|
|
231199
|
+
}
|
|
231200
|
+
}
|
|
231201
|
+
return false;
|
|
231202
|
+
}
|
|
231203
|
+
async function validateAuthorizeProbes(compiler, sources) {
|
|
231204
|
+
for (const source of sources) {
|
|
231205
|
+
const exprs = source.authorize;
|
|
231206
|
+
if (!exprs || exprs.length === 0)
|
|
231207
|
+
continue;
|
|
231208
|
+
try {
|
|
231209
|
+
await compiler.loadQuery(buildAuthorizeProbe(exprs)).getPreparedQuery();
|
|
231210
|
+
} catch (err) {
|
|
231211
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
231212
|
+
throw new ModelCompilationError({
|
|
231213
|
+
message: `Invalid #(authorize) annotation on source "${source.name ?? "(unnamed)"}" [${exprs.join(" | ")}]: ${detail}`
|
|
231214
|
+
});
|
|
231215
|
+
}
|
|
231216
|
+
}
|
|
231217
|
+
}
|
|
231218
|
+
function parseAuthorizeAnnotation(annotation) {
|
|
231219
|
+
const trimmed2 = annotation.trim();
|
|
231220
|
+
let body;
|
|
231221
|
+
if (trimmed2.startsWith(FILE_PREFIX)) {
|
|
231222
|
+
body = trimmed2.slice(FILE_PREFIX.length).trim();
|
|
231223
|
+
} else if (trimmed2.startsWith(SOURCE_PREFIX)) {
|
|
231224
|
+
body = trimmed2.slice(SOURCE_PREFIX.length).trim();
|
|
231225
|
+
} else {
|
|
231226
|
+
return null;
|
|
231227
|
+
}
|
|
231228
|
+
return unwrapQuotedExpression(body);
|
|
231229
|
+
}
|
|
231230
|
+
function collectAuthorizeExprs(annotations) {
|
|
231231
|
+
const exprs = [];
|
|
231232
|
+
for (const annotation of annotations) {
|
|
231233
|
+
const expr = parseAuthorizeAnnotation(annotation);
|
|
231234
|
+
if (expr !== null) {
|
|
231235
|
+
exprs.push(expr);
|
|
231236
|
+
}
|
|
231237
|
+
}
|
|
231238
|
+
return exprs;
|
|
231239
|
+
}
|
|
231240
|
+
function unwrapQuotedExpression(body) {
|
|
231241
|
+
if (body.length < 2 || body[0] !== '"') {
|
|
231242
|
+
throw new Error(`authorize annotation expression must be a double-quoted string, got: ${body || "(empty)"}`);
|
|
231243
|
+
}
|
|
231244
|
+
let expr = "";
|
|
231245
|
+
let i = 1;
|
|
231246
|
+
let closed = false;
|
|
231247
|
+
for (;i < body.length; i++) {
|
|
231248
|
+
const ch = body[i];
|
|
231249
|
+
if (ch === "\\" && i + 1 < body.length) {
|
|
231250
|
+
const next = body[i + 1];
|
|
231251
|
+
if (next === '"' || next === "\\") {
|
|
231252
|
+
expr += next;
|
|
231253
|
+
i++;
|
|
231254
|
+
continue;
|
|
231255
|
+
}
|
|
231256
|
+
}
|
|
231257
|
+
if (ch === '"') {
|
|
231258
|
+
closed = true;
|
|
231259
|
+
i++;
|
|
231260
|
+
break;
|
|
231261
|
+
}
|
|
231262
|
+
expr += ch;
|
|
231263
|
+
}
|
|
231264
|
+
if (!closed) {
|
|
231265
|
+
throw new Error(`authorize annotation has mismatched quotes: ${body}`);
|
|
231266
|
+
}
|
|
231267
|
+
const rest = body.slice(i).trim();
|
|
231268
|
+
if (rest.length > 0) {
|
|
231269
|
+
throw new Error(`authorize annotation has unexpected content after the expression: ${rest}`);
|
|
231270
|
+
}
|
|
231271
|
+
if (expr.trim().length === 0) {
|
|
231272
|
+
throw new Error("authorize annotation has an empty expression body");
|
|
231273
|
+
}
|
|
231274
|
+
return expr;
|
|
231275
|
+
}
|
|
231276
|
+
|
|
231277
|
+
// src/service/annotations.ts
|
|
231278
|
+
import { Annotations } from "@malloydata/malloy";
|
|
231279
|
+
function isReservedRoute(route) {
|
|
231280
|
+
return route === "" || !/[\p{L}\p{N}]/u.test(route);
|
|
231281
|
+
}
|
|
231282
|
+
function modelAnnotations(modelDef) {
|
|
231283
|
+
const registry = modelDef.modelAnnotations ?? {};
|
|
231284
|
+
const visited = new Set;
|
|
231285
|
+
const order = [];
|
|
231286
|
+
const visit = (id) => {
|
|
231287
|
+
if (visited.has(id))
|
|
231288
|
+
return;
|
|
231289
|
+
visited.add(id);
|
|
231290
|
+
const entry = registry[id];
|
|
231291
|
+
if (!entry)
|
|
231292
|
+
return;
|
|
231293
|
+
for (const dep of entry.inheritsFrom)
|
|
231294
|
+
visit(dep);
|
|
231295
|
+
order.push(id);
|
|
231296
|
+
};
|
|
231297
|
+
visit(modelDef.modelID);
|
|
231298
|
+
let folded;
|
|
231299
|
+
for (const id of order) {
|
|
231300
|
+
const own = registry[id].ownNotes;
|
|
231301
|
+
if (!own.notes?.length && !own.blockNotes?.length)
|
|
231302
|
+
continue;
|
|
231303
|
+
folded = {
|
|
231304
|
+
notes: own.notes,
|
|
231305
|
+
blockNotes: own.blockNotes,
|
|
231306
|
+
inherits: folded
|
|
231307
|
+
};
|
|
231308
|
+
}
|
|
231309
|
+
return folded ?? {};
|
|
231310
|
+
}
|
|
231311
|
+
function annotationTexts(annote) {
|
|
231312
|
+
const texts = new Annotations(annote).texts();
|
|
231313
|
+
return texts.length > 0 ? texts : undefined;
|
|
231314
|
+
}
|
|
231315
|
+
|
|
231123
231316
|
// src/service/given.ts
|
|
231124
231317
|
function malloyGivenToApi(given) {
|
|
231125
231318
|
const type = given.type;
|
|
@@ -231127,10 +231320,89 @@ function malloyGivenToApi(given) {
|
|
|
231127
231320
|
return {
|
|
231128
231321
|
name: given.name,
|
|
231129
231322
|
type: renderedType,
|
|
231130
|
-
annotations: given.
|
|
231323
|
+
annotations: given.annotations.forRoute(undefined).filter((note) => !isReservedRoute(note.route)).map((note) => note.text),
|
|
231324
|
+
default: given._internal?.defaultText
|
|
231131
231325
|
};
|
|
231132
231326
|
}
|
|
231133
231327
|
|
|
231328
|
+
// src/service/source_extraction.ts
|
|
231329
|
+
import {
|
|
231330
|
+
isSourceDef
|
|
231331
|
+
} from "@malloydata/malloy";
|
|
231332
|
+
function extractSourcesFromModelDef(modelDef, givens, onParseError) {
|
|
231333
|
+
const filterMap = new Map;
|
|
231334
|
+
const authorizeMap = new Map;
|
|
231335
|
+
const fileLevelAuthorize = collectAuthorizeExprs((modelAnnotations(modelDef).notes ?? []).map((note) => note.text));
|
|
231336
|
+
const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef(obj)).map((sourceObj) => {
|
|
231337
|
+
const struct = sourceObj;
|
|
231338
|
+
const sourceName = struct.as || struct.name;
|
|
231339
|
+
const annotations = annotationTexts(struct.annotations);
|
|
231340
|
+
const collected = [];
|
|
231341
|
+
let cur = struct.annotations;
|
|
231342
|
+
while (cur) {
|
|
231343
|
+
if (cur.blockNotes) {
|
|
231344
|
+
collected.push(cur.blockNotes.map((note) => note.text));
|
|
231345
|
+
}
|
|
231346
|
+
cur = cur.inherits;
|
|
231347
|
+
}
|
|
231348
|
+
const allAnnotations = collected.reverse().flat();
|
|
231349
|
+
let filters;
|
|
231350
|
+
if (allAnnotations.length > 0) {
|
|
231351
|
+
try {
|
|
231352
|
+
const parsed = parseFilters(allAnnotations);
|
|
231353
|
+
if (parsed.length > 0) {
|
|
231354
|
+
filterMap.set(sourceName, parsed);
|
|
231355
|
+
const fields = struct.fields;
|
|
231356
|
+
filters = parsed.map((f) => {
|
|
231357
|
+
const field = fields.find((fd) => (fd.as || fd.name) === f.dimension);
|
|
231358
|
+
return {
|
|
231359
|
+
name: f.name,
|
|
231360
|
+
dimension: f.dimension,
|
|
231361
|
+
type: f.type,
|
|
231362
|
+
implicit: f.implicit,
|
|
231363
|
+
required: f.required,
|
|
231364
|
+
dimensionType: field?.type
|
|
231365
|
+
};
|
|
231366
|
+
});
|
|
231367
|
+
}
|
|
231368
|
+
} catch (err) {
|
|
231369
|
+
onParseError?.(sourceName, err);
|
|
231370
|
+
}
|
|
231371
|
+
}
|
|
231372
|
+
const ownNotes = (struct.annotations?.blockNotes ?? []).map((note) => note.text);
|
|
231373
|
+
const effective = [
|
|
231374
|
+
...fileLevelAuthorize,
|
|
231375
|
+
...collectAuthorizeExprs(ownNotes)
|
|
231376
|
+
];
|
|
231377
|
+
let authorize;
|
|
231378
|
+
if (effective.length > 0) {
|
|
231379
|
+
authorizeMap.set(sourceName, effective);
|
|
231380
|
+
authorize = effective;
|
|
231381
|
+
}
|
|
231382
|
+
const views = struct.fields.filter((field) => field.type === "turtle").filter((turtle) => turtle.pipeline.map((stage) => stage.type).every((type) => type === "reduce")).map((turtle) => ({
|
|
231383
|
+
name: turtle.as || turtle.name,
|
|
231384
|
+
annotations: annotationTexts(turtle.annotations)
|
|
231385
|
+
}));
|
|
231386
|
+
return {
|
|
231387
|
+
name: sourceName,
|
|
231388
|
+
annotations,
|
|
231389
|
+
views,
|
|
231390
|
+
filters,
|
|
231391
|
+
givens,
|
|
231392
|
+
authorize
|
|
231393
|
+
};
|
|
231394
|
+
});
|
|
231395
|
+
return { sources, filterMap, authorizeMap };
|
|
231396
|
+
}
|
|
231397
|
+
function extractQueriesFromModelDef(modelDef) {
|
|
231398
|
+
const isNamedQuery = (obj) => obj.type === "query";
|
|
231399
|
+
return Object.values(modelDef.contents).filter(isNamedQuery).map((queryObj) => ({
|
|
231400
|
+
name: queryObj.as || queryObj.name,
|
|
231401
|
+
sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
|
|
231402
|
+
annotations: annotationTexts(queryObj.annotations)
|
|
231403
|
+
}));
|
|
231404
|
+
}
|
|
231405
|
+
|
|
231134
231406
|
// src/service/model_limits.ts
|
|
231135
231407
|
init_errors();
|
|
231136
231408
|
function resolveModelQueryRowLimit(userLimit, { defaultLimit, maxRows }) {
|
|
@@ -231168,6 +231440,7 @@ class Model {
|
|
|
231168
231440
|
compilationError;
|
|
231169
231441
|
filterMap;
|
|
231170
231442
|
givens;
|
|
231443
|
+
fileLevelAuthorize = [];
|
|
231171
231444
|
meter = import_api4.metrics.getMeter("publisher");
|
|
231172
231445
|
queryExecutionHistogram = this.meter.createHistogram("malloy_model_query_duration", {
|
|
231173
231446
|
description: "How long it takes to execute a Malloy model query",
|
|
@@ -231187,6 +231460,11 @@ class Model {
|
|
|
231187
231460
|
this.compilationError = compilationError;
|
|
231188
231461
|
this.filterMap = filterMap ?? new Map;
|
|
231189
231462
|
this.givens = givens;
|
|
231463
|
+
try {
|
|
231464
|
+
this.fileLevelAuthorize = this.modelDef ? collectAuthorizeExprs((modelAnnotations(this.modelDef).notes ?? []).map((note) => note.text)) : [];
|
|
231465
|
+
} catch {
|
|
231466
|
+
this.fileLevelAuthorize = [];
|
|
231467
|
+
}
|
|
231190
231468
|
this.modelInfo = modelInfo ?? (this.modelDef ? modelDefToModelInfo(this.modelDef) : undefined);
|
|
231191
231469
|
if (this.filterMap.size > 0) {
|
|
231192
231470
|
logger.warn(`Model "${packageName}/${modelPath}" uses deprecated #(filter) annotations. Migrate to given: — see https://github.com/malloydata/publisher/blob/main/docs/givens.md`, {
|
|
@@ -231199,12 +231477,78 @@ class Model {
|
|
|
231199
231477
|
getFilters(sourceName) {
|
|
231200
231478
|
return this.filterMap.get(sourceName) ?? [];
|
|
231201
231479
|
}
|
|
231480
|
+
getAuthorize(sourceName) {
|
|
231481
|
+
return this.sources?.find((source) => source.name === sourceName)?.authorize ?? [];
|
|
231482
|
+
}
|
|
231483
|
+
effectiveAuthorizeFor(sourceName) {
|
|
231484
|
+
if (sourceName && this.sources?.some((s) => s.name === sourceName)) {
|
|
231485
|
+
return this.getAuthorize(sourceName);
|
|
231486
|
+
}
|
|
231487
|
+
return this.fileLevelAuthorize;
|
|
231488
|
+
}
|
|
231489
|
+
async assertAuthorized(sourceName, givens) {
|
|
231490
|
+
const exprs = this.effectiveAuthorizeFor(sourceName);
|
|
231491
|
+
if (exprs.length === 0)
|
|
231492
|
+
return;
|
|
231493
|
+
const label = sourceName ?? "(query)";
|
|
231494
|
+
const deny = () => {
|
|
231495
|
+
throw new AccessDeniedError(`Access denied for source "${label}".`);
|
|
231496
|
+
};
|
|
231497
|
+
if (!this.modelMaterializer)
|
|
231498
|
+
deny();
|
|
231499
|
+
let passed = false;
|
|
231500
|
+
try {
|
|
231501
|
+
passed = await evaluateAuthorize(this.modelMaterializer, exprs, givens);
|
|
231502
|
+
} catch (err) {
|
|
231503
|
+
logger.debug("Authorize probe failed; denying", {
|
|
231504
|
+
sourceName: label,
|
|
231505
|
+
modelPath: this.modelPath,
|
|
231506
|
+
error: err instanceof Error ? err.message : String(err)
|
|
231507
|
+
});
|
|
231508
|
+
deny();
|
|
231509
|
+
}
|
|
231510
|
+
if (!passed)
|
|
231511
|
+
deny();
|
|
231512
|
+
}
|
|
231513
|
+
async resolveAuthorizeSourceFromRunnable(runnable) {
|
|
231514
|
+
try {
|
|
231515
|
+
const prepared = await runnable.getPreparedQuery();
|
|
231516
|
+
const structRef = prepared._query?.structRef;
|
|
231517
|
+
if (typeof structRef === "string")
|
|
231518
|
+
return structRef;
|
|
231519
|
+
if (structRef && typeof structRef === "object") {
|
|
231520
|
+
const s = structRef;
|
|
231521
|
+
return s.as || s.name;
|
|
231522
|
+
}
|
|
231523
|
+
} catch {}
|
|
231524
|
+
return;
|
|
231525
|
+
}
|
|
231202
231526
|
extractSourceName(query) {
|
|
231203
231527
|
if (!query)
|
|
231204
231528
|
return;
|
|
231205
|
-
const runMatch = query.match(/run\s*:\s*(\w+)\s*->/);
|
|
231206
|
-
const arrowMatch = query.match(/^\s*(\w+)\s*->/m);
|
|
231207
|
-
return runMatch?.[1] ?? arrowMatch?.[1];
|
|
231529
|
+
const runMatch = query.match(/run\s*:\s*(?:`([^`]+)`|(\w+))\s*->/);
|
|
231530
|
+
const arrowMatch = query.match(/^\s*(?:`([^`]+)`|(\w+))\s*->/m);
|
|
231531
|
+
return runMatch?.[1] ?? runMatch?.[2] ?? arrowMatch?.[1] ?? arrowMatch?.[2];
|
|
231532
|
+
}
|
|
231533
|
+
resolveFilterSource(query) {
|
|
231534
|
+
const target = this.extractSourceName(query);
|
|
231535
|
+
if (!target || !query)
|
|
231536
|
+
return;
|
|
231537
|
+
const aliasOf = new Map;
|
|
231538
|
+
const declRe = /source\s*:\s*(\w+)\s+is\s+(\w+)/g;
|
|
231539
|
+
let match;
|
|
231540
|
+
while ((match = declRe.exec(query)) !== null) {
|
|
231541
|
+
aliasOf.set(match[1], match[2]);
|
|
231542
|
+
}
|
|
231543
|
+
let current = target;
|
|
231544
|
+
const seen = new Set;
|
|
231545
|
+
while (current && !seen.has(current)) {
|
|
231546
|
+
if (this.filterMap.has(current))
|
|
231547
|
+
return current;
|
|
231548
|
+
seen.add(current);
|
|
231549
|
+
current = aliasOf.get(current);
|
|
231550
|
+
}
|
|
231551
|
+
return;
|
|
231208
231552
|
}
|
|
231209
231553
|
static async create(packageName, packagePath, modelPath, malloyConfig, options) {
|
|
231210
231554
|
const { runtime, modelURL, importBaseURL, dataStyles, modelType } = await Model.getModelRuntime(packagePath, modelPath, malloyConfig, options);
|
|
@@ -231221,10 +231565,11 @@ class Model {
|
|
|
231221
231565
|
modelDef = compiledModel._modelDef;
|
|
231222
231566
|
const malloyGivens = Array.from(compiledModel.givens.values());
|
|
231223
231567
|
givens = malloyGivens.length > 0 ? malloyGivens.map(malloyGivenToApi) : undefined;
|
|
231224
|
-
const sourceResult = Model.getSources(
|
|
231568
|
+
const sourceResult = Model.getSources(modelDef, givens);
|
|
231225
231569
|
sources = sourceResult.sources;
|
|
231226
231570
|
filterMap = sourceResult.filterMap;
|
|
231227
|
-
queries = Model.getQueries(
|
|
231571
|
+
queries = Model.getQueries(modelDef);
|
|
231572
|
+
await validateAuthorizeProbes(modelMaterializer, sources ?? []);
|
|
231228
231573
|
const imports = modelDef.imports || [];
|
|
231229
231574
|
const importedSourceNames = new Set;
|
|
231230
231575
|
for (const importLocation of imports) {
|
|
@@ -231340,6 +231685,10 @@ class Model {
|
|
|
231340
231685
|
let runnable;
|
|
231341
231686
|
if (!this.modelMaterializer || !this.modelDef || !this.modelInfo)
|
|
231342
231687
|
throw new BadRequestError("Model has no queryable entities.");
|
|
231688
|
+
const earlySource = sourceName || (queryName ? this.queries?.find((q) => q.name === queryName)?.sourceName : undefined) || this.extractSourceName(query);
|
|
231689
|
+
if (earlySource) {
|
|
231690
|
+
await this.assertAuthorized(earlySource, givens ?? {});
|
|
231691
|
+
}
|
|
231343
231692
|
try {
|
|
231344
231693
|
let queryString;
|
|
231345
231694
|
if (!sourceName && !queryName && query) {
|
|
@@ -231360,8 +231709,9 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231360
231709
|
});
|
|
231361
231710
|
throw new BadRequestError("Invalid query request. (Query AND !sourceName) OR (queryName AND sourceName) must be defined.");
|
|
231362
231711
|
}
|
|
231712
|
+
const isAdHocQuery = !sourceName && !queryName && !!query;
|
|
231363
231713
|
if (!bypassFilters) {
|
|
231364
|
-
const effectiveSource =
|
|
231714
|
+
const effectiveSource = isAdHocQuery ? this.resolveFilterSource(query) : sourceName;
|
|
231365
231715
|
if (effectiveSource) {
|
|
231366
231716
|
const filters = this.getFilters(effectiveSource);
|
|
231367
231717
|
if (filters.length > 0) {
|
|
@@ -231370,7 +231720,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231370
231720
|
}
|
|
231371
231721
|
}
|
|
231372
231722
|
}
|
|
231373
|
-
runnable = this.modelMaterializer.
|
|
231723
|
+
runnable = this.modelMaterializer.loadRestrictedQuery(queryString);
|
|
231374
231724
|
} catch (error) {
|
|
231375
231725
|
if (error instanceof BadRequestError) {
|
|
231376
231726
|
throw error;
|
|
@@ -231393,6 +231743,10 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231393
231743
|
});
|
|
231394
231744
|
throw new BadRequestError(`Invalid query: ${errorMessage}`);
|
|
231395
231745
|
}
|
|
231746
|
+
const compiledSource = await this.resolveAuthorizeSourceFromRunnable(runnable);
|
|
231747
|
+
if (!(compiledSource && compiledSource === earlySource)) {
|
|
231748
|
+
await this.assertAuthorized(compiledSource, givens ?? {});
|
|
231749
|
+
}
|
|
231396
231750
|
const maxRows = getMaxQueryRows();
|
|
231397
231751
|
const maxBytes = getMaxResponseBytes();
|
|
231398
231752
|
const rowLimit = resolveModelQueryRowLimit((await runnable.getPreparedResult({ givens })).resultExplore.limit, { defaultLimit: getDefaultQueryRowLimit(), maxRows });
|
|
@@ -231461,25 +231815,19 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231461
231815
|
givens: this.givens
|
|
231462
231816
|
};
|
|
231463
231817
|
}
|
|
231818
|
+
serializeNewSources(newSources) {
|
|
231819
|
+
return newSources?.map((source) => JSON.stringify(this.givens && this.givens.length > 0 ? { ...source, givens: this.givens } : source));
|
|
231820
|
+
}
|
|
231464
231821
|
async getNotebookModel() {
|
|
231465
231822
|
const notebookCells = this.runnableNotebookCells.map((cell) => {
|
|
231466
231823
|
return {
|
|
231467
231824
|
type: cell.type,
|
|
231468
231825
|
text: cell.text,
|
|
231469
|
-
newSources: cell.newSources
|
|
231826
|
+
newSources: this.serializeNewSources(cell.newSources),
|
|
231470
231827
|
queryInfo: cell.queryInfo ? JSON.stringify(cell.queryInfo) : undefined
|
|
231471
231828
|
};
|
|
231472
231829
|
});
|
|
231473
|
-
const allAnnotations = [];
|
|
231474
|
-
if (this.modelDef) {
|
|
231475
|
-
let currentAnnotation = this.modelDef.annotation;
|
|
231476
|
-
while (currentAnnotation) {
|
|
231477
|
-
if (currentAnnotation.notes) {
|
|
231478
|
-
allAnnotations.push(...currentAnnotation.notes.map((note) => note.text));
|
|
231479
|
-
}
|
|
231480
|
-
currentAnnotation = currentAnnotation.inherits;
|
|
231481
|
-
}
|
|
231482
|
-
}
|
|
231830
|
+
const allAnnotations = this.modelDef ? new Annotations2(modelAnnotations(this.modelDef)).texts() : [];
|
|
231483
231831
|
return {
|
|
231484
231832
|
type: "notebook",
|
|
231485
231833
|
packageName: this.packageName,
|
|
@@ -231509,6 +231857,10 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231509
231857
|
text: cell.text
|
|
231510
231858
|
};
|
|
231511
231859
|
}
|
|
231860
|
+
if (cell.runnable) {
|
|
231861
|
+
const authorizeSource = await this.resolveAuthorizeSourceFromRunnable(cell.runnable);
|
|
231862
|
+
await this.assertAuthorized(authorizeSource, givens ?? {});
|
|
231863
|
+
}
|
|
231512
231864
|
let queryName = undefined;
|
|
231513
231865
|
let queryResult = undefined;
|
|
231514
231866
|
if (cell.runnable) {
|
|
@@ -231571,7 +231923,7 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231571
231923
|
text: cell.text,
|
|
231572
231924
|
queryName,
|
|
231573
231925
|
result: queryResult,
|
|
231574
|
-
newSources: cell.newSources
|
|
231926
|
+
newSources: this.serializeNewSources(cell.newSources)
|
|
231575
231927
|
};
|
|
231576
231928
|
}
|
|
231577
231929
|
static async getModelRuntime(packagePath, modelPath, malloyConfig, options) {
|
|
@@ -231611,63 +231963,11 @@ run: ${sourceName ? sourceName + "->" : ""}${queryName}`;
|
|
|
231611
231963
|
malloyConfig.wrapConnections(() => new FixedConnectionMap(input, "duckdb"));
|
|
231612
231964
|
return malloyConfig;
|
|
231613
231965
|
}
|
|
231614
|
-
static getQueries(
|
|
231615
|
-
|
|
231616
|
-
return Object.values(modelDef.contents).filter(isNamedQuery).map((queryObj) => ({
|
|
231617
|
-
name: queryObj.as || queryObj.name,
|
|
231618
|
-
sourceName: typeof queryObj.structRef === "string" ? queryObj.structRef : undefined,
|
|
231619
|
-
annotations: queryObj?.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text)
|
|
231620
|
-
}));
|
|
231966
|
+
static getQueries(modelDef) {
|
|
231967
|
+
return extractQueriesFromModelDef(modelDef);
|
|
231621
231968
|
}
|
|
231622
|
-
static getSources(
|
|
231623
|
-
const filterMap =
|
|
231624
|
-
const sources = Object.values(modelDef.contents).filter((obj) => isSourceDef(obj)).map((sourceObj) => {
|
|
231625
|
-
const sourceName = sourceObj.as || sourceObj.name;
|
|
231626
|
-
const annotations = sourceObj.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text);
|
|
231627
|
-
const collectedAnnotations = [];
|
|
231628
|
-
let curAnnotation = sourceObj.annotation;
|
|
231629
|
-
while (curAnnotation) {
|
|
231630
|
-
if (curAnnotation.blockNotes) {
|
|
231631
|
-
collectedAnnotations.push(curAnnotation.blockNotes.map((note) => note.text));
|
|
231632
|
-
}
|
|
231633
|
-
curAnnotation = curAnnotation.inherits;
|
|
231634
|
-
}
|
|
231635
|
-
const allAnnotations = collectedAnnotations.reverse().flat();
|
|
231636
|
-
let filters;
|
|
231637
|
-
if (allAnnotations.length > 0) {
|
|
231638
|
-
try {
|
|
231639
|
-
const parsed = parseFilters(allAnnotations);
|
|
231640
|
-
if (parsed.length > 0) {
|
|
231641
|
-
filterMap.set(sourceName, parsed);
|
|
231642
|
-
const structFields = sourceObj.fields;
|
|
231643
|
-
filters = parsed.map((f) => {
|
|
231644
|
-
const field = structFields.find((fd) => (fd.as || fd.name) === f.dimension);
|
|
231645
|
-
return {
|
|
231646
|
-
name: f.name,
|
|
231647
|
-
dimension: f.dimension,
|
|
231648
|
-
type: f.type,
|
|
231649
|
-
implicit: f.implicit,
|
|
231650
|
-
required: f.required,
|
|
231651
|
-
dimensionType: field?.type
|
|
231652
|
-
};
|
|
231653
|
-
});
|
|
231654
|
-
}
|
|
231655
|
-
} catch (err) {
|
|
231656
|
-
logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err });
|
|
231657
|
-
}
|
|
231658
|
-
}
|
|
231659
|
-
const views = sourceObj.fields.filter((turtleObj) => turtleObj.type === "turtle").filter((turtleObj) => turtleObj.pipeline.map((stage) => stage.type).every((type) => type == "reduce")).map((turtleObj) => ({
|
|
231660
|
-
name: turtleObj.as || turtleObj.name,
|
|
231661
|
-
annotations: turtleObj?.annotation?.blockNotes?.filter((note) => note.at.url.includes(modelPath)).map((note) => note.text)
|
|
231662
|
-
}));
|
|
231663
|
-
return {
|
|
231664
|
-
name: sourceName,
|
|
231665
|
-
annotations,
|
|
231666
|
-
views,
|
|
231667
|
-
filters,
|
|
231668
|
-
givens
|
|
231669
|
-
};
|
|
231670
|
-
});
|
|
231969
|
+
static getSources(modelDef, givens) {
|
|
231970
|
+
const { sources, filterMap } = extractSourcesFromModelDef(modelDef, givens, (sourceName, err) => logger.warn(`Failed to parse filter annotations on source "${sourceName}"`, { error: err }));
|
|
231671
231971
|
return { sources, filterMap };
|
|
231672
231972
|
}
|
|
231673
231973
|
static async getModelMaterializer(runtime, importBaseURL, modelURL, modelPath) {
|
|
@@ -232470,7 +232770,7 @@ ${source}` : source;
|
|
|
232470
232770
|
const packagePath = safeJoinUnderRoot(this.environmentPath, packageName);
|
|
232471
232771
|
const _package = await Package.create(this.environmentName, packageName, packagePath, () => this.malloyConfig.malloyConfig);
|
|
232472
232772
|
if (existingPackage !== undefined && reload) {
|
|
232473
|
-
this.retireConnectionGeneration(`package ${packageName}`, () => existingPackage.getMalloyConfig().
|
|
232773
|
+
this.retireConnectionGeneration(`package ${packageName}`, () => existingPackage.getMalloyConfig().shutdown("close"));
|
|
232474
232774
|
}
|
|
232475
232775
|
this.packages.set(packageName, _package);
|
|
232476
232776
|
this.setPackageStatus(packageName, "serving" /* SERVING */);
|
|
@@ -232592,7 +232892,7 @@ ${source}` : source;
|
|
|
232592
232892
|
this.packages.set(packageName, newPackage);
|
|
232593
232893
|
this.setPackageStatus(packageName, "serving" /* SERVING */);
|
|
232594
232894
|
if (oldPackage) {
|
|
232595
|
-
this.retireConnectionGeneration(`package ${packageName}`, () => oldPackage.getMalloyConfig().
|
|
232895
|
+
this.retireConnectionGeneration(`package ${packageName}`, () => oldPackage.getMalloyConfig().shutdown("close"));
|
|
232596
232896
|
}
|
|
232597
232897
|
if (retiredPath) {
|
|
232598
232898
|
const pathToClean = retiredPath;
|
|
@@ -232708,7 +233008,7 @@ ${source}` : source;
|
|
|
232708
233008
|
} else if (packageStatus?.status === "serving" /* SERVING */) {
|
|
232709
233009
|
this.setPackageStatus(packageName, "unloading" /* UNLOADING */);
|
|
232710
233010
|
}
|
|
232711
|
-
this.retireConnectionGeneration(`package ${packageName}`, () => _package.getMalloyConfig().
|
|
233011
|
+
this.retireConnectionGeneration(`package ${packageName}`, () => _package.getMalloyConfig().shutdown("close"));
|
|
232712
233012
|
const canonicalPath = safeJoinUnderRoot(this.environmentPath, packageName);
|
|
232713
233013
|
const retiredPath = this.allocateRetiredPath(packageName);
|
|
232714
233014
|
let renamed = false;
|
|
@@ -232761,7 +233061,7 @@ ${source}` : source;
|
|
|
232761
233061
|
}
|
|
232762
233062
|
}
|
|
232763
233063
|
async closeAllConnections() {
|
|
232764
|
-
const packageReleases = await Promise.allSettled(Array.from(this.packages.values(), (pkg) => pkg.getMalloyConfig().
|
|
233064
|
+
const packageReleases = await Promise.allSettled(Array.from(this.packages.values(), (pkg) => pkg.getMalloyConfig().shutdown("close")));
|
|
232765
233065
|
for (const result of packageReleases) {
|
|
232766
233066
|
if (result.status === "rejected") {
|
|
232767
233067
|
logger.error(`Error closing package connections for environment ${this.environmentName}`, { error: result.reason });
|
|
@@ -233188,12 +233488,14 @@ class EnvironmentStore {
|
|
|
233188
233488
|
return Promise.all(Array.from(this.environments.values()).map((environment) => environment.serialize()));
|
|
233189
233489
|
}
|
|
233190
233490
|
async getStatus() {
|
|
233491
|
+
const baseState = getOperationalState();
|
|
233492
|
+
const operationalState2 = baseState !== "draining" && (this.memoryGovernor?.isBackpressured() ?? false) ? "throttled" : baseState;
|
|
233191
233493
|
const status = {
|
|
233192
233494
|
timestamp: Date.now(),
|
|
233193
233495
|
environments: [],
|
|
233194
233496
|
initialized: this.isInitialized,
|
|
233195
233497
|
frozenConfig: isPublisherConfigFrozen(this.serverRootPath),
|
|
233196
|
-
operationalState:
|
|
233498
|
+
operationalState: operationalState2
|
|
233197
233499
|
};
|
|
233198
233500
|
const environments = await this.listEnvironments(true);
|
|
233199
233501
|
await Promise.all(environments.map(async (environment) => {
|
|
@@ -238655,41 +238957,6 @@ import { Manifest } from "@malloydata/malloy";
|
|
|
238655
238957
|
|
|
238656
238958
|
// src/service/materialized_table_gc.ts
|
|
238657
238959
|
init_logger();
|
|
238658
|
-
import {
|
|
238659
|
-
DatabricksDialect,
|
|
238660
|
-
DuckDBDialect,
|
|
238661
|
-
MySQLDialect,
|
|
238662
|
-
PostgresDialect,
|
|
238663
|
-
SnowflakeDialect,
|
|
238664
|
-
StandardSQLDialect,
|
|
238665
|
-
TrinoDialect
|
|
238666
|
-
} from "@malloydata/malloy";
|
|
238667
|
-
|
|
238668
|
-
// src/service/quoting.ts
|
|
238669
|
-
function quoteTablePath(path10, dialect) {
|
|
238670
|
-
return path10.split(".").map((seg) => dialect.quoteTablePath(seg)).join(".");
|
|
238671
|
-
}
|
|
238672
|
-
function splitTablePath(tableName) {
|
|
238673
|
-
const lastDot = tableName.lastIndexOf(".");
|
|
238674
|
-
if (lastDot >= 0) {
|
|
238675
|
-
return {
|
|
238676
|
-
schemaPrefix: tableName.substring(0, lastDot + 1),
|
|
238677
|
-
bareName: tableName.substring(lastDot + 1)
|
|
238678
|
-
};
|
|
238679
|
-
}
|
|
238680
|
-
return { schemaPrefix: "", bareName: tableName };
|
|
238681
|
-
}
|
|
238682
|
-
|
|
238683
|
-
// src/service/materialized_table_gc.ts
|
|
238684
|
-
var DIALECTS = Object.freeze({
|
|
238685
|
-
duckdb: new DuckDBDialect,
|
|
238686
|
-
standardsql: new StandardSQLDialect,
|
|
238687
|
-
trino: new TrinoDialect,
|
|
238688
|
-
postgres: new PostgresDialect,
|
|
238689
|
-
snowflake: new SnowflakeDialect,
|
|
238690
|
-
mysql: new MySQLDialect,
|
|
238691
|
-
databricks: new DatabricksDialect
|
|
238692
|
-
});
|
|
238693
238960
|
function liveTableKey(connectionName, tableName) {
|
|
238694
238961
|
return `${connectionName}::${tableName}`;
|
|
238695
238962
|
}
|
|
@@ -238735,17 +239002,6 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238735
239002
|
}
|
|
238736
239003
|
};
|
|
238737
239004
|
}
|
|
238738
|
-
const dialect = DIALECTS[connection.dialectName];
|
|
238739
|
-
if (!dialect) {
|
|
238740
|
-
return {
|
|
238741
|
-
error: {
|
|
238742
|
-
buildId: entry.buildId,
|
|
238743
|
-
tableName: entry.tableName,
|
|
238744
|
-
connectionName: entry.connectionName,
|
|
238745
|
-
error: `No dialect registered for '${connection.dialectName}'`
|
|
238746
|
-
}
|
|
238747
|
-
};
|
|
238748
|
-
}
|
|
238749
239005
|
if (ctx.dryRun) {
|
|
238750
239006
|
return {
|
|
238751
239007
|
dropped: {
|
|
@@ -238757,7 +239013,6 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238757
239013
|
}
|
|
238758
239014
|
};
|
|
238759
239015
|
}
|
|
238760
|
-
const quoted = (p) => quoteTablePath(p, dialect);
|
|
238761
239016
|
try {
|
|
238762
239017
|
await ctx.manifestService.deleteEntry(ctx.environmentId, entry.id);
|
|
238763
239018
|
} catch (err) {
|
|
@@ -238784,7 +239039,7 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238784
239039
|
});
|
|
238785
239040
|
} else {
|
|
238786
239041
|
try {
|
|
238787
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239042
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${entry.tableName}`);
|
|
238788
239043
|
} catch (err) {
|
|
238789
239044
|
logger.warn("GC: deleted manifest row but failed to drop materialized table (orphaned)", {
|
|
238790
239045
|
tableName: entry.tableName,
|
|
@@ -238794,7 +239049,7 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238794
239049
|
}
|
|
238795
239050
|
}
|
|
238796
239051
|
try {
|
|
238797
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239052
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
238798
239053
|
} catch (err) {
|
|
238799
239054
|
logger.warn("GC: failed to drop staging table (best-effort)", {
|
|
238800
239055
|
stagingTableName,
|
|
@@ -238826,6 +239081,18 @@ async function dropManifestEntries(entries, ctx) {
|
|
|
238826
239081
|
return { dropped, errors: errors2 };
|
|
238827
239082
|
}
|
|
238828
239083
|
|
|
239084
|
+
// src/service/quoting.ts
|
|
239085
|
+
function splitTablePath(tableName) {
|
|
239086
|
+
const lastDot = tableName.lastIndexOf(".");
|
|
239087
|
+
if (lastDot >= 0) {
|
|
239088
|
+
return {
|
|
239089
|
+
schemaPrefix: tableName.substring(0, lastDot + 1),
|
|
239090
|
+
bareName: tableName.substring(lastDot + 1)
|
|
239091
|
+
};
|
|
239092
|
+
}
|
|
239093
|
+
return { schemaPrefix: "", bareName: tableName };
|
|
239094
|
+
}
|
|
239095
|
+
|
|
238829
239096
|
// src/service/materialization_service.ts
|
|
238830
239097
|
var STAGING_BUILD_ID_LEN = 12;
|
|
238831
239098
|
function stagingSuffix(buildId) {
|
|
@@ -238851,9 +239118,9 @@ async function resolvePackageConnections(pkg, names) {
|
|
|
238851
239118
|
function manifestTableKey(connectionName, tableName) {
|
|
238852
239119
|
return `${connectionName}::${tableName}`;
|
|
238853
239120
|
}
|
|
238854
|
-
async function tablePhysicallyExists(connection,
|
|
239121
|
+
async function tablePhysicallyExists(connection, tableName) {
|
|
238855
239122
|
try {
|
|
238856
|
-
await connection.runSQL(`SELECT 1 FROM ${
|
|
239123
|
+
await connection.runSQL(`SELECT 1 FROM ${tableName} WHERE 1=0`);
|
|
238857
239124
|
return true;
|
|
238858
239125
|
} catch {
|
|
238859
239126
|
return false;
|
|
@@ -239108,7 +239375,7 @@ class MaterializationService {
|
|
|
239108
239375
|
importBaseURL
|
|
239109
239376
|
});
|
|
239110
239377
|
const malloyModel = await modelMaterializer.getModel();
|
|
239111
|
-
const modelTag = malloyModel.
|
|
239378
|
+
const modelTag = malloyModel.annotations.parseAsTag("!").tag;
|
|
239112
239379
|
if (!modelTag.has("experimental", "persistence")) {
|
|
239113
239380
|
logger.debug("Model has no ##! experimental.persistence tag, skipping", { modelPath });
|
|
239114
239381
|
continue;
|
|
@@ -239138,7 +239405,7 @@ class MaterializationService {
|
|
|
239138
239405
|
});
|
|
239139
239406
|
const tableOwners = new Map;
|
|
239140
239407
|
for (const [sourceID, source] of Object.entries(allSources)) {
|
|
239141
|
-
const tableName = source.
|
|
239408
|
+
const tableName = source.annotations.parseAsTag("@").tag.text("name") || source.name;
|
|
239142
239409
|
const key = `${source.connectionName}::${tableName}`;
|
|
239143
239410
|
const existing = tableOwners.get(key);
|
|
239144
239411
|
if (existing) {
|
|
@@ -239177,14 +239444,12 @@ class MaterializationService {
|
|
|
239177
239444
|
connectionDigests
|
|
239178
239445
|
});
|
|
239179
239446
|
const connectionName = persistSource.connectionName;
|
|
239180
|
-
const tableName = persistSource.
|
|
239181
|
-
const {
|
|
239182
|
-
const stagingTableName = `${
|
|
239183
|
-
const dialect = persistSource.dialect;
|
|
239184
|
-
const quoted = (p) => quoteTablePath(p, dialect);
|
|
239447
|
+
const tableName = persistSource.annotations.parseAsTag("@").tag.text("name") || persistSource.name;
|
|
239448
|
+
const { bareName } = splitTablePath(tableName);
|
|
239449
|
+
const stagingTableName = `${tableName}${stagingSuffix(buildId)}`;
|
|
239185
239450
|
const tableKey = manifestTableKey(connectionName, tableName);
|
|
239186
239451
|
if (!knownMaterializedTables.has(tableKey)) {
|
|
239187
|
-
if (await tablePhysicallyExists(connection,
|
|
239452
|
+
if (await tablePhysicallyExists(connection, tableName)) {
|
|
239188
239453
|
throw new BadRequestError(`Refusing to materialize source '${persistSource.name}': ` + `target table '${tableName}' already exists on connection ` + `'${connectionName}' but was not created by a previous ` + `materialization build. Use '#@ persist name=...' to ` + `choose a different table name, or drop the existing ` + `table manually if it is no longer needed.`);
|
|
239189
239454
|
}
|
|
239190
239455
|
}
|
|
@@ -239193,14 +239458,14 @@ class MaterializationService {
|
|
|
239193
239458
|
connectionName
|
|
239194
239459
|
});
|
|
239195
239460
|
const startTime = performance.now();
|
|
239196
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239461
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
239197
239462
|
try {
|
|
239198
|
-
await connection.runSQL(`CREATE TABLE ${
|
|
239199
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239200
|
-
await connection.runSQL(`ALTER TABLE ${
|
|
239463
|
+
await connection.runSQL(`CREATE TABLE ${stagingTableName} AS (${buildSQL})`);
|
|
239464
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${tableName}`);
|
|
239465
|
+
await connection.runSQL(`ALTER TABLE ${stagingTableName} RENAME TO ${bareName}`);
|
|
239201
239466
|
} catch (err) {
|
|
239202
239467
|
try {
|
|
239203
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239468
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
239204
239469
|
} catch (cleanupErr) {
|
|
239205
239470
|
logger.warn("Build: failed to clean up staging table after a failed rebuild; physical leak", {
|
|
239206
239471
|
stagingTableName,
|