gaard-api 0.2.14__tar.gz → 0.2.17__tar.gz

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 (82) hide show
  1. {gaard_api-0.2.14/src/gaard_api.egg-info → gaard_api-0.2.17}/PKG-INFO +5 -5
  2. {gaard_api-0.2.14 → gaard_api-0.2.17}/pyproject.toml +5 -5
  3. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/database_initial.sql +52 -0
  4. gaard_api-0.2.17/src/gaard_api/admin/database_updates.sql +73 -0
  5. gaard_api-0.2.17/src/gaard_api/admin/metadata.py +53 -0
  6. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/migration_runner.py +38 -4
  7. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/models.py +122 -0
  8. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/services.py +16 -0
  9. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/assets/main.js +38 -26
  10. gaard_api-0.2.17/src/gaard_api/analysis_findings.py +544 -0
  11. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/admin.py +165 -60
  12. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/analysis.py +820 -21
  13. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/dashboards.py +8 -14
  14. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/query.py +34 -20
  15. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/core/settings.py +6 -0
  16. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/main.py +1 -1
  17. {gaard_api-0.2.14 → gaard_api-0.2.17/src/gaard_api.egg-info}/PKG-INFO +5 -5
  18. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api.egg-info/SOURCES.txt +2 -0
  19. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api.egg-info/requires.txt +4 -4
  20. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_admin_api.py +653 -11
  21. gaard_api-0.2.17/tests/test_analysis_api.py +1156 -0
  22. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_database_migrations.py +128 -1
  23. gaard_api-0.2.14/src/gaard_api/admin/database_updates.sql +0 -23
  24. gaard_api-0.2.14/tests/test_analysis_api.py +0 -586
  25. {gaard_api-0.2.14 → gaard_api-0.2.17}/MANIFEST.in +0 -0
  26. {gaard_api-0.2.14 → gaard_api-0.2.17}/README.md +0 -0
  27. {gaard_api-0.2.14 → gaard_api-0.2.17}/setup.cfg +0 -0
  28. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/__init__.py +0 -0
  29. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/__init__.py +0 -0
  30. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/database.py +0 -0
  31. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/database_legacy_updates.sql +0 -0
  32. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/defaults.py +0 -0
  33. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/prompt_runtime.py +0 -0
  34. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin/security.py +0 -0
  35. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/assets/getgaard.svg +0 -0
  36. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/assets/identity.js +0 -0
  37. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/assets/styles.css +0 -0
  38. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack-all.js +0 -0
  39. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/assets/vendor/gridstack/gridstack.min.css +0 -0
  40. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/index.html +0 -0
  41. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/admin-web/package.json +0 -0
  42. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/__init__.py +0 -0
  43. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/__init__.py +0 -0
  44. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/prompts.py +0 -0
  45. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api/v1/schema.py +0 -0
  46. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/api_registry.py +0 -0
  47. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/auth_dependencies.py +0 -0
  48. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/auth_hooks.py +0 -0
  49. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/cli.py +0 -0
  50. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/cli_commands.py +0 -0
  51. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/conversations.py +0 -0
  52. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/core/__init__.py +0 -0
  53. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/core/error_handlers.py +0 -0
  54. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/core/schema_cache.py +0 -0
  55. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/example_data/__init__.py +0 -0
  56. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/example_data/medical_poc/__init__.py +0 -0
  57. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/example_data/medical_poc/schema.sql +0 -0
  58. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/example_data/medical_poc/seed.sql +0 -0
  59. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/example_database.py +0 -0
  60. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/extension_services.py +0 -0
  61. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/extensions.py +0 -0
  62. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/license.py +0 -0
  63. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/package_updates.py +0 -0
  64. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/py.typed +0 -0
  65. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/query_hooks.py +0 -0
  66. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/server_cli.py +0 -0
  67. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/siem.py +0 -0
  68. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api/tls_http.py +0 -0
  69. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api.egg-info/dependency_links.txt +0 -0
  70. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api.egg-info/entry_points.txt +0 -0
  71. {gaard_api-0.2.14 → gaard_api-0.2.17}/src/gaard_api.egg-info/top_level.txt +0 -0
  72. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_api_cli.py +0 -0
  73. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_api_registry.py +0 -0
  74. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_conversation_api.py +0 -0
  75. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_error_handlers.py +0 -0
  76. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_license_api.py +0 -0
  77. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_license_service.py +0 -0
  78. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_prompt_runtime.py +0 -0
  79. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_query_error_messages.py +0 -0
  80. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_server_cli.py +0 -0
  81. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_settings.py +0 -0
  82. {gaard_api-0.2.14 → gaard_api-0.2.17}/tests/test_tls_http.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gaard-api
3
- Version: 0.2.14
3
+ Version: 0.2.17
4
4
  Summary: GAARD backend web services providing admin interface
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: gaard-core==0.2.11
8
- Requires-Dist: gaard-connectors==0.2.11
9
- Requires-Dist: gaard-llm==0.2.11
10
- Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.11
7
+ Requires-Dist: gaard-core==0.2.17
8
+ Requires-Dist: gaard-connectors==0.2.17
9
+ Requires-Dist: gaard-llm==0.2.17
10
+ Requires-Dist: gaard-plugin-api<0.3.0,>=0.2.17
11
11
  Requires-Dist: fastapi>=0.111.0
12
12
  Requires-Dist: uvicorn[standard]>=0.30.0
13
13
  Requires-Dist: pydantic>=2.7.0
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gaard-api"
7
- version = "0.2.14"
7
+ version = "0.2.17"
8
8
  description = "GAARD backend web services providing admin interface"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
 
