mirror of
https://github.com/dromara/hertzbeat.git
synced 2026-09-17 09:40:58 +00:00
[fix] consolidate unreleased 1.9.0 migrations into V190 and fix PG/MySQL startup (#4368)
This commit is contained in:
+1
-1
@@ -48,7 +48,7 @@ class FlywayConfigurationTest {
|
|||||||
@Test
|
@Test
|
||||||
void repairsAndRetriesWhenHistoryHasFailedMigration() {
|
void repairsAndRetriesWhenHistoryHasFailedMigration() {
|
||||||
Mockito.when(flyway.migrate())
|
Mockito.when(flyway.migrate())
|
||||||
.thenThrow(validateException("Detected failed migration to version 181 (update column)"))
|
.thenThrow(validateException("Detected failed migration to version 190 (update column)"))
|
||||||
.thenReturn(null);
|
.thenReturn(null);
|
||||||
|
|
||||||
configuration.delayedFlywayInitializer(flyway, enabledProperties());
|
configuration.delayedFlywayInitializer(flyway, enabledProperties());
|
||||||
|
|||||||
@@ -168,6 +168,14 @@
|
|||||||
<version>${testcontainers.version}</version>
|
<version>${testcontainers.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Test-only driver for the migration regression; the postgresql driver is already
|
||||||
|
on the test classpath transitively. Version comes from the root dependencyManagement
|
||||||
|
and nothing here is packaged into the distribution. -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
UPDATE hzb_sop_schedule
|
|
||||||
SET enabled = 0
|
|
||||||
WHERE creator IS NULL
|
|
||||||
OR TRIM(creator) = '';
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_schedule_creator_conversation
|
|
||||||
ON hzb_sop_schedule(creator, conversation_id);
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
-- Enlarge alert define expr to fit rules binding many monitors (#4171)
|
|
||||||
ALTER TABLE HZB_ALERT_DEFINE ALTER COLUMN expr CLOB;
|
|
||||||
+13
-18
@@ -14,23 +14,8 @@
|
|||||||
-- KIND, either express or implied. See the License for the
|
-- KIND, either express or implied. See the License for the
|
||||||
-- specific language governing permissions and limitations
|
-- specific language governing permissions and limitations
|
||||||
-- under the License.
|
-- under the License.
|
||||||
|
-- Schema changes for release 1.9.0.
|
||||||
-- Licensed to the Apache Software Foundation (ASF) under one
|
-- Every statement below is safe to re-run.
|
||||||
-- 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.
|
|
||||||
|
|
||||||
-- Scheduled SOP execution configurations
|
-- Scheduled SOP execution configurations
|
||||||
CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
||||||
@@ -39,7 +24,7 @@ CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
|||||||
sop_name VARCHAR(64) NOT NULL COMMENT 'Name of the SOP skill to execute',
|
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',
|
sop_params VARCHAR(1024) COMMENT 'SOP execution parameters in JSON format',
|
||||||
cron_expression VARCHAR(64) NOT NULL COMMENT 'Cron expression for scheduling',
|
cron_expression VARCHAR(64) NOT NULL COMMENT 'Cron expression for scheduling',
|
||||||
enabled TINYINT DEFAULT 1 COMMENT 'Whether the schedule is enabled',
|
enabled BOOLEAN DEFAULT TRUE COMMENT 'Whether the schedule is enabled',
|
||||||
last_run_time DATETIME COMMENT 'Last execution time',
|
last_run_time DATETIME COMMENT 'Last execution time',
|
||||||
next_run_time DATETIME COMMENT 'Next scheduled execution time',
|
next_run_time DATETIME COMMENT 'Next scheduled execution time',
|
||||||
creator VARCHAR(64) COMMENT 'Creator of this record',
|
creator VARCHAR(64) COMMENT 'Creator of this record',
|
||||||
@@ -50,3 +35,13 @@ CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_schedule_conversation_id ON hzb_sop_schedule(conversation_id);
|
CREATE INDEX IF NOT EXISTS idx_schedule_conversation_id ON hzb_sop_schedule(conversation_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_schedule_enabled_next ON hzb_sop_schedule(enabled, next_run_time);
|
CREATE INDEX IF NOT EXISTS idx_schedule_enabled_next ON hzb_sop_schedule(enabled, next_run_time);
|
||||||
|
-- idx_schedule_creator_conversation is declared by @Index on SopSchedule and created by Hibernate.
|
||||||
|
|
||||||
|
-- Disable SOP schedules that have no owner to scope them to
|
||||||
|
UPDATE hzb_sop_schedule
|
||||||
|
SET enabled = FALSE
|
||||||
|
WHERE creator IS NULL
|
||||||
|
OR TRIM(creator) = '';
|
||||||
|
|
||||||
|
-- Enlarge alert define expr to fit rules binding many monitors
|
||||||
|
ALTER TABLE HZB_ALERT_DEFINE ALTER COLUMN expr CLOB;
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
UPDATE hzb_sop_schedule
|
|
||||||
SET enabled = 0
|
|
||||||
WHERE creator IS NULL
|
|
||||||
OR TRIM(creator) = '';
|
|
||||||
|
|
||||||
CREATE INDEX idx_schedule_creator_conversation
|
|
||||||
ON hzb_sop_schedule(creator, conversation_id);
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
-- Enlarge alert define expr to fit rules binding many monitors (#4171)
|
|
||||||
DELIMITER //
|
|
||||||
|
|
||||||
CREATE PROCEDURE ModifyAlertDefineExprColumn()
|
|
||||||
BEGIN
|
|
||||||
DECLARE table_exists INT;
|
|
||||||
DECLARE col_exists INT;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO table_exists
|
|
||||||
FROM INFORMATION_SCHEMA.TABLES
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hzb_alert_define';
|
|
||||||
|
|
||||||
IF table_exists = 1 THEN
|
|
||||||
SELECT COUNT(*) INTO col_exists
|
|
||||||
FROM INFORMATION_SCHEMA.COLUMNS
|
|
||||||
WHERE TABLE_SCHEMA = DATABASE()
|
|
||||||
AND TABLE_NAME = 'hzb_alert_define'
|
|
||||||
AND COLUMN_NAME = 'expr'
|
|
||||||
AND DATA_TYPE != 'longtext';
|
|
||||||
|
|
||||||
IF col_exists = 1 THEN
|
|
||||||
ALTER TABLE hzb_alert_define MODIFY COLUMN expr LONGTEXT;
|
|
||||||
END IF;
|
|
||||||
END IF;
|
|
||||||
END //
|
|
||||||
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
CALL ModifyAlertDefineExprColumn();
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS ModifyAlertDefineExprColumn;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
+46
-18
@@ -14,23 +14,8 @@
|
|||||||
-- KIND, either express or implied. See the License for the
|
-- KIND, either express or implied. See the License for the
|
||||||
-- specific language governing permissions and limitations
|
-- specific language governing permissions and limitations
|
||||||
-- under the License.
|
-- under the License.
|
||||||
|
-- Schema changes for release 1.9.0.
|
||||||
-- Licensed to the Apache Software Foundation (ASF) under one
|
-- Every statement below is safe to re-run.
|
||||||
-- 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.
|
|
||||||
|
|
||||||
-- Scheduled SOP execution configurations
|
-- Scheduled SOP execution configurations
|
||||||
CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
||||||
@@ -39,7 +24,7 @@ CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
|||||||
sop_name VARCHAR(64) NOT NULL COMMENT 'Name of the SOP skill to execute',
|
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',
|
sop_params VARCHAR(1024) COMMENT 'SOP execution parameters in JSON format',
|
||||||
cron_expression VARCHAR(64) NOT NULL COMMENT 'Cron expression for scheduling',
|
cron_expression VARCHAR(64) NOT NULL COMMENT 'Cron expression for scheduling',
|
||||||
enabled TINYINT DEFAULT 1 COMMENT 'Whether the schedule is enabled',
|
enabled BOOLEAN DEFAULT TRUE COMMENT 'Whether the schedule is enabled',
|
||||||
last_run_time DATETIME COMMENT 'Last execution time',
|
last_run_time DATETIME COMMENT 'Last execution time',
|
||||||
next_run_time DATETIME COMMENT 'Next scheduled execution time',
|
next_run_time DATETIME COMMENT 'Next scheduled execution time',
|
||||||
creator VARCHAR(64) COMMENT 'Creator of this record',
|
creator VARCHAR(64) COMMENT 'Creator of this record',
|
||||||
@@ -49,3 +34,46 @@ CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
|||||||
INDEX idx_schedule_conversation_id (conversation_id),
|
INDEX idx_schedule_conversation_id (conversation_id),
|
||||||
INDEX idx_schedule_enabled_next (enabled, next_run_time)
|
INDEX idx_schedule_enabled_next (enabled, next_run_time)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
-- idx_schedule_creator_conversation is declared by @Index on SopSchedule and created by Hibernate.
|
||||||
|
|
||||||
|
-- Disable SOP schedules that have no owner to scope them to
|
||||||
|
UPDATE hzb_sop_schedule
|
||||||
|
SET enabled = FALSE
|
||||||
|
WHERE creator IS NULL
|
||||||
|
OR TRIM(creator) = '';
|
||||||
|
|
||||||
|
-- Enlarge alert define expr to fit rules binding many monitors.
|
||||||
|
-- Keep the DELIMITER block last so the routine body cannot swallow the plain
|
||||||
|
-- statements above it.
|
||||||
|
DROP PROCEDURE IF EXISTS ModifyAlertDefineExprColumn;
|
||||||
|
|
||||||
|
DELIMITER //
|
||||||
|
|
||||||
|
CREATE PROCEDURE ModifyAlertDefineExprColumn()
|
||||||
|
BEGIN
|
||||||
|
DECLARE table_exists INT;
|
||||||
|
DECLARE col_exists INT;
|
||||||
|
|
||||||
|
SELECT COUNT(*) INTO table_exists
|
||||||
|
FROM INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'hzb_alert_define';
|
||||||
|
|
||||||
|
IF table_exists = 1 THEN
|
||||||
|
SELECT COUNT(*) INTO col_exists
|
||||||
|
FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'hzb_alert_define'
|
||||||
|
AND COLUMN_NAME = 'expr'
|
||||||
|
AND DATA_TYPE != 'longtext';
|
||||||
|
|
||||||
|
IF col_exists = 1 THEN
|
||||||
|
ALTER TABLE hzb_alert_define MODIFY COLUMN expr LONGTEXT;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
END //
|
||||||
|
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
|
CALL ModifyAlertDefineExprColumn();
|
||||||
|
|
||||||
|
DROP PROCEDURE IF EXISTS ModifyAlertDefineExprColumn;
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
UPDATE hzb_sop_schedule
|
|
||||||
SET enabled = 0
|
|
||||||
WHERE creator IS NULL
|
|
||||||
OR BTRIM(creator) = '';
|
|
||||||
|
|
||||||
CREATE INDEX idx_schedule_creator_conversation
|
|
||||||
ON hzb_sop_schedule(creator, conversation_id);
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
-- 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.
|
|
||||||
|
|
||||||
-- Enlarge alert define expr to fit rules binding many monitors (#4171)
|
|
||||||
ALTER TABLE HZB_ALERT_DEFINE ALTER COLUMN expr TYPE TEXT;
|
|
||||||
commit;
|
|
||||||
+13
-18
@@ -14,23 +14,8 @@
|
|||||||
-- KIND, either express or implied. See the License for the
|
-- KIND, either express or implied. See the License for the
|
||||||
-- specific language governing permissions and limitations
|
-- specific language governing permissions and limitations
|
||||||
-- under the License.
|
-- under the License.
|
||||||
|
-- Schema changes for release 1.9.0.
|
||||||
-- Licensed to the Apache Software Foundation (ASF) under one
|
-- Every statement below is safe to re-run.
|
||||||
-- 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.
|
|
||||||
|
|
||||||
-- Scheduled SOP execution configurations
|
-- Scheduled SOP execution configurations
|
||||||
CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
||||||
@@ -39,7 +24,7 @@ CREATE TABLE IF NOT EXISTS hzb_sop_schedule (
|
|||||||
sop_name VARCHAR(64) NOT NULL,
|
sop_name VARCHAR(64) NOT NULL,
|
||||||
sop_params VARCHAR(1024),
|
sop_params VARCHAR(1024),
|
||||||
cron_expression VARCHAR(64) NOT NULL,
|
cron_expression VARCHAR(64) NOT NULL,
|
||||||
enabled SMALLINT DEFAULT 1,
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
last_run_time TIMESTAMP,
|
last_run_time TIMESTAMP,
|
||||||
next_run_time TIMESTAMP,
|
next_run_time TIMESTAMP,
|
||||||
creator VARCHAR(64),
|
creator VARCHAR(64),
|
||||||
@@ -59,3 +44,13 @@ COMMENT ON COLUMN hzb_sop_schedule.next_run_time IS 'Next scheduled execution ti
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_schedule_conversation_id ON hzb_sop_schedule(conversation_id);
|
CREATE INDEX IF NOT EXISTS idx_schedule_conversation_id ON hzb_sop_schedule(conversation_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_schedule_enabled_next ON hzb_sop_schedule(enabled, next_run_time);
|
CREATE INDEX IF NOT EXISTS idx_schedule_enabled_next ON hzb_sop_schedule(enabled, next_run_time);
|
||||||
|
-- idx_schedule_creator_conversation is declared by @Index on SopSchedule and created by Hibernate.
|
||||||
|
|
||||||
|
-- Disable SOP schedules that have no owner to scope them to
|
||||||
|
UPDATE hzb_sop_schedule
|
||||||
|
SET enabled = FALSE
|
||||||
|
WHERE creator IS NULL
|
||||||
|
OR BTRIM(creator) = '';
|
||||||
|
|
||||||
|
-- Enlarge alert define expr to fit rules binding many monitors
|
||||||
|
ALTER TABLE HZB_ALERT_DEFINE ALTER COLUMN expr TYPE TEXT;
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
/*
|
||||||
|
* 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.startup.migration;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.flywaydb.core.Flyway;
|
||||||
|
import org.flywaydb.core.api.MigrationInfo;
|
||||||
|
import org.flywaydb.core.api.MigrationVersion;
|
||||||
|
import org.flywaydb.core.api.configuration.FluentConfiguration;
|
||||||
|
import org.flywaydb.core.api.output.MigrateResult;
|
||||||
|
import org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.hibernate.SpringImplicitNamingStrategy;
|
||||||
|
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||||
|
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||||
|
import org.yaml.snakeyaml.Yaml;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upgrade regression for the 1.9.0 migration chain, run once per supported dialect.
|
||||||
|
*
|
||||||
|
* <p>The scenario mirrors a production start: Hibernate {@code ddl-auto} builds the schema
|
||||||
|
* from the entity model first, then Flyway runs, exactly as {@code FlywayConfiguration}
|
||||||
|
* orders them with {@code @DependsOn("entityManagerFactory")}. The database is first taken
|
||||||
|
* to the last officially released state (V180) and only then is V190 applied, so the test
|
||||||
|
* covers the upgrade path a 1.8.0 user actually takes.
|
||||||
|
*/
|
||||||
|
abstract class AbstractMigrationUpgradeTest {
|
||||||
|
|
||||||
|
protected static final String SOP_SCHEDULE_TABLE = "hzb_sop_schedule";
|
||||||
|
|
||||||
|
protected static final String ALERT_DEFINE_TABLE = "hzb_alert_define";
|
||||||
|
|
||||||
|
private static final String LAST_RELEASED_VERSION = "180";
|
||||||
|
|
||||||
|
private static final String CURRENT_VERSION = "190";
|
||||||
|
|
||||||
|
private static final List<String> WITHDRAWN_VERSIONS = List.of("181", "182", "183");
|
||||||
|
|
||||||
|
private static final List<String> SCHEDULE_INDEX_CONVERSATION = List.of("conversation_id");
|
||||||
|
|
||||||
|
private static final List<String> SCHEDULE_INDEX_ENABLED_NEXT = List.of("enabled", "next_run_time");
|
||||||
|
|
||||||
|
private static final List<String> SCHEDULE_INDEX_CREATOR_CONVERSATION = List.of("creator", "conversation_id");
|
||||||
|
|
||||||
|
private static final String SEEDED_EXPR = "usage>90";
|
||||||
|
|
||||||
|
private static final Map<String, Object> SHIPPED_FLYWAY_SETTINGS = loadShippedFlywaySettings();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data source of the freshly created database under test.
|
||||||
|
*
|
||||||
|
* @return data source
|
||||||
|
*/
|
||||||
|
protected abstract DataSource dataSource();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flyway vendor directory holding the dialect specific migrations.
|
||||||
|
*
|
||||||
|
* @return vendor directory name
|
||||||
|
*/
|
||||||
|
protected abstract String vendor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the type the database itself reports for a column.
|
||||||
|
*
|
||||||
|
* @param table table name
|
||||||
|
* @param column column name
|
||||||
|
* @return the declared type, normalised to lower case
|
||||||
|
* @throws SQLException on query failure
|
||||||
|
*/
|
||||||
|
protected abstract String declaredType(String table, String column) throws SQLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The types this dialect uses for a boolean column. More than one spelling is legitimate:
|
||||||
|
* on MySQL Hibernate declares {@code bit(1)} while the migration's own {@code BOOLEAN}
|
||||||
|
* lands as {@code tinyint(1)}, and both hold the same values.
|
||||||
|
*
|
||||||
|
* @return accepted types of {@code hzb_sop_schedule.enabled}
|
||||||
|
*/
|
||||||
|
protected abstract Set<String> expectedBooleanTypes();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type {@code hzb_alert_define.expr} must have once V190 has enlarged it.
|
||||||
|
*
|
||||||
|
* @return expected type of {@code hzb_alert_define.expr}
|
||||||
|
*/
|
||||||
|
protected abstract String expectedEnlargedTextType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stored routines left behind in the schema under test.
|
||||||
|
*
|
||||||
|
* @return routine names, empty when the migrations cleaned up after themselves
|
||||||
|
* @throws SQLException on query failure
|
||||||
|
*/
|
||||||
|
protected abstract List<String> storedRoutines() throws SQLException;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("V190 upgrades a V180 database whose schedule table Hibernate owns")
|
||||||
|
void upgradesDatabaseWhoseScheduleTableHibernateOwns() throws SQLException {
|
||||||
|
startFromLastReleasedState();
|
||||||
|
seedSchedules();
|
||||||
|
seedAlertDefine();
|
||||||
|
|
||||||
|
Flyway flyway = flyway();
|
||||||
|
assertPendingVersions(flyway, List.of(CURRENT_VERSION));
|
||||||
|
|
||||||
|
MigrateResult result = flyway.migrate();
|
||||||
|
assertEquals(1, result.migrationsExecuted, "V190 must be the only migration left to apply");
|
||||||
|
assertEquals(CURRENT_VERSION, result.targetSchemaVersion);
|
||||||
|
|
||||||
|
assertScheduleRowsScopedToOwner();
|
||||||
|
assertBooleanColumn();
|
||||||
|
assertEquals(expectedEnlargedTextType(), declaredType(ALERT_DEFINE_TABLE, "expr"));
|
||||||
|
assertSeededExprSurvived();
|
||||||
|
assertOversizedExprRoundTrips();
|
||||||
|
assertScheduleIndexes(List.of(
|
||||||
|
SCHEDULE_INDEX_CONVERSATION,
|
||||||
|
SCHEDULE_INDEX_ENABLED_NEXT,
|
||||||
|
SCHEDULE_INDEX_CREATOR_CONVERSATION));
|
||||||
|
assertNoStoredRoutinesLeftBehind();
|
||||||
|
assertHistoryChain();
|
||||||
|
|
||||||
|
assertSecondStartupChangesNothing();
|
||||||
|
assertScheduleRowsScopedToOwner();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("V190 upgrades a V180 database whose schedule table the migration has to create")
|
||||||
|
void upgradesDatabaseWhoseScheduleTableTheMigrationCreates() throws SQLException {
|
||||||
|
startFromLastReleasedState();
|
||||||
|
// Model the ordering where Flyway sees the table first: the migration, not Hibernate,
|
||||||
|
// has to create hzb_sop_schedule. This is the path that made PostgreSQL declare the
|
||||||
|
// column SMALLINT, after which the script's own "SET enabled = FALSE" failed with 42804.
|
||||||
|
execute("DROP TABLE " + SOP_SCHEDULE_TABLE);
|
||||||
|
|
||||||
|
Flyway flyway = flyway();
|
||||||
|
MigrateResult result = flyway.migrate();
|
||||||
|
assertEquals(1, result.migrationsExecuted, "V190 must be the only migration left to apply");
|
||||||
|
|
||||||
|
assertBooleanColumn();
|
||||||
|
assertEquals(expectedEnlargedTextType(), declaredType(ALERT_DEFINE_TABLE, "expr"));
|
||||||
|
assertScheduleIndexes(List.of(SCHEDULE_INDEX_CONVERSATION, SCHEDULE_INDEX_ENABLED_NEXT));
|
||||||
|
assertNoStoredRoutinesLeftBehind();
|
||||||
|
assertHistoryChain();
|
||||||
|
|
||||||
|
// The migration ran against an empty table, so replay its own update statement to prove
|
||||||
|
// the column it created accepts the FALSE literal the script uses.
|
||||||
|
seedSchedules();
|
||||||
|
execute("UPDATE " + SOP_SCHEDULE_TABLE + " SET enabled = FALSE WHERE creator IS NULL OR TRIM(creator) = ''");
|
||||||
|
assertScheduleRowsScopedToOwner();
|
||||||
|
|
||||||
|
assertSecondStartupChangesNothing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Brings the database to the state a 1.8.0 installation is in: schema built by Hibernate,
|
||||||
|
* migration history applied up to and including the last released version.
|
||||||
|
*/
|
||||||
|
private void startFromLastReleasedState() {
|
||||||
|
createSchemaWithHibernate();
|
||||||
|
MigrateResult result = flyway(LAST_RELEASED_VERSION).migrate();
|
||||||
|
assertEquals(LAST_RELEASED_VERSION, result.targetSchemaVersion,
|
||||||
|
"the released baseline must stop at V" + LAST_RELEASED_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the schema the way the application does at startup, from the scanned entity model.
|
||||||
|
*/
|
||||||
|
private void createSchemaWithHibernate() {
|
||||||
|
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
|
||||||
|
factory.setDataSource(dataSource());
|
||||||
|
factory.setPackagesToScan("org.apache.hertzbeat");
|
||||||
|
factory.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.setProperty("hibernate.hbm2ddl.auto", "update");
|
||||||
|
// The naming strategies Spring Boot applies by default, so the generated column names
|
||||||
|
// are the ones the running application has.
|
||||||
|
properties.setProperty("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
|
||||||
|
properties.setProperty("hibernate.physical_naming_strategy",
|
||||||
|
CamelCaseToUnderscoresNamingStrategy.class.getName());
|
||||||
|
factory.setJpaProperties(properties);
|
||||||
|
factory.afterPropertiesSet();
|
||||||
|
factory.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Flyway flyway() {
|
||||||
|
return flyway(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures Flyway from the shipped {@code application.yml} rather than from a copy of it,
|
||||||
|
* so the regression cannot drift away from what the application actually runs. The
|
||||||
|
* {@code {vendor}} placeholder is resolved the way Spring Boot resolves it at startup.
|
||||||
|
*
|
||||||
|
* @param target version to stop at, {@code null} to migrate to the latest
|
||||||
|
* @return configured Flyway instance
|
||||||
|
*/
|
||||||
|
private Flyway flyway(String target) {
|
||||||
|
List<String> locations = shippedSetting("locations", List.class).stream()
|
||||||
|
.map(location -> String.valueOf(location).replace("{vendor}", vendor()))
|
||||||
|
.toList();
|
||||||
|
FluentConfiguration configuration = Flyway.configure()
|
||||||
|
.dataSource(dataSource())
|
||||||
|
.locations(locations.toArray(new String[0]))
|
||||||
|
.baselineOnMigrate(shippedSetting("baseline-on-migrate", Boolean.class))
|
||||||
|
.baselineVersion(String.valueOf(shippedSetting("baseline-version", Object.class)))
|
||||||
|
.cleanDisabled(shippedSetting("clean-disabled", Boolean.class));
|
||||||
|
if (target != null) {
|
||||||
|
configuration.target(MigrationVersion.fromVersion(target));
|
||||||
|
}
|
||||||
|
return configuration.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> T shippedSetting(String key, Class<T> type) {
|
||||||
|
Object value = SHIPPED_FLYWAY_SETTINGS.get(key);
|
||||||
|
assertNotNull(value, () -> "spring.flyway." + key + " is not declared in application.yml");
|
||||||
|
return type.cast(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<String, Object> loadShippedFlywaySettings() {
|
||||||
|
try (InputStream stream = AbstractMigrationUpgradeTest.class.getClassLoader()
|
||||||
|
.getResourceAsStream("application.yml")) {
|
||||||
|
assertNotNull(stream, "application.yml must be on the test classpath");
|
||||||
|
for (Object document : new Yaml().loadAll(stream)) {
|
||||||
|
if (!(document instanceof Map<?, ?> root)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Object spring = root.get("spring");
|
||||||
|
if (spring instanceof Map<?, ?> springSection
|
||||||
|
&& springSection.get("flyway") instanceof Map<?, ?> flyway) {
|
||||||
|
return (Map<String, Object>) flyway;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("cannot read the shipped application.yml", e);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("application.yml declares no spring.flyway section");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPendingVersions(Flyway flyway, List<String> expected) {
|
||||||
|
List<String> pending = new ArrayList<>();
|
||||||
|
for (MigrationInfo info : flyway.info().pending()) {
|
||||||
|
pending.add(info.getVersion().getVersion());
|
||||||
|
}
|
||||||
|
assertEquals(expected, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSecondStartupChangesNothing() {
|
||||||
|
Flyway restart = flyway();
|
||||||
|
assertEquals(0, restart.info().pending().length, "a restart must have nothing left to apply");
|
||||||
|
MigrateResult result = restart.migrate();
|
||||||
|
assertEquals(0, result.migrationsExecuted, "a restart must not re-run any migration");
|
||||||
|
assertTrue(restart.validateWithResult().validationSuccessful,
|
||||||
|
"a restart must validate cleanly against the recorded history");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertHistoryChain() {
|
||||||
|
List<String> applied = new ArrayList<>();
|
||||||
|
for (MigrationInfo info : flyway().info().applied()) {
|
||||||
|
assertFalse(info.getState().isFailed(),
|
||||||
|
() -> "migration " + info.getVersion() + " is recorded as failed");
|
||||||
|
if (info.getVersion() != null) {
|
||||||
|
applied.add(info.getVersion().getVersion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(1, applied.stream().filter(CURRENT_VERSION::equals).count(),
|
||||||
|
() -> "V" + CURRENT_VERSION + " must be recorded exactly once, history is " + applied);
|
||||||
|
for (String withdrawn : WITHDRAWN_VERSIONS) {
|
||||||
|
assertTrue(applied.stream().noneMatch(withdrawn::equals),
|
||||||
|
() -> "withdrawn version V" + withdrawn + " must not appear in the history, history is " + applied);
|
||||||
|
}
|
||||||
|
assertEquals(CURRENT_VERSION, applied.get(applied.size() - 1),
|
||||||
|
() -> "the chain must end at V" + CURRENT_VERSION + ", history is " + applied);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedSchedules() throws SQLException {
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("INSERT INTO " + SOP_SCHEDULE_TABLE
|
||||||
|
+ " (conversation_id, sop_name, cron_expression, enabled, creator) VALUES (?, ?, ?, ?, ?)")) {
|
||||||
|
addSchedule(statement, 1L, "owned", "alice");
|
||||||
|
addSchedule(statement, 2L, "null-owner", null);
|
||||||
|
addSchedule(statement, 3L, "blank-owner", " ");
|
||||||
|
statement.executeBatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSchedule(PreparedStatement statement, long conversationId, String name, String creator)
|
||||||
|
throws SQLException {
|
||||||
|
statement.setLong(1, conversationId);
|
||||||
|
statement.setString(2, name);
|
||||||
|
statement.setString(3, "0 0 * * * ?");
|
||||||
|
statement.setBoolean(4, true);
|
||||||
|
statement.setString(5, creator);
|
||||||
|
statement.addBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertScheduleRowsScopedToOwner() throws SQLException {
|
||||||
|
Map<String, Boolean> enabledByName = new LinkedHashMap<>();
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
Statement statement = connection.createStatement();
|
||||||
|
ResultSet rs = statement.executeQuery("SELECT sop_name, enabled FROM " + SOP_SCHEDULE_TABLE)) {
|
||||||
|
while (rs.next()) {
|
||||||
|
enabledByName.put(rs.getString("sop_name"), rs.getBoolean("enabled"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(Boolean.TRUE, enabledByName.get("owned"), "a schedule with an owner must stay enabled");
|
||||||
|
assertEquals(Boolean.FALSE, enabledByName.get("null-owner"), "a schedule without an owner must be disabled");
|
||||||
|
assertEquals(Boolean.FALSE, enabledByName.get("blank-owner"), "a blank owner must count as no owner");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void seedAlertDefine() throws SQLException {
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement("INSERT INTO " + ALERT_DEFINE_TABLE
|
||||||
|
+ " (name, expr, enable) VALUES (?, ?, ?)")) {
|
||||||
|
statement.setString(1, "seeded");
|
||||||
|
statement.setString(2, SEEDED_EXPR);
|
||||||
|
statement.setBoolean(3, true);
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertSeededExprSurvived() throws SQLException {
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"SELECT expr FROM " + ALERT_DEFINE_TABLE + " WHERE name = ?")) {
|
||||||
|
statement.setString(1, "seeded");
|
||||||
|
try (ResultSet rs = statement.executeQuery()) {
|
||||||
|
assertTrue(rs.next(), "the alert define seeded before the upgrade must still be there");
|
||||||
|
assertEquals(SEEDED_EXPR, rs.getString("expr"), "widening expr must not rewrite existing rows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOversizedExprRoundTrips() throws SQLException {
|
||||||
|
String expr = "a".repeat(100_000);
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
PreparedStatement insert = connection.prepareStatement("INSERT INTO " + ALERT_DEFINE_TABLE
|
||||||
|
+ " (name, expr, enable) VALUES (?, ?, ?)")) {
|
||||||
|
insert.setString(1, "oversized");
|
||||||
|
insert.setString(2, expr);
|
||||||
|
insert.setBoolean(3, true);
|
||||||
|
insert.executeUpdate();
|
||||||
|
}
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
PreparedStatement select = connection.prepareStatement(
|
||||||
|
"SELECT expr FROM " + ALERT_DEFINE_TABLE + " WHERE name = ?")) {
|
||||||
|
select.setString(1, "oversized");
|
||||||
|
try (ResultSet rs = select.executeQuery()) {
|
||||||
|
assertTrue(rs.next(), "the oversized alert define must have been stored");
|
||||||
|
assertEquals(expr.length(), rs.getString("expr").length(),
|
||||||
|
"expr must hold an expression far beyond the pre-1.9.0 limit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertBooleanColumn() throws SQLException {
|
||||||
|
String actual = declaredType(SOP_SCHEDULE_TABLE, "enabled");
|
||||||
|
assertTrue(expectedBooleanTypes().contains(actual),
|
||||||
|
() -> "enabled must be a boolean column, but is declared " + actual
|
||||||
|
+ "; expected one of " + expectedBooleanTypes());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertNoStoredRoutinesLeftBehind() throws SQLException {
|
||||||
|
List<String> routines = storedRoutines();
|
||||||
|
assertTrue(routines.isEmpty(), () -> "migrations left stored routines behind: " + routines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertScheduleIndexes(List<List<String>> expected) throws SQLException {
|
||||||
|
Map<String, List<String>> indexes = indexColumns(SOP_SCHEDULE_TABLE);
|
||||||
|
List<List<String>> actual = indexes.values().stream()
|
||||||
|
.filter(columns -> !columns.equals(List.of("id")))
|
||||||
|
.toList();
|
||||||
|
for (List<String> signature : expected) {
|
||||||
|
assertEquals(1, actual.stream().filter(signature::equals).count(),
|
||||||
|
() -> "index on " + signature + " must exist exactly once, found " + indexes);
|
||||||
|
}
|
||||||
|
assertEquals(expected.size(), actual.size(), () -> "unexpected indexes on " + SOP_SCHEDULE_TABLE + ": " + indexes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, List<String>> indexColumns(String table) throws SQLException {
|
||||||
|
Map<String, List<String>> indexes = new LinkedHashMap<>();
|
||||||
|
try (Connection connection = dataSource().getConnection()) {
|
||||||
|
String[] identity = resolveTable(connection, table);
|
||||||
|
assertNotNull(identity, () -> "table " + table + " does not exist");
|
||||||
|
try (ResultSet rs = connection.getMetaData()
|
||||||
|
.getIndexInfo(identity[0], identity[1], identity[2], false, false)) {
|
||||||
|
while (rs.next()) {
|
||||||
|
String name = rs.getString("INDEX_NAME");
|
||||||
|
String column = rs.getString("COLUMN_NAME");
|
||||||
|
if (name == null || column == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
indexes.computeIfAbsent(name.toLowerCase(Locale.ROOT), key -> new ArrayList<>())
|
||||||
|
.add(column.toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indexes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the catalog, schema and stored spelling of a table. The lookup is pinned to the
|
||||||
|
* catalog and schema of the connection: MySQL Connector/J defaults {@code nullCatalogMeansCurrent}
|
||||||
|
* to false, so an unqualified lookup would happily match a table of the same name in another
|
||||||
|
* database on the same server.
|
||||||
|
*
|
||||||
|
* @param connection connection to the database under test
|
||||||
|
* @param table table name, in any case
|
||||||
|
* @return catalog, schema and table name, or {@code null} when the table does not exist
|
||||||
|
* @throws SQLException on metadata failure
|
||||||
|
*/
|
||||||
|
private String[] resolveTable(Connection connection, String table) throws SQLException {
|
||||||
|
String catalog = connection.getCatalog();
|
||||||
|
String schema = connection.getSchema();
|
||||||
|
try (ResultSet rs = connection.getMetaData().getTables(catalog, schema, "%", new String[]{"TABLE"})) {
|
||||||
|
while (rs.next()) {
|
||||||
|
if (table.equalsIgnoreCase(rs.getString("TABLE_NAME"))) {
|
||||||
|
return new String[]{rs.getString("TABLE_CAT"), rs.getString("TABLE_SCHEM"),
|
||||||
|
rs.getString("TABLE_NAME")};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a statement against the database under test.
|
||||||
|
*
|
||||||
|
* @param sql statement to run
|
||||||
|
* @throws SQLException on execution failure
|
||||||
|
*/
|
||||||
|
protected void execute(String sql) throws SQLException {
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a single value query against the database under test.
|
||||||
|
*
|
||||||
|
* @param sql query returning one column
|
||||||
|
* @param parameters query parameters
|
||||||
|
* @return the first column of the first row, lower cased, or {@code null} when there is no row
|
||||||
|
* @throws SQLException on execution failure
|
||||||
|
*/
|
||||||
|
protected String queryString(String sql, Object... parameters) throws SQLException {
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
for (int index = 0; index < parameters.length; index++) {
|
||||||
|
statement.setObject(index + 1, parameters[index]);
|
||||||
|
}
|
||||||
|
try (ResultSet rs = statement.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = rs.getString(1);
|
||||||
|
return value == null ? null : value.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a query collecting a single string column.
|
||||||
|
*
|
||||||
|
* @param sql query returning one column
|
||||||
|
* @return every value of the first column
|
||||||
|
* @throws SQLException on execution failure
|
||||||
|
*/
|
||||||
|
protected List<String> queryStrings(String sql) throws SQLException {
|
||||||
|
List<String> values = new ArrayList<>();
|
||||||
|
try (Connection connection = dataSource().getConnection();
|
||||||
|
Statement statement = connection.createStatement();
|
||||||
|
ResultSet rs = statement.executeQuery(sql)) {
|
||||||
|
while (rs.next()) {
|
||||||
|
values.add(rs.getString(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until the container database accepts JDBC connections. MySQL briefly listens while
|
||||||
|
* its entrypoint is still bootstrapping, so a single successful connect is not enough.
|
||||||
|
*
|
||||||
|
* @param url jdbc url
|
||||||
|
* @param username user name
|
||||||
|
* @param password password
|
||||||
|
*/
|
||||||
|
protected static void awaitDatabaseReady(String url, String username, String password) {
|
||||||
|
long deadline = System.currentTimeMillis() + Duration.ofMinutes(5).toMillis();
|
||||||
|
int consecutiveSuccesses = 0;
|
||||||
|
SQLException last = null;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
try (Connection connection = DriverManager.getConnection(url, username, password);
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("SELECT 1");
|
||||||
|
if (++consecutiveSuccesses == 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
last = e;
|
||||||
|
consecutiveSuccesses = 0;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000L);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("interrupted while waiting for the database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("timed out waiting for " + url, last);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
@@ -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.startup.migration;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the 1.9.0 migration upgrade regression on H2, the default embedded database.
|
||||||
|
*/
|
||||||
|
@DisplayName("1.9.0 migration upgrade on H2")
|
||||||
|
class H2MigrationUpgradeTest extends AbstractMigrationUpgradeTest {
|
||||||
|
|
||||||
|
private static final AtomicInteger DATABASE_SEQUENCE = new AtomicInteger();
|
||||||
|
|
||||||
|
private DriverManagerDataSource dataSource;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createDatabase() {
|
||||||
|
// MODE=MYSQL matches the URL the shipped application.yml uses.
|
||||||
|
dataSource = new DriverManagerDataSource(
|
||||||
|
"jdbc:h2:mem:migration_" + DATABASE_SEQUENCE.incrementAndGet() + ";MODE=MYSQL;DB_CLOSE_DELAY=-1",
|
||||||
|
"sa", "");
|
||||||
|
dataSource.setDriverClassName("org.h2.Driver");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void dropDatabase() throws SQLException {
|
||||||
|
execute("SHUTDOWN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DataSource dataSource() {
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String vendor() {
|
||||||
|
return "h2";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String declaredType(String table, String column) throws SQLException {
|
||||||
|
return queryString("SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS "
|
||||||
|
+ "WHERE TABLE_NAME = ? AND COLUMN_NAME = ?",
|
||||||
|
table.toUpperCase(Locale.ROOT), column.toUpperCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> expectedBooleanTypes() {
|
||||||
|
return Set.of("boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String expectedEnlargedTextType() {
|
||||||
|
return "character large object";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> storedRoutines() throws SQLException {
|
||||||
|
return queryStrings("SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'PUBLIC'");
|
||||||
|
}
|
||||||
|
}
|
||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* 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.startup.migration;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import java.io.File;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guards the shape of the Flyway migration chain shipped by the startup module.
|
||||||
|
* The chain is part of the release contract: 1.8.0 ended at V180 and 1.9.0 adds
|
||||||
|
* exactly one migration per dialect, V190.
|
||||||
|
*/
|
||||||
|
class MigrationInventoryTest {
|
||||||
|
|
||||||
|
private static final List<String> VENDORS = List.of("h2", "mysql", "postgresql");
|
||||||
|
|
||||||
|
private static final Pattern VERSIONED_SCRIPT = Pattern.compile("^V(\\d+)__.+\\.sql$");
|
||||||
|
|
||||||
|
private static final int LAST_RELEASED_VERSION = 180;
|
||||||
|
|
||||||
|
private static final int CURRENT_VERSION = 190;
|
||||||
|
|
||||||
|
private static final List<Integer> WITHDRAWN_VERSIONS = List.of(181, 182, 183);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("every dialect ends the chain with V180 followed by V190")
|
||||||
|
void chainEndsWithLastReleasedVersionThenCurrentVersion() throws URISyntaxException {
|
||||||
|
for (String vendor : VENDORS) {
|
||||||
|
List<Integer> versions = versionsOf(vendor);
|
||||||
|
|
||||||
|
assertTrue(versions.size() >= 2, () -> vendor + " must ship at least V180 and V190");
|
||||||
|
assertEquals(CURRENT_VERSION, versions.get(versions.size() - 1),
|
||||||
|
() -> vendor + " must end at V" + CURRENT_VERSION + " but ends at " + versions);
|
||||||
|
assertEquals(LAST_RELEASED_VERSION, versions.get(versions.size() - 2),
|
||||||
|
() -> vendor + " must apply V" + CURRENT_VERSION + " directly after the last released V"
|
||||||
|
+ LAST_RELEASED_VERSION + " but has " + versions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("the withdrawn pre-release versions V181, V182 and V183 are gone")
|
||||||
|
void withdrawnVersionsAreAbsent() throws URISyntaxException {
|
||||||
|
for (String vendor : VENDORS) {
|
||||||
|
List<Integer> versions = versionsOf(vendor);
|
||||||
|
for (Integer withdrawn : WITHDRAWN_VERSIONS) {
|
||||||
|
assertFalse(versions.contains(withdrawn),
|
||||||
|
() -> vendor + " still ships the withdrawn pre-release migration V" + withdrawn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("all dialects ship the same versions, each exactly once")
|
||||||
|
void allDialectsShipTheSameVersions() throws URISyntaxException {
|
||||||
|
Map<String, List<Integer>> byVendor = new LinkedHashMap<>();
|
||||||
|
for (String vendor : VENDORS) {
|
||||||
|
List<Integer> versions = versionsOf(vendor);
|
||||||
|
assertEquals(versions.size(), versions.stream().distinct().count(),
|
||||||
|
() -> vendor + " declares a version more than once: " + versions);
|
||||||
|
byVendor.put(vendor, versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Integer> reference = byVendor.get(VENDORS.get(0));
|
||||||
|
for (Map.Entry<String, List<Integer>> entry : byVendor.entrySet()) {
|
||||||
|
assertEquals(reference, entry.getValue(),
|
||||||
|
() -> entry.getKey() + " diverges from " + VENDORS.get(0) + ": "
|
||||||
|
+ entry.getValue() + " vs " + reference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the migration versions of one dialect from the packaged classpath location,
|
||||||
|
* which is exactly what {@code spring.flyway.locations} resolves at runtime.
|
||||||
|
*
|
||||||
|
* @param vendor flyway vendor directory name
|
||||||
|
* @return the versions in ascending order
|
||||||
|
* @throws URISyntaxException when the classpath location cannot be resolved to a directory
|
||||||
|
*/
|
||||||
|
private static List<Integer> versionsOf(String vendor) throws URISyntaxException {
|
||||||
|
URL location = MigrationInventoryTest.class.getClassLoader().getResource("db/migration/" + vendor);
|
||||||
|
assertNotNull(location, () -> "missing migration location db/migration/" + vendor);
|
||||||
|
|
||||||
|
File[] files = new File(location.toURI()).listFiles();
|
||||||
|
assertNotNull(files, () -> "db/migration/" + vendor + " is not a readable directory");
|
||||||
|
|
||||||
|
List<Integer> versions = new ArrayList<>();
|
||||||
|
for (File file : files) {
|
||||||
|
Matcher matcher = VERSIONED_SCRIPT.matcher(file.getName());
|
||||||
|
if (matcher.matches()) {
|
||||||
|
versions.add(Integer.valueOf(matcher.group(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
versions.sort(Integer::compareTo);
|
||||||
|
return List.copyOf(versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("no dialect directory holds an unversioned or misnamed script")
|
||||||
|
void everyScriptFollowsTheVersionedNaming() throws URISyntaxException {
|
||||||
|
for (String vendor : VENDORS) {
|
||||||
|
URL location = MigrationInventoryTest.class.getClassLoader().getResource("db/migration/" + vendor);
|
||||||
|
assertNotNull(location, () -> "missing migration location db/migration/" + vendor);
|
||||||
|
File[] files = new File(location.toURI()).listFiles();
|
||||||
|
assertNotNull(files, () -> "db/migration/" + vendor + " is not a readable directory");
|
||||||
|
|
||||||
|
List<String> unexpected = Arrays.stream(files)
|
||||||
|
.map(File::getName)
|
||||||
|
.filter(name -> !VERSIONED_SCRIPT.matcher(name).matches())
|
||||||
|
.toList();
|
||||||
|
assertTrue(unexpected.isEmpty(),
|
||||||
|
() -> "db/migration/" + vendor + " holds non-migration files: " + unexpected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -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.startup.migration;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||||
|
import org.testcontainers.DockerClientFactory;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.containers.wait.strategy.Wait;
|
||||||
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the 1.9.0 migration upgrade regression on MySQL, where the pre-release scripts
|
||||||
|
* failed with {@code 1061 Duplicate key name}. Requires a Docker daemon, which the
|
||||||
|
* backend CI runner provides.
|
||||||
|
*/
|
||||||
|
@DisplayName("1.9.0 migration upgrade on MySQL")
|
||||||
|
class MysqlMigrationUpgradeTest extends AbstractMigrationUpgradeTest {
|
||||||
|
|
||||||
|
private static final DockerImageName IMAGE = DockerImageName.parse("mysql:8.4");
|
||||||
|
|
||||||
|
private static final String ROOT_USER = "root";
|
||||||
|
|
||||||
|
private static final String ROOT_PASSWORD = "root123";
|
||||||
|
|
||||||
|
private static final int MYSQL_PORT = 3306;
|
||||||
|
|
||||||
|
private static final AtomicInteger DATABASE_SEQUENCE = new AtomicInteger();
|
||||||
|
|
||||||
|
private static GenericContainer<?> container;
|
||||||
|
|
||||||
|
private DriverManagerDataSource dataSource;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void startContainer() {
|
||||||
|
assumeTrue(DockerClientFactory.instance().isDockerAvailable(),
|
||||||
|
"a Docker daemon is required to verify the MySQL migrations");
|
||||||
|
container = new GenericContainer<>(IMAGE)
|
||||||
|
.withExposedPorts(MYSQL_PORT)
|
||||||
|
.withEnv("MYSQL_ROOT_PASSWORD", ROOT_PASSWORD)
|
||||||
|
.waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(5)));
|
||||||
|
container.start();
|
||||||
|
awaitDatabaseReady(jdbcUrl("mysql"), ROOT_USER, ROOT_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void stopContainer() {
|
||||||
|
if (container != null) {
|
||||||
|
container.stop();
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createDatabase() throws SQLException {
|
||||||
|
String database = "hertzbeat_migration_" + DATABASE_SEQUENCE.incrementAndGet();
|
||||||
|
try (Connection connection = DriverManager.getConnection(jdbcUrl("mysql"), ROOT_USER, ROOT_PASSWORD);
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("CREATE DATABASE " + database);
|
||||||
|
}
|
||||||
|
dataSource = new DriverManagerDataSource(jdbcUrl(database), ROOT_USER, ROOT_PASSWORD);
|
||||||
|
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jdbcUrl(String database) {
|
||||||
|
return "jdbc:mysql://" + container.getHost() + ":" + container.getMappedPort(MYSQL_PORT) + "/" + database
|
||||||
|
+ "?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DataSource dataSource() {
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String vendor() {
|
||||||
|
return "mysql";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String declaredType(String table, String column) throws SQLException {
|
||||||
|
return queryString("SELECT COLUMN_TYPE FROM INFORMATION_SCHEMA.COLUMNS "
|
||||||
|
+ "WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?", table, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> expectedBooleanTypes() {
|
||||||
|
// Hibernate declares bit(1); the migration's BOOLEAN lands as tinyint(1).
|
||||||
|
return Set.of("tinyint(1)", "bit(1)");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String expectedEnlargedTextType() {
|
||||||
|
return "longtext";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> storedRoutines() throws SQLException {
|
||||||
|
return queryStrings("SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE()");
|
||||||
|
}
|
||||||
|
}
|
||||||
+130
@@ -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.startup.migration;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import org.junit.jupiter.api.AfterAll;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||||
|
import org.testcontainers.DockerClientFactory;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.containers.wait.strategy.Wait;
|
||||||
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the 1.9.0 migration upgrade regression on PostgreSQL, where the pre-release scripts
|
||||||
|
* failed with {@code 42804 column "enabled" is of type boolean but expression is of type
|
||||||
|
* integer}. Requires a Docker daemon, which the backend CI runner provides.
|
||||||
|
*/
|
||||||
|
@DisplayName("1.9.0 migration upgrade on PostgreSQL")
|
||||||
|
class PostgresqlMigrationUpgradeTest extends AbstractMigrationUpgradeTest {
|
||||||
|
|
||||||
|
private static final DockerImageName IMAGE = DockerImageName.parse("postgres:17-alpine");
|
||||||
|
|
||||||
|
private static final String USERNAME = "postgres";
|
||||||
|
|
||||||
|
private static final String PASSWORD = "postgres123";
|
||||||
|
|
||||||
|
private static final int POSTGRESQL_PORT = 5432;
|
||||||
|
|
||||||
|
private static final AtomicInteger DATABASE_SEQUENCE = new AtomicInteger();
|
||||||
|
|
||||||
|
private static GenericContainer<?> container;
|
||||||
|
|
||||||
|
private DriverManagerDataSource dataSource;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void startContainer() {
|
||||||
|
assumeTrue(DockerClientFactory.instance().isDockerAvailable(),
|
||||||
|
"a Docker daemon is required to verify the PostgreSQL migrations");
|
||||||
|
container = new GenericContainer<>(IMAGE)
|
||||||
|
.withExposedPorts(POSTGRESQL_PORT)
|
||||||
|
.withEnv("POSTGRES_PASSWORD", PASSWORD)
|
||||||
|
.waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(5)));
|
||||||
|
container.start();
|
||||||
|
awaitDatabaseReady(jdbcUrl("postgres"), USERNAME, PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterAll
|
||||||
|
static void stopContainer() {
|
||||||
|
if (container != null) {
|
||||||
|
container.stop();
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createDatabase() throws SQLException {
|
||||||
|
String database = "hertzbeat_migration_" + DATABASE_SEQUENCE.incrementAndGet();
|
||||||
|
try (Connection connection = DriverManager.getConnection(jdbcUrl("postgres"), USERNAME, PASSWORD);
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
statement.execute("CREATE DATABASE " + database);
|
||||||
|
}
|
||||||
|
dataSource = new DriverManagerDataSource(jdbcUrl(database), USERNAME, PASSWORD);
|
||||||
|
dataSource.setDriverClassName("org.postgresql.Driver");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String jdbcUrl(String database) {
|
||||||
|
return "jdbc:postgresql://" + container.getHost() + ":" + container.getMappedPort(POSTGRESQL_PORT)
|
||||||
|
+ "/" + database;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected DataSource dataSource() {
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String vendor() {
|
||||||
|
return "postgresql";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String declaredType(String table, String column) throws SQLException {
|
||||||
|
return queryString("SELECT udt_name FROM information_schema.columns "
|
||||||
|
+ "WHERE table_schema = current_schema() AND table_name = ? AND column_name = ?", table, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Set<String> expectedBooleanTypes() {
|
||||||
|
return Set.of("bool");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String expectedEnlargedTextType() {
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> storedRoutines() throws SQLException {
|
||||||
|
return queryStrings("SELECT routine_name FROM information_schema.routines "
|
||||||
|
+ "WHERE routine_schema = current_schema()");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user