2 changed files with 445 additions and 0 deletions
@ -0,0 +1,111 @@ |
|||||
|
-- Flyway V35: 行业技能词云知识库表结构 |
||||
|
-- 来源与许可:ESCO v1.2.1 提供职位/技能/分类关系,O*NET 31.0 提供 Importance。 |
||||
|
-- 本迁移只建立结构,代表性种子数据由 V36 导入。 |
||||
|
|
||||
|
CREATE TABLE skill_source_versions ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
source_name VARCHAR(100) NOT NULL COMMENT '来源名称:ESCO/O*NET', |
||||
|
source_version VARCHAR(150) NOT NULL COMMENT '固定来源版本', |
||||
|
license VARCHAR(255) NOT NULL COMMENT '来源许可证或复用条件', |
||||
|
attribution_url VARCHAR(500) NOT NULL COMMENT '官方来源或归属地址', |
||||
|
imported_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '导入时间', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_skill_source_versions_source UNIQUE (source_name, source_version) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='技能词库来源版本'; |
||||
|
|
||||
|
CREATE TABLE skill_industries ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
code VARCHAR(80) NOT NULL COMMENT '项目行业稳定编码', |
||||
|
name_zh VARCHAR(120) NOT NULL COMMENT '中文行业名称', |
||||
|
source_code VARCHAR(120) COMMENT '上游或项目映射来源编码', |
||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_skill_industries_code UNIQUE (code) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='技能词云行业'; |
||||
|
|
||||
|
CREATE TABLE skill_occupations ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
industry_id BIGINT NOT NULL COMMENT '所属行业', |
||||
|
code VARCHAR(100) NOT NULL COMMENT '职位稳定编码', |
||||
|
name_zh VARCHAR(150) NOT NULL COMMENT '中文职位名称', |
||||
|
name_en VARCHAR(180) COMMENT '英文职位名称', |
||||
|
source_code VARCHAR(150) NOT NULL COMMENT 'ESCO 职位来源 ID', |
||||
|
source_version_id BIGINT NOT NULL COMMENT '来源版本', |
||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_skill_occupations_industry_code UNIQUE (industry_id, code), |
||||
|
CONSTRAINT fk_skill_occupations_industry FOREIGN KEY (industry_id) REFERENCES skill_industries (id), |
||||
|
CONSTRAINT fk_skill_occupations_source_version FOREIGN KEY (source_version_id) REFERENCES skill_source_versions (id) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='技能词云职位'; |
||||
|
|
||||
|
CREATE TABLE skill_categories ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
occupation_id BIGINT NOT NULL COMMENT '所属职位', |
||||
|
code VARCHAR(100) NOT NULL COMMENT '技能分类稳定编码', |
||||
|
name_zh VARCHAR(120) NOT NULL COMMENT '中文分类名称', |
||||
|
suggestion_template_code VARCHAR(30) NOT NULL COMMENT '学习建议模板编码', |
||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_skill_categories_occupation_code UNIQUE (occupation_id, code), |
||||
|
CONSTRAINT ck_skill_categories_suggestion_template CHECK ( |
||||
|
suggestion_template_code IN ('PROGRAMMING_LANGUAGE', 'FRAMEWORK', 'TOOL', 'GENERAL') |
||||
|
), |
||||
|
CONSTRAINT fk_skill_categories_occupation FOREIGN KEY (occupation_id) REFERENCES skill_occupations (id) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职位技能分类'; |
||||
|
|
||||
|
CREATE TABLE skills ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
code VARCHAR(120) NOT NULL COMMENT '项目技能稳定编码', |
||||
|
name_zh VARCHAR(180) NOT NULL COMMENT '中文标准技能名称', |
||||
|
name_en VARCHAR(180) COMMENT '英文标准技能名称', |
||||
|
source_type VARCHAR(20) NOT NULL COMMENT '来源类型', |
||||
|
source_code VARCHAR(180) NOT NULL COMMENT 'ESCO/O*NET 或项目来源 ID', |
||||
|
source_version_id BIGINT NOT NULL COMMENT '来源版本', |
||||
|
sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_skills_code UNIQUE (code), |
||||
|
CONSTRAINT uk_skills_source UNIQUE (source_type, source_code), |
||||
|
CONSTRAINT ck_skills_source_type CHECK (source_type IN ('ESCO', 'O*NET', 'PROJECT')), |
||||
|
CONSTRAINT fk_skills_source_version FOREIGN KEY (source_version_id) REFERENCES skill_source_versions (id) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标准技能'; |
||||
|
|
||||
|
CREATE TABLE skill_aliases ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
skill_id BIGINT NOT NULL COMMENT '标准技能', |
||||
|
alias VARCHAR(180) NOT NULL COMMENT '技能别名', |
||||
|
normalized_alias VARCHAR(180) NOT NULL COMMENT '归一化技能别名', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_skill_aliases_normalized_alias UNIQUE (normalized_alias), |
||||
|
CONSTRAINT uk_skill_aliases_skill_alias UNIQUE (skill_id, alias), |
||||
|
CONSTRAINT fk_skill_aliases_skill FOREIGN KEY (skill_id) REFERENCES skills (id) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='标准技能别名'; |
||||
|
|
||||
|
CREATE TABLE occupation_skills ( |
||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY, |
||||
|
occupation_id BIGINT NOT NULL COMMENT '职位', |
||||
|
skill_id BIGINT NOT NULL COMMENT '标准技能', |
||||
|
category_id BIGINT NOT NULL COMMENT '职位内技能分类', |
||||
|
importance DECIMAL(4,2) COMMENT 'O*NET Importance,0-5,允许为空', |
||||
|
relationship_type VARCHAR(20) NOT NULL COMMENT '职位技能关系类型', |
||||
|
source_version_id BIGINT NOT NULL COMMENT 'Importance 来源版本', |
||||
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
||||
|
CONSTRAINT uk_occupation_skills_occupation_skill UNIQUE (occupation_id, skill_id), |
||||
|
CONSTRAINT ck_occupation_skills_importance CHECK ( |
||||
|
importance IS NULL OR (importance >= 0.00 AND importance <= 5.00) |
||||
|
), |
||||
|
CONSTRAINT ck_occupation_skills_relationship_type CHECK ( |
||||
|
relationship_type IN ('ESSENTIAL', 'OPTIONAL', 'PROJECT') |
||||
|
), |
||||
|
CONSTRAINT fk_occupation_skills_occupation FOREIGN KEY (occupation_id) REFERENCES skill_occupations (id), |
||||
|
CONSTRAINT fk_occupation_skills_skill FOREIGN KEY (skill_id) REFERENCES skills (id), |
||||
|
CONSTRAINT fk_occupation_skills_category FOREIGN KEY (category_id) REFERENCES skill_categories (id), |
||||
|
CONSTRAINT fk_occupation_skills_source_version FOREIGN KEY (source_version_id) REFERENCES skill_source_versions (id), |
||||
|
KEY idx_occupation_skills_occupation_category (occupation_id, category_id), |
||||
|
KEY idx_occupation_skills_occupation_importance (occupation_id, importance) |
||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='职位技能关系与热度'; |
||||
@ -0,0 +1,334 @@ |
|||||
|
package com.mydreamjob.skill; |
||||
|
|
||||
|
import org.junit.jupiter.api.AfterEach; |
||||
|
import org.junit.jupiter.api.Test; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.boot.test.context.SpringBootTest; |
||||
|
import org.springframework.boot.test.mock.mockito.MockBean; |
||||
|
import org.springframework.dao.DataAccessException; |
||||
|
import org.springframework.data.redis.core.RedisTemplate; |
||||
|
import org.springframework.jdbc.core.JdbcTemplate; |
||||
|
import org.springframework.test.context.ActiveProfiles; |
||||
|
import org.springframework.test.context.TestPropertySource; |
||||
|
|
||||
|
import javax.sql.DataSource; |
||||
|
import java.math.BigDecimal; |
||||
|
import java.sql.Connection; |
||||
|
import java.sql.DatabaseMetaData; |
||||
|
import java.sql.ResultSet; |
||||
|
import java.sql.SQLException; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.LinkedHashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.TreeMap; |
||||
|
|
||||
|
import static org.junit.jupiter.api.Assertions.assertEquals; |
||||
|
import static org.junit.jupiter.api.Assertions.assertNull; |
||||
|
import static org.junit.jupiter.api.Assertions.assertThrows; |
||||
|
import static org.junit.jupiter.api.Assertions.assertTrue; |
||||
|
|
||||
|
@SpringBootTest |
||||
|
@TestPropertySource(properties = |
||||
|
"spring.datasource.url=jdbc:h2:mem:integrationdb_skill_taxonomy;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") |
||||
|
@ActiveProfiles("integration-test") |
||||
|
class SkillTaxonomyMigrationTest { |
||||
|
|
||||
|
@Autowired |
||||
|
private JdbcTemplate jdbcTemplate; |
||||
|
|
||||
|
@Autowired |
||||
|
private DataSource dataSource; |
||||
|
|
||||
|
@MockBean |
||||
|
private RedisTemplate<String, Object> redisTemplate; |
||||
|
|
||||
|
private Long sourceVersionId; |
||||
|
private Long industryId; |
||||
|
private Long occupationId; |
||||
|
private Long categoryId; |
||||
|
private Long skillId; |
||||
|
private Long secondSkillId; |
||||
|
private Long occupationSkillId; |
||||
|
|
||||
|
@Test |
||||
|
void migrationCreatesTaxonomyTablesColumnsConstraintsAndIndexes() throws SQLException { |
||||
|
assertTableAndColumns("skill_source_versions", |
||||
|
"id", "source_name", "source_version", "license", "attribution_url", |
||||
|
"imported_at", "created_at", "updated_at"); |
||||
|
assertTableAndColumns("skill_industries", |
||||
|
"id", "code", "name_zh", "source_code", "sort_order", "created_at", "updated_at"); |
||||
|
assertTableAndColumns("skill_occupations", |
||||
|
"id", "industry_id", "code", "name_zh", "name_en", "source_code", |
||||
|
"source_version_id", "sort_order", "created_at", "updated_at"); |
||||
|
assertTableAndColumns("skill_categories", |
||||
|
"id", "occupation_id", "code", "name_zh", "suggestion_template_code", |
||||
|
"sort_order", "created_at", "updated_at"); |
||||
|
assertTableAndColumns("skills", |
||||
|
"id", "code", "name_zh", "name_en", "source_type", "source_code", |
||||
|
"source_version_id", "sort_order", "created_at", "updated_at"); |
||||
|
assertTableAndColumns("skill_aliases", |
||||
|
"id", "skill_id", "alias", "normalized_alias", "created_at"); |
||||
|
assertTableAndColumns("occupation_skills", |
||||
|
"id", "occupation_id", "skill_id", "category_id", "importance", |
||||
|
"relationship_type", "source_version_id", "created_at", "updated_at"); |
||||
|
|
||||
|
assertUniqueIndex("skill_source_versions", "source_name", "source_version"); |
||||
|
assertUniqueIndex("skill_industries", "code"); |
||||
|
assertUniqueIndex("skill_occupations", "industry_id", "code"); |
||||
|
assertUniqueIndex("skill_categories", "occupation_id", "code"); |
||||
|
assertUniqueIndex("skills", "code"); |
||||
|
assertUniqueIndex("skills", "source_type", "source_code"); |
||||
|
assertUniqueIndex("skill_aliases", "normalized_alias"); |
||||
|
assertUniqueIndex("skill_aliases", "skill_id", "alias"); |
||||
|
assertUniqueIndex("occupation_skills", "occupation_id", "skill_id"); |
||||
|
|
||||
|
assertForeignKey("skill_occupations", "industry_id", "skill_industries", "id"); |
||||
|
assertForeignKey("skill_occupations", "source_version_id", "skill_source_versions", "id"); |
||||
|
assertForeignKey("skill_categories", "occupation_id", "skill_occupations", "id"); |
||||
|
assertForeignKey("skills", "source_version_id", "skill_source_versions", "id"); |
||||
|
assertForeignKey("skill_aliases", "skill_id", "skills", "id"); |
||||
|
assertForeignKey("occupation_skills", "occupation_id", "skill_occupations", "id"); |
||||
|
assertForeignKey("occupation_skills", "skill_id", "skills", "id"); |
||||
|
assertForeignKey("occupation_skills", "category_id", "skill_categories", "id"); |
||||
|
assertForeignKey("occupation_skills", "source_version_id", "skill_source_versions", "id"); |
||||
|
|
||||
|
assertIndex("occupation_skills", "occupation_id", "category_id"); |
||||
|
assertIndex("occupation_skills", "occupation_id", "importance"); |
||||
|
} |
||||
|
|
||||
|
@Test |
||||
|
void minimumTaxonomyChainIsUsableAndCoversCoreConstraints() { |
||||
|
sourceVersionId = insertAndReturnId( |
||||
|
"INSERT INTO skill_source_versions " + |
||||
|
"(source_name, source_version, license, attribution_url, imported_at) " + |
||||
|
"VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)", |
||||
|
"ESCO", "test-1.2.1", "Commission Decision 2011/833/EU", |
||||
|
"https://esco.ec.europa.eu/en/use-esco/download"); |
||||
|
industryId = insertAndReturnId( |
||||
|
"INSERT INTO skill_industries (code, name_zh, source_code, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?)", |
||||
|
"TEST_INTERNET", "互联网", "PROJECT-INTERNET", 1); |
||||
|
occupationId = insertAndReturnId( |
||||
|
"INSERT INTO skill_occupations " + |
||||
|
"(industry_id, code, name_zh, name_en, source_code, source_version_id, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)", |
||||
|
industryId, "TEST_FRONTEND", "前端开发工程师", "Frontend Developer", |
||||
|
"ESCO-2513.2", sourceVersionId, 1); |
||||
|
categoryId = insertAndReturnId( |
||||
|
"INSERT INTO skill_categories " + |
||||
|
"(occupation_id, code, name_zh, suggestion_template_code, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?)", |
||||
|
occupationId, "TEST_PROGRAMMING_LANGUAGE", "编程语言", |
||||
|
"PROGRAMMING_LANGUAGE", 1); |
||||
|
skillId = insertAndReturnId( |
||||
|
"INSERT INTO skills " + |
||||
|
"(code, name_zh, name_en, source_type, source_code, source_version_id, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?, ?, ?)", |
||||
|
"TEST_SKILL_JAVA", "Java", "Java", "ESCO", "ESCO-SKILL-JAVA", |
||||
|
sourceVersionId, 1); |
||||
|
insertAndReturnId( |
||||
|
"INSERT INTO skill_aliases (skill_id, alias, normalized_alias) VALUES (?, ?, ?)", |
||||
|
skillId, "Java SE", "java se"); |
||||
|
occupationSkillId = insertAndReturnId( |
||||
|
"INSERT INTO occupation_skills " + |
||||
|
"(occupation_id, skill_id, category_id, importance, relationship_type, source_version_id) " + |
||||
|
"VALUES (?, ?, ?, ?, ?, ?)", |
||||
|
occupationId, skillId, categoryId, null, "ESSENTIAL", sourceVersionId); |
||||
|
|
||||
|
Map<String, Object> joined = jdbcTemplate.queryForMap( |
||||
|
"SELECT s.name_zh AS skill_name, c.name_zh AS category_name, os.importance " + |
||||
|
"FROM occupation_skills os " + |
||||
|
"JOIN skills s ON s.id = os.skill_id " + |
||||
|
"JOIN skill_categories c ON c.id = os.category_id " + |
||||
|
"WHERE os.id = ?", occupationSkillId); |
||||
|
assertEquals("Java", joined.get("skill_name")); |
||||
|
assertEquals("编程语言", joined.get("category_name")); |
||||
|
assertNull(joined.get("importance")); |
||||
|
|
||||
|
jdbcTemplate.update("UPDATE occupation_skills SET importance = ? WHERE id = ?", |
||||
|
new BigDecimal("4.25"), occupationSkillId); |
||||
|
assertEquals(new BigDecimal("4.25"), jdbcTemplate.queryForObject( |
||||
|
"SELECT importance FROM occupation_skills WHERE id = ?", |
||||
|
BigDecimal.class, occupationSkillId)); |
||||
|
|
||||
|
assertConstraintFailure( |
||||
|
"INSERT INTO skill_source_versions " + |
||||
|
"(source_name, source_version, license, attribution_url) VALUES (?, ?, ?, ?)", |
||||
|
"ESCO", "test-1.2.1", "duplicate", "https://example.invalid"); |
||||
|
assertConstraintFailure( |
||||
|
"INSERT INTO skill_categories " + |
||||
|
"(occupation_id, code, name_zh, suggestion_template_code, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?)", |
||||
|
occupationId, "TEST_INVALID_CATEGORY", "非法分类", "INVALID_TEMPLATE", 2); |
||||
|
assertConstraintFailure( |
||||
|
"INSERT INTO skills " + |
||||
|
"(code, name_zh, source_type, source_code, source_version_id, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?, ?)", |
||||
|
"TEST_INVALID_SKILL", "非法技能", "INVALID_SOURCE", "INVALID-SOURCE-1", |
||||
|
sourceVersionId, 2); |
||||
|
secondSkillId = insertAndReturnId( |
||||
|
"INSERT INTO skills " + |
||||
|
"(code, name_zh, source_type, source_code, source_version_id, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?, ?)", |
||||
|
"TEST_SKILL_SPRING", "Spring Boot", "ESCO", "ESCO-SKILL-SPRING", |
||||
|
sourceVersionId, 2); |
||||
|
assertConstraintFailure( |
||||
|
"INSERT INTO skills " + |
||||
|
"(code, name_zh, source_type, source_code, source_version_id, sort_order) " + |
||||
|
"VALUES (?, ?, ?, ?, ?, ?)", |
||||
|
"TEST_SKILL_NULL_NAME", null, "PROJECT", "PROJECT-NULL-NAME", |
||||
|
sourceVersionId, 3); |
||||
|
assertConstraintFailure( |
||||
|
"INSERT INTO occupation_skills " + |
||||
|
"(occupation_id, skill_id, category_id, relationship_type, source_version_id) " + |
||||
|
"VALUES (?, ?, ?, ?, ?)", |
||||
|
occupationId, secondSkillId, categoryId, "INVALID_RELATIONSHIP", sourceVersionId); |
||||
|
assertConstraintFailure( |
||||
|
"INSERT INTO skill_aliases (skill_id, alias, normalized_alias) VALUES (?, ?, ?)", |
||||
|
secondSkillId, "Java SE alternate", "java se"); |
||||
|
|
||||
|
assertTrue(jdbcTemplate.queryForObject( |
||||
|
"SELECT COUNT(*) FROM skill_aliases WHERE skill_id = ? AND normalized_alias = ?", |
||||
|
Integer.class, skillId, "java se") == 1); |
||||
|
} |
||||
|
|
||||
|
@AfterEach |
||||
|
void cleanUp() { |
||||
|
if (occupationSkillId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM occupation_skills WHERE id = ?", occupationSkillId); |
||||
|
} |
||||
|
if (secondSkillId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM skill_aliases WHERE skill_id = ?", secondSkillId); |
||||
|
jdbcTemplate.update("DELETE FROM skills WHERE id = ?", secondSkillId); |
||||
|
} |
||||
|
if (skillId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM skill_aliases WHERE skill_id = ?", skillId); |
||||
|
jdbcTemplate.update("DELETE FROM skills WHERE id = ?", skillId); |
||||
|
} |
||||
|
if (categoryId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM skill_categories WHERE id = ?", categoryId); |
||||
|
} |
||||
|
if (occupationId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM skill_occupations WHERE id = ?", occupationId); |
||||
|
} |
||||
|
if (industryId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM skill_industries WHERE id = ?", industryId); |
||||
|
} |
||||
|
if (sourceVersionId != null) { |
||||
|
jdbcTemplate.update("DELETE FROM skill_source_versions WHERE id = ?", sourceVersionId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void assertTableAndColumns(String table, String... columns) throws SQLException { |
||||
|
try (Connection connection = dataSource.getConnection()) { |
||||
|
DatabaseMetaData metadata = connection.getMetaData(); |
||||
|
String actualTable = findTable(metadata, table); |
||||
|
assertTrue(actualTable != null, "Missing table: " + table); |
||||
|
|
||||
|
List<String> actualColumns = new ArrayList<>(); |
||||
|
try (ResultSet resultSet = metadata.getColumns(null, null, actualTable, null)) { |
||||
|
while (resultSet.next()) { |
||||
|
actualColumns.add(resultSet.getString("COLUMN_NAME").toLowerCase()); |
||||
|
} |
||||
|
} |
||||
|
for (String column : columns) { |
||||
|
assertTrue(actualColumns.contains(column.toLowerCase()), |
||||
|
"Missing column " + table + "." + column + "; actual=" + actualColumns); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void assertUniqueIndex(String table, String... columns) throws SQLException { |
||||
|
assertTrue(hasIndex(table, true, columns), |
||||
|
"Missing unique index on " + table + "(" + String.join(", ", columns) + ")"); |
||||
|
} |
||||
|
|
||||
|
private void assertIndex(String table, String... columns) throws SQLException { |
||||
|
assertTrue(hasIndex(table, false, columns), |
||||
|
"Missing index on " + table + "(" + String.join(", ", columns) + ")"); |
||||
|
} |
||||
|
|
||||
|
private boolean hasIndex(String table, boolean unique, String... expectedColumns) throws SQLException { |
||||
|
try (Connection connection = dataSource.getConnection()) { |
||||
|
DatabaseMetaData metadata = connection.getMetaData(); |
||||
|
String actualTable = findTable(metadata, table); |
||||
|
if (actualTable == null) { |
||||
|
return false; |
||||
|
} |
||||
|
Map<String, TreeMap<Short, String>> indexes = new LinkedHashMap<>(); |
||||
|
try (ResultSet resultSet = metadata.getIndexInfo(null, null, actualTable, false, false)) { |
||||
|
while (resultSet.next()) { |
||||
|
String indexName = resultSet.getString("INDEX_NAME"); |
||||
|
if (indexName == null || resultSet.getBoolean("NON_UNIQUE") == unique) { |
||||
|
continue; |
||||
|
} |
||||
|
indexes.computeIfAbsent(indexName, ignored -> new TreeMap<>()) |
||||
|
.put(resultSet.getShort("ORDINAL_POSITION"), |
||||
|
resultSet.getString("COLUMN_NAME").toLowerCase()); |
||||
|
} |
||||
|
} |
||||
|
List<String> expected = new ArrayList<>(); |
||||
|
for (String column : expectedColumns) { |
||||
|
expected.add(column.toLowerCase()); |
||||
|
} |
||||
|
return indexes.values().stream() |
||||
|
.map(TreeMap::values) |
||||
|
.map(values -> new ArrayList<>(values)) |
||||
|
.anyMatch(expected::equals); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void assertForeignKey(String childTable, String childColumn, |
||||
|
String parentTable, String parentColumn) throws SQLException { |
||||
|
try (Connection connection = dataSource.getConnection()) { |
||||
|
DatabaseMetaData metadata = connection.getMetaData(); |
||||
|
String actualChildTable = findTable(metadata, childTable); |
||||
|
String actualParentTable = findTable(metadata, parentTable); |
||||
|
if (actualChildTable == null || actualParentTable == null) { |
||||
|
throw new AssertionError("Missing table for foreign key " + childTable + "." + childColumn); |
||||
|
} |
||||
|
try (ResultSet resultSet = metadata.getImportedKeys(null, null, actualChildTable)) { |
||||
|
while (resultSet.next()) { |
||||
|
if (actualParentTable.equalsIgnoreCase(resultSet.getString("PKTABLE_NAME")) |
||||
|
&& childColumn.equalsIgnoreCase(resultSet.getString("FKCOLUMN_NAME")) |
||||
|
&& parentColumn.equalsIgnoreCase(resultSet.getString("PKCOLUMN_NAME"))) { |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
throw new AssertionError("Missing foreign key " + childTable + "." + childColumn |
||||
|
+ " -> " + parentTable + "." + parentColumn); |
||||
|
} |
||||
|
|
||||
|
private String findTable(DatabaseMetaData metadata, String expected) throws SQLException { |
||||
|
try (ResultSet resultSet = metadata.getTables(null, null, null, new String[]{"TABLE"})) { |
||||
|
while (resultSet.next()) { |
||||
|
String actual = resultSet.getString("TABLE_NAME"); |
||||
|
if (expected.equalsIgnoreCase(actual)) { |
||||
|
return actual; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private Long insertAndReturnId(String sql, Object... args) { |
||||
|
jdbcTemplate.update(sql, args); |
||||
|
String table = extractTable(sql); |
||||
|
return jdbcTemplate.queryForObject("SELECT MAX(id) FROM " + table, Long.class); |
||||
|
} |
||||
|
|
||||
|
private String extractTable(String sql) { |
||||
|
String upper = sql.toUpperCase(); |
||||
|
int fromIndex = upper.indexOf(" INTO "); |
||||
|
int openIndex = sql.indexOf('(', fromIndex); |
||||
|
return sql.substring(fromIndex + " INTO ".length(), openIndex).trim(); |
||||
|
} |
||||
|
|
||||
|
private void assertConstraintFailure(String sql, Object... args) { |
||||
|
assertThrows(DataAccessException.class, () -> jdbcTemplate.update(sql, args)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
Loading…
Reference in new issue