@malloy-publisher/server 0.0.235 → 0.0.236
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 +47 -17
- package/dist/package_load_worker.mjs +8 -1
- package/dist/server.mjs +471 -54
- package/package.json +5 -2
- package/.eslintrc.json +0 -44
- package/.prettierignore +0 -1
- package/.prettierrc +0 -4
- package/build.ts +0 -97
- 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,148 +0,0 @@
|
|
|
1
|
-
import { Environment } from "../DatabaseInterface";
|
|
2
|
-
import { DuckDBConnection } from "./DuckDBConnection";
|
|
3
|
-
|
|
4
|
-
export class EnvironmentRepository {
|
|
5
|
-
constructor(private db: DuckDBConnection) {}
|
|
6
|
-
|
|
7
|
-
private generateId(): string {
|
|
8
|
-
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
private now(): Date {
|
|
12
|
-
return new Date();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async listEnvironments(): Promise<Environment[]> {
|
|
16
|
-
const rows = await this.db.all<Record<string, unknown>>(
|
|
17
|
-
"SELECT * FROM environments ORDER BY name",
|
|
18
|
-
);
|
|
19
|
-
return rows.map(this.mapToEnvironment);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async getEnvironmentById(id: string): Promise<Environment | null> {
|
|
23
|
-
const row = await this.db.get<Record<string, unknown>>(
|
|
24
|
-
"SELECT * FROM environments WHERE id = ?",
|
|
25
|
-
[id],
|
|
26
|
-
);
|
|
27
|
-
return row ? this.mapToEnvironment(row) : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async getEnvironmentByName(name: string): Promise<Environment | null> {
|
|
31
|
-
const row = await this.db.get<Record<string, unknown>>(
|
|
32
|
-
"SELECT * FROM environments WHERE name = ?",
|
|
33
|
-
[name],
|
|
34
|
-
);
|
|
35
|
-
return row ? this.mapToEnvironment(row) : null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async createEnvironment(
|
|
39
|
-
environment: Omit<Environment, "id" | "createdAt" | "updatedAt">,
|
|
40
|
-
): Promise<Environment> {
|
|
41
|
-
const id = this.generateId();
|
|
42
|
-
const now = this.now();
|
|
43
|
-
|
|
44
|
-
const params = [
|
|
45
|
-
id,
|
|
46
|
-
environment.name,
|
|
47
|
-
environment.path,
|
|
48
|
-
environment.description || null,
|
|
49
|
-
environment.metadata ? JSON.stringify(environment.metadata) : null,
|
|
50
|
-
now.toISOString(),
|
|
51
|
-
now.toISOString(),
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
await this.db.run(
|
|
56
|
-
`INSERT INTO environments (id, name, path, description, metadata, created_at, updated_at)
|
|
57
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
58
|
-
params,
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
id,
|
|
63
|
-
...environment,
|
|
64
|
-
createdAt: now,
|
|
65
|
-
updatedAt: now,
|
|
66
|
-
};
|
|
67
|
-
} catch (err: unknown) {
|
|
68
|
-
const error = err as Error;
|
|
69
|
-
// If unique constraint violation, return existing environment
|
|
70
|
-
if (
|
|
71
|
-
error.message?.includes("UNIQUE") ||
|
|
72
|
-
error.message?.includes("Constraint")
|
|
73
|
-
) {
|
|
74
|
-
const existing = await this.db.get<Record<string, unknown>>(
|
|
75
|
-
"SELECT * FROM environments WHERE name = ?",
|
|
76
|
-
[environment.name],
|
|
77
|
-
);
|
|
78
|
-
if (existing) {
|
|
79
|
-
console.log("Returning existing environment");
|
|
80
|
-
return this.mapToEnvironment(existing);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async updateEnvironment(
|
|
88
|
-
id: string,
|
|
89
|
-
updates: Partial<Environment>,
|
|
90
|
-
): Promise<Environment> {
|
|
91
|
-
const existing = await this.getEnvironmentById(id);
|
|
92
|
-
if (!existing) {
|
|
93
|
-
throw new Error(`Environment with id ${id} not found`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const now = this.now();
|
|
97
|
-
const setClauses: string[] = [];
|
|
98
|
-
const params: unknown[] = [];
|
|
99
|
-
|
|
100
|
-
if (updates.name !== undefined && updates.name !== existing.name) {
|
|
101
|
-
setClauses.push(`name = ?`);
|
|
102
|
-
params.push(updates.name);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
if (updates.path !== undefined && updates.path !== existing.path) {
|
|
106
|
-
setClauses.push(`path = ?`);
|
|
107
|
-
params.push(updates.path);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (updates.description !== undefined) {
|
|
111
|
-
setClauses.push(`description = ?`);
|
|
112
|
-
params.push(updates.description);
|
|
113
|
-
}
|
|
114
|
-
if (updates.metadata !== undefined) {
|
|
115
|
-
setClauses.push(`metadata = ?`);
|
|
116
|
-
params.push(JSON.stringify(updates.metadata));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
setClauses.push(`updated_at = ?`);
|
|
120
|
-
params.push(now.toISOString());
|
|
121
|
-
params.push(id);
|
|
122
|
-
|
|
123
|
-
await this.db.run(
|
|
124
|
-
`UPDATE environments SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
125
|
-
params,
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
return this.getEnvironmentById(id) as Promise<Environment>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
async deleteEnvironment(id: string): Promise<void> {
|
|
132
|
-
await this.db.run("DELETE FROM environments WHERE id = ?", [id]);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
private mapToEnvironment(row: Record<string, unknown>): Environment {
|
|
136
|
-
return {
|
|
137
|
-
id: row.id as string,
|
|
138
|
-
name: row.name as string,
|
|
139
|
-
path: row.path as string,
|
|
140
|
-
description: row.description as string | undefined,
|
|
141
|
-
metadata: row.metadata
|
|
142
|
-
? JSON.parse(row.metadata as string)
|
|
143
|
-
: undefined,
|
|
144
|
-
createdAt: new Date(row.created_at as string),
|
|
145
|
-
updatedAt: new Date(row.updated_at as string),
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { DuckDBConnection } from "./DuckDBConnection";
|
|
3
|
-
import { MaterializationRepository } from "./MaterializationRepository";
|
|
4
|
-
import { initializeSchema } from "./schema";
|
|
5
|
-
|
|
6
|
-
// Capture the SQL + params of reads (all) and writes (run) without touching a
|
|
7
|
-
// real DuckDB.
|
|
8
|
-
function fakeRepo() {
|
|
9
|
-
const calls: Array<{ sql: string; params: unknown[] }> = [];
|
|
10
|
-
const runCalls: Array<{ sql: string; params: unknown[] }> = [];
|
|
11
|
-
const db = {
|
|
12
|
-
all: async (sql: string, params: unknown[]) => {
|
|
13
|
-
calls.push({ sql, params });
|
|
14
|
-
return [];
|
|
15
|
-
},
|
|
16
|
-
run: async (sql: string, params: unknown[]) => {
|
|
17
|
-
runCalls.push({ sql, params });
|
|
18
|
-
},
|
|
19
|
-
} as unknown as DuckDBConnection;
|
|
20
|
-
return { repo: new MaterializationRepository(db), calls, runCalls };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe("MaterializationRepository list bounds", () => {
|
|
24
|
-
it("per-package list is unbounded when no limit is given (no LIMIT clause)", async () => {
|
|
25
|
-
const { repo, calls } = fakeRepo();
|
|
26
|
-
await repo.list("env-1", "pkg-a");
|
|
27
|
-
expect(calls[0].sql).not.toContain("LIMIT");
|
|
28
|
-
expect(calls[0].sql).not.toContain("OFFSET");
|
|
29
|
-
expect(calls[0].params).toEqual(["env-1", "pkg-a"]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("per-package list honors an explicit limit/offset", async () => {
|
|
33
|
-
const { repo, calls } = fakeRepo();
|
|
34
|
-
await repo.list("env-1", "pkg-a", { limit: 5, offset: 10 });
|
|
35
|
-
expect(calls[0].sql).toContain("LIMIT ?");
|
|
36
|
-
expect(calls[0].sql).toContain("OFFSET ?");
|
|
37
|
-
expect(calls[0].params).toEqual(["env-1", "pkg-a", 5, 10]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("environment-scoped list always applies a default cap (500)", async () => {
|
|
41
|
-
const { repo, calls } = fakeRepo();
|
|
42
|
-
await repo.listByEnvironment("env-1");
|
|
43
|
-
expect(calls[0].sql).toContain("LIMIT ?");
|
|
44
|
-
expect(calls[0].params).toEqual(["env-1", 500]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("environment-scoped list honors an explicit limit over the default", async () => {
|
|
48
|
-
const { repo, calls } = fakeRepo();
|
|
49
|
-
await repo.listByEnvironment("env-1", { limit: 25 });
|
|
50
|
-
expect(calls[0].params).toEqual(["env-1", 25]);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// The restart-recovery anchor (MaterializationScheduler.recoveryAnchor) hangs on
|
|
55
|
-
// getLatestScheduledFireAt's `json_extract_string(metadata,'$.trigger')` filter,
|
|
56
|
-
// which a fake db can't exercise. Drive the real query against an in-memory
|
|
57
|
-
// DuckDB so the trigger filter and ORDER BY are covered, not just the SQL shape.
|
|
58
|
-
describe("MaterializationRepository.getLatestScheduledFireAt (real DuckDB)", () => {
|
|
59
|
-
const ENV_ID = "env-sched";
|
|
60
|
-
const PKG = "pkg-a";
|
|
61
|
-
const dbs: DuckDBConnection[] = [];
|
|
62
|
-
|
|
63
|
-
afterEach(async () => {
|
|
64
|
-
while (dbs.length) await dbs.pop()!.close();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
async function freshRepo(): Promise<{
|
|
68
|
-
db: DuckDBConnection;
|
|
69
|
-
repo: MaterializationRepository;
|
|
70
|
-
}> {
|
|
71
|
-
const db = new DuckDBConnection(":memory:");
|
|
72
|
-
dbs.push(db);
|
|
73
|
-
await db.initialize();
|
|
74
|
-
await initializeSchema(db);
|
|
75
|
-
// materializations.environment_id is a FK to environments(id).
|
|
76
|
-
await db.run(
|
|
77
|
-
`INSERT INTO environments (id, name, path, created_at, updated_at)
|
|
78
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
79
|
-
[
|
|
80
|
-
ENV_ID,
|
|
81
|
-
"sched-env",
|
|
82
|
-
"/tmp/sched-env",
|
|
83
|
-
"2026-01-01T00:00:00.000Z",
|
|
84
|
-
"2026-01-01T00:00:00.000Z",
|
|
85
|
-
],
|
|
86
|
-
);
|
|
87
|
-
return { db, repo: new MaterializationRepository(db) };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Insert a terminal run directly (active_key NULL so repeated inserts for the
|
|
91
|
-
// same package don't trip the unique active-key index). `trigger === undefined`
|
|
92
|
-
// stores metadata NULL; `null` stores a trigger-less object; a string stores
|
|
93
|
-
// that trigger.
|
|
94
|
-
async function insertRun(
|
|
95
|
-
db: DuckDBConnection,
|
|
96
|
-
id: string,
|
|
97
|
-
createdAt: string,
|
|
98
|
-
trigger: string | null | undefined,
|
|
99
|
-
): Promise<void> {
|
|
100
|
-
const metadata =
|
|
101
|
-
trigger === undefined
|
|
102
|
-
? null
|
|
103
|
-
: JSON.stringify(
|
|
104
|
-
trigger === null ? { note: "manual" } : { trigger },
|
|
105
|
-
);
|
|
106
|
-
await db.run(
|
|
107
|
-
`INSERT INTO materializations
|
|
108
|
-
(id, environment_id, package_name, status, active_key, metadata, manifest, created_at, updated_at)
|
|
109
|
-
VALUES (?, ?, ?, 'MANIFEST_FILE_READY', NULL, ?, NULL, ?, ?)`,
|
|
110
|
-
[id, ENV_ID, PKG, metadata, createdAt, createdAt],
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const day = (d: string) => `2026-${d}T12:00:00.000Z`;
|
|
115
|
-
|
|
116
|
-
it("returns the created_at of the newest SCHEDULER run", async () => {
|
|
117
|
-
const { db, repo } = await freshRepo();
|
|
118
|
-
await insertRun(db, "s1", day("01-15"), "SCHEDULER");
|
|
119
|
-
await insertRun(db, "s2", day("03-15"), "SCHEDULER");
|
|
120
|
-
const at = await repo.getLatestScheduledFireAt(ENV_ID, PKG);
|
|
121
|
-
expect(at).not.toBeNull();
|
|
122
|
-
expect(at!.toISOString().slice(0, 10)).toBe("2026-03-15");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("ignores ON_DEMAND and trigger-less runs even when they are newer", async () => {
|
|
126
|
-
const { db, repo } = await freshRepo();
|
|
127
|
-
await insertRun(db, "s1", day("02-01"), "SCHEDULER"); // the only match
|
|
128
|
-
await insertRun(db, "d1", day("06-01"), "ON_DEMAND"); // newer, wrong trigger
|
|
129
|
-
await insertRun(db, "n1", day("07-01"), null); // newer, no trigger key
|
|
130
|
-
await insertRun(db, "m1", day("08-01"), undefined); // newer, metadata NULL
|
|
131
|
-
const at = await repo.getLatestScheduledFireAt(ENV_ID, PKG);
|
|
132
|
-
expect(at).not.toBeNull();
|
|
133
|
-
expect(at!.toISOString().slice(0, 10)).toBe("2026-02-01");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("returns null when the package has no SCHEDULER run", async () => {
|
|
137
|
-
const { db, repo } = await freshRepo();
|
|
138
|
-
await insertRun(db, "d1", day("06-01"), "ON_DEMAND");
|
|
139
|
-
await insertRun(db, "m1", day("07-01"), undefined);
|
|
140
|
-
expect(await repo.getLatestScheduledFireAt(ENV_ID, PKG)).toBeNull();
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("scopes to the requested package (ignores another package's SCHEDULER run)", async () => {
|
|
144
|
-
const { db, repo } = await freshRepo();
|
|
145
|
-
await db.run(
|
|
146
|
-
`INSERT INTO materializations
|
|
147
|
-
(id, environment_id, package_name, status, active_key, metadata, manifest, created_at, updated_at)
|
|
148
|
-
VALUES (?, ?, 'other-pkg', 'MANIFEST_FILE_READY', NULL, ?, NULL, ?, ?)`,
|
|
149
|
-
[
|
|
150
|
-
"x1",
|
|
151
|
-
ENV_ID,
|
|
152
|
-
JSON.stringify({ trigger: "SCHEDULER" }),
|
|
153
|
-
day("09-01"),
|
|
154
|
-
day("09-01"),
|
|
155
|
-
],
|
|
156
|
-
);
|
|
157
|
-
expect(await repo.getLatestScheduledFireAt(ENV_ID, PKG)).toBeNull();
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// The publisher tracks materialization *records*; dropping the physical tables
|
|
162
|
-
// is opt-in (deleteMaterialization({ dropTables })) and otherwise the caller's
|
|
163
|
-
// responsibility. These cascade deletes therefore remove records only — they
|
|
164
|
-
// must never issue DDL — so a materialized table intentionally outlives the
|
|
165
|
-
// record when an environment or package is deleted. Locking that in guards the
|
|
166
|
-
// contract against a future refactor that "helpfully" adds a DROP here (which
|
|
167
|
-
// would fire against a possibly-already-torn-down connection on env delete).
|
|
168
|
-
//
|
|
169
|
-
// This pins DDL out of the low-level record deletes; it does not close the
|
|
170
|
-
// standalone GC gap (#901: nothing drops those tables today, so they accumulate
|
|
171
|
-
// in the warehouse). #901's best-effort drop-before-teardown lives ABOVE these
|
|
172
|
-
// methods, so adding it completes the design and leaves this test green.
|
|
173
|
-
describe("MaterializationRepository cascade deletes are records-only", () => {
|
|
174
|
-
it("deleteByEnvironmentId issues a single DELETE and no DDL", async () => {
|
|
175
|
-
const { repo, runCalls } = fakeRepo();
|
|
176
|
-
await repo.deleteByEnvironmentId("env-1");
|
|
177
|
-
expect(runCalls.length).toBe(1);
|
|
178
|
-
expect(runCalls[0].sql).toContain("DELETE FROM materializations");
|
|
179
|
-
expect(runCalls[0].sql).not.toContain("DROP");
|
|
180
|
-
expect(runCalls[0].params).toEqual(["env-1"]);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("deleteByPackage issues a single DELETE and no DDL", async () => {
|
|
184
|
-
const { repo, runCalls } = fakeRepo();
|
|
185
|
-
await repo.deleteByPackage("env-1", "pkg-a");
|
|
186
|
-
expect(runCalls.length).toBe(1);
|
|
187
|
-
expect(runCalls[0].sql).toContain("DELETE FROM materializations");
|
|
188
|
-
expect(runCalls[0].sql).not.toContain("DROP");
|
|
189
|
-
expect(runCalls[0].params).toEqual(["env-1", "pkg-a"]);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
BuildManifestResult,
|
|
3
|
-
Materialization,
|
|
4
|
-
MaterializationStatus,
|
|
5
|
-
MaterializationUpdate,
|
|
6
|
-
} from "../DatabaseInterface";
|
|
7
|
-
import { DuckDBConnection } from "./DuckDBConnection";
|
|
8
|
-
|
|
9
|
-
const TERMINAL_STATUSES: ReadonlySet<MaterializationStatus> = new Set([
|
|
10
|
-
"MANIFEST_FILE_READY",
|
|
11
|
-
"FAILED",
|
|
12
|
-
"CANCELLED",
|
|
13
|
-
]);
|
|
14
|
-
|
|
15
|
-
/** Non-terminal statuses count as "active" for the single-active guard. */
|
|
16
|
-
const ACTIVE_STATUSES: readonly MaterializationStatus[] = [
|
|
17
|
-
"PENDING",
|
|
18
|
-
"MANIFEST_ROWS_READY",
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
function activeKeyFor(environmentId: string, packageName: string): string {
|
|
22
|
-
return `${environmentId}|${packageName}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Default cap for the environment-wide materialization list. That query spans
|
|
27
|
-
* every package in the environment, so on a busy env (e.g. a frequent schedule)
|
|
28
|
-
* the history grows without bound; when a caller passes no explicit limit, fall
|
|
29
|
-
* back to this bound rather than loading and serializing every row. Callers that
|
|
30
|
-
* page (the SDK "show more") always pass their own limit and are unaffected.
|
|
31
|
-
*/
|
|
32
|
-
const DEFAULT_ENVIRONMENT_LIST_LIMIT = 500;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Thrown when an atomic insert loses a race on (environment, package) active
|
|
36
|
-
* materialization. Surfaced separately from a generic DB error so the service
|
|
37
|
-
* layer can translate to `MaterializationConflictError`.
|
|
38
|
-
*/
|
|
39
|
-
export class DuplicateActiveMaterializationError extends Error {
|
|
40
|
-
constructor(environmentId: string, packageName: string) {
|
|
41
|
-
super(
|
|
42
|
-
`Active materialization already exists for (${environmentId}, ${packageName})`,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* DuckDB-backed repository for package materializations.
|
|
49
|
-
*
|
|
50
|
-
* A Materialization tracks a single build run for an (environment, package)
|
|
51
|
-
* pair through its lifecycle: PENDING -> MANIFEST_ROWS_READY ->
|
|
52
|
-
* MANIFEST_FILE_READY, or -> FAILED | CANCELLED.
|
|
53
|
-
*/
|
|
54
|
-
export class MaterializationRepository {
|
|
55
|
-
constructor(private db: DuckDBConnection) {}
|
|
56
|
-
|
|
57
|
-
private generateId(): string {
|
|
58
|
-
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private now(): Date {
|
|
62
|
-
return new Date();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Per-package list: intentionally UNBOUNDED when no limit is passed —
|
|
66
|
-
// existing callers may rely on the full history for a single package. Only
|
|
67
|
-
// the environment-wide `listByEnvironment` (which spans every package)
|
|
68
|
-
// applies a default cap; do not add one here.
|
|
69
|
-
async list(
|
|
70
|
-
environmentId: string,
|
|
71
|
-
packageName: string,
|
|
72
|
-
options?: { limit?: number; offset?: number },
|
|
73
|
-
): Promise<Materialization[]> {
|
|
74
|
-
let sql =
|
|
75
|
-
"SELECT * FROM materializations WHERE environment_id = ? AND package_name = ? ORDER BY created_at DESC";
|
|
76
|
-
const params: unknown[] = [environmentId, packageName];
|
|
77
|
-
if (options?.limit !== undefined) {
|
|
78
|
-
sql += " LIMIT ?";
|
|
79
|
-
params.push(options.limit);
|
|
80
|
-
}
|
|
81
|
-
if (options?.offset !== undefined) {
|
|
82
|
-
sql += " OFFSET ?";
|
|
83
|
-
params.push(options.offset);
|
|
84
|
-
}
|
|
85
|
-
const rows = await this.db.all<Record<string, unknown>>(sql, params);
|
|
86
|
-
return rows.map(this.mapRow);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** All materializations across every package in an environment, newest first. */
|
|
90
|
-
async listByEnvironment(
|
|
91
|
-
environmentId: string,
|
|
92
|
-
options?: { limit?: number; offset?: number },
|
|
93
|
-
): Promise<Materialization[]> {
|
|
94
|
-
let sql =
|
|
95
|
-
"SELECT * FROM materializations WHERE environment_id = ? ORDER BY created_at DESC";
|
|
96
|
-
const params: unknown[] = [environmentId];
|
|
97
|
-
// Always bound the result: an uncapped env-wide fetch is unbounded.
|
|
98
|
-
sql += " LIMIT ?";
|
|
99
|
-
params.push(options?.limit ?? DEFAULT_ENVIRONMENT_LIST_LIMIT);
|
|
100
|
-
if (options?.offset !== undefined) {
|
|
101
|
-
sql += " OFFSET ?";
|
|
102
|
-
params.push(options.offset);
|
|
103
|
-
}
|
|
104
|
-
const rows = await this.db.all<Record<string, unknown>>(sql, params);
|
|
105
|
-
return rows.map(this.mapRow);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* `created_at` of the newest scheduler-fired (metadata.trigger=SCHEDULER)
|
|
110
|
-
* materialization for (env, package), or null if none. The standalone
|
|
111
|
-
* scheduler uses this to recover a missed occurrence across a restart:
|
|
112
|
-
* re-anchoring `nextFireAt` to `nextAfter(lastScheduledFire)` fires exactly
|
|
113
|
-
* one catch-up after downtime (then jumps forward), rather than skipping the
|
|
114
|
-
* occurrence entirely. `metadata` is a JSON column, so the trigger is read
|
|
115
|
-
* with `json_extract_string`.
|
|
116
|
-
*/
|
|
117
|
-
async getLatestScheduledFireAt(
|
|
118
|
-
environmentId: string,
|
|
119
|
-
packageName: string,
|
|
120
|
-
): Promise<Date | null> {
|
|
121
|
-
const row = await this.db.get<{ created_at: unknown }>(
|
|
122
|
-
`SELECT created_at FROM materializations
|
|
123
|
-
WHERE environment_id = ? AND package_name = ?
|
|
124
|
-
AND json_extract_string(metadata, '$.trigger') = 'SCHEDULER'
|
|
125
|
-
ORDER BY created_at DESC
|
|
126
|
-
LIMIT 1`,
|
|
127
|
-
[environmentId, packageName],
|
|
128
|
-
);
|
|
129
|
-
return row?.created_at ? new Date(row.created_at as string) : null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async getById(id: string): Promise<Materialization | null> {
|
|
133
|
-
const row = await this.db.get<Record<string, unknown>>(
|
|
134
|
-
"SELECT * FROM materializations WHERE id = ?",
|
|
135
|
-
[id],
|
|
136
|
-
);
|
|
137
|
-
return row ? this.mapRow(row) : null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async getActive(
|
|
141
|
-
environmentId: string,
|
|
142
|
-
packageName: string,
|
|
143
|
-
): Promise<Materialization | null> {
|
|
144
|
-
const placeholders = ACTIVE_STATUSES.map(() => "?").join(", ");
|
|
145
|
-
const row = await this.db.get<Record<string, unknown>>(
|
|
146
|
-
`SELECT * FROM materializations WHERE environment_id = ? AND package_name = ? AND status IN (${placeholders})`,
|
|
147
|
-
[environmentId, packageName, ...ACTIVE_STATUSES],
|
|
148
|
-
);
|
|
149
|
-
return row ? this.mapRow(row) : null;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async create(
|
|
153
|
-
environmentId: string,
|
|
154
|
-
packageName: string,
|
|
155
|
-
status: MaterializationStatus = "PENDING",
|
|
156
|
-
metadata: Record<string, unknown> | null = null,
|
|
157
|
-
): Promise<Materialization> {
|
|
158
|
-
const id = this.generateId();
|
|
159
|
-
const now = this.now();
|
|
160
|
-
const iso = now.toISOString();
|
|
161
|
-
// Set active_key iff the row is in a non-terminal state. The unique
|
|
162
|
-
// index on active_key makes the race-free conditional insert: a second
|
|
163
|
-
// concurrent create on the same (environment, package) fails here rather
|
|
164
|
-
// than in a check-then-write window.
|
|
165
|
-
const activeKey = TERMINAL_STATUSES.has(status)
|
|
166
|
-
? null
|
|
167
|
-
: activeKeyFor(environmentId, packageName);
|
|
168
|
-
const metadataJson = metadata ? JSON.stringify(metadata) : null;
|
|
169
|
-
|
|
170
|
-
try {
|
|
171
|
-
const rows = await this.db.all<Record<string, unknown>>(
|
|
172
|
-
`INSERT INTO materializations (id, environment_id, package_name, status, active_key, metadata, manifest, created_at, updated_at)
|
|
173
|
-
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?)
|
|
174
|
-
RETURNING *`,
|
|
175
|
-
[
|
|
176
|
-
id,
|
|
177
|
-
environmentId,
|
|
178
|
-
packageName,
|
|
179
|
-
status,
|
|
180
|
-
activeKey,
|
|
181
|
-
metadataJson,
|
|
182
|
-
iso,
|
|
183
|
-
iso,
|
|
184
|
-
],
|
|
185
|
-
);
|
|
186
|
-
return this.mapRow(rows[0]);
|
|
187
|
-
} catch (err) {
|
|
188
|
-
if (isUniqueViolation(err, "idx_materializations_active_key")) {
|
|
189
|
-
throw new DuplicateActiveMaterializationError(
|
|
190
|
-
environmentId,
|
|
191
|
-
packageName,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
throw err;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async update(
|
|
199
|
-
id: string,
|
|
200
|
-
updates: MaterializationUpdate,
|
|
201
|
-
): Promise<Materialization> {
|
|
202
|
-
const now = this.now();
|
|
203
|
-
const setClauses: string[] = [];
|
|
204
|
-
const params: unknown[] = [];
|
|
205
|
-
|
|
206
|
-
if (updates.status !== undefined) {
|
|
207
|
-
setClauses.push(`status = ?`);
|
|
208
|
-
params.push(updates.status);
|
|
209
|
-
// Clear active_key on any transition to a terminal state; set it on
|
|
210
|
-
// any transition to a non-terminal state. The unique index
|
|
211
|
-
// guarantees we can never end up with two active rows for the same
|
|
212
|
-
// (environment, package).
|
|
213
|
-
if (TERMINAL_STATUSES.has(updates.status)) {
|
|
214
|
-
setClauses.push(`active_key = NULL`);
|
|
215
|
-
} else {
|
|
216
|
-
setClauses.push(
|
|
217
|
-
`active_key = environment_id || '|' || package_name`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (updates.startedAt !== undefined) {
|
|
222
|
-
setClauses.push(`started_at = ?`);
|
|
223
|
-
params.push(updates.startedAt.toISOString());
|
|
224
|
-
}
|
|
225
|
-
if (updates.completedAt !== undefined) {
|
|
226
|
-
setClauses.push(`completed_at = ?`);
|
|
227
|
-
params.push(updates.completedAt.toISOString());
|
|
228
|
-
}
|
|
229
|
-
if (updates.error !== undefined) {
|
|
230
|
-
setClauses.push(`error = ?`);
|
|
231
|
-
params.push(updates.error);
|
|
232
|
-
}
|
|
233
|
-
if (updates.metadata !== undefined) {
|
|
234
|
-
setClauses.push(`metadata = ?`);
|
|
235
|
-
params.push(
|
|
236
|
-
updates.metadata ? JSON.stringify(updates.metadata) : null,
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
if (updates.manifest !== undefined) {
|
|
240
|
-
setClauses.push(`manifest = ?`);
|
|
241
|
-
params.push(
|
|
242
|
-
updates.manifest ? JSON.stringify(updates.manifest) : null,
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
setClauses.push(`updated_at = ?`);
|
|
247
|
-
params.push(now.toISOString());
|
|
248
|
-
params.push(id);
|
|
249
|
-
|
|
250
|
-
await this.db.run(
|
|
251
|
-
`UPDATE materializations SET ${setClauses.join(", ")} WHERE id = ?`,
|
|
252
|
-
params,
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
const updated = await this.getById(id);
|
|
256
|
-
if (!updated) {
|
|
257
|
-
throw new Error(`Materialization ${id} not found after update`);
|
|
258
|
-
}
|
|
259
|
-
return updated;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
async deleteById(id: string): Promise<void> {
|
|
263
|
-
await this.db.run("DELETE FROM materializations WHERE id = ?", [id]);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Cascade deletes remove the publisher's records only; they intentionally
|
|
267
|
-
// issue no DDL. Dropping the physical tables is opt-in
|
|
268
|
-
// (deleteMaterialization({ dropTables })) and otherwise the caller's
|
|
269
|
-
// responsibility, so a materialized table outlives its record when an
|
|
270
|
-
// environment or package is deleted. Adding a DROP here would also risk
|
|
271
|
-
// firing against a connection already torn down by the delete path.
|
|
272
|
-
//
|
|
273
|
-
// Standalone caveat (#901): with no control plane owning physical GC,
|
|
274
|
-
// nothing drops those tables today, so they accumulate in the warehouse.
|
|
275
|
-
// That is an open follow-up, not a settled contract — its best-effort
|
|
276
|
-
// drop-before-teardown belongs ABOVE these methods, so it completes this
|
|
277
|
-
// design rather than moving DDL into the low-level record deletes.
|
|
278
|
-
async deleteByEnvironmentId(environmentId: string): Promise<void> {
|
|
279
|
-
await this.db.run(
|
|
280
|
-
"DELETE FROM materializations WHERE environment_id = ?",
|
|
281
|
-
[environmentId],
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async deleteByPackage(
|
|
286
|
-
environmentId: string,
|
|
287
|
-
packageName: string,
|
|
288
|
-
): Promise<void> {
|
|
289
|
-
await this.db.run(
|
|
290
|
-
"DELETE FROM materializations WHERE environment_id = ? AND package_name = ?",
|
|
291
|
-
[environmentId, packageName],
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
private mapRow(row: Record<string, unknown>): Materialization {
|
|
296
|
-
const metadata = parseJsonColumn<Record<string, unknown>>(row.metadata);
|
|
297
|
-
return {
|
|
298
|
-
id: row.id as string,
|
|
299
|
-
environmentId: row.environment_id as string,
|
|
300
|
-
packageName: row.package_name as string,
|
|
301
|
-
status: row.status as MaterializationStatus,
|
|
302
|
-
manifest: parseJsonColumn<BuildManifestResult>(row.manifest),
|
|
303
|
-
metadata,
|
|
304
|
-
startedAt: row.started_at ? new Date(row.started_at as string) : null,
|
|
305
|
-
completedAt: row.completed_at
|
|
306
|
-
? new Date(row.completed_at as string)
|
|
307
|
-
: null,
|
|
308
|
-
error: row.error != null ? (row.error as string) : null,
|
|
309
|
-
createdAt: new Date(row.created_at as string),
|
|
310
|
-
updatedAt: new Date(row.updated_at as string),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/** Parse a nullable JSON text column, returning null on absence or parse error. */
|
|
316
|
-
function parseJsonColumn<T>(value: unknown): T | null {
|
|
317
|
-
if (value == null) return null;
|
|
318
|
-
try {
|
|
319
|
-
return JSON.parse(value as string) as T;
|
|
320
|
-
} catch {
|
|
321
|
-
return null;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* DuckDB surfaces unique-constraint violations as plain Errors whose message
|
|
327
|
-
* mentions the violated index. We match on the index name rather than a
|
|
328
|
-
* generic substring so we don't misclassify unrelated constraint errors.
|
|
329
|
-
*/
|
|
330
|
-
function isUniqueViolation(err: unknown, indexName: string): boolean {
|
|
331
|
-
if (!(err instanceof Error)) return false;
|
|
332
|
-
const msg = err.message;
|
|
333
|
-
return (
|
|
334
|
-
msg.includes(indexName) || /duplicate key|unique constraint/i.test(msg)
|
|
335
|
-
);
|
|
336
|
-
}
|