12
12
  dependencies = [
13
- "gaard-core==0.2.11",
14
- "gaard-connectors==0.2.11",
15
- "gaard-llm==0.2.11",
16
- "gaard-plugin-api>=0.2.11,<0.3.0",
13
+ "gaard-core==0.2.17",
14
+ "gaard-connectors==0.2.17",
15
+ "gaard-llm==0.2.17",
16
+ "gaard-plugin-api>=0.2.17,<0.3.0",
17
17
  "fastapi>=0.111.0",
18
18
  "uvicorn[standard]>=0.30.0",
19
19
  "pydantic>=2.7.0",
@@ -7,6 +7,10 @@ CREATE TABLE IF NOT EXISTS admin_sessions (id INTEGER NOT NULL, token_hash VARCH
7
7
  CREATE UNIQUE INDEX IF NOT EXISTS ix_admin_sessions_token_hash ON admin_sessions (token_hash);
8
8
  CREATE TABLE IF NOT EXISTS admin_settings ("key" VARCHAR(255) NOT NULL, value TEXT NOT NULL, updated_at DATETIME NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY ("key"));
9
9
  CREATE TABLE IF NOT EXISTS admin_users (id INTEGER NOT NULL, username VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, auth_provider VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL, is_system_admin BOOLEAN NOT NULL, enterprise_access BOOLEAN NOT NULL, password_hash TEXT NOT NULL, must_change_password BOOLEAN NOT NULL, is_provisioned BOOLEAN NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_admin_users_auth_provider_username UNIQUE (auth_provider, username));
10
+ CREATE TABLE IF NOT EXISTS analysis_findings (id INTEGER NOT NULL, finding_id VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, connector_id INTEGER NOT NULL, business_logic_suggestion_id INTEGER, statement TEXT NOT NULL, finding_type VARCHAR(100) NOT NULL, confidence FLOAT NOT NULL, critique TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, status VARCHAR(50) NOT NULL, evidence_state VARCHAR(50) NOT NULL, decision VARCHAR(50) NOT NULL, decision_confidence FLOAT, verdict TEXT NOT NULL, decision_scope_json TEXT NOT NULL, decision_evidence_refs_json TEXT NOT NULL, decided_by VARCHAR(255) NOT NULL, contract_version VARCHAR(20) NOT NULL, decisions_json TEXT NOT NULL, evidence_updates_json TEXT NOT NULL, used_in_steps_json TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id));
11
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_findings_finding_id ON analysis_findings (finding_id);
12
+ CREATE TABLE IF NOT EXISTS analysis_finding_decisions (id INTEGER NOT NULL, decision_id VARCHAR(64) NOT NULL, idempotency_key VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, finding_id VARCHAR(64) NOT NULL, radar_run_id VARCHAR(255) NOT NULL, decision VARCHAR(50) NOT NULL, confidence FLOAT NOT NULL, verdict TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, is_current BOOLEAN NOT NULL, active BOOLEAN NOT NULL, contract_version VARCHAR(20) NOT NULL, actor_id VARCHAR(255) NOT NULL, actor_type VARCHAR(50) NOT NULL, actor_username VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_analysis_finding_decisions_idempotency_key UNIQUE (idempotency_key));
13
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_finding_decisions_decision_id ON analysis_finding_decisions (decision_id);
10
14
  CREATE TABLE IF NOT EXISTS analysis_sessions (id INTEGER NOT NULL, session_id VARCHAR(64) NOT NULL, status VARCHAR(50) NOT NULL, user_id VARCHAR(255) NOT NULL, datasource_id VARCHAR(255) NOT NULL, question TEXT NOT NULL, context_json TEXT NOT NULL, events_json TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id));
11
15
  CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_sessions_session_id ON analysis_sessions (session_id);
12
16
  CREATE TABLE IF NOT EXISTS business_knowledge_claims (id INTEGER NOT NULL, connector_id INTEGER NOT NULL, knowledge_type VARCHAR(100) NOT NULL, status VARCHAR(50) NOT NULL, claim_text TEXT NOT NULL, subject_json TEXT NOT NULL, evidence_json TEXT NOT NULL, confidence FLOAT NOT NULL, source VARCHAR(100) NOT NULL, request_id VARCHAR(255) NOT NULL, audit_reference VARCHAR(255) NOT NULL, requires_approval BOOLEAN NOT NULL, created_at DATETIME NOT NULL, last_verified_at DATETIME, updated_at DATETIME NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (id));
