@malloy-publisher/server 0.0.230 → 0.0.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.docker.md +4 -0
- package/dist/app/api-doc.yaml +74 -3
- package/dist/app/assets/{EnvironmentPage-wa_EPkwK.js → EnvironmentPage-DXEaZIPx.js} +1 -1
- package/dist/app/assets/{HomePage-jnCrupQp.js → HomePage-kofsqpZt.js} +1 -1
- package/dist/app/assets/{LightMode-DYbwNULZ.js → LightMode-CNhIlIlJ.js} +1 -1
- package/dist/app/assets/{MainPage-CuJLrPNI.js → MainPage-Bgqo8jCy.js} +1 -1
- package/dist/app/assets/{MaterializationsPage-D_67x2ee.js → MaterializationsPage-CgBlgGz2.js} +1 -1
- package/dist/app/assets/{ModelPage-D5JtAWqR.js → ModelPage-B0TjoDtf.js} +1 -1
- package/dist/app/assets/{PackagePage-BRwtqUSG.js → PackagePage-BL8vnFj1.js} +1 -1
- package/dist/app/assets/{RouteError-CBNNrnSD.js → RouteError-BzPby0X2.js} +1 -1
- package/dist/app/assets/{ThemeEditorPage-CTCeBneA.js → ThemeEditorPage-CTEP_9r3.js} +1 -1
- package/dist/app/assets/{WorkbookPage-SN6f1RBm.js → WorkbookPage-BwM3BmKw.js} +1 -1
- package/dist/app/assets/{core-Dp3q5Ieu.es-CD5FvM2s.js → core-CK68iv6w.es-CpRxXBt7.js} +1 -1
- package/dist/app/assets/{index-C_tJstcx.js → index-B33zGctF.js} +15 -15
- package/dist/app/assets/{index-DU4r7GdU.js → index-BabP-V-S.js} +346 -321
- package/dist/app/assets/{index-B3Nn8Vm2.js → index-BkiWKaAF.js} +266 -265
- package/dist/app/assets/{index-BLCx1EdC.js → index-CmkW1MiE.js} +1 -1
- package/dist/app/assets/{index-CfmBVB6M.js → index-tXJXwdyj.js} +1 -1
- package/dist/app/index.html +1 -1
- package/dist/instrumentation.mjs +2 -0
- package/dist/package_load_worker.mjs +11 -1
- package/dist/server.mjs +3186 -433
- package/package.json +12 -12
- package/scripts/bake-duckdb-extensions.js +4 -1
- package/src/config.spec.ts +39 -0
- package/src/config.ts +135 -0
- package/src/controller/materialization.controller.spec.ts +62 -0
- package/src/controller/materialization.controller.ts +15 -0
- package/src/controller/package.controller.spec.ts +6 -0
- package/src/controller/package.controller.ts +7 -2
- package/src/errors.ts +19 -0
- package/src/logger.spec.ts +18 -1
- package/src/logger.ts +3 -1
- package/src/materialization_metrics.spec.ts +89 -4
- package/src/materialization_metrics.ts +155 -5
- package/src/mcp/skills/skills_bundle.json +1 -1
- package/src/mcp/tools/embedding_index.spec.ts +1236 -0
- package/src/mcp/tools/embedding_index.ts +808 -0
- package/src/mcp/tools/get_context_eval.ts +194 -45
- package/src/mcp/tools/get_context_tool.spec.ts +295 -2
- package/src/mcp/tools/get_context_tool.ts +159 -10
- package/src/pg_helpers.spec.ts +201 -0
- package/src/pg_helpers.ts +44 -5
- package/src/server.ts +24 -0
- package/src/service/build_plan.spec.ts +128 -2
- package/src/service/build_plan.ts +239 -17
- package/src/service/connection.ts +263 -7
- package/src/service/connection_config.spec.ts +48 -0
- package/src/service/connection_config.ts +21 -8
- package/src/service/connection_federation.spec.ts +184 -0
- package/src/service/embedding_provider.spec.ts +329 -0
- package/src/service/embedding_provider.ts +236 -0
- package/src/service/environment.ts +274 -12
- package/src/service/environment_store.spec.ts +678 -3
- package/src/service/environment_store.ts +449 -33
- package/src/service/environment_store_clone.spec.ts +350 -0
- package/src/service/manifest_loader.spec.ts +68 -13
- package/src/service/manifest_loader.ts +67 -19
- package/src/service/materialization_build_session.spec.ts +435 -0
- package/src/service/materialization_build_session.ts +681 -0
- package/src/service/materialization_eligibility.spec.ts +158 -0
- package/src/service/materialization_eligibility.ts +305 -0
- package/src/service/materialization_serve_transform.spec.ts +1003 -0
- package/src/service/materialization_serve_transform.ts +779 -0
- package/src/service/materialization_service.spec.ts +774 -7
- package/src/service/materialization_service.ts +1107 -42
- package/src/service/materialization_test_fixtures.ts +7 -0
- package/src/service/model.spec.ts +207 -0
- package/src/service/model.ts +540 -52
- package/src/service/model_storage_serve.spec.ts +193 -0
- package/src/service/model_storage_serve_joins.spec.ts +193 -0
- package/src/service/package.spec.ts +196 -0
- package/src/service/package.ts +385 -17
- package/src/service/persistence_policy.spec.ts +109 -0
- package/src/storage/duckdb/schema.ts +37 -0
- package/tests/fixtures/xlsx/database.xlsx +0 -0
- package/tests/integration/first_boot/readiness_line.integration.spec.ts +177 -0
- package/tests/integration/materialization/manifest_binding.integration.spec.ts +104 -0
- package/tests/integration/mcp/mcp_get_context_semantic.integration.spec.ts +235 -0
|
@@ -126,6 +126,12 @@ export function fakeSource(opts: {
|
|
|
126
126
|
* assert what physical name a downstream build sees for its upstream.
|
|
127
127
|
*/
|
|
128
128
|
onGetSQL?: (sqlOpts: unknown) => void;
|
|
129
|
+
/**
|
|
130
|
+
* The compiled `_sourceDef` the materialization-eligibility gate walks
|
|
131
|
+
* (parameters/givens). Defaults to an empty def (eligible); the gate only
|
|
132
|
+
* runs for `storage=` sources, so colocated fakes never touch it.
|
|
133
|
+
*/
|
|
134
|
+
sourceDef?: unknown;
|
|
129
135
|
}): PersistSource {
|
|
130
136
|
const fields = opts.annotationFields;
|
|
131
137
|
return {
|
|
@@ -133,6 +139,7 @@ export function fakeSource(opts: {
|
|
|
133
139
|
sourceID: opts.name,
|
|
134
140
|
connectionName: opts.connectionName ?? "duckdb",
|
|
135
141
|
dialectName: opts.dialectName ?? "duckdb",
|
|
142
|
+
_sourceDef: opts.sourceDef ?? {},
|
|
136
143
|
makeBuildId: () => opts.sourceEntityId,
|
|
137
144
|
getSQL: (sqlOpts?: unknown) => {
|
|
138
145
|
opts.onGetSQL?.(sqlOpts);
|
|
@@ -886,6 +886,213 @@ describe("service/model", () => {
|
|
|
886
886
|
});
|
|
887
887
|
});
|
|
888
888
|
|
|
889
|
+
// A binding that declares `freshnessFallback=live` says the tier is an
|
|
890
|
+
// optimisation, not a dependency: a store that fails under a routed query
|
|
891
|
+
// degrades to serving live. The failure modes here are all silent-by-nature —
|
|
892
|
+
// a wrong row cap returns an EMPTY success, a mislabelled metric inflates the
|
|
893
|
+
// tier's headline KPI while it is broken — so they need assertions rather than
|
|
894
|
+
// an end-to-end observation.
|
|
895
|
+
describe("runtime storage failure → freshnessFallback=live", () => {
|
|
896
|
+
const originalMode = process.env.PERSIST_STORAGE_MODE;
|
|
897
|
+
const originalDefaultRows = process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT;
|
|
898
|
+
|
|
899
|
+
afterEach(() => {
|
|
900
|
+
sinon.restore();
|
|
901
|
+
for (const [name, original] of [
|
|
902
|
+
["PERSIST_STORAGE_MODE", originalMode],
|
|
903
|
+
["PUBLISHER_DEFAULT_QUERY_ROW_LIMIT", originalDefaultRows],
|
|
904
|
+
] as const) {
|
|
905
|
+
if (original === undefined) delete process.env[name];
|
|
906
|
+
else process.env[name] = original;
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
const binding = (sourceName: string, freshnessFallback?: string) =>
|
|
911
|
+
({
|
|
912
|
+
sourceName,
|
|
913
|
+
connectionName: "lake",
|
|
914
|
+
virtualHandle: `eid-${sourceName}`,
|
|
915
|
+
tablePath: `lake.t_${sourceName}`,
|
|
916
|
+
schema: [{ name: "region", type: "string" }],
|
|
917
|
+
freshnessFallback,
|
|
918
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
919
|
+
}) as any;
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* A Model routed to storage, where the storage runnable fails and the live
|
|
923
|
+
* one succeeds. `loadServeShapeQuery` is stubbed because the real one
|
|
924
|
+
* compiles a transient model — the logic under test is the catch, and the
|
|
925
|
+
* SHAPE'S bindings (not the package's) are what it must consult.
|
|
926
|
+
*/
|
|
927
|
+
function routedModel(opts: {
|
|
928
|
+
shapeBindings: unknown[];
|
|
929
|
+
packageBindings?: unknown[];
|
|
930
|
+
storageFailsAt: "prepare" | "run";
|
|
931
|
+
liveRunFails?: boolean;
|
|
932
|
+
livePreparedLimit?: number;
|
|
933
|
+
}) {
|
|
934
|
+
const storageErr = new Error("store table missing");
|
|
935
|
+
const storageRunnable = {
|
|
936
|
+
getPreparedResult:
|
|
937
|
+
opts.storageFailsAt === "prepare"
|
|
938
|
+
? sinon.stub().rejects(storageErr)
|
|
939
|
+
: sinon.stub().resolves({ resultExplore: { limit: 0 } }),
|
|
940
|
+
run:
|
|
941
|
+
opts.storageFailsAt === "run"
|
|
942
|
+
? sinon.stub().rejects(storageErr)
|
|
943
|
+
: sinon.stub().resolves({}),
|
|
944
|
+
};
|
|
945
|
+
const fakeResult = {
|
|
946
|
+
_queryResult: { data: { rawData: [] } },
|
|
947
|
+
totalRows: 1,
|
|
948
|
+
data: { value: [] },
|
|
949
|
+
connectionName: "live_pg",
|
|
950
|
+
};
|
|
951
|
+
const liveRun = opts.liveRunFails
|
|
952
|
+
? sinon.stub().rejects(new Error("warehouse down"))
|
|
953
|
+
: sinon.stub().resolves(fakeResult);
|
|
954
|
+
const liveRunnable = {
|
|
955
|
+
getPreparedResult: sinon.stub().resolves({
|
|
956
|
+
resultExplore: { limit: opts.livePreparedLimit ?? 0 },
|
|
957
|
+
}),
|
|
958
|
+
run: liveRun,
|
|
959
|
+
};
|
|
960
|
+
sinon
|
|
961
|
+
.stub(API.util, "wrapResult")
|
|
962
|
+
.returns({ rows: [] } as unknown as ReturnType<
|
|
963
|
+
typeof API.util.wrapResult
|
|
964
|
+
>);
|
|
965
|
+
const modelMaterializer = {
|
|
966
|
+
loadQuery: sinon.stub().returns(liveRunnable),
|
|
967
|
+
loadRestrictedQuery: sinon.stub().returns(liveRunnable),
|
|
968
|
+
};
|
|
969
|
+
const model = new Model(
|
|
970
|
+
packageName,
|
|
971
|
+
mockModelPath,
|
|
972
|
+
{},
|
|
973
|
+
"model",
|
|
974
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
975
|
+
modelMaterializer as any,
|
|
976
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
977
|
+
{ contents: {}, exports: [], queryList: [] } as any,
|
|
978
|
+
undefined,
|
|
979
|
+
undefined,
|
|
980
|
+
undefined,
|
|
981
|
+
undefined,
|
|
982
|
+
undefined,
|
|
983
|
+
);
|
|
984
|
+
process.env.PERSIST_STORAGE_MODE = "on";
|
|
985
|
+
model.setServeBindings(
|
|
986
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
987
|
+
(opts.packageBindings ?? opts.shapeBindings) as any,
|
|
988
|
+
);
|
|
989
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
990
|
+
model.setServeMalloyConfig({} as any);
|
|
991
|
+
sinon
|
|
992
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
993
|
+
.stub(model as any, "loadServeShapeQuery")
|
|
994
|
+
.resolves({
|
|
995
|
+
runnable: storageRunnable,
|
|
996
|
+
virtualMap: { v: "lake.t_x" },
|
|
997
|
+
bindings: opts.shapeBindings,
|
|
998
|
+
});
|
|
999
|
+
const recordStub = sinon.stub(
|
|
1000
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1001
|
+
(model as any).queryExecutionHistogram,
|
|
1002
|
+
"record",
|
|
1003
|
+
);
|
|
1004
|
+
return { model, liveRun, recordStub };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
it("caps the live retry by the LIVE row limit when the store fails at prepare", async () => {
|
|
1008
|
+
// `rowLimit` is assigned from the storage prepare, so a prepare-time
|
|
1009
|
+
// failure leaves it 0 — which the connector reads as "stop before the
|
|
1010
|
+
// first row", returning a successful EMPTY answer instead of the data.
|
|
1011
|
+
process.env.PUBLISHER_DEFAULT_QUERY_ROW_LIMIT = "250";
|
|
1012
|
+
const { model, liveRun } = routedModel({
|
|
1013
|
+
shapeBindings: [binding("daily", "live")],
|
|
1014
|
+
storageFailsAt: "prepare",
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
await model.getQueryResults(undefined, undefined, "run: daily -> x");
|
|
1018
|
+
|
|
1019
|
+
expect(liveRun.calledOnce).toBe(true);
|
|
1020
|
+
expect(liveRun.firstCall.args[0].rowLimit).toBe(250);
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
it("does not record a live-served answer as a storage hit", async () => {
|
|
1024
|
+
// The hit rate is the tier's headline KPI; counting a fallback as a hit
|
|
1025
|
+
// makes it RISE while the tier is broken.
|
|
1026
|
+
const { model, recordStub } = routedModel({
|
|
1027
|
+
shapeBindings: [binding("daily", "live")],
|
|
1028
|
+
storageFailsAt: "run",
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
await model.getQueryResults(undefined, undefined, "run: daily -> x");
|
|
1032
|
+
|
|
1033
|
+
const success = recordStub
|
|
1034
|
+
.getCalls()
|
|
1035
|
+
.map((c) => c.args[1] as Record<string, string>)
|
|
1036
|
+
.find((a) => a["malloy.model.query.status"] === "success");
|
|
1037
|
+
expect(success?.["malloy.model.query.served_from"]).toBe(
|
|
1038
|
+
"live_fallback",
|
|
1039
|
+
);
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
it("maps a failure of the live retry like any other query failure", async () => {
|
|
1043
|
+
// A broad outage takes the warehouse down alongside the store, so the
|
|
1044
|
+
// retry failing is ordinary — and an unmapped throw turns a clean 400
|
|
1045
|
+
// into a 500 with no error metric behind it.
|
|
1046
|
+
const { model, recordStub } = routedModel({
|
|
1047
|
+
shapeBindings: [binding("daily", "live")],
|
|
1048
|
+
storageFailsAt: "run",
|
|
1049
|
+
liveRunFails: true,
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
await expect(
|
|
1053
|
+
model.getQueryResults(undefined, undefined, "run: daily -> x"),
|
|
1054
|
+
).rejects.toThrow(BadRequestError);
|
|
1055
|
+
|
|
1056
|
+
const errored = recordStub
|
|
1057
|
+
.getCalls()
|
|
1058
|
+
.map((c) => c.args[1] as Record<string, string>)
|
|
1059
|
+
.find((a) => a["malloy.model.query.status"] === "error");
|
|
1060
|
+
expect(errored).toBeDefined();
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
it("decides on the SHAPE's bindings, not the package's", async () => {
|
|
1064
|
+
// Bindings are pushed package-wide and `freshnessFallback` is per entry,
|
|
1065
|
+
// so a mixed set is normal. A query whose shape carries only the `live`
|
|
1066
|
+
// binding must still degrade — a `stale_ok` sibling it never touched
|
|
1067
|
+
// cannot veto it.
|
|
1068
|
+
const { model, liveRun } = routedModel({
|
|
1069
|
+
shapeBindings: [binding("daily", "live")],
|
|
1070
|
+
packageBindings: [
|
|
1071
|
+
binding("daily", "live"),
|
|
1072
|
+
binding("other", "stale_ok"),
|
|
1073
|
+
],
|
|
1074
|
+
storageFailsAt: "run",
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
await model.getQueryResults(undefined, undefined, "run: daily -> x");
|
|
1078
|
+
|
|
1079
|
+
expect(liveRun.calledOnce).toBe(true);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
it("keeps surfacing the error when the shape carries a non-live binding", async () => {
|
|
1083
|
+
// `fail` and the `stale_ok` default are fail-closed: the caller asked to
|
|
1084
|
+
// hear about it rather than be served a slower answer.
|
|
1085
|
+
const { model } = routedModel({
|
|
1086
|
+
shapeBindings: [binding("daily", "stale_ok")],
|
|
1087
|
+
storageFailsAt: "run",
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
await expect(
|
|
1091
|
+
model.getQueryResults(undefined, undefined, "run: daily -> x"),
|
|
1092
|
+
).rejects.toThrow(BadRequestError);
|
|
1093
|
+
});
|
|
1094
|
+
});
|
|
1095
|
+
|
|
889
1096
|
describe("static methods", () => {
|
|
890
1097
|
describe("getModelRuntime", () => {
|
|
891
1098
|
it("should throw ModelNotFoundError for invalid modelPath", async () => {
|