@malloy-publisher/server 0.0.203 → 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 +17 -0
- package/dist/app/assets/{EnvironmentPage-BVQ7glKP.js → EnvironmentPage-CX06cjOF.js} +1 -1
- package/dist/app/assets/HomePage-CNFt_eUU.js +1 -0
- package/dist/app/assets/{MainPage-bYOWcgDP.js → MainPage-nUJ9YatG.js} +1 -1
- package/dist/app/assets/{PackagePage-N1ZBNJul.js → MaterializationsPage-B5goxVXW.js} +1 -1
- package/dist/app/assets/{ModelPage-DT0gjNy1.js → ModelPage-Ba7Xh4lL.js} +1 -1
- package/dist/app/assets/PackagePage-BaEVdEAG.js +1 -0
- package/dist/app/assets/{RouteError-_J-EBz7W.js → RouteError-BShQjZio.js} +1 -1
- package/dist/app/assets/{WorkbookPage-Bjs9Nm-_.js → WorkbookPage-CBn6ZjJW.js} +1 -1
- package/dist/app/assets/{core-BPLlx5VM.es-C2ARtwWI.js → core-DECXYL4E.es-OaRfXwuQ.js} +1 -1
- package/dist/app/assets/{index-CqUWJELr.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 +415 -152
- 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/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-D9drXoZX.js +0 -1
- package/dist/app/assets/index-BeNwIeYQ.js +0 -454
- package/dist/app/assets/index-Dx7qi2LO.js +0 -1803
- package/dist/app/assets/index.umd-BXm2lnUO.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 });
|
|
@@ -238657,41 +238957,6 @@ import { Manifest } from "@malloydata/malloy";
|
|
|
238657
238957
|
|
|
238658
238958
|
// src/service/materialized_table_gc.ts
|
|
238659
238959
|
init_logger();
|
|
238660
|
-
import {
|
|
238661
|
-
DatabricksDialect,
|
|
238662
|
-
DuckDBDialect,
|
|
238663
|
-
MySQLDialect,
|
|
238664
|
-
PostgresDialect,
|
|
238665
|
-
SnowflakeDialect,
|
|
238666
|
-
StandardSQLDialect,
|
|
238667
|
-
TrinoDialect
|
|
238668
|
-
} from "@malloydata/malloy";
|
|
238669
|
-
|
|
238670
|
-
// src/service/quoting.ts
|
|
238671
|
-
function quoteTablePath(path10, dialect) {
|
|
238672
|
-
return path10.split(".").map((seg) => dialect.quoteTablePath(seg)).join(".");
|
|
238673
|
-
}
|
|
238674
|
-
function splitTablePath(tableName) {
|
|
238675
|
-
const lastDot = tableName.lastIndexOf(".");
|
|
238676
|
-
if (lastDot >= 0) {
|
|
238677
|
-
return {
|
|
238678
|
-
schemaPrefix: tableName.substring(0, lastDot + 1),
|
|
238679
|
-
bareName: tableName.substring(lastDot + 1)
|
|
238680
|
-
};
|
|
238681
|
-
}
|
|
238682
|
-
return { schemaPrefix: "", bareName: tableName };
|
|
238683
|
-
}
|
|
238684
|
-
|
|
238685
|
-
// src/service/materialized_table_gc.ts
|
|
238686
|
-
var DIALECTS = Object.freeze({
|
|
238687
|
-
duckdb: new DuckDBDialect,
|
|
238688
|
-
standardsql: new StandardSQLDialect,
|
|
238689
|
-
trino: new TrinoDialect,
|
|
238690
|
-
postgres: new PostgresDialect,
|
|
238691
|
-
snowflake: new SnowflakeDialect,
|
|
238692
|
-
mysql: new MySQLDialect,
|
|
238693
|
-
databricks: new DatabricksDialect
|
|
238694
|
-
});
|
|
238695
238960
|
function liveTableKey(connectionName, tableName) {
|
|
238696
238961
|
return `${connectionName}::${tableName}`;
|
|
238697
238962
|
}
|
|
@@ -238737,17 +239002,6 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238737
239002
|
}
|
|
238738
239003
|
};
|
|
238739
239004
|
}
|
|
238740
|
-
const dialect = DIALECTS[connection.dialectName];
|
|
238741
|
-
if (!dialect) {
|
|
238742
|
-
return {
|
|
238743
|
-
error: {
|
|
238744
|
-
buildId: entry.buildId,
|
|
238745
|
-
tableName: entry.tableName,
|
|
238746
|
-
connectionName: entry.connectionName,
|
|
238747
|
-
error: `No dialect registered for '${connection.dialectName}'`
|
|
238748
|
-
}
|
|
238749
|
-
};
|
|
238750
|
-
}
|
|
238751
239005
|
if (ctx.dryRun) {
|
|
238752
239006
|
return {
|
|
238753
239007
|
dropped: {
|
|
@@ -238759,7 +239013,6 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238759
239013
|
}
|
|
238760
239014
|
};
|
|
238761
239015
|
}
|
|
238762
|
-
const quoted = (p) => quoteTablePath(p, dialect);
|
|
238763
239016
|
try {
|
|
238764
239017
|
await ctx.manifestService.deleteEntry(ctx.environmentId, entry.id);
|
|
238765
239018
|
} catch (err) {
|
|
@@ -238786,7 +239039,7 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238786
239039
|
});
|
|
238787
239040
|
} else {
|
|
238788
239041
|
try {
|
|
238789
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239042
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${entry.tableName}`);
|
|
238790
239043
|
} catch (err) {
|
|
238791
239044
|
logger.warn("GC: deleted manifest row but failed to drop materialized table (orphaned)", {
|
|
238792
239045
|
tableName: entry.tableName,
|
|
@@ -238796,7 +239049,7 @@ async function processOneEntry(entry, ctx, liveTables) {
|
|
|
238796
239049
|
}
|
|
238797
239050
|
}
|
|
238798
239051
|
try {
|
|
238799
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239052
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
238800
239053
|
} catch (err) {
|
|
238801
239054
|
logger.warn("GC: failed to drop staging table (best-effort)", {
|
|
238802
239055
|
stagingTableName,
|
|
@@ -238828,6 +239081,18 @@ async function dropManifestEntries(entries, ctx) {
|
|
|
238828
239081
|
return { dropped, errors: errors2 };
|
|
238829
239082
|
}
|
|
238830
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
|
+
|
|
238831
239096
|
// src/service/materialization_service.ts
|
|
238832
239097
|
var STAGING_BUILD_ID_LEN = 12;
|
|
238833
239098
|
function stagingSuffix(buildId) {
|
|
@@ -238853,9 +239118,9 @@ async function resolvePackageConnections(pkg, names) {
|
|
|
238853
239118
|
function manifestTableKey(connectionName, tableName) {
|
|
238854
239119
|
return `${connectionName}::${tableName}`;
|
|
238855
239120
|
}
|
|
238856
|
-
async function tablePhysicallyExists(connection,
|
|
239121
|
+
async function tablePhysicallyExists(connection, tableName) {
|
|
238857
239122
|
try {
|
|
238858
|
-
await connection.runSQL(`SELECT 1 FROM ${
|
|
239123
|
+
await connection.runSQL(`SELECT 1 FROM ${tableName} WHERE 1=0`);
|
|
238859
239124
|
return true;
|
|
238860
239125
|
} catch {
|
|
238861
239126
|
return false;
|
|
@@ -239110,7 +239375,7 @@ class MaterializationService {
|
|
|
239110
239375
|
importBaseURL
|
|
239111
239376
|
});
|
|
239112
239377
|
const malloyModel = await modelMaterializer.getModel();
|
|
239113
|
-
const modelTag = malloyModel.
|
|
239378
|
+
const modelTag = malloyModel.annotations.parseAsTag("!").tag;
|
|
239114
239379
|
if (!modelTag.has("experimental", "persistence")) {
|
|
239115
239380
|
logger.debug("Model has no ##! experimental.persistence tag, skipping", { modelPath });
|
|
239116
239381
|
continue;
|
|
@@ -239140,7 +239405,7 @@ class MaterializationService {
|
|
|
239140
239405
|
});
|
|
239141
239406
|
const tableOwners = new Map;
|
|
239142
239407
|
for (const [sourceID, source] of Object.entries(allSources)) {
|
|
239143
|
-
const tableName = source.
|
|
239408
|
+
const tableName = source.annotations.parseAsTag("@").tag.text("name") || source.name;
|
|
239144
239409
|
const key = `${source.connectionName}::${tableName}`;
|
|
239145
239410
|
const existing = tableOwners.get(key);
|
|
239146
239411
|
if (existing) {
|
|
@@ -239179,14 +239444,12 @@ class MaterializationService {
|
|
|
239179
239444
|
connectionDigests
|
|
239180
239445
|
});
|
|
239181
239446
|
const connectionName = persistSource.connectionName;
|
|
239182
|
-
const tableName = persistSource.
|
|
239183
|
-
const {
|
|
239184
|
-
const stagingTableName = `${
|
|
239185
|
-
const dialect = persistSource.dialect;
|
|
239186
|
-
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)}`;
|
|
239187
239450
|
const tableKey = manifestTableKey(connectionName, tableName);
|
|
239188
239451
|
if (!knownMaterializedTables.has(tableKey)) {
|
|
239189
|
-
if (await tablePhysicallyExists(connection,
|
|
239452
|
+
if (await tablePhysicallyExists(connection, tableName)) {
|
|
239190
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.`);
|
|
239191
239454
|
}
|
|
239192
239455
|
}
|
|
@@ -239195,14 +239458,14 @@ class MaterializationService {
|
|
|
239195
239458
|
connectionName
|
|
239196
239459
|
});
|
|
239197
239460
|
const startTime = performance.now();
|
|
239198
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239461
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
239199
239462
|
try {
|
|
239200
|
-
await connection.runSQL(`CREATE TABLE ${
|
|
239201
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239202
|
-
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}`);
|
|
239203
239466
|
} catch (err) {
|
|
239204
239467
|
try {
|
|
239205
|
-
await connection.runSQL(`DROP TABLE IF EXISTS ${
|
|
239468
|
+
await connection.runSQL(`DROP TABLE IF EXISTS ${stagingTableName}`);
|
|
239206
239469
|
} catch (cleanupErr) {
|
|
239207
239470
|
logger.warn("Build: failed to clean up staging table after a failed rebuild; physical leak", {
|
|
239208
239471
|
stagingTableName,
|