@@ -23,6 +27,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_dashboards_dashboard_id ON dashboards (dash
23
27
  CREATE TABLE IF NOT EXISTS data_query_audit_logs (id INTEGER NOT NULL, occurred_at DATETIME NOT NULL, type VARCHAR(50) NOT NULL, user_id VARCHAR(255) NOT NULL, datasource_id VARCHAR(255) NOT NULL, question TEXT NOT NULL, answer TEXT NOT NULL, sql TEXT NOT NULL, llm_sql_language VARCHAR(50) NOT NULL, output_classification VARCHAR(50) NOT NULL, metadata_json TEXT NOT NULL, PRIMARY KEY (id), CONSTRAINT data_query_audit_type CHECK (type IN ('info', 'sql_error', 'access_error')), CONSTRAINT output_classification CHECK (output_classification IN ('personal_data', 'sensitive_data', 'technical_data', 'neutral_data', 'unknown')));
24
28
  CREATE TABLE IF NOT EXISTS database_migration_tags (tag VARCHAR(255) NOT NULL, applied_at DATETIME NOT NULL, PRIMARY KEY (tag));
25
29
  CREATE TABLE IF NOT EXISTS datasource_connectors (id INTEGER NOT NULL, connector_key VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, database_type VARCHAR(50) NOT NULL, database_url TEXT NOT NULL, sql_dialect VARCHAR(50) NOT NULL, active BOOLEAN NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_datasource_connectors_key UNIQUE (connector_key));
30
+ UPDATE datasource_connectors SET database_type = 'duckdb-file' WHERE database_type = 'duckdb-excel';
31
+ UPDATE datasource_connectors SET database_url = REPLACE(database_url, 'duckdb-excel://', 'duckdb-file://') WHERE database_url LIKE 'duckdb-excel://%';
26
32
  CREATE TABLE IF NOT EXISTS datasource_schema_caches (connector_id INTEGER NOT NULL, schema_json TEXT NOT NULL, table_settings_json TEXT NOT NULL, formatted_schema TEXT NOT NULL, introspected_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (connector_id));
27
33
  CREATE TABLE IF NOT EXISTS overview_widgets (id INTEGER NOT NULL, widget_key VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, widget_type VARCHAR(50) NOT NULL, datasource_key VARCHAR(255) NOT NULL, question TEXT NOT NULL, sql TEXT NOT NULL, result_mode VARCHAR(50) NOT NULL, position INTEGER NOT NULL, grid_width INTEGER NOT NULL, grid_height INTEGER NOT NULL, active BOOLEAN NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_overview_widgets_key UNIQUE (widget_key));
28
34
  CREATE TABLE IF NOT EXISTS prompt_templates (id INTEGER NOT NULL, prompt_key VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, system_prompt TEXT NOT NULL, user_prompt_template TEXT NOT NULL, version INTEGER NOT NULL, active BOOLEAN NOT NULL, updated_by VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_prompt_templates_key UNIQUE (prompt_key));
@@ -41,6 +47,21 @@ CREATE INDEX IF NOT EXISTS ix_admin_sessions_username ON admin_sessions (usernam
41
47
  CREATE INDEX IF NOT EXISTS ix_admin_users_auth_provider ON admin_users (auth_provider);
42
48
  CREATE INDEX IF NOT EXISTS ix_admin_users_role ON admin_users (role);
43
49
  CREATE INDEX IF NOT EXISTS ix_admin_users_username ON admin_users (username);
50
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_connector_id ON analysis_findings (connector_id);
51
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_decision ON analysis_findings (decision);
52
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_evidence_state ON analysis_findings (evidence_state);
53
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_finding_type ON analysis_findings (finding_type);
54
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_investigation_id ON analysis_findings (investigation_id);
55
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_owner_user_id ON analysis_findings (owner_user_id);
56
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_status ON analysis_findings (status);
57
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_active ON analysis_finding_decisions (active);
58
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_actor_id ON analysis_finding_decisions (actor_id);
59
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_actor_type ON analysis_finding_decisions (actor_type);
60
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_decision ON analysis_finding_decisions (decision);
61
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_finding_id ON analysis_finding_decisions (finding_id);
62
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_investigation_id ON analysis_finding_decisions (investigation_id);
63
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_is_current ON analysis_finding_decisions (is_current);
64
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_radar_run_id ON analysis_finding_decisions (radar_run_id);
44
65
  CREATE INDEX IF NOT EXISTS ix_analysis_sessions_datasource_id ON analysis_sessions (datasource_id);
45
66
  CREATE INDEX IF NOT EXISTS ix_analysis_sessions_status ON analysis_sessions (status);
46
67
  CREATE INDEX IF NOT EXISTS ix_analysis_sessions_user_id ON analysis_sessions (user_id);
@@ -88,6 +109,11 @@ CREATE INDEX IF NOT EXISTS ix_user_datasource_selections_owner_username ON user_
88
109
  CREATE INDEX IF NOT EXISTS ix_user_saved_metrics_owner_user_id ON user_saved_metrics (owner_user_id);
89
110
  CREATE INDEX IF NOT EXISTS ix_user_saved_metrics_owner_username ON user_saved_metrics (owner_username);
90
111
  CREATE INDEX IF NOT EXISTS ix_user_saved_metrics_widget_key ON user_saved_metrics (widget_key);
112
+ CREATE TABLE IF NOT EXISTS duckdb_file_imports (id VARCHAR(36) NOT NULL, source_directory VARCHAR(2048) NOT NULL, status VARCHAR(16) NOT NULL, database_url VARCHAR(2048), storage_key VARCHAR(255) NOT NULL, preview_key VARCHAR(255), options_json TEXT NOT NULL, created_at DATETIME NOT NULL, started_at DATETIME, completed_at DATETIME, error_message TEXT, PRIMARY KEY (id), UNIQUE (storage_key));
113
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_imports_status ON duckdb_file_imports(status);
114
+ CREATE TABLE IF NOT EXISTS duckdb_file_materializations (datasource_key VARCHAR(255) NOT NULL, materialization_name VARCHAR(255) NOT NULL, import_id VARCHAR(36) NOT NULL, created_at DATETIME NOT NULL, PRIMARY KEY (datasource_key), UNIQUE (materialization_name), UNIQUE (import_id), FOREIGN KEY(import_id) REFERENCES duckdb_file_imports(id) ON DELETE CASCADE);
115
+ CREATE TABLE IF NOT EXISTS duckdb_file_relations (id INTEGER NOT NULL, import_id VARCHAR(36) NOT NULL, source_file VARCHAR(1024) NOT NULL, source_format VARCHAR(32) NOT NULL, source_member VARCHAR(1024), adapter_key VARCHAR(64) NOT NULL, relation_name VARCHAR(255) NOT NULL, row_count INTEGER NOT NULL, column_count INTEGER NOT NULL, source_size_bytes INTEGER NOT NULL, imported_at DATETIME NOT NULL, PRIMARY KEY (id), FOREIGN KEY(import_id) REFERENCES duckdb_file_imports(id) ON DELETE CASCADE);
116
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_relations_import_id ON duckdb_file_relations(import_id);
91
117
 
92
118
  -- dialect: postgresql
93
119
  CREATE TABLE IF NOT EXISTS admin_audit_logs (id SERIAL NOT NULL, occurred_at TIMESTAMP WITH TIME ZONE NOT NULL, actor VARCHAR(255) NOT NULL, action VARCHAR(255) NOT NULL, resource_type VARCHAR(255) NOT NULL, resource_id VARCHAR(255) NOT NULL, details_json TEXT NOT NULL, PRIMARY KEY (id));
@@ -95,6 +121,10 @@ CREATE TABLE IF NOT EXISTS admin_sessions (id SERIAL NOT NULL, token_hash VARCHA
95
121
  CREATE UNIQUE INDEX IF NOT EXISTS ix_admin_sessions_token_hash ON admin_sessions (token_hash);
96
122
  CREATE TABLE IF NOT EXISTS admin_settings ("key" VARCHAR(255) NOT NULL, value TEXT NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY ("key"));
97
123
  CREATE TABLE IF NOT EXISTS admin_users (id SERIAL NOT NULL, username VARCHAR(255) NOT NULL, display_name VARCHAR(255) NOT NULL, auth_provider VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL, is_system_admin BOOLEAN NOT NULL, enterprise_access BOOLEAN NOT NULL, password_hash TEXT NOT NULL, must_change_password BOOLEAN NOT NULL, is_provisioned BOOLEAN NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_admin_users_auth_provider_username UNIQUE (auth_provider, username));
124
+ CREATE TABLE IF NOT EXISTS analysis_findings (id SERIAL NOT NULL, finding_id VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, connector_id INTEGER NOT NULL, business_logic_suggestion_id INTEGER, statement TEXT NOT NULL, finding_type VARCHAR(100) NOT NULL, confidence FLOAT NOT NULL, critique TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, status VARCHAR(50) NOT NULL, evidence_state VARCHAR(50) NOT NULL, decision VARCHAR(50) NOT NULL, decision_confidence FLOAT, verdict TEXT NOT NULL, decision_scope_json TEXT NOT NULL, decision_evidence_refs_json TEXT NOT NULL, decided_by VARCHAR(255) NOT NULL, contract_version VARCHAR(20) NOT NULL, decisions_json TEXT NOT NULL, evidence_updates_json TEXT NOT NULL, used_in_steps_json TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id));
125
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_findings_finding_id ON analysis_findings (finding_id);
126
+ CREATE TABLE IF NOT EXISTS analysis_finding_decisions (id SERIAL NOT NULL, decision_id VARCHAR(64) NOT NULL, idempotency_key VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, finding_id VARCHAR(64) NOT NULL, radar_run_id VARCHAR(255) NOT NULL, decision VARCHAR(50) NOT NULL, confidence FLOAT NOT NULL, verdict TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, is_current BOOLEAN NOT NULL, active BOOLEAN NOT NULL, contract_version VARCHAR(20) NOT NULL, actor_id VARCHAR(255) NOT NULL, actor_type VARCHAR(50) NOT NULL, actor_username VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_analysis_finding_decisions_idempotency_key UNIQUE (idempotency_key));
127
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_finding_decisions_decision_id ON analysis_finding_decisions (decision_id);
98
128
  CREATE TABLE IF NOT EXISTS analysis_sessions (id SERIAL NOT NULL, session_id VARCHAR(64) NOT NULL, status VARCHAR(50) NOT NULL, user_id VARCHAR(255) NOT NULL, datasource_id VARCHAR(255) NOT NULL, question TEXT NOT NULL, context_json TEXT NOT NULL, events_json TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id));
99
129
  CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_sessions_session_id ON analysis_sessions (session_id);
100
130
  CREATE TABLE IF NOT EXISTS business_knowledge_claims (id SERIAL NOT NULL, connector_id INTEGER NOT NULL, knowledge_type VARCHAR(100) NOT NULL, status VARCHAR(50) NOT NULL, claim_text TEXT NOT NULL, subject_json TEXT NOT NULL, evidence_json TEXT NOT NULL, confidence FLOAT NOT NULL, source VARCHAR(100) NOT NULL, request_id VARCHAR(255) NOT NULL, audit_reference VARCHAR(255) NOT NULL, requires_approval BOOLEAN NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, last_verified_at TIMESTAMP WITH TIME ZONE, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (id));
@@ -111,6 +141,8 @@ CREATE UNIQUE INDEX IF NOT EXISTS ix_dashboards_dashboard_id ON dashboards (dash
111
141
  CREATE TABLE IF NOT EXISTS data_query_audit_logs (id SERIAL NOT NULL, occurred_at TIMESTAMP WITH TIME ZONE NOT NULL, type VARCHAR(50) NOT NULL, user_id VARCHAR(255) NOT NULL, datasource_id VARCHAR(255) NOT NULL, question TEXT NOT NULL, answer TEXT NOT NULL, sql TEXT NOT NULL, llm_sql_language VARCHAR(50) NOT NULL, output_classification VARCHAR(50) NOT NULL, metadata_json TEXT NOT NULL, PRIMARY KEY (id), CONSTRAINT data_query_audit_type CHECK (type IN ('info', 'sql_error', 'access_error')), CONSTRAINT output_classification CHECK (output_classification IN ('personal_data', 'sensitive_data', 'technical_data', 'neutral_data', 'unknown')));
112
142
  CREATE TABLE IF NOT EXISTS database_migration_tags (tag VARCHAR(255) NOT NULL, applied_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (tag));
113
143
  CREATE TABLE IF NOT EXISTS datasource_connectors (id SERIAL NOT NULL, connector_key VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, database_type VARCHAR(50) NOT NULL, database_url TEXT NOT NULL, sql_dialect VARCHAR(50) NOT NULL, active BOOLEAN NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_datasource_connectors_key UNIQUE (connector_key));
144
+ UPDATE datasource_connectors SET database_type = 'duckdb-file' WHERE database_type = 'duckdb-excel';
145
+ UPDATE datasource_connectors SET database_url = REPLACE(database_url, 'duckdb-excel://', 'duckdb-file://') WHERE database_url LIKE 'duckdb-excel://%';
114
146
  CREATE TABLE IF NOT EXISTS datasource_schema_caches (connector_id INTEGER NOT NULL, schema_json TEXT NOT NULL, table_settings_json TEXT NOT NULL, formatted_schema TEXT NOT NULL, introspected_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (connector_id));
115
147
  CREATE TABLE IF NOT EXISTS overview_widgets (id SERIAL NOT NULL, widget_key VARCHAR(255) NOT NULL, label VARCHAR(255) NOT NULL, widget_type VARCHAR(50) NOT NULL, datasource_key VARCHAR(255) NOT NULL, question TEXT NOT NULL, sql TEXT NOT NULL, result_mode VARCHAR(50) NOT NULL, position INTEGER NOT NULL, grid_width INTEGER NOT NULL, grid_height INTEGER NOT NULL, active BOOLEAN NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_by VARCHAR(255) NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_overview_widgets_key UNIQUE (widget_key));
116
148
  CREATE TABLE IF NOT EXISTS prompt_templates (id SERIAL NOT NULL, prompt_key VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT NOT NULL, system_prompt TEXT NOT NULL, user_prompt_template TEXT NOT NULL, version INTEGER NOT NULL, active BOOLEAN NOT NULL, updated_by VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_prompt_templates_key UNIQUE (prompt_key));
@@ -129,6 +161,21 @@ CREATE INDEX IF NOT EXISTS ix_admin_sessions_username ON admin_sessions (usernam
129
161
  CREATE INDEX IF NOT EXISTS ix_admin_users_auth_provider ON admin_users (auth_provider);
130
162
  CREATE INDEX IF NOT EXISTS ix_admin_users_role ON admin_users (role);
131
163
  CREATE INDEX IF NOT EXISTS ix_admin_users_username ON admin_users (username);
164
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_connector_id ON analysis_findings (connector_id);
165
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_decision ON analysis_findings (decision);
166
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_evidence_state ON analysis_findings (evidence_state);
167
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_finding_type ON analysis_findings (finding_type);
168
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_investigation_id ON analysis_findings (investigation_id);
169
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_owner_user_id ON analysis_findings (owner_user_id);
170
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_status ON analysis_findings (status);
171
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_active ON analysis_finding_decisions (active);
172
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_actor_id ON analysis_finding_decisions (actor_id);
173
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_actor_type ON analysis_finding_decisions (actor_type);
174
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_decision ON analysis_finding_decisions (decision);
175
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_finding_id ON analysis_finding_decisions (finding_id);
176
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_investigation_id ON analysis_finding_decisions (investigation_id);
177
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_is_current ON analysis_finding_decisions (is_current);
178
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_radar_run_id ON analysis_finding_decisions (radar_run_id);
132
179
  CREATE INDEX IF NOT EXISTS ix_analysis_sessions_datasource_id ON analysis_sessions (datasource_id);
133
180
  CREATE INDEX IF NOT EXISTS ix_analysis_sessions_status ON analysis_sessions (status);
134
181
  CREATE INDEX IF NOT EXISTS ix_analysis_sessions_user_id ON analysis_sessions (user_id);
@@ -176,3 +223,8 @@ CREATE INDEX IF NOT EXISTS ix_user_datasource_selections_owner_username ON user_
176
223
  CREATE INDEX IF NOT EXISTS ix_user_saved_metrics_owner_user_id ON user_saved_metrics (owner_user_id);
177
224
  CREATE INDEX IF NOT EXISTS ix_user_saved_metrics_owner_username ON user_saved_metrics (owner_username);
178
225
  CREATE INDEX IF NOT EXISTS ix_user_saved_metrics_widget_key ON user_saved_metrics (widget_key);
226
+ CREATE TABLE IF NOT EXISTS duckdb_file_imports (id VARCHAR(36) NOT NULL, source_directory VARCHAR(2048) NOT NULL, status VARCHAR(16) NOT NULL, database_url VARCHAR(2048), storage_key VARCHAR(255) NOT NULL, preview_key VARCHAR(255), options_json TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, started_at TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE, error_message TEXT, PRIMARY KEY (id), UNIQUE (storage_key));
227
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_imports_status ON duckdb_file_imports(status);
228
+ CREATE TABLE IF NOT EXISTS duckdb_file_materializations (datasource_key VARCHAR(255) NOT NULL, materialization_name VARCHAR(255) NOT NULL, import_id VARCHAR(36) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (datasource_key), UNIQUE (materialization_name), UNIQUE (import_id), FOREIGN KEY(import_id) REFERENCES duckdb_file_imports(id) ON DELETE CASCADE);
229
+ CREATE TABLE IF NOT EXISTS duckdb_file_relations (id SERIAL PRIMARY KEY, import_id VARCHAR(36) NOT NULL, source_file VARCHAR(1024) NOT NULL, source_format VARCHAR(32) NOT NULL, source_member VARCHAR(1024), adapter_key VARCHAR(64) NOT NULL, relation_name VARCHAR(255) NOT NULL, row_count INTEGER NOT NULL, column_count INTEGER NOT NULL, source_size_bytes INTEGER NOT NULL, imported_at TIMESTAMP WITH TIME ZONE NOT NULL, FOREIGN KEY(import_id) REFERENCES duckdb_file_imports(id) ON DELETE CASCADE);
230
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_relations_import_id ON duckdb_file_relations(import_id);
@@ -0,0 +1,73 @@
1
+ -- Append new tagged groups only at the end of this file. Never edit, reorder, or reuse a tag.
2
+ -- Every non-comment line is one SQL command and must end with a semicolon.
3
+
4
+ -- tag: 2026-07-31.identity-ids-use-admin-user-id.v1
5
+ -- table: identity_privilege_datasource_permissions
6
+ DELETE FROM identity_privilege_datasource_permissions AS legacy WHERE EXISTS (SELECT 1 FROM admin_users AS mapped_user WHERE legacy.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username) AND EXISTS (SELECT 1 FROM identity_privilege_datasource_permissions AS duplicate WHERE duplicate.connector_id = legacy.connector_id AND duplicate.id <> legacy.id AND (duplicate.identity_id = CAST(mapped_user.id AS VARCHAR(255)) OR (duplicate.id < legacy.id AND duplicate.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username)))));
7
+ UPDATE identity_privilege_datasource_permissions AS permission SET identity_id = (SELECT CAST(mapped_user.id AS VARCHAR(255)) FROM admin_users AS mapped_user WHERE permission.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username) ORDER BY mapped_user.id LIMIT 1) WHERE EXISTS (SELECT 1 FROM admin_users AS mapped_user WHERE permission.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username));
8
+ -- table: identity_privilege_table_permissions
9
+ DELETE FROM identity_privilege_table_permissions AS legacy WHERE EXISTS (SELECT 1 FROM admin_users AS mapped_user WHERE legacy.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username) AND EXISTS (SELECT 1 FROM identity_privilege_table_permissions AS duplicate WHERE duplicate.connector_id = legacy.connector_id AND duplicate.table_name = legacy.table_name AND duplicate.id <> legacy.id AND (duplicate.identity_id = CAST(mapped_user.id AS VARCHAR(255)) OR (duplicate.id < legacy.id AND duplicate.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username)))));
10
+ UPDATE identity_privilege_table_permissions AS permission SET identity_id = (SELECT CAST(mapped_user.id AS VARCHAR(255)) FROM admin_users AS mapped_user WHERE permission.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username) ORDER BY mapped_user.id LIMIT 1) WHERE EXISTS (SELECT 1 FROM admin_users AS mapped_user WHERE permission.identity_id IN ('local:' || CAST(mapped_user.id AS VARCHAR(255)), mapped_user.auth_provider || ':' || mapped_user.username));
11
+
12
+ -- tag: 2026-08-01.dashboard-sharing.v1
13
+ -- dialect: sqlite
14
+ CREATE TABLE IF NOT EXISTS dashboard_shares (id INTEGER NOT NULL, dashboard_id VARCHAR(64) NOT NULL, target_user_id VARCHAR(255) NOT NULL, target_username VARCHAR(255) NOT NULL, access_level VARCHAR(20) NOT NULL, created_by_user_id VARCHAR(255) NOT NULL, created_by_username VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_shares_dashboard_target UNIQUE (dashboard_id, target_user_id));
15
+ -- dialect: postgresql
16
+ CREATE TABLE IF NOT EXISTS dashboard_shares (id SERIAL NOT NULL, dashboard_id VARCHAR(64) NOT NULL, target_user_id VARCHAR(255) NOT NULL, target_username VARCHAR(255) NOT NULL, access_level VARCHAR(20) NOT NULL, created_by_user_id VARCHAR(255) NOT NULL, created_by_username VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_dashboard_shares_dashboard_target UNIQUE (dashboard_id, target_user_id));
17
+ -- dialect: all
18
+ CREATE INDEX IF NOT EXISTS ix_dashboard_shares_access_level ON dashboard_shares (access_level);
19
+ CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_user_id ON dashboard_shares (created_by_user_id);
20
+ CREATE INDEX IF NOT EXISTS ix_dashboard_shares_created_by_username ON dashboard_shares (created_by_username);
21
+ CREATE INDEX IF NOT EXISTS ix_dashboard_shares_dashboard_id ON dashboard_shares (dashboard_id);
22
+ CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_user_id ON dashboard_shares (target_user_id);
23
+ CREATE INDEX IF NOT EXISTS ix_dashboard_shares_target_username ON dashboard_shares (target_username);
24
+
25
+ -- tag: 2026-08-12.duckdb-file-connector.initial.v1
26
+ -- dialect: sqlite
27
+ CREATE TABLE IF NOT EXISTS duckdb_file_imports (id VARCHAR(36) NOT NULL, mode VARCHAR(16) NOT NULL, original_filename VARCHAR(1024) NOT NULL, status VARCHAR(16) NOT NULL, database_url VARCHAR(2048), storage_key VARCHAR(255) NOT NULL, options_json TEXT NOT NULL, created_at DATETIME NOT NULL, started_at DATETIME, completed_at DATETIME, error_message TEXT, PRIMARY KEY (id), UNIQUE (storage_key));
28
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_imports_status ON duckdb_file_imports(status);
29
+ CREATE TABLE IF NOT EXISTS duckdb_file_relations (id INTEGER NOT NULL, import_id VARCHAR(36) NOT NULL, source_file VARCHAR(1024) NOT NULL, source_format VARCHAR(32) NOT NULL, source_member VARCHAR(1024), adapter_key VARCHAR(64) NOT NULL, relation_name VARCHAR(255) NOT NULL, row_count INTEGER NOT NULL, column_count INTEGER NOT NULL, source_size_bytes INTEGER NOT NULL, imported_at DATETIME NOT NULL, PRIMARY KEY (id), FOREIGN KEY(import_id) REFERENCES duckdb_file_imports(id) ON DELETE CASCADE);
30
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_relations_import_id ON duckdb_file_relations(import_id);
31
+ -- dialect: postgresql
32
+ CREATE TABLE IF NOT EXISTS duckdb_file_imports (id VARCHAR(36) NOT NULL, mode VARCHAR(16) NOT NULL, original_filename VARCHAR(1024) NOT NULL, status VARCHAR(16) NOT NULL, database_url VARCHAR(2048), storage_key VARCHAR(255) NOT NULL, options_json TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, started_at TIMESTAMP WITH TIME ZONE, completed_at TIMESTAMP WITH TIME ZONE, error_message TEXT, PRIMARY KEY (id), UNIQUE (storage_key));
33
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_imports_status ON duckdb_file_imports(status);
34
+ CREATE TABLE IF NOT EXISTS duckdb_file_relations (id SERIAL PRIMARY KEY, import_id VARCHAR(36) NOT NULL, source_file VARCHAR(1024) NOT NULL, source_format VARCHAR(32) NOT NULL, source_member VARCHAR(1024), adapter_key VARCHAR(64) NOT NULL, relation_name VARCHAR(255) NOT NULL, row_count INTEGER NOT NULL, column_count INTEGER NOT NULL, source_size_bytes INTEGER NOT NULL, imported_at TIMESTAMP WITH TIME ZONE NOT NULL, FOREIGN KEY(import_id) REFERENCES duckdb_file_imports(id) ON DELETE CASCADE);
35
+ CREATE INDEX IF NOT EXISTS ix_duckdb_file_relations_import_id ON duckdb_file_relations(import_id);
36
+
37
+ -- tag: 2026-08-14.duckdb-file-connector.migrate-duckdb-excel.v1
38
+ -- dialect: all
39
+ -- table: datasource_connectors
40
+ UPDATE datasource_connectors SET database_type = 'duckdb-file' WHERE database_type = 'duckdb-excel';
41
+ -- table: datasource_connectors
42
+ UPDATE datasource_connectors SET database_url = REPLACE(database_url, 'duckdb-excel://', 'duckdb-file://') WHERE database_url LIKE 'duckdb-excel://%';
43
+
44
+ -- tag: 2026-09-06.analysis-findings.v1
45
+ -- dialect: sqlite
46
+ CREATE TABLE IF NOT EXISTS analysis_findings (id INTEGER NOT NULL, finding_id VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, connector_id INTEGER NOT NULL, business_logic_suggestion_id INTEGER, statement TEXT NOT NULL, finding_type VARCHAR(100) NOT NULL, confidence FLOAT NOT NULL, critique TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, status VARCHAR(50) NOT NULL, evidence_state VARCHAR(50) NOT NULL, decision VARCHAR(50) NOT NULL, decision_confidence FLOAT, verdict TEXT NOT NULL, decision_scope_json TEXT NOT NULL, decision_evidence_refs_json TEXT NOT NULL, decided_by VARCHAR(255) NOT NULL, contract_version VARCHAR(20) NOT NULL, decisions_json TEXT NOT NULL, evidence_updates_json TEXT NOT NULL, used_in_steps_json TEXT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id));
47
+ -- dialect: postgresql
48
+ CREATE TABLE IF NOT EXISTS analysis_findings (id SERIAL NOT NULL, finding_id VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, owner_user_id VARCHAR(255) NOT NULL, connector_id INTEGER NOT NULL, business_logic_suggestion_id INTEGER, statement TEXT NOT NULL, finding_type VARCHAR(100) NOT NULL, confidence FLOAT NOT NULL, critique TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, status VARCHAR(50) NOT NULL, evidence_state VARCHAR(50) NOT NULL, decision VARCHAR(50) NOT NULL, decision_confidence FLOAT, verdict TEXT NOT NULL, decision_scope_json TEXT NOT NULL, decision_evidence_refs_json TEXT NOT NULL, decided_by VARCHAR(255) NOT NULL, contract_version VARCHAR(20) NOT NULL, decisions_json TEXT NOT NULL, evidence_updates_json TEXT NOT NULL, used_in_steps_json TEXT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id));
49
+ -- dialect: all
50
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_findings_finding_id ON analysis_findings (finding_id);
51
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_connector_id ON analysis_findings (connector_id);
52
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_decision ON analysis_findings (decision);
53
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_evidence_state ON analysis_findings (evidence_state);
54
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_finding_type ON analysis_findings (finding_type);
55
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_investigation_id ON analysis_findings (investigation_id);
56
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_owner_user_id ON analysis_findings (owner_user_id);
57
+ CREATE INDEX IF NOT EXISTS ix_analysis_findings_status ON analysis_findings (status);
58
+
59
+ -- tag: 2026-09-06.analysis-finding-decisions.v1
60
+ -- dialect: sqlite
61
+ CREATE TABLE IF NOT EXISTS analysis_finding_decisions (id INTEGER NOT NULL, decision_id VARCHAR(64) NOT NULL, idempotency_key VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, finding_id VARCHAR(64) NOT NULL, radar_run_id VARCHAR(255) NOT NULL, decision VARCHAR(50) NOT NULL, confidence FLOAT NOT NULL, verdict TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, is_current BOOLEAN NOT NULL, active BOOLEAN NOT NULL, contract_version VARCHAR(20) NOT NULL, actor_id VARCHAR(255) NOT NULL, actor_type VARCHAR(50) NOT NULL, actor_username VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_analysis_finding_decisions_idempotency_key UNIQUE (idempotency_key));
62
+ -- dialect: postgresql
63
+ CREATE TABLE IF NOT EXISTS analysis_finding_decisions (id SERIAL NOT NULL, decision_id VARCHAR(64) NOT NULL, idempotency_key VARCHAR(64) NOT NULL, investigation_id VARCHAR(64) NOT NULL, finding_id VARCHAR(64) NOT NULL, radar_run_id VARCHAR(255) NOT NULL, decision VARCHAR(50) NOT NULL, confidence FLOAT NOT NULL, verdict TEXT NOT NULL, scope_json TEXT NOT NULL, evidence_refs_json TEXT NOT NULL, is_current BOOLEAN NOT NULL, active BOOLEAN NOT NULL, contract_version VARCHAR(20) NOT NULL, actor_id VARCHAR(255) NOT NULL, actor_type VARCHAR(50) NOT NULL, actor_username VARCHAR(255) NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, updated_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY (id), CONSTRAINT uq_analysis_finding_decisions_idempotency_key UNIQUE (idempotency_key));
64
+ -- dialect: all
65
+ CREATE UNIQUE INDEX IF NOT EXISTS ix_analysis_finding_decisions_decision_id ON analysis_finding_decisions (decision_id);
66
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_active ON analysis_finding_decisions (active);
67
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_actor_id ON analysis_finding_decisions (actor_id);
68
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_actor_type ON analysis_finding_decisions (actor_type);
69
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_decision ON analysis_finding_decisions (decision);
70
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_finding_id ON analysis_finding_decisions (finding_id);
71
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_investigation_id ON analysis_finding_decisions (investigation_id);
72
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_is_current ON analysis_finding_decisions (is_current);
73
+ CREATE INDEX IF NOT EXISTS ix_analysis_finding_decisions_radar_run_id ON analysis_finding_decisions (radar_run_id);
@@ -0,0 +1,53 @@
1
+ """Public metadata-store API for trusted GAARD extensions.
2
+
3
+ Extensions should import metadata sessions and the shared declarative base from
4
+ this module instead of depending on the admin database implementation.
5
+ """
6
+
7
+ from collections.abc import Iterator
8
+ from contextlib import contextmanager
9
+
10
+ from sqlalchemy.orm import Session
11
+
12
+ from gaard_api.admin.database import create_session
13
+ from gaard_api.admin.models import (
14
+ Base,
15
+ DatasourceConnector,
16
+ DuckDBFileImport,
17
+ DuckDBFileMaterialization,
18
+ DuckDBFileRelation,
19
+ )
20
+
21
+ MetadataModelBase = Base
22
+
23
+
24
+ def create_metadata_session() -> Session:
25
+ """Return a session connected to the configured GAARD metadata store."""
26
+
27
+ return create_session()
28
+
29
+
30
+ @contextmanager
31
+ def metadata_session() -> Iterator[Session]:
32
+ """Open a transactional metadata session and roll back on failure."""
33
+
34
+ session = create_metadata_session()
35
+ try:
36
+ yield session
37
+ session.commit()
38
+ except BaseException:
39
+ session.rollback()
40
+ raise
41
+ finally:
42
+ session.close()
43
+
44
+
45
+ __all__ = [
46
+ "DatasourceConnector",
47
+ "DuckDBFileImport",
48
+ "DuckDBFileMaterialization",
49
+ "DuckDBFileRelation",
50
+ "MetadataModelBase",
51
+ "create_metadata_session",
52
+ "metadata_session",
53
+ ]
@@ -28,6 +28,8 @@ class SqlUpdateCommand:
28
28
  sql: str
