Provision current metadata schemas

This commit is contained in:
Logic
2026-08-09 20:14:39 +08:00
parent ad45b32881
commit 293b983aa4
23 changed files with 5563 additions and 6 deletions
@@ -0,0 +1,147 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Owns the Flyway-compatible history layout and current-baseline marker. */
final class FlywaySchemaHistory {
private static final String TABLE = "flyway_schema_history";
private final MetadataDatabaseKind kind;
FlywaySchemaHistory(MetadataDatabaseKind kind) {
this.kind = kind;
}
boolean isCurrent(Connection connection, TargetSchemaBaseline baseline) throws SQLException {
Set<String> currentTables = currentBaselineTables(connection);
if (!currentTables.contains(TABLE)) {
return false;
}
String sql = "SELECT installed_rank, version, type, script, checksum, success FROM " + TABLE;
try (Statement statement = connection.createStatement(); ResultSet result = statement.executeQuery(sql)) {
if (!result.next()) {
throw unexpectedTargetState();
}
boolean current = result.getInt("installed_rank") == 1
&& TargetSchemaBaseline.VERSION.equals(result.getString("version"))
&& TargetSchemaBaseline.TYPE.equals(result.getString("type"))
&& TargetSchemaBaseline.SCRIPT.equals(result.getString("script"))
&& baseline.checksum() == result.getInt("checksum")
&& !result.wasNull()
&& result.getBoolean("success");
if (!current || result.next() || !currentTables.contains(TargetSchemaContract.TABLE)
|| !currentTables.containsAll(baseline.expectedTables())
|| !new TargetSchemaContract(kind).matches(connection, baseline.expectedTables())) {
throw unexpectedTargetState();
}
return true;
}
}
void requireEmptyTarget(Connection connection) throws SQLException {
if (!currentCatalogSchemaObjects(connection).isEmpty()) {
throw unexpectedTargetState();
}
}
void record(
Connection connection,
TargetSchemaBaseline baseline,
String installedBy,
int executionTimeMillis) throws SQLException {
new TargetSchemaContract(kind).record(connection, baseline.expectedTables());
try (Statement statement = connection.createStatement()) {
for (String sql : createStatements()) {
statement.execute(sql);
}
}
String insert = "INSERT INTO " + TABLE
+ " (installed_rank, version, description, type, script, checksum, installed_by, execution_time, success)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
try (PreparedStatement statement = connection.prepareStatement(insert)) {
statement.setInt(1, 1);
statement.setString(2, TargetSchemaBaseline.VERSION);
statement.setString(3, TargetSchemaBaseline.DESCRIPTION);
statement.setString(4, TargetSchemaBaseline.TYPE);
statement.setString(5, TargetSchemaBaseline.SCRIPT);
statement.setInt(6, baseline.checksum());
statement.setString(7, abbreviate(installedBy, 100));
statement.setInt(8, executionTimeMillis);
statement.setBoolean(9, true);
statement.executeUpdate();
}
}
private String[] createStatements() {
String table = switch (kind) {
case MYSQL -> "CREATE TABLE " + TABLE + " ("
+ "installed_rank INT NOT NULL, version VARCHAR(50), description VARCHAR(200) NOT NULL, "
+ "type VARCHAR(20) NOT NULL, script VARCHAR(1000) NOT NULL, checksum INT, "
+ "installed_by VARCHAR(100) NOT NULL, installed_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
+ "execution_time INT NOT NULL, success BOOL NOT NULL, "
+ "CONSTRAINT flyway_schema_history_pk PRIMARY KEY (installed_rank)) ENGINE=InnoDB";
case POSTGRESQL -> "CREATE TABLE " + TABLE + " ("
+ "installed_rank INT NOT NULL, version VARCHAR(50), description VARCHAR(200) NOT NULL, "
+ "type VARCHAR(20) NOT NULL, script VARCHAR(1000) NOT NULL, checksum INTEGER, "
+ "installed_by VARCHAR(100) NOT NULL, installed_on TIMESTAMP NOT NULL DEFAULT now(), "
+ "execution_time INTEGER NOT NULL, success BOOLEAN NOT NULL, "
+ "CONSTRAINT flyway_schema_history_pk PRIMARY KEY (installed_rank))";
case H2 -> throw new IllegalArgumentException("H2 has no external target schema history");
};
return new String[]{table, "CREATE INDEX flyway_schema_history_s_idx ON " + TABLE + " (success)"};
}
private Set<String> currentBaselineTables(Connection connection) throws SQLException {
return currentCatalogSchemaObjects(connection, new String[]{"TABLE"});
}
private Set<String> currentCatalogSchemaObjects(Connection connection) throws SQLException {
return currentCatalogSchemaObjects(connection, null);
}
private Set<String> currentCatalogSchemaObjects(Connection connection, String[] types) throws SQLException {
DatabaseMetaData metadata = connection.getMetaData();
String schema = kind == MetadataDatabaseKind.POSTGRESQL ? connection.getSchema() : null;
Set<String> names = new HashSet<>();
try (ResultSet objects = metadata.getTables(connection.getCatalog(), schema, "%", types)) {
while (objects.next()) {
names.add(objects.getString("TABLE_NAME").toLowerCase(Locale.ROOT));
}
}
return Set.copyOf(names);
}
private static String abbreviate(String value, int maximumLength) {
return value.length() <= maximumLength ? value : value.substring(0, maximumLength);
}
private static SQLException unexpectedTargetState() {
return new SQLException("Target schema is not empty or does not contain the current baseline", "55000");
}
}
@@ -0,0 +1,167 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Applies the static baseline and writes a history row compatible with subsequent standard Flyway runs. */
public final class FlywayTargetSchemaProvisioner implements TargetSchemaProvisioner {
// Admission rejects multi-node migration. The lock prevents concurrent work in this JVM, while a failed MySQL DDL
// sequence can leave partial state that deliberately fails the next precondition instead of pretending to resume.
private static final ReentrantLock PROVISIONING_LOCK = new ReentrantLock();
@Override
public void provision(MetadataDatabaseConfiguration target) {
Objects.requireNonNull(target, "target");
MetadataDatabaseKind kind = supportedKind(target.kind());
PROVISIONING_LOCK.lock();
try {
provisionLocked(target, kind);
} finally {
PROVISIONING_LOCK.unlock();
}
}
private static void provisionLocked(MetadataDatabaseConfiguration target, MetadataDatabaseKind kind) {
Connection connection;
try {
connection = DriverManager.getConnection(target.jdbcUrl(), target.username(), target.password());
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.CONNECTION, exception);
}
boolean completed = false;
try {
configureTransaction(connection, kind);
provision(connection, target, kind);
commitTransaction(connection, kind);
completed = true;
} catch (TargetSchemaProvisioningException exception) {
rollbackTransaction(connection, kind);
throw exception;
} finally {
if (!completed) {
closeQuietly(connection);
}
}
try {
connection.close();
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.CLEANUP, exception);
}
}
private static void configureTransaction(Connection connection, MetadataDatabaseKind kind) {
if (kind == MetadataDatabaseKind.POSTGRESQL) {
try {
connection.setAutoCommit(false);
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.TRANSACTION, exception);
}
}
}
private static void commitTransaction(Connection connection, MetadataDatabaseKind kind) {
if (kind == MetadataDatabaseKind.POSTGRESQL) {
try {
connection.commit();
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.TRANSACTION, exception);
}
}
}
private static void rollbackTransaction(Connection connection, MetadataDatabaseKind kind) {
if (kind == MetadataDatabaseKind.POSTGRESQL) {
try {
connection.rollback();
} catch (SQLException ignored) {
// Preserve the sanitized failure from the operation phase.
}
}
}
private static void closeQuietly(Connection connection) {
try {
connection.close();
} catch (SQLException ignored) {
// Never attach raw driver diagnostics to the sanitized operation failure.
}
}
private static void provision(
Connection connection, MetadataDatabaseConfiguration target, MetadataDatabaseKind kind) {
TargetSchemaBaseline baseline;
try {
baseline = TargetSchemaBaseline.load(kind);
} catch (IOException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.BASELINE_RESOURCE, exception);
}
FlywaySchemaHistory history = new FlywaySchemaHistory(kind);
try {
if (history.isCurrent(connection, baseline)) {
return;
}
history.requireEmptyTarget(connection);
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.PRECONDITION, exception);
}
int executionTimeMillis;
try {
executionTimeMillis = execute(connection, baseline);
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.BASELINE_EXECUTION, exception);
}
try {
history.record(connection, baseline, target.username(), executionTimeMillis);
} catch (SQLException exception) {
throw failure(kind, TargetSchemaProvisioningFailure.Phase.HISTORY_WRITE, exception);
}
}
private static int execute(Connection connection, TargetSchemaBaseline baseline) throws SQLException {
long startedAt = System.nanoTime();
try (Statement statement = connection.createStatement()) {
for (String sql : baseline.statements()) {
statement.execute(sql);
}
}
return Math.toIntExact(Math.min(Integer.MAX_VALUE, (System.nanoTime() - startedAt) / 1_000_000L));
}
private static TargetSchemaProvisioningException failure(
MetadataDatabaseKind kind, TargetSchemaProvisioningFailure.Phase phase, Throwable exception) {
return new TargetSchemaProvisioningException(kind, TargetSchemaProvisioningFailure.from(phase, exception));
}
private static MetadataDatabaseKind supportedKind(MetadataDatabaseKind kind) {
return switch (Objects.requireNonNull(kind, "target kind")) {
case MYSQL -> MetadataDatabaseKind.MYSQL;
case POSTGRESQL -> MetadataDatabaseKind.POSTGRESQL;
case H2 -> throw new IllegalArgumentException("External target schema provisioning does not support H2");
};
}
}
@@ -0,0 +1,323 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Captures only JDBC metadata that is stable across compatible drivers and vendor versions. */
final class JdbcTargetSchemaState {
private JdbcTargetSchemaState() {
}
static SchemaState capture(
Connection connection,
MetadataDatabaseKind kind,
Set<String> baselineTables) throws SQLException {
DatabaseMetaData metadata = connection.getMetaData();
String catalog = connection.getCatalog();
String schema = kind == MetadataDatabaseKind.POSTGRESQL ? connection.getSchema() : null;
FactCollector facts = new FactCollector();
for (String table : baselineTables.stream().sorted().toList()) {
facts.add("table", table);
readColumns(metadata, catalog, schema, table, kind, facts);
readPrimaryKey(metadata, catalog, schema, table, facts);
readIndexes(metadata, catalog, schema, table, facts);
readForeignKeys(metadata, catalog, schema, table, facts);
}
return facts.build();
}
private static void readColumns(
DatabaseMetaData metadata,
String catalog,
String schema,
String table,
MetadataDatabaseKind kind,
FactCollector facts) throws SQLException {
try (ResultSet columns = metadata.getColumns(catalog, schema, table, null)) {
while (columns.next()) {
int jdbcType = columns.getInt("DATA_TYPE");
int size = columns.getInt("COLUMN_SIZE");
int scale = columns.getInt("DECIMAL_DIGITS");
facts.add(
"column",
table,
normalize(columns.getString("COLUMN_NAME")),
stableTypeFamily(kind, jdbcType, size, scale),
nullable(columns.getInt("NULLABLE")));
}
}
}
private static void readPrimaryKey(
DatabaseMetaData metadata,
String catalog,
String schema,
String table,
FactCollector facts) throws SQLException {
OrderedColumns columns = new OrderedColumns();
try (ResultSet keys = metadata.getPrimaryKeys(catalog, schema, table)) {
while (keys.next()) {
columns.add(keys.getShort("KEY_SEQ"), normalize(keys.getString("COLUMN_NAME")));
}
}
if (!columns.isEmpty()) {
facts.add("primary-key", table, columns.definition());
}
}
private static void readIndexes(
DatabaseMetaData metadata,
String catalog,
String schema,
String table,
FactCollector facts) throws SQLException {
Map<String, IndexColumns> indexes = new HashMap<>();
int unnamedIndex = 0;
try (ResultSet rows = metadata.getIndexInfo(catalog, schema, table, false, false)) {
while (rows.next()) {
String name = rows.getString("INDEX_NAME");
String column = rows.getString("COLUMN_NAME");
short position = rows.getShort("ORDINAL_POSITION");
if (column == null || rows.getShort("TYPE") == DatabaseMetaData.tableIndexStatistic) {
continue;
}
// Names group composite rows from one JDBC result only; the semantic fact never retains the name.
if (name == null && position == 1) {
unnamedIndex++;
}
String group = name == null ? "<unnamed-index-" + unnamedIndex + '>' : normalize(name);
boolean unique = !rows.getBoolean("NON_UNIQUE");
indexes.computeIfAbsent(group, ignored -> new IndexColumns(unique))
.add(position, normalize(column));
}
}
indexes.values().forEach(index -> facts.add(
"index", table, Boolean.toString(index.unique()), index.columns().definition()));
}
private static void readForeignKeys(
DatabaseMetaData metadata,
String catalog,
String schema,
String table,
FactCollector facts) throws SQLException {
Map<String, ForeignKeyColumns> keys = new HashMap<>();
int unnamedKey = 0;
try (ResultSet rows = metadata.getImportedKeys(catalog, schema, table)) {
while (rows.next()) {
String referencedTable = normalize(rows.getString("PKTABLE_NAME"));
String name = rows.getString("FK_NAME");
short position = rows.getShort("KEY_SEQ");
String updateRule = foreignKeyRule(rows.getShort("UPDATE_RULE"));
String deleteRule = foreignKeyRule(rows.getShort("DELETE_RULE"));
String deferrability = foreignKeyDeferrability(rows.getShort("DEFERRABILITY"));
// As with indexes, the provider name only groups rows and is absent from the persisted definition.
if (name == null && position == 1) {
unnamedKey++;
}
String group = name == null ? "<unnamed-key-" + unnamedKey + '>' : normalize(name);
keys.computeIfAbsent(group, ignored ->
new ForeignKeyColumns(referencedTable, updateRule, deleteRule, deferrability))
.add(
position,
normalize(rows.getString("FKCOLUMN_NAME")),
normalize(rows.getString("PKCOLUMN_NAME")));
}
}
keys.values().forEach(key -> facts.add(
"foreign-key", table, key.localColumns(), key.referencedTable(), key.referencedColumns(),
key.updateRule(), key.deleteRule(), key.deferrability()));
}
private static String stableTypeFamily(
MetadataDatabaseKind kind,
int jdbcType,
int size,
int scale) {
return switch (jdbcType) {
case Types.BOOLEAN -> "boolean";
case Types.BIT -> kind == MetadataDatabaseKind.MYSQL ? "boolean" : "binary-bit(" + size + ')';
case Types.TINYINT -> kind == MetadataDatabaseKind.MYSQL && size == 1 ? "boolean" : "tinyint";
case Types.SMALLINT -> "smallint";
case Types.INTEGER -> "integer";
case Types.BIGINT -> "bigint";
case Types.NUMERIC, Types.DECIMAL -> "decimal(" + size + ',' + scale + ')';
case Types.REAL -> "real";
case Types.FLOAT -> "float";
case Types.DOUBLE -> "double";
case Types.CHAR, Types.NCHAR, Types.VARCHAR, Types.NVARCHAR -> "character(" + size + ')';
case Types.LONGVARCHAR, Types.LONGNVARCHAR, Types.CLOB, Types.NCLOB -> "large-text";
case Types.BINARY, Types.VARBINARY -> "binary(" + size + ')';
case Types.LONGVARBINARY, Types.BLOB -> "large-binary";
case Types.DATE -> "date";
case Types.TIME, Types.TIME_WITH_TIMEZONE -> "time";
case Types.TIMESTAMP, Types.TIMESTAMP_WITH_TIMEZONE -> "timestamp";
default -> "jdbc-type(" + jdbcType + ')';
};
}
private static String nullable(int value) throws SQLException {
return switch (value) {
case DatabaseMetaData.columnNoNulls -> "required";
case DatabaseMetaData.columnNullable -> "nullable";
default -> throw new SQLException("Target schema column nullability is unknown", "55000");
};
}
private static String foreignKeyRule(short value) throws SQLException {
return switch (value) {
case DatabaseMetaData.importedKeyCascade -> "cascade";
case DatabaseMetaData.importedKeyRestrict -> "restrict";
case DatabaseMetaData.importedKeySetNull -> "set-null";
case DatabaseMetaData.importedKeyNoAction -> "no-action";
case DatabaseMetaData.importedKeySetDefault -> "set-default";
default -> throw new SQLException("Target schema foreign-key rule is unknown", "55000");
};
}
private static String foreignKeyDeferrability(short value) throws SQLException {
return switch (value) {
case DatabaseMetaData.importedKeyInitiallyDeferred -> "initially-deferred";
case DatabaseMetaData.importedKeyInitiallyImmediate -> "initially-immediate";
case DatabaseMetaData.importedKeyNotDeferrable -> "not-deferrable";
default -> throw new SQLException("Target schema foreign-key deferrability is unknown", "55000");
};
}
private static String normalize(String value) {
return value == null ? "<unnamed>" : value.toLowerCase(Locale.ROOT);
}
record SchemaState(Map<String, Integer> facts) {
SchemaState {
facts = Map.copyOf(facts);
}
}
private static final class FactCollector {
private final Map<String, Integer> facts = new TreeMap<>();
void add(String... parts) {
facts.merge(String.join("|", parts), 1, Integer::sum);
}
SchemaState build() {
return new SchemaState(facts);
}
}
private static class OrderedColumns {
private final Map<Short, String> columns = new TreeMap<>();
void add(short position, String column) {
columns.put(position, column);
}
boolean isEmpty() {
return columns.isEmpty();
}
String definition() {
return String.join(",", columns.values());
}
}
private static final class IndexColumns {
private final boolean unique;
private final OrderedColumns columns = new OrderedColumns();
private IndexColumns(boolean unique) {
this.unique = unique;
}
void add(short position, String column) {
columns.add(position, column);
}
boolean unique() {
return unique;
}
OrderedColumns columns() {
return columns;
}
}
private static final class ForeignKeyColumns {
private final String referencedTable;
private final String updateRule;
private final String deleteRule;
private final String deferrability;
private final OrderedColumns localColumns = new OrderedColumns();
private final OrderedColumns referencedColumns = new OrderedColumns();
private ForeignKeyColumns(
String referencedTable, String updateRule, String deleteRule, String deferrability) {
this.referencedTable = referencedTable;
this.updateRule = updateRule;
this.deleteRule = deleteRule;
this.deferrability = deferrability;
}
void add(short position, String localColumn, String referencedColumn) {
localColumns.add(position, localColumn);
referencedColumns.add(position, referencedColumn);
}
String localColumns() {
return localColumns.definition();
}
String referencedTable() {
return referencedTable;
}
String referencedColumns() {
return referencedColumns.definition();
}
String updateRule() {
return "update=" + updateRule;
}
String deleteRule() {
return "delete=" + deleteRule;
}
String deferrability() {
return "deferrability=" + deferrability;
}
}
}
@@ -0,0 +1,171 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Loaded current-version baseline plus the metadata expected by Flyway schema history. */
final class TargetSchemaBaseline {
private static final Pattern CREATE_TABLE = Pattern.compile(
"(?i)^create\\s+table\\s+(?:if\\s+not\\s+exists\\s+)?([a-z][a-z0-9_]*)\\s*\\(");
static final String VERSION = "206";
static final String DESCRIPTION = "current schema";
static final String SCRIPT = "B206__current_schema.sql";
static final String TYPE = "SQL_BASELINE";
private final List<String> statements;
private final Set<String> expectedTables;
private final int checksum;
private TargetSchemaBaseline(List<String> statements, Set<String> expectedTables, int checksum) {
this.statements = statements;
this.expectedTables = expectedTables;
this.checksum = checksum;
}
static TargetSchemaBaseline load(MetadataDatabaseKind kind) throws IOException {
String vendor = switch (kind) {
case MYSQL -> "mysql";
case POSTGRESQL -> "postgresql";
case H2 -> throw new IllegalArgumentException("H2 has no external target baseline");
};
String location = "/db/migration/" + vendor + "/" + SCRIPT;
try (InputStream input = TargetSchemaBaseline.class.getResourceAsStream(location)) {
if (input == null) {
throw new IOException("Target schema baseline resource is missing");
}
String sql = new String(input.readAllBytes(), StandardCharsets.UTF_8);
List<String> statements = splitStatements(sql);
return new TargetSchemaBaseline(statements, expectedTables(statements), checksum(sql));
}
}
List<String> statements() {
return statements;
}
int checksum() {
return checksum;
}
Set<String> expectedTables() {
return expectedTables;
}
private static int checksum(String sql) throws IOException {
CRC32 checksum = new CRC32();
try (BufferedReader reader = new BufferedReader(new StringReader(sql))) {
String line = reader.readLine();
if (line != null) {
line = removeByteOrderMark(line);
do {
checksum.update(line.getBytes(StandardCharsets.UTF_8));
} while ((line = reader.readLine()) != null);
}
}
return (int) checksum.getValue();
}
private static String removeByteOrderMark(String line) {
return line.startsWith("\ufeff") ? line.substring(1) : line;
}
private static List<String> splitStatements(String script) throws IOException {
List<String> statements = new ArrayList<>();
StringBuilder current = new StringBuilder();
char quote = 0;
boolean lineComment = false;
for (int index = 0; index < script.length(); index++) {
char character = script.charAt(index);
char next = index + 1 < script.length() ? script.charAt(index + 1) : 0;
if (lineComment) {
if (character == '\n' || character == '\r') {
lineComment = false;
current.append(character);
}
continue;
}
if (quote == 0 && character == '-' && next == '-') {
lineComment = true;
index++;
continue;
}
if (quote == 0 && (character == '\'' || character == '"' || character == '`')) {
quote = character;
current.append(character);
continue;
}
if (quote != 0 && character == quote) {
current.append(character);
if (next == quote) {
current.append(next);
index++;
} else {
quote = 0;
}
continue;
}
if (quote == 0 && character == ';') {
addStatement(statements, current);
continue;
}
current.append(character);
}
if (quote != 0) {
throw new IOException("Target schema baseline contains an unterminated quoted value");
}
addStatement(statements, current);
return List.copyOf(statements);
}
private static void addStatement(List<String> statements, StringBuilder current) {
String statement = current.toString().trim();
if (!statement.isEmpty()) {
statements.add(statement);
}
current.setLength(0);
}
private static Set<String> expectedTables(List<String> statements) throws IOException {
Set<String> tables = new LinkedHashSet<>();
for (String statement : statements) {
Matcher matcher = CREATE_TABLE.matcher(statement);
if (matcher.find()) {
tables.add(matcher.group(1).toLowerCase(Locale.ROOT));
}
}
if (tables.isEmpty()) {
throw new IOException("Target schema baseline does not declare any tables");
}
return Set.copyOf(tables);
}
}
@@ -0,0 +1,86 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Persists and compares the human-readable semantic contract for a provisioned baseline. */
final class TargetSchemaContract {
static final String TABLE = "flyway_schema_contract";
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE + " ("
+ "contract_id INT NOT NULL, database_kind VARCHAR(20) NOT NULL, definition TEXT NOT NULL, "
+ "occurrences INT NOT NULL, CONSTRAINT flyway_schema_contract_pk PRIMARY KEY (contract_id))";
private final MetadataDatabaseKind kind;
TargetSchemaContract(MetadataDatabaseKind kind) {
this.kind = kind;
}
void record(Connection connection, Set<String> baselineTables) throws SQLException {
JdbcTargetSchemaState.SchemaState state = JdbcTargetSchemaState.capture(connection, kind, baselineTables);
try (Statement statement = connection.createStatement()) {
statement.execute(CREATE_TABLE);
}
String insert = "INSERT INTO " + TABLE
+ " (contract_id, database_kind, definition, occurrences) VALUES (?, ?, ?, ?)";
try (PreparedStatement statement = connection.prepareStatement(insert)) {
int contractId = 1;
for (Map.Entry<String, Integer> fact : state.facts().entrySet()) {
statement.setInt(1, contractId++);
statement.setString(2, kind.name());
statement.setString(3, fact.getKey());
statement.setInt(4, fact.getValue());
statement.addBatch();
}
statement.executeBatch();
}
}
boolean matches(Connection connection, Set<String> baselineTables) throws SQLException {
return JdbcTargetSchemaState.capture(connection, kind, baselineTables).equals(readRecordedState(connection));
}
private JdbcTargetSchemaState.SchemaState readRecordedState(Connection connection) throws SQLException {
Map<String, Integer> facts = new TreeMap<>();
String select = "SELECT database_kind, definition, occurrences FROM " + TABLE;
try (PreparedStatement statement = connection.prepareStatement(select)) {
try (ResultSet rows = statement.executeQuery()) {
while (rows.next()) {
if (!kind.name().equals(rows.getString("database_kind"))) {
throw new SQLException("Target schema contract contains another database kind", "55000");
}
String definition = rows.getString("definition");
if (facts.put(definition, rows.getInt("occurrences")) != null) {
throw new SQLException("Target schema contract contains duplicate definitions", "55000");
}
}
}
}
return new JdbcTargetSchemaState.SchemaState(facts);
}
}
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
/** Provisions an empty external metadata target at the current schema version. */
public interface TargetSchemaProvisioner {
void provision(MetadataDatabaseConfiguration target);
}
@@ -0,0 +1,36 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Safe failure boundary that does not retain target credentials, URLs, or baseline SQL. */
public final class TargetSchemaProvisioningException extends RuntimeException {
private final TargetSchemaProvisioningFailure failure;
TargetSchemaProvisioningException(
MetadataDatabaseKind kind, TargetSchemaProvisioningFailure failure) {
super("Target schema provisioning failed for " + kind);
this.failure = failure;
}
public TargetSchemaProvisioningFailure failure() {
return failure;
}
}
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Pattern;
/** Stable failure fields suitable for a durable setup-operation diagnostic. */
public record TargetSchemaProvisioningFailure(
Phase phase,
String migrationVersion,
String sqlState,
int vendorCode) {
private static final Pattern SQL_STATE = Pattern.compile("[0-9A-Z]{5}");
public TargetSchemaProvisioningFailure {
Objects.requireNonNull(phase, "phase");
Objects.requireNonNull(migrationVersion, "migrationVersion");
}
static TargetSchemaProvisioningFailure from(Phase phase, Throwable exception) {
SQLException sqlException = findSqlException(exception);
return new TargetSchemaProvisioningFailure(
phase,
TargetSchemaBaseline.VERSION,
sqlException == null ? null : sanitizedSqlState(sqlException.getSQLState()),
sqlException == null ? 0 : sqlException.getErrorCode());
}
private static String sanitizedSqlState(String sqlState) {
if (sqlState == null) {
return null;
}
String normalized = sqlState.toUpperCase(Locale.ROOT);
return SQL_STATE.matcher(normalized).matches() ? normalized : null;
}
private static SQLException findSqlException(Throwable exception) {
Throwable current = exception;
for (int depth = 0; current != null && depth < 16; depth++) {
if (current instanceof SQLException sqlException) {
return sqlException;
}
current = current.getCause();
}
return null;
}
/** Lifecycle boundary that failed without retaining an exception or SQL text. */
public enum Phase {
CONNECTION,
BASELINE_RESOURCE,
PRECONDITION,
BASELINE_EXECUTION,
HISTORY_WRITE,
TRANSACTION,
CLEANUP
}
}
@@ -0,0 +1,294 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Logger;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.logging.Log;
import org.flywaydb.core.api.logging.LogCreator;
import org.flywaydb.core.api.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.ResourceLock;
class FlywayTargetSchemaProvisionerTest {
private static final String FLYWAY_LOG_FACTORY = "flyway-log-factory";
@Test
void rejectsEmbeddedTargetsBeforeConnectionOpen() {
MetadataDatabaseConfiguration target = new MetadataDatabaseConfiguration(
MetadataDatabaseKind.H2, "jdbc:h2:mem:not-opened", "sa", "not-retained");
assertThatThrownBy(() -> new FlywayTargetSchemaProvisioner().provision(target))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("External target schema provisioning does not support H2");
}
@Test
void failureDoesNotRetainJdbcUrlPasswordOrFlywayDetails() {
String jdbcUrl = "jdbc:mysql://invalid.example.test:3306/hertzbeat";
String password = "not-retained";
MetadataDatabaseConfiguration target =
new MetadataDatabaseConfiguration(MetadataDatabaseKind.MYSQL, jdbcUrl, "operator", password);
assertThatThrownBy(() -> new FlywayTargetSchemaProvisioner().provision(target))
.isInstanceOf(TargetSchemaProvisioningException.class)
.hasMessage("Target schema provisioning failed for MYSQL")
.hasNoCause()
.message()
.doesNotContain(jdbcUrl, password, "SELECT", "CREATE");
}
@Test
void failureExposesOnlyStableStructuredDiagnostics() throws Exception {
String jdbcUrl = "jdbc:diagnostic://private.example.test/hertzbeat?password=secret-value";
Driver driver = new DiagnosticFailureDriver(jdbcUrl);
DriverManager.registerDriver(driver);
try {
MetadataDatabaseConfiguration target = new MetadataDatabaseConfiguration(
MetadataDatabaseKind.MYSQL, jdbcUrl, "operator", "secret-value");
assertThatThrownBy(() -> new FlywayTargetSchemaProvisioner().provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception -> {
assertThat(exception.failure()).isEqualTo(new TargetSchemaProvisioningFailure(
TargetSchemaProvisioningFailure.Phase.CONNECTION,
"206",
"08006",
1045));
assertThat(exception).hasNoCause();
assertThat(exception.getMessage())
.doesNotContain(jdbcUrl, "secret-value", "SELECT", "CREATE");
});
} finally {
DriverManager.deregisterDriver(driver);
}
}
@Test
void closeFailureIsNotAttachedToSanitizedOperationFailure() throws Exception {
String jdbcUrl = "jdbc:close-failure://private.example.test/hertzbeat";
Driver driver = new CloseFailureDriver(jdbcUrl);
DriverManager.registerDriver(driver);
try {
MetadataDatabaseConfiguration target = new MetadataDatabaseConfiguration(
MetadataDatabaseKind.MYSQL, jdbcUrl, "operator", "secret-value");
assertThatThrownBy(() -> new FlywayTargetSchemaProvisioner().provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception -> {
assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.BASELINE_RESOURCE);
assertThat(exception.getSuppressed()).isEmpty();
assertThat(exception.getMessage()).doesNotContain(jdbcUrl, "secret-value", "SELECT");
});
} finally {
DriverManager.deregisterDriver(driver);
}
}
@Test
@ResourceLock(FLYWAY_LOG_FACTORY)
void provisioningDoesNotReplaceLoggerUsedByAnInterleavedFlywayOperation() throws Exception {
RecordingLogCreator recording = new RecordingLogCreator();
LogFactory.setLogCreator(recording);
CountDownLatch provisioningFinished = new CountDownLatch(1);
try (ExecutorService executor = Executors.newFixedThreadPool(2)) {
Future<?> provisioning = executor.submit(() -> {
try {
MetadataDatabaseConfiguration target = new MetadataDatabaseConfiguration(
MetadataDatabaseKind.MYSQL,
"jdbc:mysql://127.0.0.1:1/hertzbeat?connectTimeout=100",
"operator",
"test-only-password");
assertThatThrownBy(() -> new FlywayTargetSchemaProvisioner().provision(target))
.isInstanceOf(TargetSchemaProvisioningException.class);
} finally {
provisioningFinished.countDown();
}
});
Future<?> interleavedLog = executor.submit(() -> {
provisioningFinished.await();
LogFactory.getLog(FlywayTargetSchemaProvisionerTest.class).info("unrelated-flyway-operation");
return null;
});
provisioning.get();
interleavedLog.get();
assertThat(recording.messages()).contains("unrelated-flyway-operation");
} finally {
LogFactory.setConfiguration(Flyway.configure());
}
}
private static final class RecordingLogCreator implements LogCreator {
private final List<String> messages = new CopyOnWriteArrayList<>();
@Override
public Log createLogger(Class<?> clazz) {
return new RecordingLog(messages);
}
List<String> messages() {
return List.copyOf(messages);
}
}
private record RecordingLog(List<String> messages) implements Log {
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public void debug(String message) {
messages.add(message);
}
@Override
public void info(String message) {
messages.add(message);
}
@Override
public void warn(String message) {
messages.add(message);
}
@Override
public void error(String message) {
messages.add(message);
}
@Override
public void error(String message, Exception exception) {
messages.add(message);
}
@Override
public void notice(String message) {
messages.add(message);
}
}
private record DiagnosticFailureDriver(String acceptedUrl) implements Driver {
@Override
public Connection connect(String url, Properties info) throws SQLException {
if (!acceptsURL(url)) {
return null;
}
throw new SQLException("Connection failed for " + url + " after SELECT secret-value", "08006", 1045);
}
@Override
public boolean acceptsURL(String url) {
return acceptedUrl.equals(url);
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() {
return Logger.getAnonymousLogger();
}
}
private record CloseFailureDriver(String acceptedUrl) implements Driver {
@Override
public Connection connect(String url, Properties info) {
if (!acceptsURL(url)) {
return null;
}
return (Connection) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class<?>[]{Connection.class}, (proxy, method, arguments) -> {
if (method.getName().equals("close")) {
throw new SQLException("close leaked " + url + " after SELECT secret-value", "08006", 999);
}
return null;
});
}
@Override
public boolean acceptsURL(String url) {
return acceptedUrl.equals(url);
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() {
return Logger.getAnonymousLogger();
}
}
}
@@ -0,0 +1,139 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Set;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.junit.jupiter.api.Test;
class TargetSchemaContractCompatibilityTest {
private static final Set<String> TABLES = Set.of("contract_parent", "contract_child");
@Test
void equivalentSchemaDoesNotDependOnJdbcPresentationMetadata() throws Exception {
try (Connection first = schema("representation_a", "alpha", "1", "first", "asc", true);
Connection second = schema("representation_b", "beta", "2", "second", "desc", false)) {
assertThat(JdbcTargetSchemaState.capture(first, MetadataDatabaseKind.MYSQL, TABLES))
.isEqualTo(JdbcTargetSchemaState.capture(second, MetadataDatabaseKind.MYSQL, TABLES));
}
}
@Test
void recordedContractRejectsDuplicateHumanReadableDefinitions() throws Exception {
try (Connection connection = schema("duplicate_contract", "stable", "1", "remarks", "asc", false);
Statement statement = connection.createStatement()) {
TargetSchemaContract contract = new TargetSchemaContract(MetadataDatabaseKind.MYSQL);
contract.record(connection, TABLES);
statement.execute("INSERT INTO flyway_schema_contract "
+ "(contract_id, database_kind, definition, occurrences) "
+ "SELECT 9999, database_kind, definition, occurrences FROM flyway_schema_contract "
+ "FETCH FIRST 1 ROW ONLY");
assertThatThrownBy(() -> contract.matches(connection, TABLES))
.isInstanceOf(SQLException.class)
.hasMessage("Target schema contract contains duplicate definitions");
}
}
@Test
void semanticStatePreservesIntegerWidth() throws Exception {
try (Connection baseline = semanticSchema("semantic_baseline", "BIGINT", "");
Connection narrowerInteger = semanticSchema("semantic_integer", "INTEGER", "")) {
JdbcTargetSchemaState.SchemaState baselineState =
JdbcTargetSchemaState.capture(baseline, MetadataDatabaseKind.MYSQL, TABLES);
assertThat(JdbcTargetSchemaState.capture(narrowerInteger, MetadataDatabaseKind.MYSQL, TABLES))
.isNotEqualTo(baselineState);
}
}
@Test
void semanticStatePreservesForeignKeyActions() throws Exception {
try (Connection baseline = semanticSchema("foreign_key_baseline", "BIGINT", "");
Connection cascadingDelete =
semanticSchema("foreign_key_cascade", "BIGINT", " ON DELETE CASCADE")) {
JdbcTargetSchemaState.SchemaState baselineState =
JdbcTargetSchemaState.capture(baseline, MetadataDatabaseKind.MYSQL, TABLES);
assertThat(JdbcTargetSchemaState.capture(cascadingDelete, MetadataDatabaseKind.MYSQL, TABLES))
.isNotEqualTo(baselineState);
}
}
@Test
void recordedContractRejectsRowsForAnotherDatabaseKind() throws Exception {
try (Connection connection = schema("cross_kind_contract", "stable", "1", "remarks", "asc", false);
Statement statement = connection.createStatement()) {
TargetSchemaContract contract = new TargetSchemaContract(MetadataDatabaseKind.MYSQL);
contract.record(connection, TABLES);
statement.execute("INSERT INTO flyway_schema_contract "
+ "(contract_id, database_kind, definition, occurrences) "
+ "VALUES (9999, 'POSTGRESQL', 'table|intruder', 1)");
assertThatThrownBy(() -> contract.matches(connection, TABLES))
.isInstanceOfSatisfying(SQLException.class,
exception -> assertThat(exception.getSQLState()).isEqualTo("55000"));
}
}
private static Connection schema(
String database,
String objectSuffix,
String defaultValue,
String remarks,
String indexOrder,
boolean identity) throws Exception {
Connection connection = DriverManager.getConnection(
"jdbc:h2:mem:" + database + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
try (Statement statement = connection.createStatement()) {
String id = identity ? "BIGINT GENERATED BY DEFAULT AS IDENTITY" : "BIGINT";
statement.execute("CREATE TABLE contract_parent (id " + id
+ ", CONSTRAINT pk_parent_" + objectSuffix + " PRIMARY KEY (id))");
statement.execute("CREATE TABLE contract_child (id BIGINT NOT NULL, parent_id BIGINT, "
+ "label VARCHAR(64) NOT NULL DEFAULT '" + defaultValue + "', "
+ "CONSTRAINT pk_child_" + objectSuffix + " PRIMARY KEY (id), "
+ "CONSTRAINT fk_child_" + objectSuffix
+ " FOREIGN KEY (parent_id) REFERENCES contract_parent(id))");
statement.execute("COMMENT ON COLUMN contract_child.label IS '" + remarks + "'");
statement.execute("CREATE INDEX ix_child_" + objectSuffix
+ " ON contract_child(parent_id " + indexOrder + ", label " + indexOrder + ")");
}
return connection;
}
private static Connection semanticSchema(String database, String integerType, String foreignKeyAction)
throws Exception {
Connection connection = DriverManager.getConnection(
"jdbc:h2:mem:" + database + ";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1", "sa", "");
try (Statement statement = connection.createStatement()) {
statement.execute("CREATE TABLE contract_parent (id BIGINT PRIMARY KEY)");
statement.execute("CREATE TABLE contract_child (id BIGINT PRIMARY KEY, parent_id " + integerType
+ ", CONSTRAINT fk_child FOREIGN KEY (parent_id) REFERENCES contract_parent(id)"
+ foreignKeyAction + ")");
}
return connection;
}
}
@@ -0,0 +1,881 @@
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0.
--
-- Static V206 schema baseline for provisioning an empty MySQL target.
-- Future versioned migrations start at V207 or later.
create table hzb_ai_conversation (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
creator varchar(255),
modifier varchar(255),
title varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_ai_message (
conversation_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
creator varchar(255),
modifier varchar(255),
role varchar(255),
content longtext not null,
primary key (id)
) engine=InnoDB;
create table hzb_alert_define (
enable bit not null,
period integer,
times integer,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
datasource varchar(100),
name varchar(100) not null,
expr varchar(2048),
labels varchar(2048),
template varchar(2048),
annotations varchar(4096),
creator varchar(255),
modifier varchar(255),
type varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_define_monitor_bind (
alert_define_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
primary key (id)
) engine=InnoDB;
create table hzb_alert_group (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
common_labels varchar(2048),
group_key varchar(2048) character set ascii,
group_labels varchar(2048),
alert_fingerprints TEXT,
common_annotations TEXT,
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_group_converge (
enable bit,
gmt_create datetime(6),
gmt_update datetime(6),
group_interval bigint,
group_wait bigint,
id bigint not null auto_increment,
repeat_interval bigint,
name varchar(100) not null,
group_labels varchar(1024),
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_inhibit (
enable bit,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
name varchar(100) not null,
equal_labels varchar(2048),
source_labels varchar(2048),
target_labels varchar(2048),
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_silence (
enable bit not null,
match_all bit not null,
times integer,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
period_end datetime(6),
period_start datetime(6),
name varchar(100) not null,
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_single (
trigger_times integer,
active_at bigint,
end_at bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
start_at bigint,
fingerprint varchar(2048) character set ascii,
labels varchar(2048),
annotations varchar(4096),
content varchar(4096),
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_bulletin (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
fields varchar(4096),
monitor_ids varchar(4096),
app varchar(255),
creator varchar(255),
modifier varchar(255),
name varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_collector (
status tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
creator varchar(255),
ip varchar(255) not null,
mode varchar(255),
modifier varchar(255),
name varchar(255) not null,
version varchar(255),
primary key (id),
check ((status>=0))
) engine=InnoDB;
create table hzb_collector_monitor_bind (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
collector varchar(255),
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_config (
gmt_create datetime(6),
gmt_update datetime(6),
content varchar(8192),
creator varchar(255),
modifier varchar(255),
type varchar(255) not null,
primary key (type)
) engine=InnoDB;
create table hzb_define (
gmt_create datetime(6),
gmt_update datetime(6),
app varchar(255) not null,
creator varchar(255),
modifier varchar(255),
content longtext,
primary key (app)
) engine=InnoDB;
create table hzb_grafana_dashboard (
enabled bit not null,
monitor_id bigint not null,
version bigint,
folder_uid varchar(255),
slug varchar(255),
status varchar(255),
uid varchar(255),
url varchar(255),
primary key (monitor_id)
) engine=InnoDB;
create table hzb_history (
dou float(53),
int32 integer,
metric_type tinyint,
id bigint not null auto_increment,
time bigint,
str varchar(2048),
app varchar(255),
metric_labels varchar(5000),
metric varchar(255),
metrics varchar(255),
instance varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_metrics_favorite (
create_time datetime(6),
id bigint not null auto_increment,
monitor_id bigint not null,
creator varchar(255) not null,
metrics_name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_monitor (
intervals integer,
status tinyint not null,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null,
job_id bigint,
schedule_type varchar(20),
app varchar(100),
cron_expression varchar(100),
instance varchar(100),
name varchar(100),
scrape varchar(100),
annotations varchar(4096),
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
primary key (id),
check ((status<=4) and (status>=0))
) engine=InnoDB;
create table hzb_monitor_bind (
biz_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
creator varchar(255),
key_str varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_notice_receiver (
agent_id integer,
lark_receive_type tinyint,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
smn_ak varchar(22),
smn_project_id varchar(32),
smn_region varchar(32),
smn_sk varchar(42),
email varchar(100),
name varchar(100) not null,
phone varchar(100),
access_token varchar(300),
discord_bot_token varchar(300),
discord_channel_id varchar(300),
gotify_token varchar(300),
hook_auth_token varchar(300),
hook_auth_type varchar(300),
server_chan_token varchar(300),
slack_web_hook_url varchar(300),
smn_topic_urn varchar(300),
wechat_id varchar(300),
hook_url varchar(1000),
app_id varchar(255),
app_secret varchar(255),
chat_id varchar(255),
corp_id varchar(255),
creator varchar(255),
modifier varchar(255),
party_id varchar(255),
tag_id varchar(255),
tg_bot_token varchar(255),
tg_message_thread_id varchar(255),
tg_user_id varchar(255),
user_id varchar(255),
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_notice_rule (
enable bit not null,
filter_all bit not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
period_end datetime(6),
period_start datetime(6),
template_id bigint,
name varchar(100) not null,
template_name varchar(100),
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
receiver_id varchar(255) not null,
receiver_name varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_notice_template (
preset boolean default false,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
name varchar(100) not null,
creator varchar(255),
modifier varchar(255),
content text not null,
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_param (
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_param_define (
hide bit not null,
param_limit smallint,
required bit not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
name varchar(2048),
param_options varchar(2048),
app varchar(255),
creator varchar(255),
default_value varchar(255),
depend varchar(255),
field varchar(255),
key_alias varchar(255),
modifier varchar(255),
param_range varchar(255),
placeholder varchar(255),
type varchar(255),
value_alias varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_plugin_item (
id bigint not null auto_increment,
metadata_id bigint,
class_identifier varchar(255),
type enum ('POST_ALERT','POST_COLLECT'),
primary key (id)
) engine=InnoDB;
create table hzb_plugin_metadata (
enable_status bit,
param_count integer,
gmt_create datetime(6),
id bigint not null auto_increment,
creator varchar(255),
jar_file_path varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_plugin_param (
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
plugin_metadata_id bigint not null,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_push_metrics (
id bigint not null auto_increment,
monitor_id bigint,
time bigint,
metrics varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_sop_schedule (
id bigint not null auto_increment,
conversation_id bigint not null comment 'Conversation ID to push results to',
sop_name varchar(64) not null comment 'Name of the SOP skill to execute',
sop_params varchar(1024) comment 'SOP execution parameters in JSON format',
cron_expression varchar(64) not null comment 'Cron expression for scheduling',
enabled tinyint default 1 comment 'Whether the schedule is enabled',
last_run_time datetime comment 'Last execution time',
next_run_time datetime comment 'Next scheduled execution time',
creator varchar(64) comment 'Creator of this record',
modifier varchar(64) comment 'Last modifier',
gmt_create datetime default current_timestamp comment 'Create time',
gmt_update datetime default current_timestamp on update current_timestamp comment 'Update time',
primary key (id)
) engine=InnoDB;
create table hzb_status_page_component (
config_state tinyint not null,
method tinyint not null,
state tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
org_id bigint,
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_status_page_history (
abnormal integer,
normal integer,
state tinyint not null,
unknowing integer,
uptime float(53),
component_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
timestamp bigint,
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_status_page_incident (
state tinyint not null,
end_time bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
org_id bigint,
start_time bigint,
creator varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_status_page_incident_component_bind (
component_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
incident_id bigint,
primary key (id)
) engine=InnoDB;
create table hzb_status_page_incident_content (
state tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
incident_id bigint,
timestamp bigint,
creator varchar(255),
message TEXT,
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_status_page_org (
state tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
color varchar(255),
creator varchar(255),
description varchar(255) not null,
feedback varchar(255),
home varchar(255) not null,
logo varchar(255) not null,
modifier varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_tag (
type tinyint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
tag_value varchar(2048),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id),
check ((type<=3) and (type>=0))
) engine=InnoDB;
create index idx_message_conversation_id
on hzb_ai_message (conversation_id);
create index idx_alert_define_id
on hzb_alert_define_monitor_bind (alert_define_id);
create index idx_monitor_id
on hzb_alert_define_monitor_bind (monitor_id);
alter table hzb_alert_group
add constraint unique_group_key unique (group_key);
create index idx_name
on hzb_alert_group_converge (name);
alter table hzb_alert_single
add constraint unique_fingerprint unique (fingerprint);
alter table hzb_collector
add constraint uk_hzb_collector_name unique (name);
create index idx_collector_monitor_collector
on hzb_collector_monitor_bind (collector);
create index idx_collector_monitor_monitor_id
on hzb_collector_monitor_bind (monitor_id);
create index idx_hzb_history_instance
on hzb_history (instance);
create index idx_hzb_history_app
on hzb_history (app);
create index idx_hzb_history_metrics
on hzb_history (metrics);
create index idx_hzb_history_metric
on hzb_history (metric);
alter table hzb_metrics_favorite
add constraint uk_hzb_metrics_favorite unique (creator, monitor_id, metrics_name);
create index idx_hzb_monitor_app
on hzb_monitor (app);
create index idx_hzb_monitor_instance
on hzb_monitor (instance);
create index idx_hzb_monitor_name
on hzb_monitor (name);
create index index_monitor_bind
on hzb_monitor_bind (biz_id);
create index index_monitor_bin
on hzb_monitor_bind (monitor_id);
create index idx_hzb_param_monitor_id
on hzb_param (monitor_id);
alter table hzb_param
add constraint uk_hzb_param_monitor_field unique (monitor_id, field);
create index idx_hzb_plugin_param_plugin_metadata_id
on hzb_plugin_param (plugin_metadata_id);
alter table hzb_plugin_param
add constraint uk_hzb_plugin_param_metadata_field unique (plugin_metadata_id, field);
create index idx_push_metrics_monitor_id
on hzb_push_metrics (monitor_id);
create index idx_push_metrics_time
on hzb_push_metrics (time);
create index idx_schedule_conversation_id
on hzb_sop_schedule (conversation_id);
create index idx_schedule_enabled_next
on hzb_sop_schedule (enabled, next_run_time);
create index index_incident_component
on hzb_status_page_incident_component_bind (incident_id);
create index idx_incident_component_component_id
on hzb_status_page_incident_component_bind (component_id);
alter table hzb_ai_message
add constraint fk_hzb_ai_message_conversation
foreign key (conversation_id)
references hzb_ai_conversation (id);
alter table hzb_plugin_item
add constraint fk_hzb_plugin_item_metadata
foreign key (metadata_id)
references hzb_plugin_metadata (id);
alter table hzb_status_page_incident_content
add constraint fk_hzb_incident_content_incident
foreign key (incident_id)
references hzb_status_page_incident (id);
CREATE TABLE hzb_entity (
id BIGINT PRIMARY KEY COMMENT 'Entity ID',
entity_type VARCHAR(32) NOT NULL COMMENT 'Entity type',
name VARCHAR(128) NOT NULL COMMENT 'Entity name',
display_name VARCHAR(128) COMMENT 'Entity display name',
sub_type VARCHAR(128) COMMENT 'Entity subtype from HertzBeat v1 definition',
namespace VARCHAR(128) COMMENT 'Namespace',
environment VARCHAR(128) COMMENT 'Deployment environment',
status VARCHAR(32) NOT NULL COMMENT 'Aggregated entity status',
criticality VARCHAR(32) COMMENT 'Entity criticality',
owner VARCHAR(128) COMMENT 'Entity owner',
additional_owners TEXT COMMENT 'Additional owners json',
runbook VARCHAR(512) COMMENT 'Runbook URL or identifier',
lifecycle VARCHAR(64) COMMENT 'Entity lifecycle',
tier VARCHAR(64) COMMENT 'Entity tier',
system_name VARCHAR(128) COMMENT 'Owning system',
component_of TEXT COMMENT 'Parent components or systems',
components TEXT COMMENT 'Child components that belong to this system',
implemented_by TEXT COMMENT 'ImplementedBy references json',
api_interface TEXT COMMENT 'API interface definition json',
inherit_from VARCHAR(255) COMMENT 'Entity inheritance reference',
languages TEXT COMMENT 'Programming languages json',
links TEXT COMMENT 'Entity links json',
contacts TEXT COMMENT 'Entity contacts json',
integrations TEXT COMMENT 'Entity integrations json',
extensions TEXT COMMENT 'Entity custom extensions json',
hertzbeat TEXT COMMENT 'HertzBeat definition blocks json',
source VARCHAR(32) NOT NULL COMMENT 'Entity source',
description VARCHAR(512) COMMENT 'Entity description',
labels VARCHAR(4096) COMMENT 'Entity labels json',
tags TEXT COMMENT 'Entity catalog tags json',
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Entity workspace boundary',
creator VARCHAR(64) COMMENT 'Creator',
modifier VARCHAR(64) COMMENT 'Modifier',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
INDEX idx_hzb_entity_type (entity_type),
INDEX idx_hzb_entity_status (status),
INDEX idx_hzb_entity_name (name),
INDEX idx_hzb_entity_owner (owner),
INDEX idx_hzb_entity_workspace (workspace_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_entity_identity (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_id BIGINT NOT NULL COMMENT 'Entity ID',
identity_type VARCHAR(32) NOT NULL COMMENT 'Identity source type',
identity_key VARCHAR(128) NOT NULL COMMENT 'Identity key',
identity_value VARCHAR(512) NOT NULL COMMENT 'Identity value',
normalized_value VARCHAR(512) NOT NULL COMMENT 'Normalized identity value',
priority INT NOT NULL DEFAULT 40 COMMENT 'Identity priority',
primary_identity TINYINT NOT NULL DEFAULT 0 COMMENT 'Whether primary identity',
creator VARCHAR(64) COMMENT 'Creator',
modifier VARCHAR(64) COMMENT 'Modifier',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_entity_identity (entity_id, identity_key, normalized_value),
INDEX idx_hzb_entity_identity_lookup (identity_key, normalized_value),
INDEX idx_hzb_entity_identity_entity (entity_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_entity_monitor_bind (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_id BIGINT NOT NULL COMMENT 'Entity ID',
monitor_id BIGINT NOT NULL COMMENT 'Monitor ID',
bind_type VARCHAR(32) NOT NULL COMMENT 'Bind type',
bind_source VARCHAR(64) NOT NULL COMMENT 'Bind source',
status VARCHAR(16) NOT NULL COMMENT 'Bind status',
score INT NOT NULL DEFAULT 100 COMMENT 'Bind score',
match_context TEXT COMMENT 'Matched identities json',
creator VARCHAR(64) COMMENT 'Creator',
modifier VARCHAR(64) COMMENT 'Modifier',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_entity_monitor_bind (entity_id, monitor_id),
INDEX idx_hzb_entity_monitor_bind_entity (entity_id),
INDEX idx_hzb_entity_monitor_bind_monitor (monitor_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_entity_relation (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
source_entity_id BIGINT NOT NULL COMMENT 'Source entity ID',
target_entity_id BIGINT NULL COMMENT 'Target entity ID',
target_ref VARCHAR(255) COMMENT 'Target entity reference',
relation_type VARCHAR(32) NOT NULL COMMENT 'Relation type',
relation_source VARCHAR(32) NOT NULL COMMENT 'Relation source',
status VARCHAR(16) NOT NULL COMMENT 'Relation status',
score INT NOT NULL DEFAULT 100 COMMENT 'Relation score',
description VARCHAR(255) COMMENT 'Relation description',
attributes TEXT COMMENT 'Relation attributes json',
creator VARCHAR(64) COMMENT 'Creator',
modifier VARCHAR(64) COMMENT 'Modifier',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_entity_relation (source_entity_id, target_entity_id, relation_type),
INDEX idx_hzb_entity_relation_source (source_entity_id),
INDEX idx_hzb_entity_relation_target (target_entity_id),
INDEX idx_hzb_entity_relation_target_ref (target_ref)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_entity_definition_activity (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity_id BIGINT NOT NULL COMMENT 'Entity ID',
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Workspace ID',
activity_type VARCHAR(32) NOT NULL COMMENT 'Definition activity type',
format VARCHAR(16) NOT NULL COMMENT 'Definition format',
status VARCHAR(16) NOT NULL COMMENT 'Activity status',
summary VARCHAR(128) NOT NULL COMMENT 'Activity summary',
detail VARCHAR(255) COMMENT 'Activity detail',
creator VARCHAR(64) COMMENT 'Creator',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
INDEX idx_hzb_entity_definition_activity_entity (entity_id),
INDEX idx_hzb_entity_definition_activity_workspace_time (workspace_id, gmt_create),
INDEX idx_hzb_entity_definition_activity_time (gmt_create)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_entity_governance_state (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
state_scope VARCHAR(32) NOT NULL COMMENT 'Governance scope, such as discovery',
state_kind VARCHAR(32) NOT NULL COMMENT 'State kind, such as preset or activity',
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Workspace ID',
state_key VARCHAR(128) NOT NULL COMMENT 'Stable state key',
state_name VARCHAR(128) COMMENT 'State display name',
status VARCHAR(32) COMMENT 'State status',
content TEXT COMMENT 'State JSON content',
creator VARCHAR(64) COMMENT 'Creator',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
gmt_update DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_entity_governance_state_scope_kind_workspace_key (state_scope, state_kind, workspace_id, state_key),
INDEX idx_hzb_entity_governance_state_scope_kind (state_scope, state_kind),
INDEX idx_hzb_entity_governance_state_scope_kind_workspace (state_scope, state_kind, workspace_id),
INDEX idx_hzb_entity_governance_state_update (gmt_update),
INDEX idx_hzb_entity_governance_state_creator (creator)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_auth_token (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) COMMENT 'API token name',
token_hash VARCHAR(128) NOT NULL COMMENT 'SHA-256 hash of token value',
token_mask VARCHAR(64) COMMENT 'Masked token value for display',
token_scope VARCHAR(32) NOT NULL DEFAULT 'api-admin' COMMENT 'Token access scope',
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT 'Token workspace boundary',
status TINYINT NOT NULL DEFAULT 0 COMMENT 'Token status, 0 means active',
creator VARCHAR(64) COMMENT 'Token creator',
gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
expire_time DATETIME NULL COMMENT 'Expire time, null means long-lived',
last_used_time DATETIME NULL COMMENT 'Last used time',
revoked_time DATETIME NULL COMMENT 'Token revoked time',
revoked_by VARCHAR(64) COMMENT 'Token revoker',
UNIQUE KEY uk_hzb_auth_token_hash (token_hash),
INDEX idx_hzb_auth_token_creator (creator),
INDEX idx_hzb_auth_token_scope (token_scope),
INDEX idx_hzb_auth_token_workspace (workspace_id),
INDEX idx_hzb_auth_token_scope_workspace (token_scope, workspace_id),
INDEX idx_hzb_auth_token_status (status),
INDEX idx_hzb_auth_token_revoked_by (revoked_by)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_signal_saved_view (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
creator VARCHAR(255) NOT NULL COMMENT 'Saved view creator',
`signal` VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics',
view_key VARCHAR(128) NOT NULL COMMENT 'Stable saved view key',
label VARCHAR(255) NOT NULL COMMENT 'Saved view display label',
description VARCHAR(512) COMMENT 'Saved view description',
route VARCHAR(2048) NOT NULL COMMENT 'Explorer route snapshot',
query_snapshot TEXT COMMENT 'Query-state snapshot JSON',
payload TEXT COMMENT 'Additional saved view payload JSON',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_signal_saved_view_signal_key (`signal`, view_key),
INDEX idx_hzb_signal_saved_view_signal (`signal`),
INDEX idx_hzb_signal_saved_view_update (update_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_signal_dashboard_panel_draft (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
creator VARCHAR(255) NOT NULL COMMENT 'Panel draft creator',
`signal` VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics',
draft_key VARCHAR(128) NOT NULL COMMENT 'Stable dashboard panel draft key',
title VARCHAR(255) NOT NULL COMMENT 'Dashboard panel title',
description VARCHAR(512) COMMENT 'Dashboard panel description',
visualization VARCHAR(32) NOT NULL COMMENT 'Dashboard panel visualization type',
route VARCHAR(2048) NOT NULL COMMENT 'Explorer route snapshot',
query_snapshot TEXT COMMENT 'Query-state snapshot JSON',
payload TEXT COMMENT 'Additional dashboard panel payload JSON',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_signal_dashboard_panel_draft_creator_signal_key (creator, `signal`, draft_key),
INDEX idx_hzb_signal_dashboard_panel_draft_creator_signal (creator, `signal`),
INDEX idx_hzb_signal_dashboard_panel_draft_update (update_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_signal_dashboard (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
creator VARCHAR(255) NOT NULL COMMENT 'Dashboard creator',
dashboard_key VARCHAR(128) NOT NULL COMMENT 'Stable dashboard key',
title VARCHAR(255) NOT NULL COMMENT 'Dashboard title',
description VARCHAR(512) COMMENT 'Dashboard description',
tags VARCHAR(512) COMMENT 'Comma-separated dashboard tags',
layout TEXT NOT NULL COMMENT 'Dashboard layout JSON',
widgets TEXT NOT NULL COMMENT 'Dashboard widgets JSON',
variables TEXT COMMENT 'Dashboard variables JSON',
panel_map TEXT COMMENT 'Dashboard panel grouping JSON',
version VARCHAR(32) COMMENT 'Dashboard schema version',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_signal_dashboard_key (dashboard_key),
INDEX idx_hzb_signal_dashboard_update (update_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
ALTER TABLE hzb_auth_token
ADD COLUMN token_audience VARCHAR(32) NULL,
ADD COLUMN collector_id VARCHAR(128) NULL,
ADD COLUMN allowed_signals VARCHAR(64) NULL,
ADD INDEX idx_hzb_auth_token_collector (collector_id);
ALTER TABLE hzb_collector ADD COLUMN runtime_config TEXT NULL;
ALTER TABLE hzb_collector ADD COLUMN instrumentation_intake TEXT NULL;
ALTER TABLE hzb_config ADD COLUMN config_revision VARCHAR(36) NULL;
UPDATE hzb_config SET config_revision = UUID() WHERE config_revision IS NULL;
ALTER TABLE hzb_config MODIFY COLUMN config_revision VARCHAR(36) NOT NULL;
CREATE TABLE IF NOT EXISTS hzb_account (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
password_hash VARCHAR(100) NOT NULL,
roles VARCHAR(128) NOT NULL,
credential_version BIGINT NOT NULL,
disabled BOOLEAN NOT NULL,
bootstrap_slot SMALLINT,
CONSTRAINT uk_hzb_account_username UNIQUE (username),
CONSTRAINT uk_hzb_account_bootstrap UNIQUE (bootstrap_slot)
);
CREATE TABLE IF NOT EXISTS hzb_installation (
id SMALLINT PRIMARY KEY,
installation_fingerprint VARCHAR(64) NOT NULL UNIQUE,
complete BOOLEAN NOT NULL
);
@@ -176,7 +176,7 @@ CREATE TABLE hzb_auth_token (
CREATE TABLE hzb_signal_saved_view (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
creator VARCHAR(255) NOT NULL COMMENT 'Saved view creator',
signal VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics',
`signal` VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics',
view_key VARCHAR(128) NOT NULL COMMENT 'Stable saved view key',
label VARCHAR(255) NOT NULL COMMENT 'Saved view display label',
description VARCHAR(512) COMMENT 'Saved view description',
@@ -185,15 +185,15 @@ CREATE TABLE hzb_signal_saved_view (
payload TEXT COMMENT 'Additional saved view payload JSON',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_signal_saved_view_signal_key (signal, view_key),
INDEX idx_hzb_signal_saved_view_signal (signal),
UNIQUE KEY uk_hzb_signal_saved_view_signal_key (`signal`, view_key),
INDEX idx_hzb_signal_saved_view_signal (`signal`),
INDEX idx_hzb_signal_saved_view_update (update_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE hzb_signal_dashboard_panel_draft (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
creator VARCHAR(255) NOT NULL COMMENT 'Panel draft creator',
signal VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics',
`signal` VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics',
draft_key VARCHAR(128) NOT NULL COMMENT 'Stable dashboard panel draft key',
title VARCHAR(255) NOT NULL COMMENT 'Dashboard panel title',
description VARCHAR(512) COMMENT 'Dashboard panel description',
@@ -203,8 +203,8 @@ CREATE TABLE hzb_signal_dashboard_panel_draft (
payload TEXT COMMENT 'Additional dashboard panel payload JSON',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'Create time',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Update time',
UNIQUE KEY uk_hzb_signal_dashboard_panel_draft_creator_signal_key (creator, signal, draft_key),
INDEX idx_hzb_signal_dashboard_panel_draft_creator_signal (creator, signal),
UNIQUE KEY uk_hzb_signal_dashboard_panel_draft_creator_signal_key (creator, `signal`, draft_key),
INDEX idx_hzb_signal_dashboard_panel_draft_creator_signal (creator, `signal`),
INDEX idx_hzb_signal_dashboard_panel_draft_update (update_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
@@ -0,0 +1,21 @@
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing,
-- software distributed under the License is distributed on an
-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-- KIND, either express or implied. See the License for the
-- specific language governing permissions and limitations
-- under the License.
ALTER TABLE hzb_signal_saved_view
MODIFY COLUMN `signal` VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics';
ALTER TABLE hzb_signal_dashboard_panel_draft
MODIFY COLUMN `signal` VARCHAR(32) NOT NULL COMMENT 'Signal type: logs, traces, or metrics';
@@ -0,0 +1,905 @@
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0.
--
-- Static V206 schema baseline for provisioning an empty PostgreSQL target.
-- Future versioned migrations start at V207 or later.
create table hzb_ai_conversation (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
modifier varchar(255),
title varchar(255),
primary key (id)
);
create table hzb_ai_message (
conversation_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
modifier varchar(255),
role varchar(255),
content oid not null,
primary key (id)
);
create table hzb_alert_define (
enable boolean not null,
period integer,
times integer,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
datasource varchar(100),
name varchar(100) not null,
expr varchar(2048),
labels varchar(2048),
template varchar(2048),
annotations varchar(4096),
creator varchar(255),
modifier varchar(255),
type varchar(255),
primary key (id)
);
create table hzb_alert_define_monitor_bind (
alert_define_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
primary key (id)
);
create table hzb_alert_group (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
common_labels varchar(2048),
group_key varchar(2048),
group_labels varchar(2048),
alert_fingerprints TEXT,
common_annotations TEXT,
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id),
constraint unique_group_key unique (group_key)
);
create table hzb_alert_group_converge (
enable boolean,
gmt_create timestamp(6),
gmt_update timestamp(6),
group_interval bigint,
group_wait bigint,
id bigint generated by default as identity,
repeat_interval bigint,
name varchar(100) not null,
group_labels varchar(1024),
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_alert_inhibit (
enable boolean,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
name varchar(100) not null,
equal_labels varchar(2048),
source_labels varchar(2048),
target_labels varchar(2048),
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_alert_silence (
enable boolean not null,
match_all boolean not null,
times integer,
type smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
period_end timestamp(6) with time zone,
period_start timestamp(6) with time zone,
name varchar(100) not null,
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_alert_single (
trigger_times integer,
active_at bigint,
end_at bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
start_at bigint,
fingerprint varchar(2048),
labels varchar(2048),
annotations varchar(4096),
content varchar(4096),
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id),
constraint unique_fingerprint unique (fingerprint)
);
create table hzb_bulletin (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
fields varchar(4096),
monitor_ids varchar(4096),
app varchar(255),
creator varchar(255),
modifier varchar(255),
name varchar(255),
primary key (id)
);
create table hzb_collector (
status smallint not null check ((status>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
ip varchar(255) not null,
mode varchar(255),
modifier varchar(255),
name varchar(255) not null,
version varchar(255),
primary key (id),
unique (name)
);
create table hzb_collector_monitor_bind (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
collector varchar(255),
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_config (
gmt_create timestamp(6),
gmt_update timestamp(6),
content varchar(8192),
creator varchar(255),
modifier varchar(255),
type varchar(255) not null,
primary key (type)
);
create table hzb_define (
gmt_create timestamp(6),
gmt_update timestamp(6),
app varchar(255) not null,
creator varchar(255),
modifier varchar(255),
content oid,
primary key (app)
);
create table hzb_grafana_dashboard (
enabled boolean not null,
monitor_id bigint not null,
version bigint,
folder_uid varchar(255),
slug varchar(255),
status varchar(255),
uid varchar(255),
url varchar(255),
primary key (monitor_id)
);
create table hzb_history (
dou float(53),
int32 integer,
metric_type smallint,
id bigint generated by default as identity,
time bigint,
str varchar(2048),
app varchar(255),
metric_labels varchar(5000),
metric varchar(255),
metrics varchar(255),
instance varchar(255),
primary key (id)
);
create table hzb_metrics_favorite (
create_time timestamp(6),
id bigint generated by default as identity,
monitor_id bigint not null,
creator varchar(255) not null,
metrics_name varchar(255) not null,
primary key (id),
unique (creator, monitor_id, metrics_name)
);
create table hzb_monitor (
intervals integer,
status smallint not null check ((status<=4) and (status>=0)),
type smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint not null,
job_id bigint,
schedule_type varchar(20),
app varchar(100),
cron_expression varchar(100),
instance varchar(100),
name varchar(100),
scrape varchar(100),
annotations varchar(4096),
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_monitor_bind (
biz_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
creator varchar(255),
key_str varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_notice_receiver (
agent_id integer,
lark_receive_type smallint,
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
smn_ak varchar(22),
smn_project_id varchar(32),
smn_region varchar(32),
smn_sk varchar(42),
email varchar(100),
name varchar(100) not null,
phone varchar(100),
access_token varchar(300),
discord_bot_token varchar(300),
discord_channel_id varchar(300),
gotify_token varchar(300),
hook_auth_token varchar(300),
hook_auth_type varchar(300),
server_chan_token varchar(300),
slack_web_hook_url varchar(300),
smn_topic_urn varchar(300),
wechat_id varchar(300),
hook_url varchar(1000),
app_id varchar(255),
app_secret varchar(255),
chat_id varchar(255),
corp_id varchar(255),
creator varchar(255),
modifier varchar(255),
party_id varchar(255),
tag_id varchar(255),
tg_bot_token varchar(255),
tg_message_thread_id varchar(255),
tg_user_id varchar(255),
user_id varchar(255),
primary key (id)
);
create table hzb_notice_rule (
enable boolean not null,
filter_all boolean not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
period_end timestamp(6) with time zone,
period_start timestamp(6) with time zone,
template_id bigint,
name varchar(100) not null,
template_name varchar(100),
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
receiver_id varchar(255) not null,
receiver_name varchar(255),
primary key (id)
);
create table hzb_notice_template (
preset boolean default false,
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
name varchar(100) not null,
creator varchar(255),
modifier varchar(255),
content oid not null,
primary key (id)
);
create table hzb_param (
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
constraint uk_hzb_param_monitor_field unique (monitor_id, field)
);
create table hzb_param_define (
hide boolean not null,
param_limit smallint,
required boolean not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
name varchar(2048),
param_options varchar(2048),
app varchar(255),
creator varchar(255),
default_value varchar(255),
depend varchar(255),
field varchar(255),
key_alias varchar(255),
modifier varchar(255),
param_range varchar(255),
placeholder varchar(255),
type varchar(255),
value_alias varchar(255),
primary key (id)
);
create table hzb_plugin_item (
id bigint generated by default as identity,
metadata_id bigint,
class_identifier varchar(255),
type varchar(255) check ((type in ('POST_ALERT','POST_COLLECT'))),
primary key (id)
);
create table hzb_plugin_metadata (
enable_status boolean,
param_count integer,
gmt_create timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
jar_file_path varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_plugin_param (
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
plugin_metadata_id bigint not null,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
constraint uk_hzb_plugin_param_metadata_field unique (plugin_metadata_id, field)
);
create table hzb_push_metrics (
id bigint generated by default as identity,
monitor_id bigint,
time bigint,
metrics varchar(255),
primary key (id)
);
create table hzb_sop_schedule (
id bigserial primary key,
conversation_id bigint not null,
sop_name varchar(64) not null,
sop_params varchar(1024),
cron_expression varchar(64) not null,
enabled boolean default true,
last_run_time timestamp,
next_run_time timestamp,
creator varchar(64),
modifier varchar(64),
gmt_create timestamp default current_timestamp,
gmt_update timestamp default current_timestamp
);
comment on table hzb_sop_schedule is 'Scheduled SOP execution configurations';
comment on column hzb_sop_schedule.conversation_id is 'Conversation ID to push results to';
comment on column hzb_sop_schedule.sop_name is 'Name of the SOP skill to execute';
comment on column hzb_sop_schedule.sop_params is 'SOP execution parameters in JSON format';
comment on column hzb_sop_schedule.cron_expression is 'Cron expression for scheduling';
comment on column hzb_sop_schedule.enabled is 'Whether the schedule is enabled';
comment on column hzb_sop_schedule.last_run_time is 'Last execution time';
comment on column hzb_sop_schedule.next_run_time is 'Next scheduled execution time';
create table hzb_status_page_component (
config_state smallint not null,
method smallint not null,
state smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
org_id bigint,
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_status_page_history (
abnormal integer,
normal integer,
state smallint not null,
unknowing integer,
uptime float(53),
component_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
timestamp bigint,
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_status_page_incident (
state smallint not null,
end_time bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
org_id bigint,
start_time bigint,
creator varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_status_page_incident_component_bind (
component_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
incident_id bigint,
primary key (id)
);
create table hzb_status_page_incident_content (
state smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
incident_id bigint,
timestamp bigint,
creator varchar(255),
message TEXT not null,
modifier varchar(255),
primary key (id)
);
create table hzb_status_page_org (
state smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
color varchar(255),
creator varchar(255),
description varchar(255) not null,
feedback varchar(255),
home varchar(255) not null,
logo varchar(255) not null,
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_tag (
type smallint check ((type<=3) and (type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
tag_value varchar(2048),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create index idx_message_conversation_id
on hzb_ai_message (conversation_id);
create index idx_alert_define_id
on hzb_alert_define_monitor_bind (alert_define_id);
create index idx_monitor_id
on hzb_alert_define_monitor_bind (monitor_id);
create index idx_name
on hzb_alert_group_converge (name);
create index idx_collector_monitor_collector
on hzb_collector_monitor_bind (collector);
create index idx_collector_monitor_monitor_id
on hzb_collector_monitor_bind (monitor_id);
create index idx_hzb_history_instance
on hzb_history (instance);
create index idx_hzb_history_app
on hzb_history (app);
create index idx_hzb_history_metrics
on hzb_history (metrics);
create index idx_hzb_history_metric
on hzb_history (metric);
create index idx_hzb_monitor_app
on hzb_monitor (app);
create index idx_hzb_monitor_instance
on hzb_monitor (instance);
create index idx_hzb_monitor_name
on hzb_monitor (name);
create index index_monitor_bind
on hzb_monitor_bind (biz_id);
create index index_monitor_bin
on hzb_monitor_bind (monitor_id);
create index idx_hzb_param_monitor_id
on hzb_param (monitor_id);
create index idx_hzb_plugin_param_plugin_metadata_id
on hzb_plugin_param (plugin_metadata_id);
create index idx_push_metrics_monitor_id
on hzb_push_metrics (monitor_id);
create index idx_push_metrics_time
on hzb_push_metrics (time);
create index idx_schedule_conversation_id
on hzb_sop_schedule (conversation_id);
create index idx_schedule_enabled_next
on hzb_sop_schedule (enabled, next_run_time);
create index index_incident_component
on hzb_status_page_incident_component_bind (incident_id);
create index idx_incident_component_component_id
on hzb_status_page_incident_component_bind (component_id);
alter table if exists hzb_ai_message
add constraint fk_hzb_ai_message_conversation
foreign key (conversation_id)
references hzb_ai_conversation;
alter table if exists hzb_plugin_item
add constraint fk_hzb_plugin_item_metadata
foreign key (metadata_id)
references hzb_plugin_metadata;
alter table if exists hzb_status_page_incident_content
add constraint fk_hzb_incident_content_incident
foreign key (incident_id)
references hzb_status_page_incident;
CREATE TABLE hzb_entity (
id BIGINT PRIMARY KEY,
entity_type VARCHAR(32) NOT NULL,
name VARCHAR(128) NOT NULL,
display_name VARCHAR(128),
sub_type VARCHAR(128),
namespace VARCHAR(128),
environment VARCHAR(128),
status VARCHAR(32) NOT NULL,
criticality VARCHAR(32),
owner VARCHAR(128),
additional_owners TEXT,
runbook VARCHAR(512),
lifecycle VARCHAR(64),
tier VARCHAR(64),
system_name VARCHAR(128),
component_of TEXT,
components TEXT,
implemented_by TEXT,
api_interface TEXT,
inherit_from VARCHAR(255),
languages TEXT,
links TEXT,
contacts TEXT,
integrations TEXT,
extensions TEXT,
hertzbeat TEXT,
source VARCHAR(32) NOT NULL,
description VARCHAR(512),
labels VARCHAR(4096),
tags TEXT,
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default',
creator VARCHAR(64),
modifier VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
gmt_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_hzb_entity_type ON hzb_entity(entity_type);
CREATE INDEX idx_hzb_entity_status ON hzb_entity(status);
CREATE INDEX idx_hzb_entity_name ON hzb_entity(name);
CREATE INDEX idx_hzb_entity_owner ON hzb_entity(owner);
CREATE INDEX idx_hzb_entity_workspace ON hzb_entity(workspace_id);
CREATE TABLE hzb_entity_identity (
id BIGSERIAL PRIMARY KEY,
entity_id BIGINT NOT NULL,
identity_type VARCHAR(32) NOT NULL,
identity_key VARCHAR(128) NOT NULL,
identity_value VARCHAR(512) NOT NULL,
normalized_value VARCHAR(512) NOT NULL,
priority INT NOT NULL DEFAULT 40,
primary_identity BOOLEAN NOT NULL DEFAULT FALSE,
creator VARCHAR(64),
modifier VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
gmt_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_entity_identity ON hzb_entity_identity(entity_id, identity_key, normalized_value);
CREATE INDEX idx_hzb_entity_identity_lookup ON hzb_entity_identity(identity_key, normalized_value);
CREATE INDEX idx_hzb_entity_identity_entity ON hzb_entity_identity(entity_id);
CREATE TABLE hzb_entity_monitor_bind (
id BIGSERIAL PRIMARY KEY,
entity_id BIGINT NOT NULL,
monitor_id BIGINT NOT NULL,
bind_type VARCHAR(32) NOT NULL,
bind_source VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
score INT NOT NULL DEFAULT 100,
match_context TEXT,
creator VARCHAR(64),
modifier VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
gmt_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_entity_monitor_bind ON hzb_entity_monitor_bind(entity_id, monitor_id);
CREATE INDEX idx_hzb_entity_monitor_bind_entity ON hzb_entity_monitor_bind(entity_id);
CREATE INDEX idx_hzb_entity_monitor_bind_monitor ON hzb_entity_monitor_bind(monitor_id);
CREATE TABLE hzb_entity_relation (
id BIGSERIAL PRIMARY KEY,
source_entity_id BIGINT NOT NULL,
target_entity_id BIGINT,
target_ref VARCHAR(255),
relation_type VARCHAR(32) NOT NULL,
relation_source VARCHAR(32) NOT NULL,
status VARCHAR(16) NOT NULL,
score INT NOT NULL DEFAULT 100,
description VARCHAR(255),
attributes TEXT,
creator VARCHAR(64),
modifier VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
gmt_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_entity_relation ON hzb_entity_relation(source_entity_id, target_entity_id, relation_type);
CREATE INDEX idx_hzb_entity_relation_source ON hzb_entity_relation(source_entity_id);
CREATE INDEX idx_hzb_entity_relation_target ON hzb_entity_relation(target_entity_id);
CREATE INDEX idx_hzb_entity_relation_target_ref ON hzb_entity_relation(target_ref);
CREATE TABLE hzb_entity_definition_activity (
id BIGSERIAL PRIMARY KEY,
entity_id BIGINT NOT NULL,
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default',
activity_type VARCHAR(32) NOT NULL,
format VARCHAR(16) NOT NULL,
status VARCHAR(16) NOT NULL,
summary VARCHAR(128) NOT NULL,
detail VARCHAR(255),
creator VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_hzb_entity_definition_activity_entity
ON hzb_entity_definition_activity(entity_id);
CREATE INDEX idx_hzb_entity_definition_activity_workspace_time
ON hzb_entity_definition_activity(workspace_id, gmt_create);
CREATE INDEX idx_hzb_entity_definition_activity_time
ON hzb_entity_definition_activity(gmt_create);
CREATE TABLE hzb_entity_governance_state (
id BIGSERIAL PRIMARY KEY,
state_scope VARCHAR(32) NOT NULL,
state_kind VARCHAR(32) NOT NULL,
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default',
state_key VARCHAR(128) NOT NULL,
state_name VARCHAR(128),
status VARCHAR(32),
content TEXT,
creator VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
gmt_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_entity_governance_state_scope_kind_workspace_key
ON hzb_entity_governance_state(state_scope, state_kind, workspace_id, state_key);
CREATE INDEX idx_hzb_entity_governance_state_scope_kind
ON hzb_entity_governance_state(state_scope, state_kind);
CREATE INDEX idx_hzb_entity_governance_state_scope_kind_workspace
ON hzb_entity_governance_state(state_scope, state_kind, workspace_id);
CREATE INDEX idx_hzb_entity_governance_state_update
ON hzb_entity_governance_state(gmt_update);
CREATE INDEX idx_hzb_entity_governance_state_creator
ON hzb_entity_governance_state(creator);
CREATE TABLE hzb_auth_token (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255),
token_hash VARCHAR(128) NOT NULL,
token_mask VARCHAR(64),
token_scope VARCHAR(32) NOT NULL DEFAULT 'api-admin',
workspace_id VARCHAR(64) NOT NULL DEFAULT 'default',
status SMALLINT NOT NULL DEFAULT 0,
creator VARCHAR(64),
gmt_create TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_time TIMESTAMP NULL,
last_used_time TIMESTAMP NULL,
revoked_time TIMESTAMP NULL,
revoked_by VARCHAR(64)
);
CREATE UNIQUE INDEX uk_hzb_auth_token_hash ON hzb_auth_token(token_hash);
CREATE INDEX idx_hzb_auth_token_creator ON hzb_auth_token(creator);
CREATE INDEX idx_hzb_auth_token_scope ON hzb_auth_token(token_scope);
CREATE INDEX idx_hzb_auth_token_workspace ON hzb_auth_token(workspace_id);
CREATE INDEX idx_hzb_auth_token_scope_workspace ON hzb_auth_token(token_scope, workspace_id);
CREATE INDEX idx_hzb_auth_token_status ON hzb_auth_token(status);
CREATE INDEX idx_hzb_auth_token_revoked_by ON hzb_auth_token(revoked_by);
CREATE TABLE hzb_signal_saved_view (
id BIGSERIAL PRIMARY KEY,
creator VARCHAR(255) NOT NULL,
signal VARCHAR(32) NOT NULL,
view_key VARCHAR(128) NOT NULL,
label VARCHAR(255) NOT NULL,
description VARCHAR(512),
route VARCHAR(2048) NOT NULL,
query_snapshot TEXT,
payload TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_signal_saved_view_signal_key
ON hzb_signal_saved_view(signal, view_key);
CREATE INDEX idx_hzb_signal_saved_view_signal
ON hzb_signal_saved_view(signal);
CREATE INDEX idx_hzb_signal_saved_view_update
ON hzb_signal_saved_view(update_time);
CREATE TABLE hzb_signal_dashboard_panel_draft (
id BIGSERIAL PRIMARY KEY,
creator VARCHAR(255) NOT NULL,
signal VARCHAR(32) NOT NULL,
draft_key VARCHAR(128) NOT NULL,
title VARCHAR(255) NOT NULL,
description VARCHAR(512),
visualization VARCHAR(32) NOT NULL,
route VARCHAR(2048) NOT NULL,
query_snapshot TEXT,
payload TEXT,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_signal_dashboard_panel_draft_creator_signal_key
ON hzb_signal_dashboard_panel_draft(creator, signal, draft_key);
CREATE INDEX idx_hzb_signal_dashboard_panel_draft_creator_signal
ON hzb_signal_dashboard_panel_draft(creator, signal);
CREATE INDEX idx_hzb_signal_dashboard_panel_draft_update
ON hzb_signal_dashboard_panel_draft(update_time);
CREATE TABLE hzb_signal_dashboard (
id BIGSERIAL PRIMARY KEY,
creator VARCHAR(255) NOT NULL,
dashboard_key VARCHAR(128) NOT NULL,
title VARCHAR(255) NOT NULL,
description VARCHAR(512),
tags VARCHAR(512),
layout TEXT NOT NULL,
widgets TEXT NOT NULL,
variables TEXT,
panel_map TEXT,
version VARCHAR(32),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX uk_hzb_signal_dashboard_key
ON hzb_signal_dashboard(dashboard_key);
CREATE INDEX idx_hzb_signal_dashboard_update
ON hzb_signal_dashboard(update_time);
ALTER TABLE hzb_auth_token ADD COLUMN token_audience VARCHAR(32);
ALTER TABLE hzb_auth_token ADD COLUMN collector_id VARCHAR(128);
ALTER TABLE hzb_auth_token ADD COLUMN allowed_signals VARCHAR(64);
CREATE INDEX idx_hzb_auth_token_collector ON hzb_auth_token(collector_id);
ALTER TABLE hzb_collector ADD COLUMN runtime_config TEXT;
ALTER TABLE hzb_collector ADD COLUMN instrumentation_intake TEXT;
ALTER TABLE hzb_config ADD COLUMN config_revision VARCHAR(36);
UPDATE hzb_config SET config_revision = gen_random_uuid()::text WHERE config_revision IS NULL;
ALTER TABLE hzb_config ALTER COLUMN config_revision SET NOT NULL;
CREATE TABLE IF NOT EXISTS hzb_account (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
username VARCHAR(64) NOT NULL,
password_hash VARCHAR(100) NOT NULL,
roles VARCHAR(128) NOT NULL,
credential_version BIGINT NOT NULL,
disabled BOOLEAN NOT NULL,
bootstrap_slot SMALLINT,
CONSTRAINT uk_hzb_account_username UNIQUE (username),
CONSTRAINT uk_hzb_account_bootstrap UNIQUE (bootstrap_slot)
);
CREATE TABLE IF NOT EXISTS hzb_installation (
id SMALLINT PRIMARY KEY,
installation_fingerprint VARCHAR(64) NOT NULL UNIQUE,
complete BOOLEAN NOT NULL
);
@@ -0,0 +1,20 @@
-- Licensed to the Apache Software Foundation (ASF) under one
-- or more contributor license agreements. See the NOTICE file
-- distributed with this work for additional information
-- regarding copyright ownership. The ASF licenses this file
-- to you under the Apache License, Version 2.0 (the
-- "License"); you may not use this file except in compliance
-- with the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing,
-- software distributed under the License is distributed on an
-- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-- KIND, either express or implied. See the License for the
-- specific language governing permissions and limitations
-- under the License.
ALTER TABLE hzb_sop_schedule ALTER COLUMN enabled DROP DEFAULT;
ALTER TABLE hzb_sop_schedule ALTER COLUMN enabled TYPE BOOLEAN USING enabled <> 0;
ALTER TABLE hzb_sop_schedule ALTER COLUMN enabled SET DEFAULT TRUE;
@@ -0,0 +1,81 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import org.flywaydb.core.Flyway;
/** Rebuilds the current schema from an independent V159 fixture and the committed migrations. */
final class HistoricalMetadataSchema {
private static final String FIXTURE = "V159__schema.sql";
private HistoricalMetadataSchema() {
}
static void rebuild(String jdbcUrl, String username, String password, String vendor)
throws SQLException, IOException {
Flyway.configure()
.dataSource(jdbcUrl, username, password)
.locations("classpath:db/migration/" + vendor)
.cleanDisabled(false)
.load()
.clean();
try (Connection connection = DriverManager.getConnection(jdbcUrl, username, password)) {
executeFixture(connection, resource(vendor));
}
Flyway flyway = Flyway.configure()
.dataSource(jdbcUrl, username, password)
.locations("classpath:db/migration/" + vendor)
.baselineVersion("159")
.baselineOnMigrate(true)
.cleanDisabled(true)
.target("206")
.validateMigrationNaming(true)
.load();
flyway.migrate();
flyway.validate();
}
private static void executeFixture(Connection connection, String fixture) throws SQLException {
String executable = fixture.replaceAll("(?m)^--.*$", "");
try (Statement statement = connection.createStatement()) {
for (String sql : executable.split(";")) {
if (!sql.isBlank()) {
statement.execute(sql);
}
}
}
}
private static String resource(String vendor) throws IOException {
String path = "/db/historical/" + vendor + '/' + FIXTURE;
try (InputStream input = HistoricalMetadataSchema.class.getResourceAsStream(path)) {
if (input == null) {
throw new IOException("Historical schema fixture is missing: " + path);
}
return new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
}
}
@@ -0,0 +1,219 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/** Normalized JDBC metadata used to compare two schemas on the same database vendor. */
record MetadataSchemaSnapshot(
Set<Column> columns,
Set<PrimaryKey> primaryKeys,
Set<Index> indexes,
Set<ForeignKey> foreignKeys) {
static MetadataSchemaSnapshot capture(Connection connection) throws SQLException {
DatabaseMetaData metadata = connection.getMetaData();
Set<String> tables = tables(connection, metadata);
Set<Column> columns = new HashSet<>();
Set<PrimaryKey> primaryKeys = new HashSet<>();
Set<Index> indexes = new HashSet<>();
Set<ForeignKey> foreignKeys = new HashSet<>();
for (String table : tables) {
readColumns(connection, metadata, table, columns);
readPrimaryKeys(connection, metadata, table, primaryKeys);
readIndexes(connection, metadata, table, indexes);
readForeignKeys(connection, metadata, table, foreignKeys);
}
return new MetadataSchemaSnapshot(columns, primaryKeys, indexes, foreignKeys);
}
private static Set<String> tables(Connection connection, DatabaseMetaData metadata) throws SQLException {
Set<String> tables = new HashSet<>();
try (ResultSet result = metadata.getTables(connection.getCatalog(), null, "hzb_%", new String[]{"TABLE"})) {
while (result.next()) {
tables.add(normalize(result.getString("TABLE_NAME")));
}
}
return tables;
}
private static void readColumns(
Connection connection, DatabaseMetaData metadata, String table, Set<Column> columns) throws SQLException {
try (ResultSet result = metadata.getColumns(connection.getCatalog(), null, table, null)) {
while (result.next()) {
columns.add(new Column(
table,
normalize(result.getString("COLUMN_NAME")),
result.getInt("DATA_TYPE"),
normalize(result.getString("TYPE_NAME")),
result.getInt("COLUMN_SIZE"),
result.getInt("DECIMAL_DIGITS"),
result.getInt("NULLABLE"),
normalize(result.getString("COLUMN_DEF")),
normalize(result.getString("REMARKS")),
result.getInt("ORDINAL_POSITION")));
}
}
}
private static void readPrimaryKeys(
Connection connection, DatabaseMetaData metadata, String table, Set<PrimaryKey> primaryKeys)
throws SQLException {
try (ResultSet result = metadata.getPrimaryKeys(connection.getCatalog(), null, table)) {
while (result.next()) {
primaryKeys.add(new PrimaryKey(
table,
normalize(result.getString("PK_NAME")),
result.getShort("KEY_SEQ"),
normalize(result.getString("COLUMN_NAME"))));
}
}
}
private static void readIndexes(
Connection connection, DatabaseMetaData metadata, String table, Set<Index> indexes) throws SQLException {
Map<String, IndexAccumulator> collected = new HashMap<>();
try (ResultSet result = metadata.getIndexInfo(connection.getCatalog(), null, table, false, false)) {
while (result.next()) {
String name = normalize(result.getString("INDEX_NAME"));
String column = normalize(result.getString("COLUMN_NAME"));
if (name == null || column == null || result.getShort("TYPE") == DatabaseMetaData.tableIndexStatistic) {
continue;
}
String key = name + ':' + result.getBoolean("NON_UNIQUE");
collected.computeIfAbsent(key,
ignored -> new IndexAccumulator(table, name, !resultBoolean(result, "NON_UNIQUE")))
.add(resultShort(result, "ORDINAL_POSITION"), column, normalize(resultString(result, "ASC_OR_DESC")));
}
}
collected.values().stream().map(IndexAccumulator::build).forEach(indexes::add);
}
private static void readForeignKeys(
Connection connection, DatabaseMetaData metadata, String table, Set<ForeignKey> foreignKeys)
throws SQLException {
try (ResultSet result = metadata.getImportedKeys(connection.getCatalog(), null, table)) {
while (result.next()) {
foreignKeys.add(new ForeignKey(
table,
normalize(result.getString("FK_NAME")),
result.getShort("KEY_SEQ"),
normalize(result.getString("FKCOLUMN_NAME")),
normalize(result.getString("PKTABLE_NAME")),
normalize(result.getString("PKCOLUMN_NAME")),
result.getShort("UPDATE_RULE"),
result.getShort("DELETE_RULE"),
result.getShort("DEFERRABILITY")));
}
}
}
private static boolean resultBoolean(ResultSet result, String column) {
try {
return result.getBoolean(column);
} catch (SQLException exception) {
throw new IllegalStateException("JDBC metadata result is incomplete", exception);
}
}
private static short resultShort(ResultSet result, String column) {
try {
return result.getShort(column);
} catch (SQLException exception) {
throw new IllegalStateException("JDBC metadata result is incomplete", exception);
}
}
private static String resultString(ResultSet result, String column) {
try {
return result.getString(column);
} catch (SQLException exception) {
throw new IllegalStateException("JDBC metadata result is incomplete", exception);
}
}
private static String normalize(String value) {
return value == null ? null : value.toLowerCase(Locale.ROOT).replaceAll("\\s+", " ").trim();
}
record Column(
String table,
String name,
int jdbcType,
String typeName,
int size,
int scale,
int nullable,
String defaultValue,
String remarks,
int position) {
}
record PrimaryKey(String table, String name, short position, String column) {
}
record Index(String table, String name, boolean unique, List<IndexColumn> columns) {
}
record IndexColumn(short position, String name, String order) {
}
record ForeignKey(
String table,
String name,
short position,
String column,
String referencedTable,
String referencedColumn,
short updateRule,
short deleteRule,
short deferrability) {
}
private static final class IndexAccumulator {
private final String table;
private final String name;
private final boolean unique;
private final List<IndexColumn> columns = new ArrayList<>();
private IndexAccumulator(String table, String name, boolean unique) {
this.table = table;
this.name = name;
this.unique = unique;
}
void add(short position, String column, String order) {
columns.add(new IndexColumn(position, column, order));
}
Index build() {
columns.sort(java.util.Comparator.comparingInt(IndexColumn::position));
return new Index(table, name, unique, List.copyOf(columns));
}
}
}
@@ -0,0 +1,37 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import java.sql.Types;
import org.hibernate.dialect.MySQLDialect;
/** Keeps schema validation compatible with boolean encodings used by the committed MySQL migrations. */
public final class MetadataValidationMySqlDialect extends MySQLDialect {
@Override
public boolean equivalentTypes(int firstTypeCode, int secondTypeCode) {
if (isBooleanType(firstTypeCode) && isBooleanType(secondTypeCode)) {
return true;
}
return super.equivalentTypes(firstTypeCode, secondTypeCode);
}
private static boolean isBooleanType(int typeCode) {
return typeCode == Types.BIT || typeCode == Types.BOOLEAN || typeCode == Types.TINYINT;
}
}
@@ -0,0 +1,130 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
/** Static schema contract for provisioning an empty external metadata database. */
class TargetSchemaBaselineResourceTest {
private static final Pattern CREATE_TABLE = Pattern.compile(
"(?im)^\\s*create\\s+table\\s+(?:if\\s+not\\s+exists\\s+)?([a-z][a-z0-9_]*)\\s*\\(");
private static final Set<String> MAPPED_TABLES = Set.of(
"hzb_account",
"hzb_ai_conversation",
"hzb_ai_message",
"hzb_alert_define",
"hzb_alert_define_monitor_bind",
"hzb_alert_group",
"hzb_alert_group_converge",
"hzb_alert_inhibit",
"hzb_alert_silence",
"hzb_alert_single",
"hzb_auth_token",
"hzb_bulletin",
"hzb_collector",
"hzb_collector_monitor_bind",
"hzb_config",
"hzb_define",
"hzb_entity",
"hzb_entity_definition_activity",
"hzb_entity_governance_state",
"hzb_entity_identity",
"hzb_entity_monitor_bind",
"hzb_entity_relation",
"hzb_grafana_dashboard",
"hzb_history",
"hzb_installation",
"hzb_metrics_favorite",
"hzb_monitor",
"hzb_monitor_bind",
"hzb_notice_receiver",
"hzb_notice_rule",
"hzb_notice_template",
"hzb_param",
"hzb_param_define",
"hzb_plugin_item",
"hzb_plugin_metadata",
"hzb_plugin_param",
"hzb_push_metrics",
"hzb_signal_dashboard",
"hzb_signal_dashboard_panel_draft",
"hzb_signal_saved_view",
"hzb_sop_schedule",
"hzb_status_page_component",
"hzb_status_page_history",
"hzb_status_page_incident",
"hzb_status_page_incident_component_bind",
"hzb_status_page_incident_content",
"hzb_status_page_org",
"hzb_tag");
@ParameterizedTest
@ValueSource(strings = {"mysql", "postgresql"})
void currentBaselineDeclaresEveryMappedTable(String vendor) throws IOException {
String resource = "db/migration/" + vendor + "/B206__current_schema.sql";
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) {
assertThat(input).as(resource).isNotNull();
assertThat(createdTables(new String(input.readAllBytes(), StandardCharsets.UTF_8)))
.containsExactlyInAnyOrderElementsOf(MAPPED_TABLES);
}
MetadataDatabaseKind kind = vendor.equals("mysql")
? MetadataDatabaseKind.MYSQL : MetadataDatabaseKind.POSTGRESQL;
assertThat(TargetSchemaBaseline.load(kind).expectedTables())
.containsExactlyInAnyOrderElementsOf(MAPPED_TABLES);
}
@ParameterizedTest
@ValueSource(strings = {"mysql", "postgresql"})
void historicalFixtureDeclaresImmutableV159Provenance(String vendor) throws IOException {
String resource = "db/historical/" + vendor + "/V159__schema.sql";
try (InputStream input = getClass().getClassLoader().getResourceAsStream(resource)) {
assertThat(input).as(resource).isNotNull();
String sql = new String(input.readAllBytes(), StandardCharsets.UTF_8);
assertThat(sql)
.contains("Immutable V159 schema fixture for migration-chain tests.")
.contains("Do not derive this fixture from the current baseline or later migrations.")
.doesNotContain("Static V205 schema baseline", "Future versioned migrations start at V206");
}
}
private static Set<String> createdTables(String sql) {
Matcher matcher = CREATE_TABLE.matcher(sql.toLowerCase(Locale.ROOT));
Set<String> tables = new HashSet<>();
while (matcher.find()) {
tables.add(matcher.group(1));
}
return Set.copyOf(tables);
}
static Set<String> mappedTables() {
return MAPPED_TABLES;
}
}
@@ -0,0 +1,481 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import jakarta.persistence.Entity;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.flywaydb.core.Flyway;
import org.assertj.core.api.SoftAssertions;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.testcontainers.mysql.MySQLContainer;
import org.testcontainers.postgresql.PostgreSQLContainer;
/** Real-database proof for current-version target schema provisioning. */
@EnabledIfSystemProperty(named = "hertzbeat.test.database-containers", matches = "true")
class TargetSchemaProvisionerDatabaseTest {
private static final String DATABASE = "hertzbeat";
private static final String USERNAME = "hertzbeat";
private static final String PASSWORD = "test-only-password";
@Test
void provisionsAndValidatesFreshMysqlSchema() throws Exception {
try (MySQLContainer database = new MySQLContainer("mysql:8.4")
.withDatabaseName(DATABASE)
.withUsername(USERNAME)
.withPassword(PASSWORD)
.withCommand("--lower-case-table-names=1")) {
database.start();
assertRejectsFalseEmptyStates(database.getJdbcUrl(), MetadataDatabaseKind.MYSQL);
verify(database.getJdbcUrl(), MetadataDatabaseKind.MYSQL,
MetadataValidationMySqlDialect.class.getName());
}
}
@Test
void provisionsAndValidatesFreshPostgresqlSchema() throws Exception {
try (PostgreSQLContainer database = new PostgreSQLContainer("postgres:17.6")
.withDatabaseName(DATABASE)
.withUsername(USERNAME)
.withPassword(PASSWORD)) {
database.start();
assertRejectsFalseEmptyStates(database.getJdbcUrl(), MetadataDatabaseKind.POSTGRESQL);
verify(database.getJdbcUrl(), MetadataDatabaseKind.POSTGRESQL,
"org.hibernate.dialect.PostgreSQLDialect");
}
}
private static void assertRejectsFalseEmptyStates(String jdbcUrl, MetadataDatabaseKind kind) throws Exception {
TargetSchemaProvisioner provisioner = new FlywayTargetSchemaProvisioner();
MetadataDatabaseConfiguration target =
new MetadataDatabaseConfiguration(kind, jdbcUrl, USERNAME, PASSWORD);
SoftAssertions softly = new SoftAssertions();
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD)) {
TargetSchemaBaseline baseline = TargetSchemaBaseline.load(kind);
new FlywaySchemaHistory(kind).record(connection, baseline, USERNAME, 0);
}
softly.assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
softly.assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
statement.execute("CREATE TABLE hzb_account (id INTEGER NOT NULL PRIMARY KEY)");
}
softly.assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
softly.assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
softly.assertThat(metadataTables(connection)).containsExactly("hzb_account");
statement.execute("DROP TABLE hzb_account");
statement.execute("DROP TABLE flyway_schema_history");
statement.execute("DROP TABLE flyway_schema_contract");
statement.execute("CREATE TABLE unrelated_table (id INTEGER NOT NULL PRIMARY KEY)");
}
softly.assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
softly.assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
softly.assertThat(metadataTables(connection)).isEmpty();
statement.execute("DROP TABLE unrelated_table");
statement.execute("CREATE VIEW hzb_status_page_org AS SELECT 1 AS id");
}
softly.assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
softly.assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
softly.assertThat(metadataTables(connection)).isEmpty();
statement.execute("DROP VIEW hzb_status_page_org");
}
assertRejectsPostgresqlObjects(jdbcUrl, kind, provisioner, target, softly);
assertRejectsContractWithoutHistory(jdbcUrl, kind, provisioner, target, softly);
softly.assertAll();
}
private static void assertRejectsContractWithoutHistory(
String jdbcUrl,
MetadataDatabaseKind kind,
TargetSchemaProvisioner provisioner,
MetadataDatabaseConfiguration target,
SoftAssertions softly) throws Exception {
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD)) {
new TargetSchemaContract(kind).record(connection, Set.of());
}
softly.assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
softly.assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
statement.execute("DROP TABLE flyway_schema_contract");
}
}
private static void assertRejectsPostgresqlObjects(
String jdbcUrl,
MetadataDatabaseKind kind,
TargetSchemaProvisioner provisioner,
MetadataDatabaseConfiguration target,
SoftAssertions softly) throws Exception {
if (kind != MetadataDatabaseKind.POSTGRESQL) {
return;
}
assertRejectsObject(jdbcUrl, provisioner, target, softly,
"CREATE MATERIALIZED VIEW hzb_schema_probe AS SELECT 1 AS id",
"DROP MATERIALIZED VIEW hzb_schema_probe");
assertRejectsObject(jdbcUrl, provisioner, target, softly,
"CREATE SEQUENCE hzb_schema_probe_sequence",
"DROP SEQUENCE hzb_schema_probe_sequence");
}
private static void assertRejectsObject(
String jdbcUrl,
TargetSchemaProvisioner provisioner,
MetadataDatabaseConfiguration target,
SoftAssertions softly,
String createSql,
String dropSql) throws Exception {
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
statement.execute(createSql);
}
softly.assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
softly.assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
statement.execute(dropSql);
}
}
private static void verify(String jdbcUrl, MetadataDatabaseKind kind, String dialect) throws Exception {
MetadataDatabaseConfiguration target =
new MetadataDatabaseConfiguration(kind, jdbcUrl, USERNAME, PASSWORD);
TargetSchemaProvisioner provisioner = new FlywayTargetSchemaProvisioner();
assertProvisioningLogsAreSanitized(provisioner, target);
assertCurrentBaselineAllowsAdditionalTable(provisioner, target);
assertCurrentBaselineRejectsSchemaCorruption(provisioner, target);
MetadataSchemaSnapshot baseline;
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD)) {
assertThat(metadataTables(connection)).containsExactlyInAnyOrderElementsOf(
TargetSchemaBaselineResourceTest.mappedTables());
try (Statement statement = connection.createStatement();
ResultSet history = statement.executeQuery(
"SELECT version, type, success FROM flyway_schema_history ORDER BY installed_rank")) {
assertThat(history.next()).isTrue();
assertThat(history.getString("version")).isEqualTo("206");
assertThat(history.getString("type")).isEqualTo("SQL_BASELINE");
assertThat(history.getBoolean("success")).isTrue();
assertThat(history.next()).isFalse();
}
baseline = MetadataSchemaSnapshot.capture(connection);
}
assertStandardFlywayAcceptsBaseline(jdbcUrl, kind);
validateHibernateMappings(jdbcUrl, dialect);
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD)) {
replaceEarlyMigrationIndexWithIncorrectDefinition(connection, kind);
}
HistoricalMetadataSchema.rebuild(jdbcUrl, USERNAME, PASSWORD, kind.value());
assertThat(historyRows(jdbcUrl))
.extracting(HistoryRow::version)
.containsExactly("159", "160", "170", "172", "173", "180", "181",
"200", "201", "202", "203", "204", "205", "206");
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD)) {
MetadataSchemaSnapshot migrated = MetadataSchemaSnapshot.capture(connection);
assertThat(migrated.indexes())
.filteredOn(index -> index.table().equals("hzb_monitor")
&& index.name().equals("idx_hzb_monitor_app"))
.singleElement()
.extracting(MetadataSchemaSnapshot.Index::columns)
.isEqualTo(List.of(new MetadataSchemaSnapshot.IndexColumn((short) 1, "app", "a")));
assertThat(schemaDifferences(baseline, migrated)).isEmpty();
}
}
private static List<String> schemaDifferences(
MetadataSchemaSnapshot baseline, MetadataSchemaSnapshot migrated) {
List<String> differences = new ArrayList<>();
addDifferences(differences, "column", baseline.columns(), migrated.columns());
addDifferences(differences, "primary key", baseline.primaryKeys(), migrated.primaryKeys());
addDifferences(differences, "index", baseline.indexes(), migrated.indexes());
addDifferences(differences, "foreign key", baseline.foreignKeys(), migrated.foreignKeys());
return List.copyOf(differences);
}
private static void addDifferences(
List<String> differences, String kind, Set<?> baseline, Set<?> migrated) {
baseline.stream()
.filter(value -> !migrated.contains(value))
.map(value -> "baseline-only " + kind + ": " + value)
.forEach(differences::add);
migrated.stream()
.filter(value -> !baseline.contains(value))
.map(value -> "migration-only " + kind + ": " + value)
.forEach(differences::add);
}
private static void replaceEarlyMigrationIndexWithIncorrectDefinition(
Connection connection, MetadataDatabaseKind kind) throws Exception {
try (Statement statement = connection.createStatement()) {
if (kind == MetadataDatabaseKind.MYSQL) {
statement.execute("DROP INDEX idx_hzb_monitor_app ON hzb_monitor");
} else {
statement.execute("DROP INDEX idx_hzb_monitor_app");
}
statement.execute("CREATE INDEX idx_hzb_monitor_app ON hzb_monitor(name)");
}
}
private static void assertCurrentBaselineAllowsAdditionalTable(
TargetSchemaProvisioner provisioner, MetadataDatabaseConfiguration target) throws Exception {
try (Connection connection = DriverManager.getConnection(
target.jdbcUrl(), target.username(), target.password());
Statement statement = connection.createStatement()) {
statement.execute("CREATE TABLE unrelated_table (id INTEGER NOT NULL PRIMARY KEY)");
try {
provisioner.provision(target);
} finally {
statement.execute("DROP TABLE unrelated_table");
}
}
}
private static void assertCurrentBaselineRejectsSchemaCorruption(
TargetSchemaProvisioner provisioner, MetadataDatabaseConfiguration target) throws Exception {
assertCorruptionRejected(provisioner, target,
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_account MODIFY COLUMN username VARCHAR(63) NOT NULL"
: "ALTER TABLE hzb_account ALTER COLUMN username TYPE VARCHAR(63)",
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_account MODIFY COLUMN username VARCHAR(64) NOT NULL"
: "ALTER TABLE hzb_account ALTER COLUMN username TYPE VARCHAR(64)");
assertCorruptionRejected(provisioner, target,
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_account MODIFY COLUMN credential_version INTEGER NOT NULL"
: "ALTER TABLE hzb_account ALTER COLUMN credential_version TYPE INTEGER",
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_account MODIFY COLUMN credential_version BIGINT NOT NULL"
: "ALTER TABLE hzb_account ALTER COLUMN credential_version TYPE BIGINT");
assertCorruptionRejected(provisioner, target,
target.kind() == MetadataDatabaseKind.MYSQL
? "DROP INDEX idx_hzb_monitor_app ON hzb_monitor"
: "DROP INDEX idx_hzb_monitor_app",
"CREATE INDEX idx_hzb_monitor_app ON hzb_monitor(app)");
assertCorruptionRejected(provisioner, target,
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_ai_message DROP FOREIGN KEY fk_hzb_ai_message_conversation"
: "ALTER TABLE hzb_ai_message DROP CONSTRAINT fk_hzb_ai_message_conversation",
"ALTER TABLE hzb_ai_message ADD CONSTRAINT fk_hzb_ai_message_conversation "
+ "FOREIGN KEY (conversation_id) REFERENCES hzb_ai_conversation(id)");
assertCorruptionRejected(provisioner, target,
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_ai_message DROP FOREIGN KEY fk_hzb_ai_message_conversation, "
+ "ADD CONSTRAINT fk_hzb_ai_message_conversation_cascade "
+ "FOREIGN KEY (conversation_id) "
+ "REFERENCES hzb_ai_conversation(id) ON DELETE CASCADE"
: "ALTER TABLE hzb_ai_message DROP CONSTRAINT fk_hzb_ai_message_conversation, "
+ "ADD CONSTRAINT fk_hzb_ai_message_conversation_cascade "
+ "FOREIGN KEY (conversation_id) "
+ "REFERENCES hzb_ai_conversation(id) ON DELETE CASCADE",
target.kind() == MetadataDatabaseKind.MYSQL
? "ALTER TABLE hzb_ai_message "
+ "DROP FOREIGN KEY fk_hzb_ai_message_conversation_cascade, "
+ "ADD CONSTRAINT fk_hzb_ai_message_conversation FOREIGN KEY (conversation_id) "
+ "REFERENCES hzb_ai_conversation(id)"
: "ALTER TABLE hzb_ai_message "
+ "DROP CONSTRAINT fk_hzb_ai_message_conversation_cascade, "
+ "ADD CONSTRAINT fk_hzb_ai_message_conversation FOREIGN KEY (conversation_id) "
+ "REFERENCES hzb_ai_conversation(id)");
}
private static void assertCorruptionRejected(
TargetSchemaProvisioner provisioner,
MetadataDatabaseConfiguration target,
String corruptSql,
String restoreSql) throws Exception {
try (Connection connection = DriverManager.getConnection(
target.jdbcUrl(), target.username(), target.password());
Statement statement = connection.createStatement()) {
statement.execute(corruptSql);
try {
assertThatThrownBy(() -> provisioner.provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception ->
assertThat(exception.failure().phase())
.isEqualTo(TargetSchemaProvisioningFailure.Phase.PRECONDITION));
} finally {
statement.execute(restoreSql);
}
}
}
private static void assertProvisioningLogsAreSanitized(
TargetSchemaProvisioner provisioner, MetadataDatabaseConfiguration target) throws Exception {
Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
ListAppender<ILoggingEvent> captured = new ListAppender<>();
captured.start();
root.addAppender(captured);
try {
provisioner.provision(target);
try (Connection connection = DriverManager.getConnection(
target.jdbcUrl(), target.username(), target.password())) {
TargetSchemaBaseline baseline = TargetSchemaBaseline.load(target.kind());
assertThat(new TargetSchemaContract(target.kind()).matches(connection, baseline.expectedTables()))
.isTrue();
}
provisioner.provision(target);
} finally {
root.detachAppender(captured);
captured.stop();
}
assertThat(captured.list.stream().map(ILoggingEvent::getFormattedMessage).toList().toString())
.doesNotContain(target.jdbcUrl(), target.password(), "CREATE TABLE", "INSERT INTO");
}
private static void assertStandardFlywayAcceptsBaseline(String jdbcUrl, MetadataDatabaseKind kind)
throws Exception {
List<HistoryRow> before = historyRows(jdbcUrl);
String vendor = kind == MetadataDatabaseKind.MYSQL ? "mysql" : "postgresql";
Flyway flyway = Flyway.configure()
.dataSource(jdbcUrl, USERNAME, PASSWORD)
.locations("classpath:db/migration/" + vendor)
.cleanDisabled(true)
.validateMigrationNaming(true)
.load();
flyway.validate();
flyway.migrate();
assertThat(historyRows(jdbcUrl)).isEqualTo(before);
}
private static List<HistoryRow> historyRows(String jdbcUrl) throws Exception {
List<HistoryRow> rows = new ArrayList<>();
try (Connection connection = DriverManager.getConnection(jdbcUrl, USERNAME, PASSWORD);
Statement statement = connection.createStatement();
ResultSet result = statement.executeQuery(
"SELECT installed_rank, version, description, type, script, checksum, "
+ "installed_by, installed_on, execution_time, success "
+ "FROM flyway_schema_history ORDER BY installed_rank")) {
while (result.next()) {
rows.add(new HistoryRow(
result.getInt("installed_rank"),
result.getString("version"),
result.getString("description"),
result.getString("type"),
result.getString("script"),
result.getInt("checksum"),
result.getString("installed_by"),
result.getTimestamp("installed_on").toInstant(),
result.getInt("execution_time"),
result.getBoolean("success")));
}
}
return List.copyOf(rows);
}
private record HistoryRow(
int installedRank,
String version,
String description,
String type,
String script,
int checksum,
String installedBy,
java.time.Instant installedOn,
int executionTime,
boolean success) {
}
private static Set<String> metadataTables(Connection connection) throws Exception {
Set<String> tables = new HashSet<>();
DatabaseMetaData metadata = connection.getMetaData();
try (ResultSet result = metadata.getTables(connection.getCatalog(), null, "hzb_%", new String[]{"TABLE"})) {
while (result.next()) {
tables.add(result.getString("TABLE_NAME").toLowerCase(Locale.ROOT));
}
}
return tables;
}
private static void validateHibernateMappings(String jdbcUrl, String dialect) throws Exception {
StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder()
.applySetting("jakarta.persistence.jdbc.url", jdbcUrl)
.applySetting("jakarta.persistence.jdbc.user", USERNAME)
.applySetting("jakarta.persistence.jdbc.password", PASSWORD)
.applySetting("hibernate.dialect", dialect)
.applySetting("hibernate.physical_naming_strategy",
"org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy")
.applySetting("hibernate.hbm2ddl.auto", "validate");
StandardServiceRegistry registry = registryBuilder.build();
try {
MetadataSources sources = new MetadataSources(registry);
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(Entity.class));
scanner.findCandidateComponents("org.apache.hertzbeat").stream()
.map(definition -> definition.getBeanClassName())
.map(TargetSchemaProvisionerDatabaseTest::loadClass)
.forEach(sources::addAnnotatedClass);
try (SessionFactory ignored = sources.buildMetadata().buildSessionFactory()) {
assertThat(ignored.getMetamodel().getEntities()).hasSize(48);
}
} finally {
StandardServiceRegistryBuilder.destroy(registry);
}
}
private static Class<?> loadClass(String className) {
try {
return Class.forName(className);
} catch (ClassNotFoundException exception) {
throw new IllegalStateException("Mapped entity class is unavailable", exception);
}
}
}
@@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hertzbeat.manager.setup.workflow;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseConfiguration;
import org.apache.hertzbeat.manager.setup.api.SetupApiContract.MetadataDatabaseKind;
import org.junit.jupiter.api.Test;
class TargetSchemaProvisionerMetadataFailureTest {
@Test
void metadataProviderFailureIsSanitizedAtProvisioningBoundary() throws Exception {
String jdbcUrl = "jdbc:metadata-failure://private.example.test/hertzbeat?password=secret-value";
TargetSchemaBaseline baseline = TargetSchemaBaseline.load(MetadataDatabaseKind.MYSQL);
Connection connection = currentSchemaConnection(baseline);
Driver driver = new TestConnectionDriver(jdbcUrl, connection);
DriverManager.registerDriver(driver);
try {
MetadataDatabaseConfiguration target = new MetadataDatabaseConfiguration(
MetadataDatabaseKind.MYSQL, jdbcUrl, "operator", "secret-value");
assertThatThrownBy(() -> new FlywayTargetSchemaProvisioner().provision(target))
.isInstanceOfSatisfying(TargetSchemaProvisioningException.class, exception -> {
assertThat(exception.failure()).isEqualTo(new TargetSchemaProvisioningFailure(
TargetSchemaProvisioningFailure.Phase.PRECONDITION,
TargetSchemaBaseline.VERSION,
"58000",
777));
assertThat(exception).hasNoCause();
assertThat(exception.getMessage())
.doesNotContain(jdbcUrl, "secret-value", "raw metadata diagnostic");
});
} finally {
DriverManager.deregisterDriver(driver);
}
}
private static Connection currentSchemaConnection(TargetSchemaBaseline baseline) throws Exception {
Connection connection = mock(Connection.class);
DatabaseMetaData metadata = mock(DatabaseMetaData.class);
Statement historyStatement = mock(Statement.class);
ResultSet tables = tableRows(baseline);
ResultSet history = historyRow(baseline);
when(connection.getMetaData()).thenReturn(metadata);
when(connection.createStatement()).thenReturn(historyStatement);
when(metadata.getTables(isNull(), isNull(), anyString(), any(String[].class)))
.thenReturn(tables);
when(historyStatement.executeQuery(anyString())).thenReturn(history);
when(metadata.getColumns(isNull(), isNull(), anyString(), isNull()))
.thenThrow(new SQLException("raw metadata diagnostic", "58000", 777));
return connection;
}
private static ResultSet tableRows(TargetSchemaBaseline baseline) throws Exception {
List<String> tables = new ArrayList<>(baseline.expectedTables());
tables.add("flyway_schema_history");
tables.add(TargetSchemaContract.TABLE);
AtomicInteger row = new AtomicInteger(-1);
ResultSet result = mock(ResultSet.class);
when(result.next()).thenAnswer(ignored -> row.incrementAndGet() < tables.size());
when(result.getString("TABLE_NAME")).thenAnswer(ignored -> tables.get(row.get()));
return result;
}
private static ResultSet historyRow(TargetSchemaBaseline baseline) throws Exception {
ResultSet result = mock(ResultSet.class);
when(result.next()).thenReturn(true, false);
when(result.getInt("installed_rank")).thenReturn(1);
when(result.getString("version")).thenReturn(TargetSchemaBaseline.VERSION);
when(result.getString("type")).thenReturn(TargetSchemaBaseline.TYPE);
when(result.getString("script")).thenReturn(TargetSchemaBaseline.SCRIPT);
when(result.getInt("checksum")).thenReturn(baseline.checksum());
when(result.getBoolean("success")).thenReturn(true);
return result;
}
private record TestConnectionDriver(String acceptedUrl, Connection connection) implements Driver {
@Override
public Connection connect(String url, Properties info) {
return acceptsURL(url) ? connection : null;
}
@Override
public boolean acceptsURL(String url) {
return acceptedUrl.equals(url);
}
@Override
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) {
return new DriverPropertyInfo[0];
}
@Override
public int getMajorVersion() {
return 1;
}
@Override
public int getMinorVersion() {
return 0;
}
@Override
public boolean jdbcCompliant() {
return false;
}
@Override
public Logger getParentLogger() {
return Logger.getAnonymousLogger();
}
}
}
@@ -0,0 +1,593 @@
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0.
--
-- Immutable V159 schema fixture for migration-chain tests.
-- Do not derive this fixture from the current baseline or later migrations.
create table hzb_ai_conversation (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
creator varchar(255),
modifier varchar(255),
title varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_ai_message (
conversation_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
creator varchar(255),
modifier varchar(255),
role varchar(255),
content longtext not null,
primary key (id)
) engine=InnoDB;
create table hzb_alert_define (
enable bit not null,
period integer,
times integer,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
app varchar(255),
metric varchar(255),
field varchar(255),
preset bit,
priority integer,
tags varchar(255),
datasource varchar(100),
name varchar(100) not null,
expr varchar(2048),
labels varchar(2048),
template varchar(2048),
annotations varchar(4096),
creator varchar(255),
modifier varchar(255),
type varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_define_monitor_bind (
alert_define_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
primary key (id)
) engine=InnoDB;
create table hzb_alert_group (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
common_labels varchar(2048),
group_key varchar(2048) character set ascii,
group_labels varchar(2048),
alert_fingerprints varchar(255),
common_annotations varchar(255),
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_group_converge (
enable bit,
gmt_create datetime(6),
gmt_update datetime(6),
group_interval bigint,
group_wait bigint,
id bigint not null auto_increment,
repeat_interval bigint,
name varchar(100) not null,
group_labels varchar(1024),
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_inhibit (
enable bit,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
name varchar(100) not null,
equal_labels varchar(2048),
source_labels varchar(2048),
target_labels varchar(2048),
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_silence (
enable bit not null,
match_all bit not null,
times integer,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
period_end datetime(6),
period_start datetime(6),
name varchar(100) not null,
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_alert_single (
trigger_times integer,
active_at bigint,
end_at bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
start_at bigint,
fingerprint varchar(2048) character set ascii,
labels varchar(2048),
annotations varchar(4096),
content varchar(4096),
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_bulletin (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
fields varchar(4096),
monitor_ids varchar(4096),
app varchar(255),
creator varchar(255),
modifier varchar(255),
name varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_collector (
status tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
creator varchar(255),
ip varchar(255) not null,
mode varchar(255),
modifier varchar(255),
name varchar(255) not null,
version varchar(255),
primary key (id),
check ((status>=0))
) engine=InnoDB;
create table hzb_collector_monitor_bind (
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
collector varchar(255),
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_config (
gmt_create datetime(6),
gmt_update datetime(6),
content varchar(8192),
creator varchar(255),
modifier varchar(255),
type varchar(255) not null,
primary key (type)
) engine=InnoDB;
create table hzb_define (
gmt_create datetime(6),
gmt_update datetime(6),
app varchar(255) not null,
creator varchar(255),
modifier varchar(255),
content longtext,
primary key (app)
) engine=InnoDB;
create table hzb_grafana_dashboard (
enabled bit not null,
monitor_id bigint not null,
version bigint,
folder_uid varchar(255),
slug varchar(255),
status varchar(255),
uid varchar(255),
url varchar(255),
primary key (monitor_id)
) engine=InnoDB;
create table hzb_history (
dou float(53),
int32 integer,
metric_type tinyint,
id bigint not null auto_increment,
time bigint,
str varchar(2048),
app varchar(255),
instance varchar(5000),
metric varchar(255),
metrics varchar(255),
monitor_id bigint,
primary key (id)
) engine=InnoDB;
create table hzb_metrics_favorite (
create_time datetime(6),
id bigint not null auto_increment,
monitor_id bigint not null,
creator varchar(255) not null,
metrics_name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_monitor (
intervals integer,
status tinyint not null,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null,
job_id bigint,
schedule_type varchar(20),
app varchar(100),
cron_expression varchar(100),
host varchar(100),
name varchar(100),
scrape varchar(100),
annotations varchar(4096),
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
primary key (id),
check ((status<=4) and (status>=0))
) engine=InnoDB;
create table hzb_monitor_bind (
biz_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
creator varchar(255),
key_str varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_notice_receiver (
agent_id integer,
lark_receive_type tinyint,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
smn_ak varchar(22),
smn_project_id varchar(32),
smn_region varchar(32),
smn_sk varchar(42),
email varchar(100),
name varchar(100) not null,
phone varchar(100),
access_token varchar(300),
discord_bot_token varchar(300),
discord_channel_id varchar(300),
gotify_token varchar(300),
hook_auth_token varchar(300),
hook_auth_type varchar(300),
server_chan_token varchar(300),
slack_web_hook_url varchar(300),
smn_topic_urn varchar(300),
wechat_id varchar(300),
hook_url varchar(1000),
app_id varchar(255),
app_secret varchar(255),
chat_id varchar(255),
corp_id varchar(255),
creator varchar(255),
modifier varchar(255),
party_id varchar(255),
tag_id varchar(255),
tg_bot_token varchar(255),
tg_message_thread_id varchar(255),
tg_user_id varchar(255),
user_id varchar(255),
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_notice_rule (
enable bit not null,
filter_all bit not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
period_end datetime(6),
period_start datetime(6),
template_id bigint,
name varchar(100) not null,
template_name varchar(100),
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
receiver_id varchar(255) not null,
receiver_name varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_notice_template (
preset boolean default false,
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
name varchar(100) not null,
creator varchar(255),
modifier varchar(255),
content text not null,
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_param (
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
monitor_id bigint,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_param_define (
hide bit not null,
param_limit smallint,
required bit not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
name varchar(2048),
param_options varchar(2048),
app varchar(255),
creator varchar(255),
default_value varchar(255),
depend varchar(255),
field varchar(255),
key_alias varchar(255),
modifier varchar(255),
param_range varchar(255),
placeholder varchar(255),
type varchar(255),
value_alias varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_plugin_item (
id bigint not null auto_increment,
metadata_id bigint,
class_identifier varchar(255),
type enum ('POST_ALERT','POST_COLLECT'),
primary key (id)
) engine=InnoDB;
create table hzb_plugin_metadata (
enable_status bit,
param_count integer,
gmt_create datetime(6),
id bigint not null auto_increment,
creator varchar(255),
jar_file_path varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_plugin_param (
type tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
plugin_metadata_id bigint not null,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
check ((type>=0))
) engine=InnoDB;
create table hzb_push_metrics (
id bigint not null auto_increment,
monitor_id bigint,
time bigint,
metrics varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_status_page_component (
config_state tinyint not null,
method tinyint not null,
state tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
org_id bigint,
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_status_page_history (
abnormal integer,
normal integer,
state tinyint not null,
unknowing integer,
uptime float(53),
component_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
timestamp bigint,
creator varchar(255),
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_status_page_incident (
state tinyint not null,
end_time bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
org_id bigint,
start_time bigint,
creator varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_status_page_incident_component_bind (
component_id bigint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
incident_id bigint,
primary key (id)
) engine=InnoDB;
create table hzb_status_page_incident_content (
state tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
incident_id bigint,
timestamp bigint,
creator varchar(255),
message varchar(255) not null,
modifier varchar(255),
primary key (id)
) engine=InnoDB;
create table hzb_status_page_org (
state tinyint not null,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
color varchar(255),
creator varchar(255),
description varchar(255) not null,
feedback varchar(255),
home varchar(255) not null,
logo varchar(255) not null,
modifier varchar(255),
name varchar(255) not null,
primary key (id)
) engine=InnoDB;
create table hzb_tag (
type tinyint,
gmt_create datetime(6),
gmt_update datetime(6),
id bigint not null auto_increment,
tag_value varchar(2048),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id),
check ((type<=3) and (type>=0))
) engine=InnoDB;
create index idx_message_conversation_id
on hzb_ai_message (conversation_id);
create index index_alert_define_monitor
on hzb_alert_define_monitor_bind (alert_define_id, monitor_id);
alter table hzb_alert_group
add constraint unique_group_key unique (group_key);
create index idx_name
on hzb_alert_group_converge (name);
alter table hzb_alert_single
add constraint unique_fingerprint unique (fingerprint);
alter table hzb_collector
add constraint uk_hzb_collector_name unique (name);
create index index_collector_monitor
on hzb_collector_monitor_bind (collector, monitor_id);
create index history_query_index
on hzb_history (monitor_id, app, metrics, metric);
alter table hzb_metrics_favorite
add constraint uk_hzb_metrics_favorite unique (creator, monitor_id, metrics_name);
create index monitor_query_index
on hzb_monitor (app, host, name);
create index index_monitor_bin
on hzb_monitor_bind (monitor_id);
create index idx_hzb_param_monitor_id
on hzb_param (monitor_id);
alter table hzb_param
add constraint uk_hzb_param_monitor_field unique (monitor_id, field);
create index idx_hzb_plugin_param_plugin_metadata_id
on hzb_plugin_param (plugin_metadata_id);
alter table hzb_plugin_param
add constraint uk_hzb_plugin_param_metadata_field unique (plugin_metadata_id, field);
create index push_query_index
on hzb_push_metrics (monitor_id, time);
create index index_incident_component
on hzb_status_page_incident_component_bind (incident_id);
alter table hzb_ai_message
add constraint fk_hzb_ai_message_conversation
foreign key (conversation_id)
references hzb_ai_conversation (id);
alter table hzb_plugin_item
add constraint fk_hzb_plugin_item_metadata
foreign key (metadata_id)
references hzb_plugin_metadata (id);
alter table hzb_status_page_incident_content
add constraint fk_hzb_incident_content_incident
foreign key (incident_id)
references hzb_status_page_incident (id);
@@ -0,0 +1,573 @@
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0.
--
-- Immutable V159 schema fixture for migration-chain tests.
-- Do not derive this fixture from the current baseline or later migrations.
create table hzb_ai_conversation (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
modifier varchar(255),
title varchar(255),
primary key (id)
);
create table hzb_ai_message (
conversation_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
modifier varchar(255),
role varchar(255),
content oid not null,
primary key (id)
);
create table hzb_alert_define (
enable boolean not null,
period integer,
times integer,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
app varchar(255),
metric varchar(255),
field varchar(255),
preset boolean,
priority integer,
tags varchar(255),
datasource varchar(100),
name varchar(100) not null,
expr varchar(2048),
labels varchar(2048),
template varchar(2048),
annotations varchar(4096),
creator varchar(255),
modifier varchar(255),
type varchar(255),
primary key (id)
);
create table hzb_alert_define_monitor_bind (
alert_define_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
primary key (id)
);
create table hzb_alert_group (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
common_labels varchar(2048),
group_key varchar(2048),
group_labels varchar(2048),
alert_fingerprints varchar(255),
common_annotations varchar(255),
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id),
constraint unique_group_key unique (group_key)
);
create table hzb_alert_group_converge (
enable boolean,
gmt_create timestamp(6),
gmt_update timestamp(6),
group_interval bigint,
group_wait bigint,
id bigint generated by default as identity,
repeat_interval bigint,
name varchar(100) not null,
group_labels varchar(1024),
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_alert_inhibit (
enable boolean,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
name varchar(100) not null,
equal_labels varchar(2048),
source_labels varchar(2048),
target_labels varchar(2048),
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_alert_silence (
enable boolean not null,
match_all boolean not null,
times integer,
type smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
period_end timestamp(6) with time zone,
period_start timestamp(6) with time zone,
name varchar(100) not null,
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_alert_single (
trigger_times integer,
active_at bigint,
end_at bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
start_at bigint,
fingerprint varchar(2048),
labels varchar(2048),
annotations varchar(4096),
content varchar(4096),
creator varchar(255),
modifier varchar(255),
status varchar(255),
primary key (id),
constraint unique_fingerprint unique (fingerprint)
);
create table hzb_bulletin (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
fields varchar(4096),
monitor_ids varchar(4096),
app varchar(255),
creator varchar(255),
modifier varchar(255),
name varchar(255),
primary key (id)
);
create table hzb_collector (
status smallint not null check ((status>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
ip varchar(255) not null,
mode varchar(255),
modifier varchar(255),
name varchar(255) not null,
version varchar(255),
primary key (id),
unique (name)
);
create table hzb_collector_monitor_bind (
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
collector varchar(255),
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_config (
gmt_create timestamp(6),
gmt_update timestamp(6),
content varchar(8192),
creator varchar(255),
modifier varchar(255),
type varchar(255) not null,
primary key (type)
);
create table hzb_define (
gmt_create timestamp(6),
gmt_update timestamp(6),
app varchar(255) not null,
creator varchar(255),
modifier varchar(255),
content oid,
primary key (app)
);
create table hzb_grafana_dashboard (
enabled boolean not null,
monitor_id bigint not null,
version bigint,
folder_uid varchar(255),
slug varchar(255),
status varchar(255),
uid varchar(255),
url varchar(255),
primary key (monitor_id)
);
create table hzb_history (
dou float(53),
int32 integer,
metric_type smallint,
id bigint generated by default as identity,
time bigint,
str varchar(2048),
app varchar(255),
instance varchar(5000),
metric varchar(255),
metrics varchar(255),
monitor_id bigint,
primary key (id)
);
create table hzb_metrics_favorite (
create_time timestamp(6),
id bigint generated by default as identity,
monitor_id bigint not null,
creator varchar(255) not null,
metrics_name varchar(255) not null,
primary key (id),
unique (creator, monitor_id, metrics_name)
);
create table hzb_monitor (
intervals integer,
status smallint not null check ((status<=4) and (status>=0)),
type smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint not null,
job_id bigint,
schedule_type varchar(20),
app varchar(100),
cron_expression varchar(100),
host varchar(100),
name varchar(100),
scrape varchar(100),
annotations varchar(4096),
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_monitor_bind (
biz_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
creator varchar(255),
key_str varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_notice_receiver (
agent_id integer,
lark_receive_type smallint,
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
smn_ak varchar(22),
smn_project_id varchar(32),
smn_region varchar(32),
smn_sk varchar(42),
email varchar(100),
name varchar(100) not null,
phone varchar(100),
access_token varchar(300),
discord_bot_token varchar(300),
discord_channel_id varchar(300),
gotify_token varchar(300),
hook_auth_token varchar(300),
hook_auth_type varchar(300),
server_chan_token varchar(300),
slack_web_hook_url varchar(300),
smn_topic_urn varchar(300),
wechat_id varchar(300),
hook_url varchar(1000),
app_id varchar(255),
app_secret varchar(255),
chat_id varchar(255),
corp_id varchar(255),
creator varchar(255),
modifier varchar(255),
party_id varchar(255),
tag_id varchar(255),
tg_bot_token varchar(255),
tg_message_thread_id varchar(255),
tg_user_id varchar(255),
user_id varchar(255),
primary key (id)
);
create table hzb_notice_rule (
enable boolean not null,
filter_all boolean not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
period_end timestamp(6) with time zone,
period_start timestamp(6) with time zone,
template_id bigint,
name varchar(100) not null,
template_name varchar(100),
labels varchar(2048),
creator varchar(255),
days varchar(255),
modifier varchar(255),
receiver_id varchar(255) not null,
receiver_name varchar(255),
primary key (id)
);
create table hzb_notice_template (
preset boolean default false,
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
name varchar(100) not null,
creator varchar(255),
modifier varchar(255),
content oid not null,
primary key (id)
);
create table hzb_param (
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
monitor_id bigint,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
constraint uk_hzb_param_monitor_field unique (monitor_id, field)
);
create table hzb_param_define (
hide boolean not null,
param_limit smallint,
required boolean not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
name varchar(2048),
param_options varchar(2048),
app varchar(255),
creator varchar(255),
default_value varchar(255),
depend varchar(255),
field varchar(255),
key_alias varchar(255),
modifier varchar(255),
param_range varchar(255),
placeholder varchar(255),
type varchar(255),
value_alias varchar(255),
primary key (id)
);
create table hzb_plugin_item (
id bigint generated by default as identity,
metadata_id bigint,
class_identifier varchar(255),
type varchar(255) check ((type in ('POST_ALERT','POST_COLLECT'))),
primary key (id)
);
create table hzb_plugin_metadata (
enable_status boolean,
param_count integer,
gmt_create timestamp(6),
id bigint generated by default as identity,
creator varchar(255),
jar_file_path varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_plugin_param (
type smallint not null check ((type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
plugin_metadata_id bigint not null,
field varchar(100) not null,
param_value varchar(8126),
primary key (id),
constraint uk_hzb_plugin_param_metadata_field unique (plugin_metadata_id, field)
);
create table hzb_push_metrics (
id bigint generated by default as identity,
monitor_id bigint,
time bigint,
metrics varchar(255),
primary key (id)
);
create table hzb_status_page_component (
config_state smallint not null,
method smallint not null,
state smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
org_id bigint,
labels varchar(4096),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_status_page_history (
abnormal integer,
normal integer,
state smallint not null,
unknowing integer,
uptime float(53),
component_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
timestamp bigint,
creator varchar(255),
modifier varchar(255),
primary key (id)
);
create table hzb_status_page_incident (
state smallint not null,
end_time bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
org_id bigint,
start_time bigint,
creator varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_status_page_incident_component_bind (
component_id bigint,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
incident_id bigint,
primary key (id)
);
create table hzb_status_page_incident_content (
state smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
incident_id bigint,
timestamp bigint,
creator varchar(255),
message varchar(255) not null,
modifier varchar(255),
primary key (id)
);
create table hzb_status_page_org (
state smallint not null,
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
color varchar(255),
creator varchar(255),
description varchar(255) not null,
feedback varchar(255),
home varchar(255) not null,
logo varchar(255) not null,
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create table hzb_tag (
type smallint check ((type<=3) and (type>=0)),
gmt_create timestamp(6),
gmt_update timestamp(6),
id bigint generated by default as identity,
tag_value varchar(2048),
creator varchar(255),
description varchar(255),
modifier varchar(255),
name varchar(255) not null,
primary key (id)
);
create index idx_message_conversation_id
on hzb_ai_message (conversation_id);
create index index_alert_define_monitor
on hzb_alert_define_monitor_bind (alert_define_id, monitor_id);
create index idx_name
on hzb_alert_group_converge (name);
create index index_collector_monitor
on hzb_collector_monitor_bind (collector, monitor_id);
create index history_query_index
on hzb_history (monitor_id, app, metrics, metric);
create index monitor_query_index
on hzb_monitor (app, host, name);
create index index_monitor_bin
on hzb_monitor_bind (monitor_id);
create index idx_hzb_param_monitor_id
on hzb_param (monitor_id);
create index idx_hzb_plugin_param_plugin_metadata_id
on hzb_plugin_param (plugin_metadata_id);
create index push_query_index
on hzb_push_metrics (monitor_id, time);
create index index_incident_component
on hzb_status_page_incident_component_bind (incident_id);
alter table if exists hzb_ai_message
add constraint fk_hzb_ai_message_conversation
foreign key (conversation_id)
references hzb_ai_conversation;
alter table if exists hzb_plugin_item
add constraint fk_hzb_plugin_item_metadata
foreign key (metadata_id)
references hzb_plugin_metadata;
alter table if exists hzb_status_page_incident_content
add constraint fk_hzb_incident_content_incident
foreign key (incident_id)
references hzb_status_page_incident;