diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java index 1a0180a8..d0c97c12 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/config/ClickHouseConfig.java @@ -20,6 +20,10 @@ import java.sql.Statement; *

* Spring Boot auto-configures the DataSource from {@code spring.datasource.*} properties. * This class exposes a JdbcTemplate bean and initializes the schema on startup. + *

+ * Schema initialization uses a direct JDBC connection (bypassing the DataSource) + * to avoid chicken-and-egg problems: the DataSource targets a specific database + * that may not exist yet on a fresh ClickHouse instance. */ @Configuration public class ClickHouseConfig { @@ -49,43 +53,30 @@ public class ClickHouseConfig { @PostConstruct void initSchema() { - ensureDatabaseExists(); - var jdbc = new JdbcTemplate(dataSource); - for (String schemaFile : SCHEMA_FILES) { - try { + String dbName = extractDatabaseName(datasourceUrl); + String rootUrl = datasourceUrl.replaceFirst("/[^/?]+($|\\?)", "/$1"); + + try (Connection conn = DriverManager.getConnection(rootUrl, datasourceUsername, datasourcePassword); + Statement stmt = conn.createStatement()) { + + // Create the database first + stmt.execute("CREATE DATABASE IF NOT EXISTS " + dbName); + log.info("Ensured database '{}' exists", dbName); + + // Apply schema files using fully qualified table names + for (String schemaFile : SCHEMA_FILES) { String sql = new ClassPathResource(schemaFile).getContentAsString(StandardCharsets.UTF_8); for (String statement : sql.split(";")) { String trimmed = statement.trim(); if (!trimmed.isEmpty() && !trimmed.startsWith("--")) { - jdbc.execute(trimmed); + stmt.execute(trimmed); } } log.info("Applied schema: {}", schemaFile); - } catch (Exception e) { - log.error("Failed to apply schema: {}", schemaFile, e); - throw new RuntimeException("Schema initialization failed: " + schemaFile, e); } - } - } - - /** - * Creates the ClickHouse database if it doesn't exist. - * Uses a separate connection without the database path, since the main - * DataSource connection fails if the database doesn't exist yet. - */ - private void ensureDatabaseExists() { - // Extract database name from URL: jdbc:ch://host:port/dbname -> dbname - // Strip the database path to connect at root level - String rootUrl = datasourceUrl.replaceFirst("/[^/?]+($|\\?)", "$1"); - String dbName = extractDatabaseName(datasourceUrl); - - try (Connection conn = DriverManager.getConnection(rootUrl, datasourceUsername, datasourcePassword); - Statement stmt = conn.createStatement()) { - stmt.execute("CREATE DATABASE IF NOT EXISTS " + dbName); - log.info("Ensured database '{}' exists", dbName); } catch (Exception e) { - log.error("Failed to ensure database exists", e); - throw new RuntimeException("Database creation failed", e); + log.error("ClickHouse schema initialization failed", e); + throw new RuntimeException("ClickHouse schema initialization failed", e); } } diff --git a/cameleer3-server-app/src/main/resources/clickhouse/01-schema.sql b/cameleer3-server-app/src/main/resources/clickhouse/01-schema.sql index ab56da70..3c2afd25 100644 --- a/cameleer3-server-app/src/main/resources/clickhouse/01-schema.sql +++ b/cameleer3-server-app/src/main/resources/clickhouse/01-schema.sql @@ -1,7 +1,7 @@ -- Cameleer3 ClickHouse Schema -- Tables for route executions, route diagrams, and agent metrics. -CREATE TABLE IF NOT EXISTS route_executions ( +CREATE TABLE IF NOT EXISTS cameleer3.route_executions ( execution_id String, route_id LowCardinality(String), agent_id LowCardinality(String), @@ -32,7 +32,7 @@ ORDER BY (agent_id, status, start_time, execution_id) TTL toDateTime(start_time) + toIntervalDay(30) SETTINGS ttl_only_drop_parts = 1; -CREATE TABLE IF NOT EXISTS route_diagrams ( +CREATE TABLE IF NOT EXISTS cameleer3.route_diagrams ( content_hash String, route_id LowCardinality(String), agent_id LowCardinality(String), @@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS route_diagrams ( ENGINE = ReplacingMergeTree(created_at) ORDER BY (content_hash); -CREATE TABLE IF NOT EXISTS agent_metrics ( +CREATE TABLE IF NOT EXISTS cameleer3.agent_metrics ( agent_id LowCardinality(String), collected_at DateTime64(3, 'UTC'), metric_name LowCardinality(String), diff --git a/cameleer3-server-app/src/main/resources/clickhouse/02-search-columns.sql b/cameleer3-server-app/src/main/resources/clickhouse/02-search-columns.sql index 2b11b435..024e9620 100644 --- a/cameleer3-server-app/src/main/resources/clickhouse/02-search-columns.sql +++ b/cameleer3-server-app/src/main/resources/clickhouse/02-search-columns.sql @@ -1,7 +1,7 @@ -- Phase 2: Schema extension for search, detail, and diagram linking columns. -- Adds exchange snapshot data, processor tree metadata, and diagram content hash. -ALTER TABLE route_executions +ALTER TABLE cameleer3.route_executions ADD COLUMN IF NOT EXISTS exchange_bodies String DEFAULT '', ADD COLUMN IF NOT EXISTS exchange_headers String DEFAULT '', ADD COLUMN IF NOT EXISTS processor_depths Array(UInt16) DEFAULT [], @@ -16,10 +16,10 @@ ALTER TABLE route_executions ADD COLUMN IF NOT EXISTS diagram_content_hash String DEFAULT ''; -- Skip indexes for full-text search on new text columns -ALTER TABLE route_executions +ALTER TABLE cameleer3.route_executions ADD INDEX IF NOT EXISTS idx_exchange_bodies exchange_bodies TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4, ADD INDEX IF NOT EXISTS idx_exchange_headers exchange_headers TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4; -- Skip index on error_stacktrace (not indexed in 01-schema.sql, needed for SRCH-05) -ALTER TABLE route_executions +ALTER TABLE cameleer3.route_executions ADD INDEX IF NOT EXISTS idx_error_stacktrace error_stacktrace TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4;