29
29
  dialect: str | None = None
30
30
  required_table: str | None = None
31
+ required_columns: tuple[str, ...] = ()
32
+ missing_columns: tuple[str, ...] = ()
31
33
 
32
34
 
33
35
  @dataclass(frozen=True, slots=True)
@@ -52,6 +54,8 @@ def parse_sql_updates(contents: str) -> tuple[TaggedSqlUpdate, ...]:
52
54
  current_tag: str | None = None
53
55
  current_dialect: str | None = None
54
56
  current_table: str | None = None
57
+ required_columns: list[str] = []
58
+ missing_columns: list[str] = []
55
59
  current_commands: list[SqlUpdateCommand] = []
56
60
 
57
61
  def finish_current_update() -> None:
@@ -94,6 +98,24 @@ def parse_sql_updates(contents: str) -> tuple[TaggedSqlUpdate, ...]:
94
98
  current_table = None if table_name.lower() == "all" else table_name
95
99
  continue
96
100
 
101
+ column_exists_match = _COLUMN_EXISTS_PATTERN.fullmatch(line)
102
+ if column_exists_match:
103
+ if current_tag is None or current_table is None:
104
+ raise ValueError(
105
+ f"Column condition without a tagged table on line {line_number}."
106
+ )
107
+ required_columns.append(column_exists_match.group(1))
108
+ continue
109
+
110
+ column_missing_match = _COLUMN_MISSING_PATTERN.fullmatch(line)
111
+ if column_missing_match:
112
+ if current_tag is None or current_table is None:
113
+ raise ValueError(
114
+ f"Column condition without a tagged table on line {line_number}."
115
+ )
116
+ missing_columns.append(column_missing_match.group(1))
117
+ continue
118
+
97
119
  if line.startswith("--"):
