@malloy-publisher/server 0.0.235 → 0.0.237
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 +83 -27
- package/dist/app/assets/{EnvironmentPage-BsAnavYN.js → EnvironmentPage-DaP6CpgK.js} +1 -1
- package/dist/app/assets/{HomePage-CADE138j.js → HomePage-C9yNw3iu.js} +1 -1
- package/dist/app/assets/{LightMode-Cfh7KzN8.js → LightMode-Be4-G-dX.js} +1 -1
- package/dist/app/assets/{MainPage-CO3pRlnV.js → MainPage-J_rU2SgI.js} +2 -2
- package/dist/app/assets/{MaterializationsPage-p9YjkRXZ.js → MaterializationsPage-DHgPI4MO.js} +1 -1
- package/dist/app/assets/ModelPage-BzXfjTu5.js +1 -0
- package/dist/app/assets/{PackagePage-e4kN75YR.js → PackagePage-ByoSTLJ4.js} +1 -1
- package/dist/app/assets/RouteError-CrSeKN2r.js +1 -0
- package/dist/app/assets/{ThemeEditorPage-CciagFTq.js → ThemeEditorPage-Btxnmhtk.js} +1 -1
- package/dist/app/assets/{WorkbookPage-DNWmkCXa.js → WorkbookPage-D6JLPnWO.js} +1 -1
- package/dist/app/assets/{core-Rj_4rRnA.es-BZyvITuO.js → core-BSRgt1mG.es-Bc_D2Lq4.js} +1 -1
- package/dist/app/assets/{index-CH2AcDzc.js → index-BRijWj6K.js} +15 -15
- package/dist/app/assets/{index-DQa463gC.js → index-BX4D94xw.js} +1 -1
- package/dist/app/assets/{index-VBbcc8s6.js → index-Cvy0xtb7.js} +1 -1
- package/dist/app/assets/{index-DQpV7MyA.js → index-DMAhIJNC.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/package_load_worker.mjs +59 -20
- package/dist/server.mjs +1342 -616
- package/package.json +8 -2
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -97
- package/dist/app/assets/ModelPage-C1OSTv-x.js +0 -1
- package/dist/app/assets/RouteError-CzbfOkng.js +0 -1
- package/eslint.config.mjs +0 -76
- package/k6-tests/README.md +0 -144
- package/k6-tests/bun.lock +0 -797
- package/k6-tests/load-test/load-test-crud-connections.ts +0 -263
- package/k6-tests/load-test/load-test-crud-packages.ts +0 -461
- package/k6-tests/load-test/load-test-crud-projects.ts +0 -160
- package/k6-tests/load-test/load-test-crud.ts +0 -142
- package/k6-tests/load-test/load-test.ts +0 -784
- package/k6-tests/package.json +0 -27
- package/k6-tests/scripts/run-with-otel.sh +0 -50
- package/k6-tests/smoke-test/smoke-test.ts +0 -158
- package/k6-tests/tsconfig.json +0 -29
- package/k6-tests/utils/client_factory.ts +0 -128
- package/k6-tests/utils/common.ts +0 -489
- package/k6-tests/utils/logger.ts +0 -62
- package/k6-tests/utils/types.d.ts +0 -35
- package/malloy_mcp.dxt +0 -0
- package/publisher.config.example.bigquery.json +0 -33
- package/publisher.config.example.duckdb.json +0 -23
- package/publisher.config.json +0 -23
- package/scripts/bake-duckdb-extensions.js +0 -110
- package/src/config.spec.ts +0 -1535
- package/src/config.theme.spec.ts +0 -178
- package/src/config.ts +0 -1142
- package/src/constants.ts +0 -105
- package/src/controller/compile.controller.ts +0 -40
- package/src/controller/connection.controller.spec.ts +0 -849
- package/src/controller/connection.controller.ts +0 -901
- package/src/controller/database.controller.ts +0 -24
- package/src/controller/materialization.controller.spec.ts +0 -293
- package/src/controller/materialization.controller.ts +0 -319
- package/src/controller/model.controller.ts +0 -164
- package/src/controller/package.controller.spec.ts +0 -273
- package/src/controller/package.controller.ts +0 -306
- package/src/controller/query.controller.ts +0 -152
- package/src/controller/theme.controller.ts +0 -83
- package/src/controller/watch-mode.controller.ts +0 -243
- package/src/data_styles.ts +0 -71
- package/src/default-publisher.config.json +0 -23
- package/src/ducklake_version.spec.ts +0 -163
- package/src/ducklake_version.ts +0 -153
- package/src/errors.spec.ts +0 -119
- package/src/errors.ts +0 -237
- package/src/filter_deprecation.spec.ts +0 -64
- package/src/filter_deprecation.ts +0 -42
- package/src/health.spec.ts +0 -123
- package/src/health.ts +0 -233
- package/src/heap_check.spec.ts +0 -144
- package/src/heap_check.ts +0 -144
- package/src/instrumentation.ts +0 -214
- package/src/json_utils.spec.ts +0 -51
- package/src/json_utils.ts +0 -33
- package/src/logger.spec.ts +0 -210
- package/src/logger.ts +0 -251
- package/src/malloy_pin_prereqs.spec.ts +0 -25
- package/src/materialization_metrics.spec.ts +0 -169
- package/src/materialization_metrics.ts +0 -346
- package/src/mcp/error_messages.spec.ts +0 -35
- package/src/mcp/error_messages.ts +0 -183
- package/src/mcp/handler_utils.spec.ts +0 -108
- package/src/mcp/handler_utils.ts +0 -251
- package/src/mcp/mcp_constants.ts +0 -6
- package/src/mcp/query_envelope.spec.ts +0 -229
- package/src/mcp/query_envelope.ts +0 -240
- package/src/mcp/server.protocol.spec.ts +0 -250
- package/src/mcp/server.ts +0 -87
- package/src/mcp/skills/build_skills_bundle.spec.ts +0 -51
- package/src/mcp/skills/build_skills_bundle.ts +0 -176
- package/src/mcp/skills/skills_bundle.json +0 -1
- package/src/mcp/skills/skills_bundle.spec.ts +0 -171
- package/src/mcp/tool_response.spec.ts +0 -108
- package/src/mcp/tool_response.ts +0 -138
- package/src/mcp/tools/compile_tool.spec.ts +0 -315
- package/src/mcp/tools/compile_tool.ts +0 -208
- package/src/mcp/tools/docs_search/build_docs_index.ts +0 -132
- package/src/mcp/tools/docs_search/malloy_docs_index.json +0 -1
- package/src/mcp/tools/docs_search_tool.spec.ts +0 -32
- package/src/mcp/tools/docs_search_tool.ts +0 -128
- package/src/mcp/tools/embedding_index.spec.ts +0 -1236
- package/src/mcp/tools/embedding_index.ts +0 -808
- package/src/mcp/tools/execute_query_tool.spec.ts +0 -294
- package/src/mcp/tools/execute_query_tool.ts +0 -316
- package/src/mcp/tools/get_context_eval.ts +0 -275
- package/src/mcp/tools/get_context_tool.spec.ts +0 -592
- package/src/mcp/tools/get_context_tool.ts +0 -585
- package/src/mcp/tools/reload_package_tool.spec.ts +0 -232
- package/src/mcp/tools/reload_package_tool.ts +0 -132
- package/src/mcp_config.spec.ts +0 -919
- package/src/mcp_config.ts +0 -425
- package/src/oom_guards.integration.spec.ts +0 -269
- package/src/package_load/package_load_pool.spec.ts +0 -252
- package/src/package_load/package_load_pool.ts +0 -931
- package/src/package_load/package_load_worker.ts +0 -1034
- package/src/package_load/protocol.ts +0 -364
- package/src/package_load/rpc_wait_accountant.spec.ts +0 -109
- package/src/package_load/rpc_wait_accountant.ts +0 -76
- package/src/package_load_metrics.spec.ts +0 -114
- package/src/package_load_metrics.ts +0 -127
- package/src/path_safety.spec.ts +0 -158
- package/src/path_safety.ts +0 -146
- package/src/pg_helpers.spec.ts +0 -223
- package/src/pg_helpers.ts +0 -52
- package/src/query_cap_metrics.spec.ts +0 -89
- package/src/query_cap_metrics.ts +0 -116
- package/src/query_concurrency.spec.ts +0 -247
- package/src/query_concurrency.ts +0 -237
- package/src/query_metadata_metrics.ts +0 -49
- package/src/query_param_utils.ts +0 -29
- package/src/query_timeout.spec.ts +0 -224
- package/src/query_timeout.ts +0 -179
- package/src/runtime/publisher.js +0 -328
- package/src/server-old.ts +0 -988
- package/src/server.ts +0 -2059
- package/src/service/annotations.spec.ts +0 -118
- package/src/service/annotations.ts +0 -91
- package/src/service/authorize.spec.ts +0 -154
- package/src/service/authorize.ts +0 -498
- package/src/service/authorize_integration.spec.ts +0 -2041
- package/src/service/build_plan.spec.ts +0 -685
- package/src/service/build_plan.ts +0 -797
- package/src/service/compile_authorize.spec.ts +0 -85
- package/src/service/compile_fragment_techniques.spec.ts +0 -156
- package/src/service/connection.spec.ts +0 -2360
- package/src/service/connection.ts +0 -2187
- package/src/service/connection_config.spec.ts +0 -734
- package/src/service/connection_config.ts +0 -899
- package/src/service/connection_federation.spec.ts +0 -184
- package/src/service/connection_fingerprint.spec.ts +0 -102
- package/src/service/connection_service.spec.ts +0 -711
- package/src/service/connection_service.ts +0 -291
- package/src/service/cron_evaluator.spec.ts +0 -79
- package/src/service/cron_evaluator.ts +0 -105
- package/src/service/db_utils.spec.ts +0 -1085
- package/src/service/db_utils.ts +0 -1502
- package/src/service/duckdb_instance_isolation.spec.ts +0 -137
- package/src/service/ducklake_lazy_attach.spec.ts +0 -110
- package/src/service/embedding_provider.spec.ts +0 -329
- package/src/service/embedding_provider.ts +0 -236
- package/src/service/environment.ts +0 -2191
- package/src/service/environment_admission.spec.ts +0 -344
- package/src/service/environment_compile.spec.ts +0 -197
- package/src/service/environment_store.spec.ts +0 -2274
- package/src/service/environment_store.ts +0 -2434
- package/src/service/environment_store_anchoring.spec.ts +0 -107
- package/src/service/environment_store_clone.spec.ts +0 -350
- package/src/service/explore_visibility.spec.ts +0 -434
- package/src/service/exports_probe.spec.ts +0 -107
- package/src/service/extension_fetch_policy.spec.ts +0 -256
- package/src/service/filter.spec.ts +0 -458
- package/src/service/filter.ts +0 -341
- package/src/service/filter_bypass.spec.ts +0 -418
- package/src/service/filter_integration.spec.ts +0 -1004
- package/src/service/freshness.spec.ts +0 -183
- package/src/service/freshness.ts +0 -112
- package/src/service/gcs_s3_utils.ts +0 -427
- package/src/service/given.ts +0 -105
- package/src/service/givens_integration.spec.ts +0 -219
- package/src/service/manifest_loader.spec.ts +0 -189
- package/src/service/manifest_loader.ts +0 -156
- package/src/service/materialization_build_session.spec.ts +0 -435
- package/src/service/materialization_build_session.ts +0 -681
- package/src/service/materialization_config_validation.spec.ts +0 -99
- package/src/service/materialization_config_validation.ts +0 -120
- package/src/service/materialization_eligibility.spec.ts +0 -158
- package/src/service/materialization_eligibility.ts +0 -305
- package/src/service/materialization_schedule_surface.spec.ts +0 -256
- package/src/service/materialization_scheduler.spec.ts +0 -289
- package/src/service/materialization_scheduler.ts +0 -304
- package/src/service/materialization_serve_transform.spec.ts +0 -1003
- package/src/service/materialization_serve_transform.ts +0 -779
- package/src/service/materialization_service.spec.ts +0 -2373
- package/src/service/materialization_service.ts +0 -2342
- package/src/service/materialization_test_fixtures.ts +0 -249
- package/src/service/model.spec.ts +0 -1157
- package/src/service/model.ts +0 -3370
- package/src/service/model_limits.spec.ts +0 -209
- package/src/service/model_limits.ts +0 -131
- package/src/service/model_storage_serve.spec.ts +0 -193
- package/src/service/model_storage_serve_joins.spec.ts +0 -193
- package/src/service/package.spec.ts +0 -612
- package/src/service/package.ts +0 -1799
- package/src/service/package_manifest.spec.ts +0 -230
- package/src/service/package_manifest.ts +0 -241
- package/src/service/package_memory_governor.spec.ts +0 -173
- package/src/service/package_memory_governor.ts +0 -233
- package/src/service/package_quote_bound_tables.spec.ts +0 -171
- package/src/service/package_race.spec.ts +0 -208
- package/src/service/package_reload_safety.spec.ts +0 -193
- package/src/service/package_rollback.spec.ts +0 -190
- package/src/service/package_worker_path.spec.ts +0 -645
- package/src/service/path_injection.spec.ts +0 -39
- package/src/service/persist_annotation_validation.spec.ts +0 -89
- package/src/service/persist_annotation_validation.ts +0 -59
- package/src/service/persistence_policy.spec.ts +0 -374
- package/src/service/proxy.spec.ts +0 -414
- package/src/service/proxy.ts +0 -248
- package/src/service/query_boundary.spec.ts +0 -470
- package/src/service/query_metadata.spec.ts +0 -408
- package/src/service/query_metadata.ts +0 -492
- package/src/service/query_metadata_identity.spec.ts +0 -149
- package/src/service/query_text.spec.ts +0 -79
- package/src/service/query_text.ts +0 -43
- package/src/service/quoting.spec.ts +0 -135
- package/src/service/quoting.ts +0 -78
- package/src/service/resolve_environment.ts +0 -15
- package/src/service/restricted_mode.spec.ts +0 -299
- package/src/service/source_extraction.ts +0 -225
- package/src/service/theme_store.ts +0 -199
- package/src/storage/DatabaseInterface.ts +0 -198
- package/src/storage/StorageManager.ts +0 -113
- package/src/storage/duckdb/ConnectionRepository.ts +0 -143
- package/src/storage/duckdb/DuckDBConnection.ts +0 -123
- package/src/storage/duckdb/DuckDBRepository.ts +0 -208
- package/src/storage/duckdb/EnvironmentRepository.ts +0 -148
- package/src/storage/duckdb/MaterializationRepository.spec.ts +0 -191
- package/src/storage/duckdb/MaterializationRepository.ts +0 -336
- package/src/storage/duckdb/PackageRepository.ts +0 -135
- package/src/storage/duckdb/schema.ts +0 -229
- package/src/stream_helpers.spec.ts +0 -280
- package/src/stream_helpers.ts +0 -162
- package/src/telemetry.ts +0 -20
- package/src/test_helpers/metrics_harness.ts +0 -166
- package/src/theme_key_parity.spec.ts +0 -57
- package/src/utils.ts +0 -29
- package/tests/fixtures/authorize-compile/model.malloy +0 -9
- package/tests/fixtures/authorize-compile/publisher.json +0 -4
- package/tests/fixtures/html-pages-nopublic/model.malloy +0 -1
- package/tests/fixtures/html-pages-nopublic/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/data.csv +0 -3
- package/tests/fixtures/html-pages-test/public/assets/app.css +0 -3
- package/tests/fixtures/html-pages-test/public/barehtml.html +0 -4
- package/tests/fixtures/html-pages-test/public/bodymeta.html +0 -8
- package/tests/fixtures/html-pages-test/public/data.json +0 -1
- package/tests/fixtures/html-pages-test/public/fitcomment.html +0 -11
- package/tests/fixtures/html-pages-test/public/index.html +0 -9
- package/tests/fixtures/html-pages-test/public/nohead.html +0 -8
- package/tests/fixtures/html-pages-test/public/notfit.html +0 -13
- package/tests/fixtures/html-pages-test/public/slides.html +0 -12
- package/tests/fixtures/html-pages-test/public/sub/page2.html +0 -9
- package/tests/fixtures/html-pages-test/public/unterminated.html +0 -10
- package/tests/fixtures/html-pages-test/publisher.json +0 -5
- package/tests/fixtures/html-pages-test/report.malloy +0 -1
- package/tests/fixtures/persist-multi-level/data/orders.csv +0 -5
- package/tests/fixtures/persist-multi-level/multi_level.malloy +0 -18
- package/tests/fixtures/persist-multi-level/publisher.json +0 -5
- package/tests/fixtures/persist-schedule-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-test/persist_schedule_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-test/publisher.json +0 -7
- package/tests/fixtures/persist-schedule-warn-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-schedule-warn-test/persist_warn_test.malloy +0 -11
- package/tests/fixtures/persist-schedule-warn-test/publisher.json +0 -6
- package/tests/fixtures/persist-test/data/orders.csv +0 -5
- package/tests/fixtures/persist-test/persist_test.malloy +0 -11
- package/tests/fixtures/persist-test/publisher.json +0 -5
- package/tests/fixtures/publisher.config.json +0 -15
- package/tests/fixtures/query-givens/data/orders.csv +0 -7
- package/tests/fixtures/query-givens/model.malloy +0 -34
- package/tests/fixtures/query-givens/publisher.json +0 -5
- package/tests/fixtures/xlsx/database.xlsx +0 -0
- package/tests/harness/mcp_test_setup.ts +0 -332
- package/tests/harness/rest_e2e.ts +0 -68
- package/tests/integration/authorize/compile_authorize_http.integration.spec.ts +0 -92
- package/tests/integration/concurrent_package/concurrent_package.integration.spec.ts +0 -280
- package/tests/integration/duckdb_storage/duckdb_storage.integration.spec.ts +0 -138
- package/tests/integration/first_boot/readiness_line.integration.spec.ts +0 -177
- package/tests/integration/html_pages/html_pages.integration.spec.ts +0 -440
- package/tests/integration/legacy_routes/legacy_routes.integration.spec.ts +0 -259
- package/tests/integration/materialization/environment_list.integration.spec.ts +0 -133
- package/tests/integration/materialization/freshness_gate.integration.spec.ts +0 -292
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +0 -430
- package/tests/integration/materialization/materialization_lifecycle.integration.spec.ts +0 -441
- package/tests/integration/materialization/orchestrated_rebind.integration.spec.ts +0 -150
- package/tests/integration/materialization/reference_manifest.integration.spec.ts +0 -251
- package/tests/integration/materialization/schedule_edit.integration.spec.ts +0 -126
- package/tests/integration/materialization/scheduler.integration.spec.ts +0 -200
- package/tests/integration/materialization/scheduler_recovery.integration.spec.ts +0 -172
- package/tests/integration/materialization/scheduler_transitions.integration.spec.ts +0 -256
- package/tests/integration/materialization/scheduler_wiring.integration.spec.ts +0 -143
- package/tests/integration/mcp/mcp_execute_query_tool.integration.spec.ts +0 -438
- package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +0 -235
- package/tests/integration/mcp/mcp_transport.integration.spec.ts +0 -98
- package/tests/integration/query_givens/query_givens.integration.spec.ts +0 -146
- package/tests/integration/query_givens/query_givens_authorize.integration.spec.ts +0 -121
- package/tests/integration/sdk_givens/sdk_givens.integration.spec.ts +0 -110
- package/tests/integration/watch-mode/watch_mode.integration.spec.ts +0 -421
- package/tests/unit/controller/theme.controller.spec.ts +0 -141
- package/tests/unit/duckdb/attached_databases.test.ts +0 -1243
- package/tests/unit/duckdb/duckdb_connection.test.ts +0 -181
- package/tests/unit/duckdb/legacy_schema_migration.test.ts +0 -197
- package/tests/unit/duckdb/repositories.test.ts +0 -208
- package/tests/unit/ducklake/ducklake.test.ts +0 -952
- package/tests/unit/service/theme_store.spec.ts +0 -139
- package/tsconfig.json +0 -32
|
@@ -1,797 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AtomicField,
|
|
3
|
-
BuildGraph as MalloyBuildGraph,
|
|
4
|
-
BuildNode,
|
|
5
|
-
MalloyConfig,
|
|
6
|
-
Connection as MalloyConnection,
|
|
7
|
-
PersistSource,
|
|
8
|
-
} from "@malloydata/malloy";
|
|
9
|
-
import { Annotations } from "@malloydata/malloy";
|
|
10
|
-
import { components } from "../api";
|
|
11
|
-
import { MaterializationEligibilityError } from "../errors";
|
|
12
|
-
import { MODEL_FILE_SUFFIX } from "../constants";
|
|
13
|
-
import { logger } from "../logger";
|
|
14
|
-
import {
|
|
15
|
-
recordConnectionDigestSkipped,
|
|
16
|
-
recordEligibilityRefused,
|
|
17
|
-
} from "../materialization_metrics";
|
|
18
|
-
import { errMessage } from "../utils";
|
|
19
|
-
import { assertMaterializationEligible } from "./materialization_eligibility";
|
|
20
|
-
import { Model } from "./model";
|
|
21
|
-
import { quoteIdentifier } from "./quoting";
|
|
22
|
-
|
|
23
|
-
type WireBuildGraph = components["schemas"]["BuildGraph"];
|
|
24
|
-
type WirePersistSourcePlan = components["schemas"]["PersistSourcePlan"];
|
|
25
|
-
type WireColumn = components["schemas"]["Column"];
|
|
26
|
-
type BuildPlan = components["schemas"]["BuildPlan"];
|
|
27
|
-
type WireFreshness = components["schemas"]["Freshness"];
|
|
28
|
-
type WirePackageMaterialization =
|
|
29
|
-
components["schemas"]["PackageMaterializationConfig"];
|
|
30
|
-
type QueryMetadata = components["schemas"]["QueryMetadata"];
|
|
31
|
-
|
|
32
|
-
/** The freshness `fallback` values the publisher recognizes; others are dropped. */
|
|
33
|
-
const FRESHNESS_FALLBACKS = ["live", "stale_ok", "fail"] as const;
|
|
34
|
-
type FreshnessFallback = (typeof FRESHNESS_FALLBACKS)[number];
|
|
35
|
-
|
|
36
|
-
/** One layer's contribution to the resolved freshness (all optional). */
|
|
37
|
-
interface FreshnessLayer {
|
|
38
|
-
window?: string;
|
|
39
|
-
fallback?: FreshnessFallback;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Minimal reader over a Malloy `Tag` (see `@malloydata/malloy-tag`): scalar
|
|
44
|
-
* reads by path, plus the subtree read that a property collection like
|
|
45
|
-
* `queryMetadata { … }` needs.
|
|
46
|
-
*/
|
|
47
|
-
interface ReadableTag {
|
|
48
|
-
text(...path: string[]): string | undefined;
|
|
49
|
-
tag(...path: string[]): ReadableTag | undefined;
|
|
50
|
-
entries?(): Iterable<[string, { text(): string | undefined }]>;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Minimal surface a package must expose to compile its build plan. Both
|
|
55
|
-
* {@link Package} (for the read-only `Package.buildPlan`) and the
|
|
56
|
-
* materialization service (for auto-run and orchestrated builds) satisfy it.
|
|
57
|
-
*/
|
|
58
|
-
export interface BuildPlanPackage {
|
|
59
|
-
getModelPaths(): string[];
|
|
60
|
-
getPackagePath(): string;
|
|
61
|
-
getMalloyConfig(): MalloyConfig;
|
|
62
|
-
getMalloyConnection(name: string): Promise<MalloyConnection>;
|
|
63
|
-
/**
|
|
64
|
-
* The package-level `materialization` config (from malloy-publisher.json),
|
|
65
|
-
* used as the least-specific layer when resolving per-source freshness /
|
|
66
|
-
* schedule. Optional so existing fixtures/callers that don't track it still
|
|
67
|
-
* typecheck (they resolve without a package default).
|
|
68
|
-
*/
|
|
69
|
-
getMaterializationConfig?(): WirePackageMaterialization | null;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Result of compiling a package's persist sources: the dependency-ordered
|
|
74
|
-
* build graphs, the persist sources keyed by sourceID, the per-connection
|
|
75
|
-
* digests, and the resolved live connections. The wire {@link BuildPlan} is a
|
|
76
|
-
* projection of this (see {@link deriveBuildPlan}); the build engine consumes
|
|
77
|
-
* the full structure.
|
|
78
|
-
*/
|
|
79
|
-
export interface CompiledBuildPlan {
|
|
80
|
-
graphs: MalloyBuildGraph[];
|
|
81
|
-
sources: Record<string, PersistSource>;
|
|
82
|
-
connectionDigests: Record<string, string>;
|
|
83
|
-
connections: Map<string, MalloyConnection>;
|
|
84
|
-
/**
|
|
85
|
-
* sourceID -> the package-relative path of the `.malloy` model that declares
|
|
86
|
-
* the source (the only place that mapping is known, since a sourceID's
|
|
87
|
-
* embedded modelURL is an absolute `file://` path with no package boundary).
|
|
88
|
-
* Optional so existing fixtures/callers that don't track it still typecheck.
|
|
89
|
-
*/
|
|
90
|
-
sourceModelPaths?: Record<string, string>;
|
|
91
|
-
/**
|
|
92
|
-
* Sources carrying a `#@ persist` annotation that Malloy's getBuildPlan() did
|
|
93
|
-
* NOT recognize as a materializable build root, so they produced no plan entry
|
|
94
|
-
* and would otherwise be a silent no-op (served live, never materialized). See
|
|
95
|
-
* {@link detectDroppedPersistSources}. Callers surface these: a load-time
|
|
96
|
-
* warning and a hard build failure, so a persist annotation is never silently
|
|
97
|
-
* dropped.
|
|
98
|
-
*/
|
|
99
|
-
droppedPersistSources?: { name: string; modelPath: string }[];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Output columns of a persist source, degrading to [] if unavailable. */
|
|
103
|
-
export function deriveColumns(persistSource: PersistSource): WireColumn[] {
|
|
104
|
-
try {
|
|
105
|
-
return persistSource._explore.intrinsicFields
|
|
106
|
-
.filter((f) => f.isAtomicField())
|
|
107
|
-
.map((f) => ({
|
|
108
|
-
name: f.name,
|
|
109
|
-
type: String((f as AtomicField).type),
|
|
110
|
-
}));
|
|
111
|
-
} catch (err) {
|
|
112
|
-
logger.warn("Failed to derive columns for persist source", {
|
|
113
|
-
sourceID: persistSource.sourceID,
|
|
114
|
-
error: errMessage(err),
|
|
115
|
-
});
|
|
116
|
-
return [];
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* A source's PUBLIC column names, or a throw if they can't be determined. The
|
|
122
|
-
* strict counterpart to {@link deriveColumns}, which degrades to `[]` because its
|
|
123
|
-
* caller (the wire build plan) only reports columns. Here the list decides what
|
|
124
|
-
* gets WRITTEN, so "unknown" must not read as "nothing to narrow".
|
|
125
|
-
*
|
|
126
|
-
* A name is returned only for a field that is demonstrably a public atomic field;
|
|
127
|
-
* an unreadable field list, an unreadable individual field, or an empty result all
|
|
128
|
-
* throw. Empty is a throw rather than a pass-through because a source with no
|
|
129
|
-
* public atomic columns means everything `getSQL` projects is hidden — the worst
|
|
130
|
-
* case to materialize, not a benign one.
|
|
131
|
-
*/
|
|
132
|
-
function publicColumnNames(persistSource: PersistSource): string[] {
|
|
133
|
-
let names: string[];
|
|
134
|
-
try {
|
|
135
|
-
names = persistSource._explore.intrinsicFields
|
|
136
|
-
.filter((f) => f.isAtomicField())
|
|
137
|
-
.map((f) => f.name)
|
|
138
|
-
.filter((n): n is string => typeof n === "string" && n.length > 0);
|
|
139
|
-
} catch (err) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`the compiled source's field list could not be read (${errMessage(err)})`,
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
if (names.length === 0) {
|
|
145
|
-
throw new Error("the compiled source exposes no public atomic columns");
|
|
146
|
-
}
|
|
147
|
-
return names;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Wrap a source's build SQL to project only its PUBLIC columns. `getSQL` emits
|
|
152
|
-
* every underlying column, including ones the source hides (`except:`, non-public
|
|
153
|
-
* access modifiers); this narrows the physical table to the public surface.
|
|
154
|
-
*
|
|
155
|
-
* A `storage=` build must not materialize a hidden column. Reachability THROUGH
|
|
156
|
-
* the source is separately bounded by the declared serve shape (see
|
|
157
|
-
* `narrowSchemaToPublic`, and the `shape-bounds-physical-columns` scenario that
|
|
158
|
-
* proves a physical column absent from the shape does not resolve). What this
|
|
159
|
-
* projection prevents is the hidden column's values sitting AT REST in the
|
|
160
|
-
* destination store, where direct catalog access reaches them and where the data
|
|
161
|
-
* may have crossed a trust boundary the source's visibility rules were meant to
|
|
162
|
-
* hold. Applies to BOTH storage build paths (the warehouse-passthrough
|
|
163
|
-
* single-source build and the chained "stack on the parent" downstream build).
|
|
164
|
-
*
|
|
165
|
-
* Fails CLOSED: if the public surface can't be determined the build is refused,
|
|
166
|
-
* rather than widening to everything `getSQL` projects. This matches the rest of
|
|
167
|
-
* the eligibility surface (see `assertMaterializationEligible`), and it fires
|
|
168
|
-
* before any warehouse or destination SQL runs, so a refusal writes nothing and
|
|
169
|
-
* leaves the previous generation serving.
|
|
170
|
-
*
|
|
171
|
-
* @throws {MaterializationEligibilityError} (HTTP 422) naming the source.
|
|
172
|
-
*/
|
|
173
|
-
export function projectToPublicColumns(
|
|
174
|
-
persistSource: PersistSource,
|
|
175
|
-
buildSQL: string,
|
|
176
|
-
): string {
|
|
177
|
-
let cols: string[];
|
|
178
|
-
try {
|
|
179
|
-
cols = publicColumnNames(persistSource);
|
|
180
|
-
} catch (err) {
|
|
181
|
-
recordEligibilityRefused("public_surface_unknown");
|
|
182
|
-
throw new MaterializationEligibilityError({
|
|
183
|
-
message:
|
|
184
|
-
`Source '${persistSource.name}' cannot be materialized into a ` +
|
|
185
|
-
`storage destination: its public column surface could not be ` +
|
|
186
|
-
`determined — ${errMessage(err)}. The publisher narrows a stored ` +
|
|
187
|
-
`table to the source's public columns, so it refuses the build ` +
|
|
188
|
-
`rather than materialize columns the source hides. This usually ` +
|
|
189
|
-
`means the compiled-source shape changed; drop 'storage=' to serve ` +
|
|
190
|
-
`this source live in the meantime.`,
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
const projection = cols
|
|
194
|
-
.map((n) => quoteIdentifier(n, persistSource.dialectName))
|
|
195
|
-
.join(", ");
|
|
196
|
-
return `SELECT ${projection} FROM (${buildSQL}) AS __public`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* All key=value fields of a source's `#@ persist` annotation (e.g.
|
|
201
|
-
* `{ name: "engaged_events", realization: "COPY" }`), degrading to {} if the
|
|
202
|
-
* annotation is absent or unparseable. The control plane consumes these — most
|
|
203
|
-
* importantly `name`, which it uses as the materialized table name (and which
|
|
204
|
-
* may carry a dialect-style container path like `dataset.table`). Returning the
|
|
205
|
-
* full set rather than a fixed subset means new persist directives flow through
|
|
206
|
-
* without a publisher change.
|
|
207
|
-
*/
|
|
208
|
-
export function deriveAnnotationFields(
|
|
209
|
-
persistSource: PersistSource,
|
|
210
|
-
): Record<string, string> {
|
|
211
|
-
const out: Record<string, string> = {};
|
|
212
|
-
try {
|
|
213
|
-
const tag = persistSource.annotations.parseAsTag("@").tag;
|
|
214
|
-
for (const [key, value] of tag.entries()) {
|
|
215
|
-
const text = value.text();
|
|
216
|
-
if (text !== undefined) out[key] = text;
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
// Degrade to {} — mirrors deriveColumns / selfAssignTableName.
|
|
220
|
-
}
|
|
221
|
-
return out;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Read the freshness keys (`freshness.window`, `freshness.fallback`) from one
|
|
226
|
-
* Malloy tag layer, keeping only recognized values. These are dotted/nested tag
|
|
227
|
-
* properties, so they are NOT captured by the scalar {@link deriveAnnotationFields}
|
|
228
|
-
* loop and must be read by path here. (Per-source `sharing`/`schedule` were
|
|
229
|
-
* retired — scope is package-level and a schedule is package-root-only — so they
|
|
230
|
-
* are not resolved here; declaring either on a source is a publish-time manifest
|
|
231
|
-
* error, enforced in Package.persistencePolicyWarnings.)
|
|
232
|
-
*/
|
|
233
|
-
function tagFreshnessLayer(tag: ReadableTag | undefined): FreshnessLayer {
|
|
234
|
-
if (!tag || typeof tag.text !== "function") return {};
|
|
235
|
-
const layer: FreshnessLayer = {};
|
|
236
|
-
const window = tag.text("freshness", "window");
|
|
237
|
-
if (typeof window === "string") layer.window = window;
|
|
238
|
-
const fallback = tag.text("freshness", "fallback");
|
|
239
|
-
if (
|
|
240
|
-
typeof fallback === "string" &&
|
|
241
|
-
(FRESHNESS_FALLBACKS as readonly string[]).includes(fallback)
|
|
242
|
-
) {
|
|
243
|
-
layer.fallback = fallback as FreshnessFallback;
|
|
244
|
-
}
|
|
245
|
-
return layer;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* The two homes a model-file (`##`) knob can be declared in, most specific
|
|
250
|
-
* first: the `materialization` envelope, then the bare form.
|
|
251
|
-
*
|
|
252
|
-
* The model-file level mirrors the manifest's shape verbatim — `##
|
|
253
|
-
* materialization.freshness.window="24h"` is the manifest's block in tag syntax
|
|
254
|
-
* — so that is where a file-level reader looks. The bare form
|
|
255
|
-
* (`## freshness.window="24h"`), which shipped first, stays readable underneath:
|
|
256
|
-
* the annotation rides the published package, so a package published before the
|
|
257
|
-
* envelope existed keeps resolving until it is republished. Resolution is
|
|
258
|
-
* per-property, so a file may declare one knob in each home without the envelope
|
|
259
|
-
* hiding the other.
|
|
260
|
-
*/
|
|
261
|
-
function modelTagLayers(
|
|
262
|
-
tag: ReadableTag | undefined,
|
|
263
|
-
): (ReadableTag | undefined)[] {
|
|
264
|
-
const envelope =
|
|
265
|
-
tag && typeof tag.tag === "function"
|
|
266
|
-
? tag.tag("materialization")
|
|
267
|
-
: undefined;
|
|
268
|
-
return [envelope, tag];
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** The package-level `materialization.freshness` as a resolution layer. */
|
|
272
|
-
function packageFreshnessLayer(
|
|
273
|
-
cfg: WirePackageMaterialization | null | undefined,
|
|
274
|
-
): FreshnessLayer {
|
|
275
|
-
if (!cfg) return {};
|
|
276
|
-
const layer: FreshnessLayer = {};
|
|
277
|
-
if (cfg.freshness?.window) layer.window = cfg.freshness.window;
|
|
278
|
-
const fallback = cfg.freshness?.fallback;
|
|
279
|
-
if (
|
|
280
|
-
typeof fallback === "string" &&
|
|
281
|
-
(FRESHNESS_FALLBACKS as readonly string[]).includes(fallback)
|
|
282
|
-
) {
|
|
283
|
-
layer.fallback = fallback as FreshnessFallback;
|
|
284
|
-
}
|
|
285
|
-
return layer;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** The source's `#@` tag, or undefined if it fails to parse (degrade to unset). */
|
|
289
|
-
function safeSourceTag(source: PersistSource): ReadableTag | undefined {
|
|
290
|
-
try {
|
|
291
|
-
return source.annotations.parseAsTag("@").tag as ReadableTag;
|
|
292
|
-
} catch {
|
|
293
|
-
return undefined;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* The model-file-level (`##`) tag resolved for the source, or undefined on a
|
|
299
|
-
* parse failure. The model-file layer sits between the source annotation and
|
|
300
|
-
* the package default in most-specific-wins resolution.
|
|
301
|
-
*/
|
|
302
|
-
function safeModelTag(source: PersistSource): ReadableTag | undefined {
|
|
303
|
-
try {
|
|
304
|
-
return source.modelAnnotations.parseAsTag().tag as ReadableTag;
|
|
305
|
-
} catch {
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Resolve a source's EFFECTIVE freshness with most-specific-wins precedence,
|
|
312
|
-
* per field: source annotation > model-file default > package default. Reported
|
|
313
|
-
* verbatim (unset at every level stays null so the control plane can
|
|
314
|
-
* distinguish "unset" — apply platform default — from an explicit declaration).
|
|
315
|
-
* Invalid `fallback` values are dropped, never defaulted. Freshness is valid in
|
|
316
|
-
* both scope modes.
|
|
317
|
-
*/
|
|
318
|
-
export function resolveFreshness(
|
|
319
|
-
source: PersistSource,
|
|
320
|
-
packageMaterialization: WirePackageMaterialization | null | undefined,
|
|
321
|
-
): WireFreshness | null {
|
|
322
|
-
const layers: FreshnessLayer[] = [
|
|
323
|
-
// `#@ persist` declares knobs bare — the annotation IS a materialization
|
|
324
|
-
// declaration, so there is no envelope to look under.
|
|
325
|
-
tagFreshnessLayer(safeSourceTag(source)),
|
|
326
|
-
...modelTagLayers(safeModelTag(source)).map(tagFreshnessLayer),
|
|
327
|
-
packageFreshnessLayer(packageMaterialization),
|
|
328
|
-
];
|
|
329
|
-
|
|
330
|
-
const window = layers.map((l) => l.window).find((v) => v !== undefined);
|
|
331
|
-
const fallback = layers.map((l) => l.fallback).find((v) => v !== undefined);
|
|
332
|
-
|
|
333
|
-
if (window === undefined && fallback === undefined) return null;
|
|
334
|
-
const freshness: WireFreshness = {};
|
|
335
|
-
if (window !== undefined) freshness.window = window;
|
|
336
|
-
if (fallback !== undefined) freshness.fallback = fallback;
|
|
337
|
-
return freshness;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Read the `queryMetadata` property collection from one tag layer. A collection,
|
|
342
|
-
* not a scalar: `queryMetadata { team="finance" env="prod" }` and the equivalent
|
|
343
|
-
* dotted form `queryMetadata.team="finance"` both land here, and neither is
|
|
344
|
-
* captured by the scalar {@link deriveAnnotationFields} loop.
|
|
345
|
-
*
|
|
346
|
-
* Every string-valued property is kept verbatim, including ones that violate
|
|
347
|
-
* Malloy's bag contract — publish reports those as warnings and the runtime
|
|
348
|
-
* clamps them, so an author's typo is visible somewhere instead of vanishing
|
|
349
|
-
* between the annotation and the warehouse.
|
|
350
|
-
*/
|
|
351
|
-
function tagQueryMetadataLayer(tag: ReadableTag | undefined): QueryMetadata {
|
|
352
|
-
const subtree =
|
|
353
|
-
tag && typeof tag.tag === "function"
|
|
354
|
-
? tag.tag("queryMetadata")
|
|
355
|
-
: undefined;
|
|
356
|
-
if (!subtree || typeof subtree.entries !== "function") return {};
|
|
357
|
-
const layer: QueryMetadata = {};
|
|
358
|
-
try {
|
|
359
|
-
for (const [name, value] of subtree.entries()) {
|
|
360
|
-
const text = value.text();
|
|
361
|
-
if (text !== undefined) layer[name] = text;
|
|
362
|
-
}
|
|
363
|
-
} catch {
|
|
364
|
-
// Degrade to {} — mirrors deriveAnnotationFields / deriveColumns.
|
|
365
|
-
return {};
|
|
366
|
-
}
|
|
367
|
-
return layer;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Resolve a source's EFFECTIVE per-query metadata, most-specific-wins PER
|
|
372
|
-
* PROPERTY: `#@ persist queryMetadata.*` > model-file
|
|
373
|
-
* `## materialization.queryMetadata.*` (bare `## queryMetadata.*` underneath) >
|
|
374
|
-
* package `materialization.queryMetadata`.
|
|
375
|
-
*
|
|
376
|
-
* Per-property rather than per-layer, exactly like {@link resolveFreshness}, so a
|
|
377
|
-
* package-wide `team` property survives a source that only overrides `workload`.
|
|
378
|
-
* Null when no layer declares anything, so absence on the wire always means
|
|
379
|
-
* "declared nowhere" rather than "declared empty".
|
|
380
|
-
*
|
|
381
|
-
* This is the value the publisher attaches (merged under its own context) to
|
|
382
|
-
* every statement it issues while building the source. It is deliberately absent
|
|
383
|
-
* from the source's content address: changing a tag must never re-address a
|
|
384
|
-
* table.
|
|
385
|
-
*/
|
|
386
|
-
export function resolveQueryMetadata(
|
|
387
|
-
source: PersistSource,
|
|
388
|
-
packageMaterialization: WirePackageMaterialization | null | undefined,
|
|
389
|
-
): QueryMetadata | null {
|
|
390
|
-
// Least specific first, so a more specific layer overwrites property by
|
|
391
|
-
// property.
|
|
392
|
-
const layers: QueryMetadata[] = [
|
|
393
|
-
packageMaterialization?.queryMetadata ?? {},
|
|
394
|
-
...modelTagLayers(safeModelTag(source))
|
|
395
|
-
.map(tagQueryMetadataLayer)
|
|
396
|
-
.reverse(),
|
|
397
|
-
tagQueryMetadataLayer(safeSourceTag(source)),
|
|
398
|
-
];
|
|
399
|
-
const resolved: QueryMetadata = Object.assign({}, ...layers);
|
|
400
|
-
return Object.keys(resolved).length > 0 ? resolved : null;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/** Flatten Malloy's nested BuildNode.dependsOn into a list of sourceIDs. */
|
|
404
|
-
export function flattenDependsOn(node: {
|
|
405
|
-
dependsOn: { sourceID: string }[];
|
|
406
|
-
}): string[] {
|
|
407
|
-
return node.dependsOn.map((d) => d.sourceID);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Yield every persist source of one graph in dependency order — each source's
|
|
412
|
-
* persist dependencies before the source itself — resolving each node's
|
|
413
|
-
* sourceID against the sources map and skipping nodes whose source is absent.
|
|
414
|
-
* Centralizes the graph→source walk shared by the planning and build loops.
|
|
415
|
-
*
|
|
416
|
-
* <p>Malloy's {@code getBuildPlan()} puts only the <em>root</em> persist sources
|
|
417
|
-
* (the terminals nothing else consumes) in {@code graph.nodes}; every transitive
|
|
418
|
-
* persist dependency is nested under a root in its recursive {@code dependsOn}
|
|
419
|
-
* tree (and present in {@code sources}). Walking only {@code graph.nodes}
|
|
420
|
-
* silently skips every intermediate persist source, so it never gets
|
|
421
|
-
* materialized (it only gets a table by coincidence when it shares a
|
|
422
|
-
* sourceEntityId with a root). We post-order DFS the {@code dependsOn} tree
|
|
423
|
-
* so dependencies are built first (a downstream build can then read its upstream
|
|
424
|
-
* source's freshly materialized table), deduplicating shared (diamond)
|
|
425
|
-
* dependencies by sourceID so each is yielded once. This mirrors the canonical
|
|
426
|
-
* malloy-cli reference build loop (its {@code flattenBuildNodes} +
|
|
427
|
-
* dedup-by-sourceID; see malloydata/malloy-cli src/malloy/build_graph.ts).
|
|
428
|
-
*/
|
|
429
|
-
export function* iterGraphSources(
|
|
430
|
-
graph: MalloyBuildGraph,
|
|
431
|
-
sources: Record<string, PersistSource>,
|
|
432
|
-
): Iterable<PersistSource> {
|
|
433
|
-
const seen = new Set<string>();
|
|
434
|
-
|
|
435
|
-
function* visit(node: BuildNode): Iterable<PersistSource> {
|
|
436
|
-
if (seen.has(node.sourceID)) return;
|
|
437
|
-
// Dependencies first: a source built later in the run resolves its
|
|
438
|
-
// upstream references against the tables built earlier (see the Manifest
|
|
439
|
-
// threading in executeInstructedBuild).
|
|
440
|
-
for (const dep of node.dependsOn) {
|
|
441
|
-
yield* visit(dep);
|
|
442
|
-
}
|
|
443
|
-
seen.add(node.sourceID);
|
|
444
|
-
const source = sources[node.sourceID];
|
|
445
|
-
if (source) yield source;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
for (const level of graph.nodes) {
|
|
449
|
-
for (const node of level) {
|
|
450
|
-
yield* visit(node);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* The sourceEntityId for a persist source: a stable content address of its
|
|
457
|
-
* connection identity and canonical SQL. Centralizes the
|
|
458
|
-
* (source, connectionDigests) call shape so planning, self-instruction, build,
|
|
459
|
-
* and serve-time manifest resolution all agree on the same id.
|
|
460
|
-
*
|
|
461
|
-
* <p>This is the single seam between the publisher and the Malloy compiler's
|
|
462
|
-
* source-identity recipe. Today it delegates to
|
|
463
|
-
* {@code PersistSource.makeBuildId(connectionDigest, sql)} — the compiler's
|
|
464
|
-
* current hex content hash. The connection's contribution is already
|
|
465
|
-
* fingerprint-aware: when a connection carries an API `fingerprint`, its
|
|
466
|
-
* digest IS that fingerprint verbatim (see applyConnectionFingerprint in
|
|
467
|
-
* connection.ts), so ids stay stable across credential rotation. When the
|
|
468
|
-
* compiler ships the scoped UUID5 sourceEntityId recipe (scope + connection
|
|
469
|
-
* fingerprint + canonical SQL), swap the delegation below to the new compiler
|
|
470
|
-
* API — this function is the only place the publisher derives the id.
|
|
471
|
-
*/
|
|
472
|
-
export function computeSourceEntityId(
|
|
473
|
-
source: PersistSource,
|
|
474
|
-
connectionDigests: Record<string, string>,
|
|
475
|
-
): string {
|
|
476
|
-
return source.makeBuildId(
|
|
477
|
-
connectionDigests[source.connectionName],
|
|
478
|
-
source.getSQL(),
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* Resolve a Map<name, Connection> for the names a step is about to touch.
|
|
484
|
-
* The package's MalloyConfig caches each lookup, so repeated calls are cheap.
|
|
485
|
-
* A failed lookup is logged and omitted; downstream code reports the missing
|
|
486
|
-
* connection explicitly.
|
|
487
|
-
*/
|
|
488
|
-
export async function resolvePackageConnections(
|
|
489
|
-
pkg: { getMalloyConnection(name: string): Promise<MalloyConnection> },
|
|
490
|
-
names: Iterable<string>,
|
|
491
|
-
): Promise<Map<string, MalloyConnection>> {
|
|
492
|
-
const map = new Map<string, MalloyConnection>();
|
|
493
|
-
const seen = new Set<string>();
|
|
494
|
-
for (const name of names) {
|
|
495
|
-
if (!name || seen.has(name)) continue;
|
|
496
|
-
seen.add(name);
|
|
497
|
-
try {
|
|
498
|
-
map.set(name, await pkg.getMalloyConnection(name));
|
|
499
|
-
} catch (err) {
|
|
500
|
-
logger.warn(`Failed to resolve connection ${name}`, {
|
|
501
|
-
error: errMessage(err),
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
return map;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Compile every model in the package and collect the dependency-ordered
|
|
510
|
-
* build graphs, persist sources, connection digests, and resolved
|
|
511
|
-
* connections. The build plan is a pure function of the compiled model plus
|
|
512
|
-
* connection config (no warehouse access).
|
|
513
|
-
*/
|
|
514
|
-
/**
|
|
515
|
-
* Names of sources in a compiled model that carry a `#@ persist` annotation but
|
|
516
|
-
* are absent from the model's build plan — i.e. Malloy's getBuildPlan() did not
|
|
517
|
-
* recognize them as a materializable build root and silently returned no graph
|
|
518
|
-
* for them.
|
|
519
|
-
*
|
|
520
|
-
* BACKSTOP for a Malloy getBuildPlan() gap: it silently returns no graph for a
|
|
521
|
-
* `#@ persist` source whose shape it doesn't treat as a build root (observed for
|
|
522
|
-
* a filtered pass-through `X is <table> extend { where … }`, which stays type
|
|
523
|
-
* `table`; only query-shaped sources are treated as roots). Without this check
|
|
524
|
-
* the annotation is a silent no-op (served live, no build, no error). The primary
|
|
525
|
-
* fix is in Malloy (recognize the shape or emit a diagnostic); until then, detect
|
|
526
|
-
* the annotated-but-absent source here so callers can refuse loudly.
|
|
527
|
-
*
|
|
528
|
-
* Reads the annotation exactly as Malloy's own checkPersistAnnotation does
|
|
529
|
-
* (`Annotations(def.annotations).parseAsTag('@').tag.has('persist')`). Best-effort
|
|
530
|
-
* and fail-open: any introspection failure yields no dropped names rather than
|
|
531
|
-
* risking a false positive that would wrongly fail a healthy build.
|
|
532
|
-
*/
|
|
533
|
-
function detectDroppedPersistSources(
|
|
534
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
535
|
-
malloyModel: any,
|
|
536
|
-
recognizedNames: Set<string>,
|
|
537
|
-
): string[] {
|
|
538
|
-
const dropped: string[] = [];
|
|
539
|
-
try {
|
|
540
|
-
const contents = malloyModel?._modelDef?.contents as
|
|
541
|
-
| Record<string, { type?: string; annotations?: unknown }>
|
|
542
|
-
| undefined;
|
|
543
|
-
if (!contents) return dropped;
|
|
544
|
-
for (const [name, def] of Object.entries(contents)) {
|
|
545
|
-
if (recognizedNames.has(name)) continue;
|
|
546
|
-
if (!def || !def.annotations) continue;
|
|
547
|
-
let isPersist = false;
|
|
548
|
-
try {
|
|
549
|
-
isPersist = new Annotations(def.annotations)
|
|
550
|
-
.parseAsTag("@")
|
|
551
|
-
.tag.has("persist");
|
|
552
|
-
} catch {
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
if (isPersist) dropped.push(name);
|
|
556
|
-
}
|
|
557
|
-
} catch {
|
|
558
|
-
// Fail open: never let introspection break the build plan.
|
|
559
|
-
return [];
|
|
560
|
-
}
|
|
561
|
-
return dropped;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
export async function compilePackageBuildPlan(
|
|
565
|
-
pkg: BuildPlanPackage,
|
|
566
|
-
signal?: AbortSignal,
|
|
567
|
-
): Promise<CompiledBuildPlan> {
|
|
568
|
-
const allGraphs: MalloyBuildGraph[] = [];
|
|
569
|
-
const allSources: Record<string, PersistSource> = {};
|
|
570
|
-
const sourceModelPaths: Record<string, string> = {};
|
|
571
|
-
const droppedPersistSources: { name: string; modelPath: string }[] = [];
|
|
572
|
-
|
|
573
|
-
for (const modelPath of pkg.getModelPaths()) {
|
|
574
|
-
// Only `.malloy` models declare persist sources. Skip `.malloynb`
|
|
575
|
-
// notebooks: getModel() parses a model file as a flat model and throws on
|
|
576
|
-
// the notebook's `>>>` cell delimiter, which would abort the entire
|
|
577
|
-
// package build plan and silently drop every persist source in it.
|
|
578
|
-
if (!modelPath.endsWith(MODEL_FILE_SUFFIX)) continue;
|
|
579
|
-
if (signal?.aborted) throw new Error("Build cancelled");
|
|
580
|
-
|
|
581
|
-
const { runtime, modelURL, importBaseURL } = await Model.getModelRuntime(
|
|
582
|
-
pkg.getPackagePath(),
|
|
583
|
-
modelPath,
|
|
584
|
-
pkg.getMalloyConfig(),
|
|
585
|
-
);
|
|
586
|
-
const malloyModel = await runtime
|
|
587
|
-
.loadModel(modelURL, { importBaseURL })
|
|
588
|
-
.getModel();
|
|
589
|
-
|
|
590
|
-
// getBuildPlan() THROWS "Model must have ##! experimental.persistence"
|
|
591
|
-
// on any model that lacks the flag — it does NOT return empty. So a
|
|
592
|
-
// header-less non-persist model in the package (e.g. an imported base
|
|
593
|
-
// model that only defines raw sources) would abort the entire package
|
|
594
|
-
// build plan, silently dropping every persist source in every other
|
|
595
|
-
// model. Mirror Malloy's own guard and skip such models: without the flag
|
|
596
|
-
// a model cannot carry a functioning persist source anyway. Models that
|
|
597
|
-
// DO have the flag but declare no persist source return empty graphs and
|
|
598
|
-
// are skipped by the `graphs.length === 0` check below.
|
|
599
|
-
const modelTag = malloyModel.modelAnnotations.parseAsTag("!").tag;
|
|
600
|
-
if (!modelTag.has("experimental", "persistence")) continue;
|
|
601
|
-
|
|
602
|
-
const buildPlan = malloyModel.getBuildPlan();
|
|
603
|
-
for (const msg of buildPlan.tagParseLog) {
|
|
604
|
-
logger.warn("Persist annotation issue", {
|
|
605
|
-
modelPath,
|
|
606
|
-
message: msg.message,
|
|
607
|
-
severity: msg.severity,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Detect `#@ persist` sources the plan didn't recognize (see
|
|
612
|
-
// detectDroppedPersistSources). Runs BEFORE the empty-graphs `continue`, so
|
|
613
|
-
// a model whose ONLY persist source is a dropped shape is still caught.
|
|
614
|
-
const recognizedNames = new Set(
|
|
615
|
-
Object.values(buildPlan.sources).map((s) => s.name),
|
|
616
|
-
);
|
|
617
|
-
for (const name of detectDroppedPersistSources(
|
|
618
|
-
malloyModel,
|
|
619
|
-
recognizedNames,
|
|
620
|
-
)) {
|
|
621
|
-
droppedPersistSources.push({ name, modelPath });
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (buildPlan.graphs.length === 0) continue;
|
|
625
|
-
|
|
626
|
-
allGraphs.push(...buildPlan.graphs);
|
|
627
|
-
for (const [sourceID, source] of Object.entries(buildPlan.sources)) {
|
|
628
|
-
allSources[sourceID] = source;
|
|
629
|
-
sourceModelPaths[sourceID] = modelPath;
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const connections = await resolvePackageConnections(
|
|
634
|
-
pkg,
|
|
635
|
-
allGraphs.map((g) => g.connectionName),
|
|
636
|
-
);
|
|
637
|
-
const connectionDigests: Record<string, string> = {};
|
|
638
|
-
for (const graph of allGraphs) {
|
|
639
|
-
const conn = connections.get(graph.connectionName);
|
|
640
|
-
if (!conn) {
|
|
641
|
-
// The connection failed to resolve (already warned in
|
|
642
|
-
// resolvePackageConnections). Its sourceEntityIds will be computed
|
|
643
|
-
// without a digest, so surface it as a discrete correctness signal
|
|
644
|
-
// rather than skipping silently.
|
|
645
|
-
recordConnectionDigestSkipped();
|
|
646
|
-
logger.warn("Skipping connection digest; connection did not resolve", {
|
|
647
|
-
connectionName: graph.connectionName,
|
|
648
|
-
});
|
|
649
|
-
continue;
|
|
650
|
-
}
|
|
651
|
-
if (!connectionDigests[graph.connectionName]) {
|
|
652
|
-
// getDigest() is fingerprint-aware: a connection configured with an
|
|
653
|
-
// API `fingerprint` returns it verbatim (applyConnectionFingerprint),
|
|
654
|
-
// so a credential rotation does not re-address its sources.
|
|
655
|
-
connectionDigests[graph.connectionName] = await conn.getDigest();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return {
|
|
660
|
-
graphs: allGraphs,
|
|
661
|
-
sources: allSources,
|
|
662
|
-
connectionDigests,
|
|
663
|
-
connections,
|
|
664
|
-
sourceModelPaths,
|
|
665
|
-
droppedPersistSources,
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
/** Project the Malloy build plan into the trimmed wire BuildPlan. */
|
|
670
|
-
export function deriveBuildPlan(
|
|
671
|
-
graphs: MalloyBuildGraph[],
|
|
672
|
-
sources: Record<string, PersistSource>,
|
|
673
|
-
connectionDigests: Record<string, string>,
|
|
674
|
-
sourceNames?: string[],
|
|
675
|
-
sourceModelPaths?: Record<string, string>,
|
|
676
|
-
packageMaterialization?: WirePackageMaterialization | null,
|
|
677
|
-
): BuildPlan {
|
|
678
|
-
const include = sourceNames ? new Set(sourceNames) : null;
|
|
679
|
-
|
|
680
|
-
const wireGraphs: WireBuildGraph[] = graphs.map((graph) => ({
|
|
681
|
-
connectionName: graph.connectionName,
|
|
682
|
-
nodes: graph.nodes.map((level) =>
|
|
683
|
-
level.map((node) => ({
|
|
684
|
-
sourceID: node.sourceID,
|
|
685
|
-
dependsOn: flattenDependsOn(node),
|
|
686
|
-
})),
|
|
687
|
-
),
|
|
688
|
-
}));
|
|
689
|
-
|
|
690
|
-
const wireSources: Record<string, WirePersistSourcePlan> = {};
|
|
691
|
-
for (const [sourceID, source] of Object.entries(sources)) {
|
|
692
|
-
if (include && !include.has(source.name)) continue;
|
|
693
|
-
const annotationFields = deriveAnnotationFields(source);
|
|
694
|
-
// EFFECTIVE per-source freshness, resolved most-specific-wins
|
|
695
|
-
// (source > model-file > package) and reported verbatim (null = unset at
|
|
696
|
-
// every level). Freshness is a dotted/nested tag key, so it comes from
|
|
697
|
-
// resolveFreshness rather than the scalar annotationFields map, and is
|
|
698
|
-
// valid in both scope modes. Per-source `sharing`/`schedule` are NOT
|
|
699
|
-
// emitted (retired from the contract); if a source declares either it is
|
|
700
|
-
// rejected at publish (Package.persistencePolicyWarnings) — the raw keys
|
|
701
|
-
// still ride `annotationFields` so the validator can detect them.
|
|
702
|
-
wireSources[sourceID] = {
|
|
703
|
-
name: source.name,
|
|
704
|
-
sourceID: source.sourceID,
|
|
705
|
-
connectionName: source.connectionName,
|
|
706
|
-
dialect: source.dialectName,
|
|
707
|
-
sourceEntityId: computeSourceEntityId(source, connectionDigests),
|
|
708
|
-
sql: source.getSQL(),
|
|
709
|
-
refresh: annotationFields.refresh ?? null,
|
|
710
|
-
freshness: resolveFreshness(source, packageMaterialization),
|
|
711
|
-
// EFFECTIVE per-source query metadata, resolved per property across the
|
|
712
|
-
// same layer stack as freshness. A property collection rather than a
|
|
713
|
-
// scalar, so it comes from resolveQueryMetadata rather than the
|
|
714
|
-
// annotationFields map.
|
|
715
|
-
queryMetadata: resolveQueryMetadata(source, packageMaterialization),
|
|
716
|
-
columns: deriveColumns(source),
|
|
717
|
-
annotationFields,
|
|
718
|
-
modelPath: sourceModelPaths?.[sourceID],
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
return { graphs: wireGraphs, sources: wireSources };
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/**
|
|
726
|
-
* Compile and project a package's build plan (null when the package declares no
|
|
727
|
-
* materializable persist source), plus any `#@ persist` sources that were
|
|
728
|
-
* silently dropped from the plan (see {@link detectDroppedPersistSources}) so
|
|
729
|
-
* the caller can surface a load-time warning. A deterministic property of the
|
|
730
|
-
* compiled package; feeds the read-only `Package.buildPlan` field.
|
|
731
|
-
*/
|
|
732
|
-
export async function computePackageBuildPlan(
|
|
733
|
-
pkg: BuildPlanPackage,
|
|
734
|
-
signal?: AbortSignal,
|
|
735
|
-
): Promise<{
|
|
736
|
-
plan: BuildPlan | null;
|
|
737
|
-
droppedPersistSources: { name: string; modelPath: string }[];
|
|
738
|
-
sourceEligibility: SourceEligibility;
|
|
739
|
-
}> {
|
|
740
|
-
const compiled = await compilePackageBuildPlan(pkg, signal);
|
|
741
|
-
const droppedPersistSources = compiled.droppedPersistSources ?? [];
|
|
742
|
-
const plan =
|
|
743
|
-
compiled.graphs.length === 0
|
|
744
|
-
? null
|
|
745
|
-
: deriveBuildPlan(
|
|
746
|
-
compiled.graphs,
|
|
747
|
-
compiled.sources,
|
|
748
|
-
compiled.connectionDigests,
|
|
749
|
-
undefined,
|
|
750
|
-
compiled.sourceModelPaths,
|
|
751
|
-
pkg.getMaterializationConfig?.() ?? null,
|
|
752
|
-
);
|
|
753
|
-
return {
|
|
754
|
-
plan,
|
|
755
|
-
droppedPersistSources,
|
|
756
|
-
sourceEligibility: collectSourceEligibility(compiled.sources),
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
/**
|
|
761
|
-
* Which sources may be served from a materialized table, decided HERE because
|
|
762
|
-
* this is where the compiled sources exist. The serve side needs the answer
|
|
763
|
-
* without them: binding a host's manifest deliberately skips the recompile, so it
|
|
764
|
-
* cannot re-derive eligibility and would otherwise have to take the host's word
|
|
765
|
-
* for it.
|
|
766
|
-
*
|
|
767
|
-
* Carries the ELIGIBLE names, not just the refusals, so the serve side can
|
|
768
|
-
* require a positive result. Absence is not eligibility: Malloy admits only
|
|
769
|
-
* query-shaped sources as build roots, so a `#@ persist` on a filtered
|
|
770
|
-
* pass-through (`X is <table> extend { where … }`, which stays type `table`)
|
|
771
|
-
* never reaches this record at all — and that is the row-level-access shape. A
|
|
772
|
-
* refusal-only view would read that silence as consent.
|
|
773
|
-
*
|
|
774
|
-
* Keyed by source NAME because that is what a serve binding carries.
|
|
775
|
-
*/
|
|
776
|
-
export type SourceEligibility = {
|
|
777
|
-
/** Sources compiled, examined, and found eligible. */
|
|
778
|
-
eligible: string[];
|
|
779
|
-
/** Source name -> why it was refused; for the operator-facing log. */
|
|
780
|
-
refused: Record<string, string>;
|
|
781
|
-
};
|
|
782
|
-
|
|
783
|
-
function collectSourceEligibility(
|
|
784
|
-
sources: Record<string, PersistSource>,
|
|
785
|
-
): SourceEligibility {
|
|
786
|
-
const eligible: string[] = [];
|
|
787
|
-
const refused: Record<string, string> = {};
|
|
788
|
-
for (const source of Object.values(sources)) {
|
|
789
|
-
try {
|
|
790
|
-
assertMaterializationEligible(source);
|
|
791
|
-
eligible.push(source.name);
|
|
792
|
-
} catch (err) {
|
|
793
|
-
refused[source.name] = errMessage(err);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return { eligible, refused };
|
|
797
|
-
}
|