@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.
Files changed (79) hide show
  1. package/README.docker.md +4 -0
  2. package/dist/app/api-doc.yaml +74 -3
  3. package/dist/app/assets/{EnvironmentPage-wa_EPkwK.js → EnvironmentPage-DXEaZIPx.js} +1 -1
  4. package/dist/app/assets/{HomePage-jnCrupQp.js → HomePage-kofsqpZt.js} +1 -1
  5. package/dist/app/assets/{LightMode-DYbwNULZ.js → LightMode-CNhIlIlJ.js} +1 -1
  6. package/dist/app/assets/{MainPage-CuJLrPNI.js → MainPage-Bgqo8jCy.js} +1 -1
  7. package/dist/app/assets/{MaterializationsPage-D_67x2ee.js → MaterializationsPage-CgBlgGz2.js} +1 -1
  8. package/dist/app/assets/{ModelPage-D5JtAWqR.js → ModelPage-B0TjoDtf.js} +1 -1
  9. package/dist/app/assets/{PackagePage-BRwtqUSG.js → PackagePage-BL8vnFj1.js} +1 -1
  10. package/dist/app/assets/{RouteError-CBNNrnSD.js → RouteError-BzPby0X2.js} +1 -1
  11. package/dist/app/assets/{ThemeEditorPage-CTCeBneA.js → ThemeEditorPage-CTEP_9r3.js} +1 -1
  12. package/dist/app/assets/{WorkbookPage-SN6f1RBm.js → WorkbookPage-BwM3BmKw.js} +1 -1
  13. package/dist/app/assets/{core-Dp3q5Ieu.es-CD5FvM2s.js → core-CK68iv6w.es-CpRxXBt7.js} +1 -1
  14. package/dist/app/assets/{index-C_tJstcx.js → index-B33zGctF.js} +15 -15
  15. package/dist/app/assets/{index-DU4r7GdU.js → index-BabP-V-S.js} +346 -321
  16. package/dist/app/assets/{index-B3Nn8Vm2.js → index-BkiWKaAF.js} +266 -265
  17. package/dist/app/assets/{index-BLCx1EdC.js → index-CmkW1MiE.js} +1 -1
  18. package/dist/app/assets/{index-CfmBVB6M.js → index-tXJXwdyj.js} +1 -1
  19. package/dist/app/index.html +1 -1
  20. package/dist/instrumentation.mjs +2 -0
  21. package/dist/package_load_worker.mjs +11 -1
  22. package/dist/server.mjs +3186 -433
  23. package/package.json +12 -12
  24. package/scripts/bake-duckdb-extensions.js +4 -1
  25. package/src/config.spec.ts +39 -0
  26. package/src/config.ts +135 -0
  27. package/src/controller/materialization.controller.spec.ts +62 -0
  28. package/src/controller/materialization.controller.ts +15 -0
  29. package/src/controller/package.controller.spec.ts +6 -0
  30. package/src/controller/package.controller.ts +7 -2
  31. package/src/errors.ts +19 -0
  32. package/src/logger.spec.ts +18 -1
  33. package/src/logger.ts +3 -1
  34. package/src/materialization_metrics.spec.ts +89 -4
  35. package/src/materialization_metrics.ts +155 -5
  36. package/src/mcp/skills/skills_bundle.json +1 -1
  37. package/src/mcp/tools/embedding_index.spec.ts +1236 -0
  38. package/src/mcp/tools/embedding_index.ts +808 -0
  39. package/src/mcp/tools/get_context_eval.ts +194 -45
  40. package/src/mcp/tools/get_context_tool.spec.ts +295 -2
  41. package/src/mcp/tools/get_context_tool.ts +159 -10
  42. package/src/pg_helpers.spec.ts +201 -0
  43. package/src/pg_helpers.ts +44 -5
  44. package/src/server.ts +24 -0
  45. package/src/service/build_plan.spec.ts +128 -2
  46. package/src/service/build_plan.ts +239 -17
  47. package/src/service/connection.ts +263 -7
  48. package/src/service/connection_config.spec.ts +48 -0
  49. package/src/service/connection_config.ts +21 -8
  50. package/src/service/connection_federation.spec.ts +184 -0
  51. package/src/service/embedding_provider.spec.ts +329 -0
  52. package/src/service/embedding_provider.ts +236 -0
  53. package/src/service/environment.ts +274 -12
  54. package/src/service/environment_store.spec.ts +678 -3
  55. package/src/service/environment_store.ts +449 -33
  56. package/src/service/environment_store_clone.spec.ts +350 -0
  57. package/src/service/manifest_loader.spec.ts +68 -13
  58. package/src/service/manifest_loader.ts +67 -19
  59. package/src/service/materialization_build_session.spec.ts +435 -0
  60. package/src/service/materialization_build_session.ts +681 -0
  61. package/src/service/materialization_eligibility.spec.ts +158 -0
  62. package/src/service/materialization_eligibility.ts +305 -0
  63. package/src/service/materialization_serve_transform.spec.ts +1003 -0
  64. package/src/service/materialization_serve_transform.ts +779 -0
  65. package/src/service/materialization_service.spec.ts +774 -7
  66. package/src/service/materialization_service.ts +1107 -42
  67. package/src/service/materialization_test_fixtures.ts +7 -0
  68. package/src/service/model.spec.ts +207 -0
  69. package/src/service/model.ts +540 -52
  70. package/src/service/model_storage_serve.spec.ts +193 -0
  71. package/src/service/model_storage_serve_joins.spec.ts +193 -0
  72. package/src/service/package.spec.ts +196 -0
  73. package/src/service/package.ts +385 -17
  74. package/src/service/persistence_policy.spec.ts +109 -0
  75. package/src/storage/duckdb/schema.ts +37 -0
  76. package/tests/fixtures/xlsx/database.xlsx +0 -0
  77. package/tests/integration/first_boot/readiness_line.integration.spec.ts +177 -0
  78. package/tests/integration/materialization/manifest_binding.integration.spec.ts +104 -0
  79. 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 () => {