98
120
  continue
99
121
  if current_tag is None:
@@ -110,8 +132,12 @@ def parse_sql_updates(contents: str) -> tuple[TaggedSqlUpdate, ...]:
110
132
  sql=sql,
111
133
  dialect=current_dialect,
112
134
  required_table=current_table,
135
+ required_columns=tuple(required_columns),
136
+ missing_columns=tuple(missing_columns),
113
137
  )
114
138
  )
139
+ required_columns = []
140
+ missing_columns = []
115
141
 
116
142
  finish_current_update()
117
143
  _validate_unique_tags(updates)
@@ -306,10 +332,18 @@ def _execute_update(connection: Connection, update: TaggedSqlUpdate) -> None:
306
332
  for command in update.commands:
307
333
  if command.dialect is not None and command.dialect != dialect:
308
334
  continue
309
- if command.required_table is not None and not inspect(connection).has_table(
310
- command.required_table
311
- ):
312
- continue
335
+ if command.required_table is not None:
336
+ inspector = inspect(connection)
337
+ if not inspector.has_table(command.required_table):
338
+ continue
339
+ column_names = {
340
+ str(column["name"])
341
+ for column in inspector.get_columns(command.required_table)
342
+ }
343
+ if not all(column in column_names for column in command.required_columns):
344
+ continue
345
+ if not all(column not in column_names for column in command.missing_columns):
346
+ continue
313
347
  connection.exec_driver_sql(command.sql)
314
348
 
315
349
 
@@ -35,6 +35,55 @@ class DatabaseMigrationTag(Base):
35
35
  applied_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
36
36
 
37
37
 
38
+ class DuckDBFileImport(Base):
39
+ """One extension-managed directory materialization."""
40
+
41
+ __tablename__ = "duckdb_file_imports"
42
+
43
+ id: Mapped[str] = mapped_column(String(36), primary_key=True)
44
+ source_directory: Mapped[str] = mapped_column(String(2048))
45
+ status: Mapped[str] = mapped_column(String(16), index=True)
46
+ database_url: Mapped[str | None] = mapped_column(String(2048), nullable=True)
47
+ storage_key: Mapped[str] = mapped_column(String(255), unique=True)
48
+ preview_key: Mapped[str | None] = mapped_column(String(255), nullable=True)
49
+ options_json: Mapped[str] = mapped_column(Text, default="{}")
50
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
51
+ started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
52
+ completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
53
+ error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
54
+
55
+
56
+ class DuckDBFileMaterialization(Base):
57
+ """A saved datasource key mapped to one durable DuckDB file."""
58
+
59
+ __tablename__ = "duckdb_file_materializations"
60
+
61
+ datasource_key: Mapped[str] = mapped_column(String(255), primary_key=True)
62
+ materialization_name: Mapped[str] = mapped_column(String(255), unique=True)
63
+ import_id: Mapped[str] = mapped_column(
64
+ String(36), ForeignKey("duckdb_file_imports.id", ondelete="CASCADE"), unique=True
65
+ )
66
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
67
+
68
+
69
+ class DuckDBFileRelation(Base):
70
+ __tablename__ = "duckdb_file_relations"
71
+
72
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
73
+ import_id: Mapped[str] = mapped_column(
74
+ String(36), ForeignKey("duckdb_file_imports.id", ondelete="CASCADE"), index=True
75
+ )
76
+ source_file: Mapped[str] = mapped_column(String(1024))
77
+ source_format: Mapped[str] = mapped_column(String(32))
78
+ source_member: Mapped[str | None] = mapped_column(String(1024), nullable=True)
79
+ adapter_key: Mapped[str] = mapped_column(String(64))
80
+ relation_name: Mapped[str] = mapped_column(String(255))
81
+ row_count: Mapped[int] = mapped_column(Integer)
82
+ column_count: Mapped[int] = mapped_column(Integer)
83
+ source_size_bytes: Mapped[int] = mapped_column(Integer)
84
+ imported_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
85
+
86
+
38
87
  class AdminUser(Base):
39
88
  __tablename__ = "admin_users"
40
89
  __table_args__ = (
@@ -458,6 +507,79 @@ class BusinessKnowledgeClaim(Base):
458
507
  updated_by: Mapped[str] = mapped_column(String(255), default="system")
459
508
 
460
509
 
510
+ class AnalysisFinding(Base):
511
+ """Investigation-scoped semantic knowledge and its external review audit."""
512
+
513
+ __tablename__ = "analysis_findings"
514
+
515
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
516
+ finding_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
517
+ investigation_id: Mapped[str] = mapped_column(String(64), index=True)
518
+ owner_user_id: Mapped[str] = mapped_column(String(255), index=True)
519
+ connector_id: Mapped[int] = mapped_column(Integer, index=True)
520
+ business_logic_suggestion_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
521
+ statement: Mapped[str] = mapped_column(Text)
522
+ finding_type: Mapped[str] = mapped_column(String(100), index=True)
523
+ confidence: Mapped[float] = mapped_column(Float, default=0.0)
524
+ critique: Mapped[str] = mapped_column(Text, default="")
525
+ scope_json: Mapped[str] = mapped_column(Text, default="{}")
526
+ evidence_refs_json: Mapped[str] = mapped_column(Text, default="[]")
527
+ status: Mapped[str] = mapped_column(String(50), index=True, default="pending")
528
+ evidence_state: Mapped[str] = mapped_column(String(50), index=True, default="unreviewed")
529
+ decision: Mapped[str] = mapped_column(String(50), index=True, default="")
530
+ decision_confidence: Mapped[float | None] = mapped_column(Float, nullable=True)
531
+ verdict: Mapped[str] = mapped_column(Text, default="")
532
+ decision_scope_json: Mapped[str] = mapped_column(Text, default="{}")
533
+ decision_evidence_refs_json: Mapped[str] = mapped_column(Text, default="[]")
534
+ decided_by: Mapped[str] = mapped_column(String(255), default="")
535
+ contract_version: Mapped[str] = mapped_column(String(20), default="1.0")
536
+ decisions_json: Mapped[str] = mapped_column(Text, default="[]")
537
+ evidence_updates_json: Mapped[str] = mapped_column(Text, default="[]")
538
+ used_in_steps_json: Mapped[str] = mapped_column(Text, default="[]")
539
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
540
+ updated_at: Mapped[datetime] = mapped_column(
541
+ DateTime(timezone=True),
542
+ default=utc_now,
543
+ onupdate=utc_now,
544
+ )
545
+
546
+
547
+ class AnalysisFindingDecision(Base):
548
+ """One durable external decision about an investigation-scoped finding."""
549
+
550
+ __tablename__ = "analysis_finding_decisions"
551
+ __table_args__ = (
552
+ UniqueConstraint(
553
+ "idempotency_key",
554
+ name="uq_analysis_finding_decisions_idempotency_key",
555
+ ),
556
+ )
557
+
558
+ id: Mapped[int] = mapped_column(Integer, primary_key=True)
559
+ decision_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
560
+ idempotency_key: Mapped[str] = mapped_column(String(64))
561
+ investigation_id: Mapped[str] = mapped_column(String(64), index=True)
562
+ finding_id: Mapped[str] = mapped_column(String(64), index=True)
563
+ radar_run_id: Mapped[str] = mapped_column(String(255), index=True)
564
+ decision: Mapped[str] = mapped_column(String(50), index=True)
565
+ confidence: Mapped[float] = mapped_column(Float)
566
+ verdict: Mapped[str] = mapped_column(Text)
567
+ scope_json: Mapped[str] = mapped_column(Text, default="{}")
568
+ evidence_refs_json: Mapped[str] = mapped_column(Text, default="[]")
569
+ is_current: Mapped[bool] = mapped_column(Boolean, index=True, default=True)
570
+ active: Mapped[bool] = mapped_column(Boolean, index=True, default=False)
571
+ contract_version: Mapped[str] = mapped_column(String(20), default="1.0")
572
+ actor_id: Mapped[str] = mapped_column(String(255), index=True)
573
+ actor_type: Mapped[str] = mapped_column(String(50), index=True, default="radar")
574
+ actor_username: Mapped[str] = mapped_column(String(255), default="")
575
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
576
+ updated_at: Mapped[datetime] = mapped_column(
577
+ DateTime(timezone=True),
578
+ default=utc_now,
579
+ onupdate=utc_now,
580
+ )
581
+
582
+
461
583
  class AnalysisSessionRecord(Base):
462
584
  __tablename__ = "analysis_sessions"
463
585
 
@@ -2381,6 +2381,14 @@ def get_datasource_schema_context_safe() -> (
2381
2381
  if connector is None:
2382
2382
  return None
2383
2383
 
2384
+ definition = get_connector_registry().get(connector.database_type)
2385
+ if definition.datasource_url_finalizer is not None:
2386
+ connector.database_url = definition.datasource_url_finalizer(
2387
+ session, connector.connector_key, connector.database_url
2388
+ )
2389
+ definition.validate_database_url(connector.database_url)
2390
+ session.commit()
2391
+
2384
2392
  cache = get_datasource_schema_cache(session, connector.id)
2385
2393
 
2386
2394
  if cache is None:
@@ -2422,6 +2430,14 @@ def get_datasource_schema_contexts_safe(
2422
2430
 
2423
2431
  contexts: list[tuple[DatasourceConnector, DatasourceSchemaCache]] = []
2424
2432
  for connector in connectors:
2433
+ definition = get_connector_registry().get(connector.database_type)
2434
+ if definition.datasource_url_finalizer is not None:
2435
+ connector.database_url = definition.datasource_url_finalizer(
2436
+ session, connector.connector_key, connector.database_url
2437
+ )
2438
+ definition.validate_database_url(connector.database_url)
2439
+ session.commit()
2440
+
2425
2441
  cache = get_datasource_schema_cache(session, connector.id)
2426
2442
 
2427
2443
  if cache is None: