diff --git a/mydreamjob-server/src/main/resources/db/migration/V35__skill_cloud_taxonomy.sql b/mydreamjob-server/src/main/resources/db/migration/V35__skill_cloud_taxonomy.sql index 7c0e3fd..db81f93 100644 --- a/mydreamjob-server/src/main/resources/db/migration/V35__skill_cloud_taxonomy.sql +++ b/mydreamjob-server/src/main/resources/db/migration/V35__skill_cloud_taxonomy.sql @@ -91,6 +91,9 @@ CREATE TABLE occupation_skills ( skill_id BIGINT NOT NULL COMMENT '标准技能', category_id BIGINT NOT NULL COMMENT '职位内技能分类', importance DECIMAL(4,2) COMMENT 'O*NET Importance,0-5,允许为空', + importance_source_code VARCHAR(20) COMMENT 'O*NET SOC 来源,非空热度必填', + importance_element_id VARCHAR(30) COMMENT 'O*NET Element ID,非空热度必填', + importance_is_proxy TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否为项目分类到 O*NET 元素的代理映射', relationship_type VARCHAR(20) NOT NULL COMMENT '职位技能关系类型', source_version_id BIGINT NOT NULL COMMENT 'Importance 来源版本', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -99,6 +102,12 @@ CREATE TABLE occupation_skills ( CONSTRAINT ck_occupation_skills_importance CHECK ( importance IS NULL OR (importance >= 0.00 AND importance <= 5.00) ), + CONSTRAINT ck_occupation_skills_importance_provenance CHECK ( + importance IS NULL OR (importance_source_code IS NOT NULL AND importance_element_id IS NOT NULL) + ), + CONSTRAINT ck_occupation_skills_importance_proxy CHECK ( + importance_is_proxy IN (0, 1) + ), CONSTRAINT ck_occupation_skills_relationship_type CHECK ( relationship_type IN ('ESSENTIAL', 'OPTIONAL', 'PROJECT') ), diff --git a/mydreamjob-server/src/main/resources/db/migration/V36__skill_cloud_seed.sql b/mydreamjob-server/src/main/resources/db/migration/V36__skill_cloud_seed.sql new file mode 100644 index 0000000..e4a03a4 --- /dev/null +++ b/mydreamjob-server/src/main/resources/db/migration/V36__skill_cloud_seed.sql @@ -0,0 +1,413 @@ +-- Flyway V36: ESCO/O*NET 代表性行业技能种子数据 +-- 本迁移只导入互联网、金融、教育三类行业及 10 个代表性职位。 +-- ESCO 负责职位、通用技能和分类主关系;O*NET software_skills 仅补充 ESCO 缺失的具体技术。 +-- O*NET software_skills.source_code 格式:SOC|Workplace Example|Element ID,均来自本地 O*NET 31.0 CSV 的真实行。 +-- +-- ESCO 职位映射: +-- FRONTEND_DEVELOPER -> c40a2919-48a9-40ea-b506-1f34f693496d +-- BACKEND_DEVELOPER -> f2b15a0e-e65a-438a-affb-29b9d50b77d1 +-- QA_ENGINEER -> 106f79e4-6264-45f1-9e7a-297435cd684b +-- DEVOPS_ENGINEER -> 使用后端软件开发者 ESCO URI 作为项目代理(ESCO 无更近 DevOps 节点) +-- FINANCIAL_DATA_ANALYST -> 8d586ee9-0ab1-4155-a3b0-2ca786c8e48c +-- RISK_MODELING_ENGINEER -> b09acdda-6680-49d1-a411-129610ee3bc0 +-- QUANTITATIVE_DEVELOPER -> 使用软件开发者 ESCO URI 作为项目代理(ESCO 无更近量化开发节点) +-- EDUCATION_PRODUCT_MANAGER -> a79170ad-0b7b-4697-864f-1748359aa10a +-- INSTRUCTIONAL_DESIGNER -> a9c30651-ccbc-4a80-900c-7880615cdf6e +-- EDUCATION_DATA_ANALYST -> d3edb8f8-3a06-47a0-8fb9-9b212c006aa2 +-- +-- O*NET Importance 代理 SOC: +-- FRONTEND_DEVELOPER -> 15-1254.00 Web Developers +-- BACKEND_DEVELOPER -> 15-1252.00 Software Developers +-- QA_ENGINEER -> 15-1253.00 Software Quality Assurance Analysts and Testers +-- DEVOPS_ENGINEER -> 15-1244.00 Network and Computer Systems Administrators +-- FINANCIAL_DATA_ANALYST -> 13-2031.00 Budget Analysts(目标金融分析师 SOC 在本地 O*NET 31.0 Importance CSV 中无数据,故使用语义较近的预算分析师代理) +-- RISK_MODELING_ENGINEER -> 13-2053.00 Insurance Underwriters(目标金融风险专家 SOC 在本地 Importance CSV 中无数据,故使用包含风险决策活动的保险承保人代理) +-- QUANTITATIVE_DEVELOPER -> 13-2099.01 Financial Quantitative Analysts +-- EDUCATION_PRODUCT_MANAGER -> 13-1082.00 Project Management Specialists(无直接教育产品经理 SOC,使用项目管理专家代理) +-- INSTRUCTIONAL_DESIGNER -> 25-9031.00 Instructional Coordinators +-- EDUCATION_DATA_ANALYST -> 15-2051.00 Data Scientists +-- +-- 分类代理规则(importance_is_proxy=1): +-- TECH=4.A.3.b.1 Working with Computers;PROCESS=4.A.2.a.2 Processing Information; +-- ANALYSIS=4.A.2.a.4 Analyzing Data or Information;COMPLIANCE=4.A.2.a.3 Evaluating Information to Determine Compliance with Standards; +-- DESIGN=4.A.2.b.2 Thinking Creatively;STRATEGY=4.A.2.b.4 Developing Objectives and Strategies; +-- MONITOR=4.A.1.a.2 Monitoring Processes, Materials, or Surroundings;ESTIMATE=4.A.1.b.3 Estimating the Quantifiable Characteristics of Products, Events, or Information; +-- COMM=4.A.4.a.2 Communicating with Supervisors, Peers, or Subordinates;EDU=2.C.6 Education and Training; +-- FINANCE=2.C.1.c Economics and Accounting;MATH=2.C.4.a Mathematics;SECURITY=2.C.8.a Public Safety and Security。 +-- 每条非空 importance 均保存上述代理 SOC、Element ID 和 importance_is_proxy=1,可由本地 O*NET CSV 逐条复核。 +-- 无法稳定映射 Importance 的技能保留关系并写 NULL,不伪造热度。 +-- +INSERT INTO skill_source_versions (id, source_name, source_version, license, attribution_url, imported_at) VALUES +(1, 'ESCO', '1.2.1 (Release v5.7.0, Last update 10/12/2025)', 'Commission Decision 2011/833/EU', 'https://esco.ec.europa.eu/en/use-esco/download', CURRENT_TIMESTAMP), +(2, 'O*NET', '31.0 Database', 'CC BY 4.0', 'https://www.onetcenter.org/database.html', CURRENT_TIMESTAMP); +INSERT INTO skill_industries (id, code, name_zh, source_code, sort_order) VALUES +(1, 'INTERNET', '互联网', 'PROJECT:INDUSTRY:INTERNET', 1), +(2, 'FINANCE', '金融', 'PROJECT:INDUSTRY:FINANCE', 2), +(3, 'EDUCATION', '教育', 'PROJECT:INDUSTRY:EDUCATION', 3); +-- DevOps 与量化开发没有更近的 ESCO 职位节点,source_code 显式复用软件开发者 URI;这是项目代理映射,不是上游原始职位关系。 +INSERT INTO skill_occupations (id, industry_id, code, name_zh, name_en, source_code, source_version_id, sort_order) VALUES +(1, 1, 'FRONTEND_DEVELOPER', '前端开发工程师', 'Frontend Developer', 'http://data.europa.eu/esco/occupation/c40a2919-48a9-40ea-b506-1f34f693496d', 1, 1), +(2, 1, 'BACKEND_DEVELOPER', '后端开发工程师', 'Backend Developer', 'http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1', 1, 2), +(3, 1, 'QA_ENGINEER', '测试工程师', 'Software QA Engineer', 'http://data.europa.eu/esco/occupation/106f79e4-6264-45f1-9e7a-297435cd684b', 1, 3), +(4, 1, 'DEVOPS_ENGINEER', 'DevOps 工程师', 'DevOps Engineer', 'http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1', 1, 4), +(5, 2, 'FINANCIAL_DATA_ANALYST', '金融数据分析师', 'Financial Data Analyst', 'http://data.europa.eu/esco/occupation/8d586ee9-0ab1-4155-a3b0-2ca786c8e48c', 1, 1), +(6, 2, 'RISK_MODELING_ENGINEER', '风控建模工程师', 'Risk Modeling Engineer', 'http://data.europa.eu/esco/occupation/b09acdda-6680-49d1-a411-129610ee3bc0', 1, 2), +(7, 2, 'QUANTITATIVE_DEVELOPER', '量化开发工程师', 'Quantitative Developer', 'http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1', 1, 3), +(8, 3, 'EDUCATION_PRODUCT_MANAGER', '教育产品经理', 'Education Product Manager', 'http://data.europa.eu/esco/occupation/a79170ad-0b7b-4697-864f-1748359aa10a', 1, 1), +(9, 3, 'INSTRUCTIONAL_DESIGNER', '教学设计师', 'Instructional Designer', 'http://data.europa.eu/esco/occupation/a9c30651-ccbc-4a80-900c-7880615cdf6e', 1, 2), +(10, 3, 'EDUCATION_DATA_ANALYST', '教育数据分析师', 'Education Data Analyst', 'http://data.europa.eu/esco/occupation/d3edb8f8-3a06-47a0-8fb9-9b212c006aa2', 1, 3); +INSERT INTO skill_categories (id, occupation_id, code, name_zh, suggestion_template_code, sort_order) VALUES +(101, 1, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(102, 1, 'WEB_BASIC', 'Web 基础', 'FRAMEWORK', 2), +(103, 1, 'FRAMEWORK', '前端框架', 'FRAMEWORK', 3), +(104, 1, 'TOOLS', '工程化工具', 'TOOL', 4), +(105, 1, 'BROWSER_DEBUG', '浏览器与调试', 'TOOL', 5), +(106, 1, 'TEST', '测试', 'GENERAL', 6), +(107, 1, 'COLLABORATION', '版本控制与协作', 'GENERAL', 7), +(201, 2, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(202, 2, 'FRAMEWORK', '后端框架与平台', 'FRAMEWORK', 2), +(203, 2, 'DATABASE', '数据库', 'TOOL', 3), +(204, 2, 'CACHE_MESSAGING', '缓存与消息', 'TOOL', 4), +(205, 2, 'CLOUD_DEVOPS', '云/DevOps', 'TOOL', 5), +(206, 2, 'TEST', '测试', 'GENERAL', 6), +(207, 2, 'TOOLS', '工程化工具', 'TOOL', 7), +(208, 2, 'SECURITY', '安全', 'GENERAL', 8), +(301, 3, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(302, 3, 'TEST_DESIGN', '测试设计', 'GENERAL', 2), +(303, 3, 'AUTOMATION_TEST', '自动化测试框架', 'FRAMEWORK', 3), +(304, 3, 'API_DATABASE_TEST', '接口与数据库测试', 'TOOL', 4), +(305, 3, 'PERFORMANCE_TEST', '性能测试', 'TOOL', 5), +(306, 3, 'CICD_TOOLS', 'CI/CD 与工程化工具', 'TOOL', 6), +(401, 4, 'LANGUAGE_SCRIPT', '编程语言与脚本', 'PROGRAMMING_LANGUAGE', 1), +(402, 4, 'CLOUD', '云平台', 'TOOL', 2), +(403, 4, 'CONTAINER', '容器与编排', 'TOOL', 3), +(404, 4, 'CICD', 'CI/CD', 'TOOL', 4), +(405, 4, 'IAC', '配置管理与 IaC', 'TOOL', 5), +(406, 4, 'OBSERVABILITY', '可观测性', 'TOOL', 6), +(407, 4, 'SECURITY', '安全', 'GENERAL', 7), +(408, 4, 'DATABASE', '数据库', 'TOOL', 8), +(501, 5, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(502, 5, 'ANALYSIS_VISUALIZATION', '数据分析与可视化', 'TOOL', 2), +(503, 5, 'DATABASE_WAREHOUSE', '数据库与数据仓库', 'TOOL', 3), +(504, 5, 'STATISTICS_MODELING', '统计与建模', 'FRAMEWORK', 4), +(505, 5, 'FINANCE_PLATFORM', '金融计算框架/平台', 'FRAMEWORK', 5), +(506, 5, 'FINANCE_DOMAIN', '金融领域知识', 'GENERAL', 6), +(507, 5, 'TOOLS', '工程化工具', 'TOOL', 7), +(601, 6, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(602, 6, 'STATISTICS_MODELING', '统计与建模', 'FRAMEWORK', 2), +(603, 6, 'RISK_MODEL', '风险模型', 'GENERAL', 3), +(604, 6, 'MODEL_PLATFORM', '建模框架/平台', 'FRAMEWORK', 4), +(605, 6, 'DATA_DATABASE', '数据处理与数据库', 'TOOL', 5), +(606, 6, 'MODEL_VALIDATION', '模型验证', 'GENERAL', 6), +(607, 6, 'DEPLOY_TOOLS', '部署与工程化工具', 'TOOL', 7), +(701, 7, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(702, 7, 'QUANT_PLATFORM', '量化建模框架/平台', 'FRAMEWORK', 2), +(703, 7, 'MARKET_DATA', '市场与金融数据', 'TOOL', 3), +(704, 7, 'DATABASE', '数据库', 'TOOL', 4), +(705, 7, 'PERFORMANCE', '低延迟与高性能工程', 'TOOL', 5), +(706, 7, 'TEST', '测试', 'GENERAL', 6), +(707, 7, 'CLOUD_DEVOPS', '云/DevOps', 'TOOL', 7), +(801, 8, 'EDU_DOMAIN', '教育领域知识', 'GENERAL', 1), +(802, 8, 'PRODUCT_ANALYSIS', '产品分析方法', 'TOOL', 2), +(803, 8, 'REQUIREMENTS_PROCESS', '需求与流程', 'GENERAL', 3), +(804, 8, 'DATA_ANALYSIS', '数据分析', 'TOOL', 4), +(805, 8, 'EDU_PLATFORM', '教育平台与工具', 'TOOL', 5), +(806, 8, 'COLLAB_TOOLS', '协作与工程化工具', 'GENERAL', 6), +(901, 9, 'INSTRUCTIONAL_DESIGN', '教学设计', 'FRAMEWORK', 1), +(902, 9, 'COURSE_PLATFORM', '课程与学习平台', 'TOOL', 2), +(903, 9, 'DIGITAL_CONTENT', '数字化内容工具', 'TOOL', 3), +(904, 9, 'ASSESSMENT', '评估设计', 'GENERAL', 4), +(905, 9, 'LEARNING_ANALYSIS', '学习数据分析', 'TOOL', 5), +(906, 9, 'ACCESSIBILITY', '无障碍与规范', 'GENERAL', 6), +(1001, 10, 'LANGUAGE', '编程语言', 'PROGRAMMING_LANGUAGE', 1), +(1002, 10, 'ANALYSIS_VISUALIZATION', '数据分析与可视化', 'TOOL', 2), +(1003, 10, 'DATABASE_SQL', '数据库与 SQL', 'TOOL', 3), +(1004, 10, 'STATISTICS_MODELING', '统计与建模', 'FRAMEWORK', 4), +(1005, 10, 'EDU_DOMAIN', '教育领域知识', 'GENERAL', 5), +(1006, 10, 'REPORTING_TOOLS', '报表与工程化工具', 'TOOL', 6); +INSERT INTO skills (id, code, name_zh, name_en, source_type, source_code, source_version_id, sort_order) VALUES +(1, 'JAVA', 'Java', 'Java (computer programming)', 'ESCO', 'http://data.europa.eu/esco/skill/19a8293b-8e95-4de3-983f-77484079c389', 1, 1), +(2, 'PYTHON', 'Python', 'Python (computer programming)', 'ESCO', 'http://data.europa.eu/esco/skill/ccd0a1d9-afda-43d9-b901-96344886e14d', 1, 2), +(3, 'JAVASCRIPT', 'JavaScript', 'JavaScript', 'ESCO', 'http://data.europa.eu/esco/skill/3cd569a2-4f88-4c1e-9995-8dce8c5e51a7', 1, 3), +(4, 'TYPESCRIPT', 'TypeScript', 'TypeScript', 'ESCO', 'http://data.europa.eu/esco/skill/867137fb-ff1b-4ca3-99f3-cb6969aa2c68', 1, 4), +(5, 'SQL', 'SQL', 'SQL', 'ESCO', 'http://data.europa.eu/esco/skill/598de5b0-5b58-4ea7-8058-a4bc4d18c742', 1, 5), +(6, 'CSHARP', 'C#', 'C#', 'ESCO', 'http://data.europa.eu/esco/skill/4c016b68-4116-468c-9dc6-42710c239e4a', 1, 6), +(7, 'HTML', 'HTML', 'use markup languages', 'ESCO', 'http://data.europa.eu/esco/skill/0af062de-eb43-41e9-9b96-249e2cd22d26', 1, 7), +(8, 'CSS', 'CSS', 'CSS', 'ESCO', 'http://data.europa.eu/esco/skill/e5d1f825-60ed-4bdd-872a-e748c387f777', 1, 8), +(9, 'R', 'R', 'R', 'ESCO', 'http://data.europa.eu/esco/skill/51586df8-1c46-4b47-8583-773cb63bf00b', 1, 9), +(10, 'MYSQL', 'MySQL', 'MySQL', 'ESCO', 'http://data.europa.eu/esco/skill/4da171e5-779c-4983-a76f-91c16751e99f', 1, 10), +(11, 'POSTGRESQL', 'PostgreSQL', 'PostgreSQL', 'ESCO', 'http://data.europa.eu/esco/skill/a8d07b5a-c1a1-42c6-9d53-db9c7a2ca996', 1, 11), +(12, 'STATISTICS', '统计学', 'statistics', 'ESCO', 'http://data.europa.eu/esco/skill/7ee4c2ea-b349-4bd2-81a3-ec31475d4833', 1, 12), +(13, 'MACHINE_LEARNING', '机器学习', 'utilise machine learning', 'ESCO', 'http://data.europa.eu/esco/skill/8369c2d6-c100-4cf6-bd83-9668d8678433', 1, 13), +(14, 'FINANCIAL_ANALYSIS', '金融分析', 'financial analysis', 'ESCO', 'http://data.europa.eu/esco/skill/99571e68-801f-49af-a897-5f75996642e1', 1, 14), +(15, 'RISK_MANAGEMENT', '风险管理', 'risk management', 'ESCO', 'http://data.europa.eu/esco/skill/6eff134b-e34f-4d6e-a6e8-5e47cf2228d0', 1, 15), +(16, 'INSTRUCTIONAL_DESIGN', '教学设计', 'instructional design models', 'ESCO', 'http://data.europa.eu/esco/skill/e3145bd7-7f35-44ec-a5dc-4417c5c9ab9a', 1, 16), +(17, 'CURRICULUM_DEVELOPMENT', '课程开发', 'advise on curriculum development', 'ESCO', 'http://data.europa.eu/esco/skill/142a5319-60cb-42a2-891f-90ba92af18b1', 1, 17), +(18, 'LEARNING_MANAGEMENT_SYSTEMS', '学习管理系统', 'learning management systems', 'ESCO', 'http://data.europa.eu/esco/skill/6c80d53c-d8c9-41fe-998f-091fca208834', 1, 18), +(19, 'EDUCATIONAL_TECHNOLOGY', '教育技术', 'develop e-learning plan', 'ESCO', 'http://data.europa.eu/esco/skill/8a6394d1-f44f-4b0d-81d5-7201fc025ba8', 1, 19), +(20, 'COMMUNICATION', '沟通能力', 'communication', 'ESCO', 'http://data.europa.eu/esco/skill/15d76317-c71a-4fa2-aadc-2ecc34e627b7', 1, 20), +(21, 'CRITICAL_THINKING', '批判性思维', 'use logical reasoning', 'ESCO', 'http://data.europa.eu/esco/skill/a9e6718b-b9d8-44c2-b06e-85ceed9d144c', 1, 21), +(22, 'COLLABORATION', '团队协作', 'cooperate with colleagues', 'ESCO', 'http://data.europa.eu/esco/skill/082967d4-b94c-4305-aa13-35a6aa8c5c7c', 1, 22), +(23, 'PROJECT_MANAGEMENT', '项目管理', 'project management', 'ESCO', 'http://data.europa.eu/esco/skill/7111b95d-0ce3-441a-9d92-4c75d05c4388', 1, 23), +(24, 'AGILE', '敏捷开发', 'Agile development', 'ESCO', 'http://data.europa.eu/esco/skill/dba46f87-0831-49cd-a1c7-340a653c0221', 1, 24), +(25, 'JENKINS', 'Jenkins', 'Jenkins (tools for software configuration management)', 'ESCO', 'http://data.europa.eu/esco/skill/f47a1998-0beb-43be-9f46-380aa4d183da', 1, 25), +(26, 'REACT', 'React', 'React', 'O*NET', '15-1254.00|React|2.E.7.h', 2, 26), +(27, 'VUE_JS', 'Vue.js', 'Vue.js', 'O*NET', '15-1254.00|Vue.js|2.E.7.h', 2, 27), +(28, 'NODE_JS', 'Node.js', 'Node.js', 'O*NET', '15-1254.00|Node.js|2.E.7.h', 2, 28), +(29, 'SPRING_BOOT', 'Spring Boot', 'Spring Boot', 'O*NET', '15-1252.00|Spring Boot|2.E.10.a', 2, 29), +(30, 'DJANGO', 'Django', 'Django', 'O*NET', '15-1252.00|Django|2.E.7.h', 2, 30), +(31, 'FLASK', 'Flask', 'Flask', 'O*NET', '15-2051.00|Flask|2.E.7.b', 2, 31), +(32, 'GIT', 'Git', 'Git', 'O*NET', '15-1252.00|Git|2.E.5.c', 2, 32), +(33, 'DOCKER', 'Docker', 'Docker', 'O*NET', '15-1252.00|Docker|2.E.10.a', 2, 33), +(34, 'KUBERNETES', 'Kubernetes', 'Kubernetes', 'O*NET', '15-1252.00|Kubernetes|2.E.10.a', 2, 34), +(35, 'GRADLE', 'Gradle', 'Gradle', 'O*NET', '15-1252.00|Gradle|2.E.7.e', 2, 35), +(36, 'MONGODB', 'MongoDB', 'MongoDB', 'O*NET', '15-1252.00|MongoDB|2.E.6.d', 2, 36), +(37, 'REDIS', 'Redis', 'Redis', 'O*NET', '15-1252.00|Redis|2.E.6.d', 2, 37), +(38, 'SELENIUM', 'Selenium', 'Selenium', 'O*NET', '15-1253.00|Selenium|2.E.7.f', 2, 38), +(39, 'JUNIT', 'JUnit', 'JUnit', 'O*NET', '15-1253.00|JUnit|2.E.7.f', 2, 39), +(40, 'POSTMAN', 'Postman', 'Postman', 'O*NET', '15-1253.00|Postman|2.E.13.c', 2, 40), +(41, 'PANDAS', 'pandas', 'pandas', 'O*NET', '15-2051.00|pandas|2.E.6.f', 2, 41), +(42, 'NUMPY', 'NumPy', 'NumPy', 'O*NET', '15-2051.00|NumPy|2.E.6.f', 2, 42), +(43, 'TABLEAU', 'Tableau', 'Tableau', 'O*NET', '15-2051.00|Tableau|2.E.6.m', 2, 43), +(44, 'POWER_BI', 'Microsoft Power BI', 'Microsoft Power BI', 'O*NET', '15-2051.00|Microsoft Power BI|2.E.6.m', 2, 44), +(45, 'MICROSOFT_EXCEL', 'Microsoft Excel', 'Microsoft Excel', 'O*NET', '15-2051.00|Microsoft Excel|2.E.4.i', 2, 45), +(46, 'TENSORFLOW', 'TensorFlow', 'TensorFlow', 'O*NET', '15-2051.00|TensorFlow|2.E.9.d', 2, 46), +(47, 'PYTORCH', 'PyTorch', 'PyTorch', 'O*NET', '15-2051.00|PyTorch|2.E.6.f', 2, 47), +(48, 'SCIKIT_LEARN', 'Scikit-learn', 'Scikit-learn', 'O*NET', '15-2051.00|Scikit-learn|2.E.7.b', 2, 48), +(49, 'APACHE_SPARK', 'Apache Spark', 'Apache Spark', 'O*NET', '15-2051.00|Apache Spark|2.E.6.m', 2, 49), +(50, 'FIGMA', 'Figma', 'Figma', 'O*NET', '15-1254.00|Figma|2.E.7.d', 2, 50), +(51, 'BASH', 'Bash', 'Bash', 'O*NET', '15-1252.00|Bash|2.E.13.c', 2, 51), +(52, 'GO', 'Go', 'Go', 'O*NET', '15-1252.00|Go|2.E.7.b', 2, 52), +(53, 'CPP', 'C++', 'C++', 'O*NET', '15-1252.00|C++|2.E.7.e', 2, 53), +(54, 'GOOGLE_CHROME', 'Google Chrome', 'Google Chrome', 'O*NET', '17-2051.02|Google Chrome|2.E.10.e', 2, 54), +(55, 'DEBUGGING_SOFTWARE', '调试软件', 'Debugging software', 'O*NET', '17-2071.00|Debugging software|2.E.7.f', 2, 55), +(56, 'APACHE_KAFKA', 'Apache Kafka', 'Apache Kafka', 'O*NET', '15-1299.07|Apache Kafka|2.E.7.b', 2, 56), +(57, 'SONARQUBE', 'SonarQube', 'SonarQube', 'O*NET', '15-1252.00|SonarQube|2.E.7.f', 2, 57), +(58, 'CHEF', 'Chef', 'Chef', 'O*NET', '15-1255.00|Chef|2.E.7.a', 2, 58), +(59, 'GITLAB', 'GitLab', 'GitLab', 'O*NET', '15-1299.08|GitLab|2.E.10.a', 2, 59), +(60, 'NAGIOS', 'Nagios', 'Nagios', 'O*NET', '11-3021.00|Nagios|2.E.11.a', 2, 60), +(61, 'NMAP', 'Nmap', 'Nmap', 'O*NET', '13-1199.07|Nmap|2.E.14.d', 2, 61), +(62, 'BLOOMBERG_PROFESSIONAL', 'Bloomberg Professional', 'Bloomberg Professional', 'O*NET', '41-3031.00|Bloomberg Professional|2.E.2.d', 2, 62), +(63, 'MOODLE', 'Moodle', 'Moodle', 'O*NET', '25-2022.00|Moodle|2.E.8.b', 2, 63), +(64, 'ARTICULATE_STORYLINE', 'Articulate Storyline', 'Articulate Storyline', 'O*NET', '25-9031.00|Articulate Storyline|2.E.8.b', 2, 64), +(65, 'MICROSOFT_POWERPOINT', 'Microsoft PowerPoint', 'Microsoft PowerPoint', 'O*NET', '27-4012.00|Microsoft PowerPoint|2.E.4.f', 2, 65), +(66, 'AXURE_RP', 'Axure RP', 'Axure RP', 'O*NET', '15-1255.00|Axure RP|2.E.4.g', 2, 66), +(67, 'ADOBE_PHOTOSHOP', 'Adobe Photoshop', 'Adobe Photoshop', 'O*NET', '19-3092.00|Adobe Photoshop|2.E.4.b', 2, 67), +(68, 'APACHE_JMETER', 'Apache JMeter', 'Apache JMeter', 'O*NET', '15-1253.00|Apache JMeter|2.E.7.e', 2, 68), +(69, 'ADOBE_ACROBAT', 'Adobe Acrobat', 'Adobe Acrobat', 'O*NET', '11-1011.00|Adobe Acrobat|2.E.5.b', 2, 69), +(70, 'MICROSOFT_AZURE', 'Microsoft Azure', 'Microsoft Azure software', 'O*NET', '41-4011.00|Microsoft Azure software|2.E.7.b', 2, 70), +(71, 'AWS', 'AWS', 'Amazon Web Services AWS software', 'O*NET', '11-1021.00|Amazon Web Services AWS software|2.E.6.f', 2, 71), +(72, 'WIRESHARK', 'Wireshark', 'Wireshark', 'O*NET', '15-1241.01|Wireshark|2.E.11.a', 2, 72); +INSERT INTO skill_aliases (id, skill_id, alias, normalized_alias) VALUES +(1, 1, 'Java (computer programming)', 'java (computer programming)'), +(2, 2, 'Python (computer programming)', 'python (computer programming)'), +(3, 3, 'JavaScript', 'javascript'), +(4, 4, 'TypeScript', 'typescript'), +(5, 5, 'SQL', 'sql'), +(6, 6, 'C#', 'c#'), +(7, 7, 'HTML', 'html'), +(8, 8, 'CSS', 'css'), +(9, 9, 'R', 'r'), +(10, 10, 'MySQL', 'mysql'), +(11, 11, 'PostgreSQL', 'postgresql'), +(12, 12, 'statistics', 'statistics'), +(13, 13, 'machine learning', 'machine learning'), +(14, 14, 'financial analysis', 'financial analysis'), +(15, 15, 'risk management', 'risk management'), +(16, 16, 'instructional design', 'instructional design'), +(17, 17, 'curriculum development', 'curriculum development'), +(18, 18, 'learning management systems', 'learning management systems'), +(19, 19, 'educational technology', 'educational technology'), +(20, 20, 'communication', 'communication'), +(21, 21, 'critical thinking', 'critical thinking'), +(22, 22, 'collaboration', 'collaboration'), +(23, 23, 'project management', 'project management'), +(24, 24, 'agile', 'agile'), +(25, 25, 'Jenkins (tools for software configuration management)', 'jenkins (tools for software configuration management)'), +(26, 26, 'React', 'react'), +(27, 27, 'Vue.js', 'vuejs'), +(28, 28, 'Node.js', 'nodejs'), +(29, 29, 'Spring Boot', 'spring boot'), +(30, 30, 'Django', 'django'), +(31, 31, 'Flask', 'flask'), +(32, 32, 'Git', 'git'), +(33, 33, 'Docker', 'docker'), +(34, 34, 'Kubernetes', 'kubernetes'), +(35, 35, 'Gradle', 'gradle'), +(36, 36, 'MongoDB', 'mongodb'), +(37, 37, 'Redis', 'redis'), +(38, 38, 'Selenium', 'selenium'), +(39, 39, 'JUnit', 'junit'), +(40, 40, 'Postman', 'postman'), +(41, 41, 'pandas', 'pandas'), +(42, 42, 'NumPy', 'numpy'), +(43, 43, 'Tableau', 'tableau'), +(44, 44, 'Microsoft Power BI', 'microsoft power bi'), +(45, 45, 'Microsoft Excel', 'microsoft excel'), +(46, 46, 'TensorFlow', 'tensorflow'), +(47, 47, 'PyTorch', 'pytorch'), +(48, 48, 'scikit-learn', 'scikit learn'), +(49, 49, 'Apache Spark', 'apache spark'), +(50, 50, 'Figma', 'figma'), +(51, 51, 'Bash', 'bash'), +(52, 52, 'Go', 'go'), +(53, 53, 'C++', 'c++'), +(54, 54, 'Google Chrome', 'google chrome'), +(55, 55, 'debugging software', 'debugging software'), +(56, 56, 'Apache Kafka', 'apache kafka'), +(57, 57, 'SonarQube', 'sonarqube'), +(58, 58, 'Chef', 'chef'), +(59, 59, 'GitLab', 'gitlab'), +(60, 60, 'Nagios', 'nagios'), +(61, 61, 'Nmap', 'nmap'), +(62, 62, 'Bloomberg Professional', 'bloomberg professional'), +(63, 63, 'Moodle', 'moodle'), +(64, 64, 'Articulate Storyline', 'articulate storyline'), +(65, 65, 'Microsoft PowerPoint', 'microsoft powerpoint'), +(66, 66, 'Axure RP', 'axure rp'), +(67, 67, 'Adobe Photoshop', 'adobe photoshop'), +(68, 68, 'Apache JMeter', 'apache jmeter'), +(69, 69, 'Adobe Acrobat', 'adobe acrobat'), +(70, 70, 'Microsoft Azure software', 'microsoft azure software'), +(71, 71, 'Amazon Web Services AWS software', 'amazon web services aws software'), +(72, 72, 'Wireshark', 'wireshark'); +INSERT INTO occupation_skills (id, occupation_id, skill_id, category_id, importance, importance_source_code, importance_element_id, importance_is_proxy, relationship_type, source_version_id) VALUES +(1, 1, 3, 101, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(2, 1, 4, 101, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(3, 1, 7, 102, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(4, 1, 8, 102, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(5, 1, 26, 103, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(6, 1, 27, 103, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(7, 1, 28, 104, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(8, 1, 35, 104, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(9, 1, 54, 105, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(10, 1, 55, 105, 4.88, '15-1254.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(11, 1, 38, 106, 3.33, '15-1254.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(12, 1, 39, 106, 3.33, '15-1254.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(13, 1, 32, 107, 3.88, '15-1254.00', '4.A.4.a.2', 1, 'ESSENTIAL', 2), +(14, 1, 24, 107, 3.88, '15-1254.00', '4.A.4.a.2', 1, 'ESSENTIAL', 2), +(15, 2, 1, 201, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(16, 2, 2, 201, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(17, 2, 29, 202, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(18, 2, 30, 202, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(19, 2, 10, 203, 4.38, '15-1252.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(20, 2, 11, 203, 4.38, '15-1252.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(21, 2, 37, 204, 4.38, '15-1252.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(22, 2, 56, 204, 4.38, '15-1252.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(23, 2, 33, 205, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(24, 2, 34, 205, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(25, 2, 39, 206, 3.94, '15-1252.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(26, 2, 40, 206, 3.94, '15-1252.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(27, 2, 32, 207, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(28, 2, 35, 207, 4.61, '15-1252.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(29, 2, 61, 208, 2.57, '15-1252.00', '2.C.8.a', 1, 'ESSENTIAL', 2), +(30, 2, 57, 208, 2.57, '15-1252.00', '2.C.8.a', 1, 'ESSENTIAL', 2), +(31, 3, 1, 301, 4.94, '15-1253.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(32, 3, 2, 301, 4.94, '15-1253.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(33, 3, 40, 302, 4.24, '15-1253.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(34, 3, 21, 302, 4.24, '15-1253.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(35, 3, 38, 303, 4.94, '15-1253.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(36, 3, 39, 303, 4.94, '15-1253.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(37, 3, 5, 304, 4.38, '15-1253.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(38, 3, 10, 304, 4.38, '15-1253.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(39, 3, 68, 305, 2.94, '15-1253.00', '4.A.1.b.3', 1, 'ESSENTIAL', 2), +(40, 3, 51, 305, 2.94, '15-1253.00', '4.A.1.b.3', 1, 'ESSENTIAL', 2), +(41, 3, 25, 306, 4.94, '15-1253.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(42, 3, 59, 306, 4.94, '15-1253.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(43, 4, 2, 401, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(44, 4, 51, 401, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(45, 4, 71, 402, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(46, 4, 70, 402, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(47, 4, 33, 403, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(48, 4, 34, 403, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(49, 4, 25, 404, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(50, 4, 59, 404, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(51, 4, 58, 405, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(52, 4, 32, 405, 4.83, '15-1244.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(53, 4, 60, 406, 3.60, '15-1244.00', '4.A.1.a.2', 1, 'ESSENTIAL', 2), +(54, 4, 72, 406, 3.60, '15-1244.00', '4.A.1.a.2', 1, 'ESSENTIAL', 2), +(55, 4, 61, 407, 2.34, '15-1244.00', '2.C.8.a', 1, 'ESSENTIAL', 2), +(56, 4, 57, 407, 2.34, '15-1244.00', '2.C.8.a', 1, 'ESSENTIAL', 2), +(57, 4, 10, 408, 4.03, '15-1244.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(58, 4, 11, 408, 4.03, '15-1244.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(59, 5, 2, 501, 4.48, '13-2031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(60, 5, 9, 501, 4.48, '13-2031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(61, 5, 41, 502, 4.01, '13-2031.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(62, 5, 43, 502, 4.01, '13-2031.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(63, 5, 5, 503, 4.44, '13-2031.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(64, 5, 11, 503, 4.44, '13-2031.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(65, 5, 12, 504, 4.03, '13-2031.00', '2.C.4.a', 1, 'ESSENTIAL', 2), +(66, 5, 13, 504, 4.03, '13-2031.00', '2.C.4.a', 1, 'ESSENTIAL', 2), +(67, 5, 62, 505, 4.48, '13-2031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(68, 5, 45, 505, 4.48, '13-2031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(69, 5, 14, 506, 4.42, '13-2031.00', '2.C.1.c', 1, 'ESSENTIAL', 2), +(70, 5, 15, 506, 4.42, '13-2031.00', '2.C.1.c', 1, 'ESSENTIAL', 2), +(71, 5, 32, 507, 4.48, '13-2031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(72, 5, 44, 507, 4.48, '13-2031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(73, 6, 2, 601, 4.41, '13-2053.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(74, 6, 9, 601, 4.41, '13-2053.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(75, 6, 12, 602, 3.27, '13-2053.00', '2.C.4.a', 1, 'ESSENTIAL', 2), +(76, 6, 13, 602, 3.27, '13-2053.00', '2.C.4.a', 1, 'ESSENTIAL', 2), +(77, 6, 15, 603, 4.43, '13-2053.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(78, 6, 21, 603, 4.43, '13-2053.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(79, 6, 46, 604, 4.41, '13-2053.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(80, 6, 48, 604, 4.41, '13-2053.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(81, 6, 5, 605, 4.13, '13-2053.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(82, 6, 49, 605, 4.13, '13-2053.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(83, 6, 39, 606, 3.90, '13-2053.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(84, 6, 57, 606, 3.90, '13-2053.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(85, 6, 33, 607, 4.41, '13-2053.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(86, 6, 32, 607, 4.41, '13-2053.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(87, 7, 2, 701, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(88, 7, 53, 701, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(89, 7, 52, 701, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(90, 7, 42, 702, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(91, 7, 48, 702, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(92, 7, 62, 703, 4.20, '13-2099.01', '2.C.1.c', 1, 'ESSENTIAL', 2), +(93, 7, 14, 703, 4.20, '13-2099.01', '2.C.1.c', 1, 'ESSENTIAL', 2), +(94, 7, 11, 704, 4.50, '13-2099.01', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(95, 7, 5, 704, 4.50, '13-2099.01', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(96, 7, 49, 705, 3.53, '13-2099.01', '4.A.1.b.3', 1, 'ESSENTIAL', 2), +(97, 7, 56, 705, 3.53, '13-2099.01', '4.A.1.b.3', 1, 'ESSENTIAL', 2), +(98, 7, 39, 706, 3.05, '13-2099.01', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(99, 7, 68, 706, 3.05, '13-2099.01', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(100, 7, 33, 707, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(101, 7, 34, 707, 4.68, '13-2099.01', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(102, 8, 16, 801, 2.52, '13-1082.00', '2.C.6', 1, 'ESSENTIAL', 2), +(103, 8, 17, 801, 2.52, '13-1082.00', '2.C.6', 1, 'ESSENTIAL', 2), +(104, 8, 66, 802, 3.94, '13-1082.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(105, 8, 44, 802, 3.94, '13-1082.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(106, 8, 23, 803, 3.73, '13-1082.00', '4.A.2.b.4', 1, 'ESSENTIAL', 2), +(107, 8, 24, 803, 3.73, '13-1082.00', '4.A.2.b.4', 1, 'ESSENTIAL', 2), +(108, 8, 43, 804, 3.94, '13-1082.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(109, 8, 2, 804, 3.94, '13-1082.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(110, 8, 18, 805, 4.45, '13-1082.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(111, 8, 63, 805, 4.45, '13-1082.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(112, 8, 32, 806, 4.73, '13-1082.00', '4.A.4.a.2', 1, 'ESSENTIAL', 2), +(113, 8, 45, 806, 4.73, '13-1082.00', '4.A.4.a.2', 1, 'ESSENTIAL', 2), +(114, 9, 16, 901, 4.78, '25-9031.00', '2.C.6', 1, 'ESSENTIAL', 2), +(115, 9, 17, 901, 4.78, '25-9031.00', '2.C.6', 1, 'ESSENTIAL', 2), +(116, 9, 18, 902, 4.45, '25-9031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(117, 9, 63, 902, 4.45, '25-9031.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(118, 9, 64, 903, 3.72, '25-9031.00', '4.A.2.b.2', 1, 'ESSENTIAL', 2), +(119, 9, 50, 903, 3.72, '25-9031.00', '4.A.2.b.2', 1, 'ESSENTIAL', 2), +(120, 9, 19, 904, 3.83, '25-9031.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(121, 9, 21, 904, 3.83, '25-9031.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(122, 9, 12, 905, 4.19, '25-9031.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(123, 9, 45, 905, 4.19, '25-9031.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(124, 9, 69, 906, 3.83, '25-9031.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(125, 9, 20, 906, 3.83, '25-9031.00', '4.A.2.a.3', 1, 'ESSENTIAL', 2), +(126, 10, 2, 1001, 4.75, '15-2051.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(127, 10, 9, 1001, 4.75, '15-2051.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(128, 10, 43, 1002, 4.83, '15-2051.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(129, 10, 44, 1002, 4.83, '15-2051.00', '4.A.2.a.4', 1, 'ESSENTIAL', 2), +(130, 10, 5, 1003, 4.71, '15-2051.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(131, 10, 11, 1003, 4.71, '15-2051.00', '4.A.2.a.2', 1, 'ESSENTIAL', 2), +(132, 10, 12, 1004, 4.52, '15-2051.00', '2.C.4.a', 1, 'ESSENTIAL', 2), +(133, 10, 13, 1004, 4.52, '15-2051.00', '2.C.4.a', 1, 'ESSENTIAL', 2), +(134, 10, 17, 1005, 2.28, '15-2051.00', '2.C.6', 1, 'ESSENTIAL', 2), +(135, 10, 19, 1005, 2.28, '15-2051.00', '2.C.6', 1, 'ESSENTIAL', 2), +(136, 10, 45, 1006, 4.75, '15-2051.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(137, 10, 65, 1006, 4.75, '15-2051.00', '4.A.3.b.1', 1, 'ESSENTIAL', 2), +(138, 1, 50, 104, NULL, NULL, NULL, 0, 'OPTIONAL', 2), +(139, 9, 67, 903, NULL, NULL, NULL, 0, 'OPTIONAL', 2); diff --git a/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillCloudSeedDataIntegrationTest.java b/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillCloudSeedDataIntegrationTest.java new file mode 100644 index 0000000..59aaab9 --- /dev/null +++ b/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillCloudSeedDataIntegrationTest.java @@ -0,0 +1,1030 @@ +package com.mydreamjob.skill; + +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.core.io.ClassPathResource; +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 java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.regex.Pattern; + +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SpringBootTest +@TestPropertySource(properties = + "spring.datasource.url=jdbc:h2:mem:integrationdb_skill_seed;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") +@ActiveProfiles("integration-test") +class SkillCloudSeedDataIntegrationTest { + + private static final String EVIDENCE_RESOURCE = "skill-cloud/onet-31-reference.csv"; + private static final String EVIDENCE_HEADER = + "evidence_type,source_soc,element_id,data_value,workplace_example,source_file"; + private static final BigDecimal IMPORTANCE_TOLERANCE = new BigDecimal("0.001"); + private static final Set IMPORTANCE_SOURCE_FILES = Set.of( + "work_activities.csv", "knowledge.csv", "essential_skills.csv", "transferable_skills.csv"); + + private static final Pattern ONET_SKILL_SOURCE_PATTERN = + Pattern.compile("^\\d{2}-\\d{4}\\.\\d{2}\\|[^|]+\\|2\\.E\\.[0-9.a-z]+$"); + + private static final Map> EXPECTED_CATEGORIES = Map.ofEntries( + Map.entry("FRONTEND_DEVELOPER", List.of( + "编程语言", "Web 基础", "前端框架", "工程化工具", "浏览器与调试", "测试", "版本控制与协作")), + Map.entry("BACKEND_DEVELOPER", List.of( + "编程语言", "后端框架与平台", "数据库", "缓存与消息", "云/DevOps", "测试", "工程化工具", "安全")), + Map.entry("QA_ENGINEER", List.of( + "编程语言", "测试设计", "自动化测试框架", "接口与数据库测试", "性能测试", "CI/CD 与工程化工具")), + Map.entry("DEVOPS_ENGINEER", List.of( + "编程语言与脚本", "云平台", "容器与编排", "CI/CD", "配置管理与 IaC", "可观测性", "安全", "数据库")), + Map.entry("FINANCIAL_DATA_ANALYST", List.of( + "编程语言", "数据分析与可视化", "数据库与数据仓库", "统计与建模", "金融计算框架/平台", "金融领域知识", "工程化工具")), + Map.entry("RISK_MODELING_ENGINEER", List.of( + "编程语言", "统计与建模", "风险模型", "建模框架/平台", "数据处理与数据库", "模型验证", "部署与工程化工具")), + Map.entry("QUANTITATIVE_DEVELOPER", List.of( + "编程语言", "量化建模框架/平台", "市场与金融数据", "数据库", "低延迟与高性能工程", "测试", "云/DevOps")), + Map.entry("EDUCATION_PRODUCT_MANAGER", List.of( + "教育领域知识", "产品分析方法", "需求与流程", "数据分析", "教育平台与工具", "协作与工程化工具")), + Map.entry("INSTRUCTIONAL_DESIGNER", List.of( + "教学设计", "课程与学习平台", "数字化内容工具", "评估设计", "学习数据分析", "无障碍与规范")), + Map.entry("EDUCATION_DATA_ANALYST", List.of( + "编程语言", "数据分析与可视化", "数据库与 SQL", "统计与建模", "教育领域知识", "报表与工程化工具")) + ); + + private static final Map EXPECTED_ESCO_OCCUPATION_SOURCES = Map.ofEntries( + Map.entry("FRONTEND_DEVELOPER", + "http://data.europa.eu/esco/occupation/c40a2919-48a9-40ea-b506-1f34f693496d"), + Map.entry("BACKEND_DEVELOPER", + "http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1"), + Map.entry("QA_ENGINEER", + "http://data.europa.eu/esco/occupation/106f79e4-6264-45f1-9e7a-297435cd684b"), + Map.entry("DEVOPS_ENGINEER", + "http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1"), + Map.entry("FINANCIAL_DATA_ANALYST", + "http://data.europa.eu/esco/occupation/8d586ee9-0ab1-4155-a3b0-2ca786c8e48c"), + Map.entry("RISK_MODELING_ENGINEER", + "http://data.europa.eu/esco/occupation/b09acdda-6680-49d1-a411-129610ee3bc0"), + Map.entry("QUANTITATIVE_DEVELOPER", + "http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1"), + Map.entry("EDUCATION_PRODUCT_MANAGER", + "http://data.europa.eu/esco/occupation/a79170ad-0b7b-4697-864f-1748359aa10a"), + Map.entry("INSTRUCTIONAL_DESIGNER", + "http://data.europa.eu/esco/occupation/a9c30651-ccbc-4a80-900c-7880615cdf6e"), + Map.entry("EDUCATION_DATA_ANALYST", + "http://data.europa.eu/esco/occupation/d3edb8f8-3a06-47a0-8fb9-9b212c006aa2") + ); + + @Autowired + private JdbcTemplate jdbcTemplate; + + @MockBean + private RedisTemplate redisTemplate; + + @Test + void seedDataCoversIndustriesOccupationsAndEveryDeclaredCategory() { + assertEquals(3L, count("SELECT COUNT(*) FROM skill_industries")); + assertEquals(10L, count("SELECT COUNT(*) FROM skill_occupations")); + assertEquals(68L, count("SELECT COUNT(*) FROM skill_categories")); + + for (Map.Entry> entry : EXPECTED_CATEGORIES.entrySet()) { + String occupationCode = entry.getKey(); + List actualCategories = jdbcTemplate.queryForList( + "SELECT c.name_zh " + + "FROM skill_categories c " + + "JOIN skill_occupations o ON o.id = c.occupation_id " + + "WHERE o.code = ? " + + "ORDER BY c.sort_order, c.id", + String.class, occupationCode); + assertEquals(entry.getValue(), actualCategories, + "Category coverage mismatch for " + occupationCode); + + for (String categoryName : entry.getValue()) { + long relations = count( + "SELECT COUNT(*) FROM occupation_skills os " + + "JOIN skill_occupations o ON o.id = os.occupation_id " + + "JOIN skill_categories c ON c.id = os.category_id " + + "WHERE o.code = ? AND c.name_zh = ?", + occupationCode, categoryName); + assertTrue(relations >= 1L, + "Category has no skill relation: occupation=" + occupationCode + + ", category=" + categoryName + ", actualRelations=" + relations); + } + + Map coverage = jdbcTemplate.queryForMap( + "SELECT COUNT(os.skill_id) AS skill_count, " + + "SUM(CASE WHEN os.importance IS NOT NULL THEN 1 ELSE 0 END) AS weighted_count " + + "FROM skill_occupations o " + + "LEFT JOIN occupation_skills os ON os.occupation_id = o.id " + + "WHERE o.code = ? " + + "GROUP BY o.id", + occupationCode); + assertTrue(number(coverage.get("skill_count")) >= 10, + occupationCode + " must have at least ten skills"); + assertTrue(number(coverage.get("weighted_count")) >= 10, + occupationCode + " must have at least ten weighted skills"); + } + } + + @Test + void occupationNodesUseApprovedEscoUrisAndOnetSkillSourcesAreTraceable() { + assertEquals(10L, count( + "SELECT COUNT(*) FROM skill_occupations " + + "WHERE source_version_id = 1 " + + "AND source_code LIKE 'http://data.europa.eu/esco/occupation/%'")); + + for (Map.Entry entry : EXPECTED_ESCO_OCCUPATION_SOURCES.entrySet()) { + assertEquals(1L, count( + "SELECT COUNT(*) FROM skill_occupations o " + + "JOIN skill_source_versions sv ON sv.id = o.source_version_id " + + "WHERE o.code = ? AND o.source_code = ? AND sv.source_name = 'ESCO'", + entry.getKey(), entry.getValue()), + "Unexpected ESCO source for " + entry.getKey()); + } + + Map snapshot = loadEvidence("SOFTWARE"); + List> rows = jdbcTemplate.queryForList( + "SELECT id, code, source_code FROM skills WHERE source_type = 'O*NET' ORDER BY id"); + assertEquals(47, rows.size(), "Unexpected O*NET skill source count"); + + for (Map row : rows) { + long skillId = number(row.get("id")); + String skillCode = (String) row.get("code"); + String sourceCode = (String) row.get("source_code"); + assertTrue(ONET_SKILL_SOURCE_PATTERN.matcher(sourceCode).matches(), + "Invalid O*NET software_skills source: skillId=" + skillId + + ", skillCode=" + skillCode + ", sourceCode=" + sourceCode); + + String[] parts = sourceCode.split("\\|", -1); + EvidenceRow evidence = snapshot.get(softwareKey(parts[0], parts[1], parts[2])); + assertNotNull(evidence, + "Missing SOFTWARE evidence snapshot: skillId=" + skillId + + ", skillCode=" + skillCode + ", expectedSourceCode=" + sourceCode); + assertEquals("software_skills.csv", evidence.sourceFile(), + "Unexpected source file for skillId=" + skillId + ", skillCode=" + skillCode); + } + + assertEquals(rows.size(), snapshot.size(), + "SOFTWARE snapshot must contain exactly the 47 referenced O*NET source triples"); + + assertOnetSkillSource("REACT", "15-1254.00|React|2.E.7.h"); + assertOnetSkillSource("GOOGLE_CHROME", "17-2051.02|Google Chrome|2.E.10.e"); + assertOnetSkillSource("APACHE_KAFKA", "15-1299.07|Apache Kafka|2.E.7.b"); + assertOnetSkillSource("GITLAB", "15-1299.08|GitLab|2.E.10.a"); + assertOnetSkillSource("ARTICULATE_STORYLINE", "25-9031.00|Articulate Storyline|2.E.8.b"); + } + + @Test + void everyWeightHasTraceableOnetProvenanceAndKnownRepresentativeValue() { + assertEquals(139L, count("SELECT COUNT(*) FROM occupation_skills")); + assertEquals(2L, count("SELECT COUNT(*) FROM occupation_skills WHERE importance IS NULL")); + assertEquals(0L, count( + "SELECT COUNT(*) FROM occupation_skills " + + "WHERE importance IS NOT NULL " + + "AND (importance_source_code IS NULL OR TRIM(importance_source_code) = '' " + + "OR importance_element_id IS NULL OR TRIM(importance_element_id) = '')")); + assertEquals(0L, count( + "SELECT COUNT(*) FROM occupation_skills " + + "WHERE importance_is_proxy IS NULL OR importance_is_proxy NOT IN (0, 1)")); + assertEquals(0L, count( + "SELECT COUNT(*) FROM occupation_skills os " + + "JOIN skill_source_versions sv ON sv.id = os.source_version_id " + + "WHERE os.importance IS NOT NULL AND sv.source_name <> 'O*NET'")); + + Map snapshot = loadEvidence("IMPORTANCE"); + List> rows = jdbcTemplate.queryForList( + "SELECT os.id, o.code AS occupation_code, s.code AS skill_code, os.importance, " + + "os.importance_source_code, os.importance_element_id, os.importance_is_proxy " + + "FROM occupation_skills os " + + "JOIN skill_occupations o ON o.id = os.occupation_id " + + "JOIN skills s ON s.id = os.skill_id " + + "WHERE os.importance IS NOT NULL " + + "ORDER BY os.id"); + assertEquals(137, rows.size(), "Unexpected non-null Importance relation count"); + + for (Map row : rows) { + long relationId = number(row.get("id")); + String occupationCode = (String) row.get("occupation_code"); + String skillCode = (String) row.get("skill_code"); + String sourceCode = (String) row.get("importance_source_code"); + String elementId = (String) row.get("importance_element_id"); + BigDecimal importance = (BigDecimal) row.get("importance"); + int proxy = ((Number) row.get("importance_is_proxy")).intValue(); + + assertTrue(proxy == 0 || proxy == 1, + "Invalid importance_is_proxy: relationId=" + relationId + + ", occupationCode=" + occupationCode + ", skillCode=" + skillCode + + ", actual=" + proxy); + assertNotNull(sourceCode, + "Missing Importance SOC: relationId=" + relationId + + ", occupationCode=" + occupationCode + ", skillCode=" + skillCode); + assertNotNull(elementId, + "Missing Importance Element ID: relationId=" + relationId + + ", occupationCode=" + occupationCode + ", skillCode=" + skillCode); + assertNotNull(importance, + "Missing Importance value: relationId=" + relationId + + ", occupationCode=" + occupationCode + ", skillCode=" + skillCode); + + EvidenceRow evidence = snapshot.get(importanceKey(sourceCode, elementId)); + assertNotNull(evidence, + "Missing IMPORTANCE evidence snapshot: relationId=" + relationId + + ", occupationCode=" + occupationCode + ", skillCode=" + skillCode + + ", expectedSource=" + sourceCode + "|" + elementId + + "|" + importance); + assertNotNull(evidence.dataValue(), + "IMPORTANCE snapshot has empty Data Value: relationId=" + relationId + + ", sourceCode=" + sourceCode + ", elementId=" + elementId); + BigDecimal difference = evidence.dataValue().subtract(importance).abs(); + assertTrue(difference.compareTo(IMPORTANCE_TOLERANCE) <= 0, + "Importance mismatch: relationId=" + relationId + + ", occupationCode=" + occupationCode + ", skillCode=" + skillCode + + ", sourceCode=" + sourceCode + ", elementId=" + elementId + + ", expected=" + evidence.dataValue() + ", actual=" + importance + + ", absoluteDifference=" + difference + + ", tolerance=" + IMPORTANCE_TOLERANCE); + } + + assertEquals(rows.stream() + .map(row -> importanceKey( + (String) row.get("importance_source_code"), + (String) row.get("importance_element_id"))) + .distinct() + .count(), + snapshot.size(), + "IMPORTANCE snapshot must contain exactly the referenced unique SOC/Element ID combinations"); + + assertImportance("FRONTEND_DEVELOPER", "JAVASCRIPT", + "15-1254.00", "4.A.3.b.1", "4.88"); + assertImportance("BACKEND_DEVELOPER", "JAVA", + "15-1252.00", "4.A.3.b.1", "4.61"); + assertImportance("QA_ENGINEER", "SELENIUM", + "15-1253.00", "4.A.3.b.1", "4.94"); + assertImportance("DEVOPS_ENGINEER", "DOCKER", + "15-1244.00", "4.A.3.b.1", "4.83"); + assertImportance("FINANCIAL_DATA_ANALYST", "PYTHON", + "13-2031.00", "4.A.3.b.1", "4.48"); + assertImportance("RISK_MODELING_ENGINEER", "PYTHON", + "13-2053.00", "4.A.3.b.1", "4.41"); + assertImportance("QUANTITATIVE_DEVELOPER", "CPP", + "13-2099.01", "4.A.3.b.1", "4.68"); + assertImportance("EDUCATION_PRODUCT_MANAGER", "PROJECT_MANAGEMENT", + "13-1082.00", "4.A.2.b.4", "3.73"); + assertImportance("INSTRUCTIONAL_DESIGNER", "INSTRUCTIONAL_DESIGN", + "25-9031.00", "2.C.6", "4.78"); + assertImportance("EDUCATION_DATA_ANALYST", "PYTHON", + "15-2051.00", "4.A.3.b.1", "4.75"); + + assertNullImportance("FRONTEND_DEVELOPER", "FIGMA"); + assertNullImportance("INSTRUCTIONAL_DESIGNER", "ADOBE_PHOTOSHOP"); + } + + @Test + void evidenceLookupRejectsMissingOrTamperedOnetRows() { + Map software = loadEvidence("SOFTWARE"); + assertTrue(hasSoftware(software, "15-1254.00", "React", "2.E.7.h"), + "Known SOFTWARE evidence row should be present"); + assertFalse(hasSoftware(software, "99-9999.00", "React", "2.E.7.h"), + "Missing SOC must not match SOFTWARE evidence"); + assertFalse(hasSoftware(software, "15-1254.00", "Not React", "2.E.7.h"), + "Missing workplace example must not match SOFTWARE evidence"); + assertFalse(hasSoftware(software, "15-1254.00", "React", "9.Z.9.z"), + "Missing Element ID must not match SOFTWARE evidence"); + + Map importance = loadEvidence("IMPORTANCE"); + EvidenceRow sample = importance.values().iterator().next(); + assertTrue(hasImportance(importance, sample.sourceSoc(), sample.elementId(), sample.dataValue()), + "Known IMPORTANCE evidence row should be present"); + assertFalse(hasImportance(importance, "99-9999.00", sample.elementId(), sample.dataValue()), + "Missing SOC must not match IMPORTANCE evidence"); + assertFalse(hasImportance(importance, sample.sourceSoc(), "9.Z.9.z", sample.dataValue()), + "Missing Element ID must not match IMPORTANCE evidence"); + assertFalse(hasImportance(importance, sample.sourceSoc(), sample.elementId(), + sample.dataValue().add(new BigDecimal("0.01"))), + "Tampered Data Value must not match IMPORTANCE evidence"); + } + + @Test + void skillsHaveChineseNamesAliasesAndTraceableSourceVersions() { + assertEquals(0L, count( + "SELECT COUNT(*) FROM skills WHERE name_zh IS NULL OR TRIM(name_zh) = ''")); + assertEquals(0L, count( + "SELECT COUNT(*) FROM skills WHERE source_code IS NULL OR TRIM(source_code) = ''")); + assertEquals(0L, count( + "SELECT COUNT(*) FROM skills s " + + "WHERE NOT EXISTS (SELECT 1 FROM skill_aliases a WHERE a.skill_id = s.id)")); + assertEquals(0L, count( + "SELECT COUNT(*) FROM (" + + "SELECT normalized_alias FROM skill_aliases " + + "GROUP BY normalized_alias HAVING COUNT(*) > 1" + + ") duplicates")); + + assertEquals(0L, count( + "SELECT COUNT(*) FROM skills s " + + "JOIN skill_source_versions sv ON sv.id = s.source_version_id " + + "WHERE (s.source_type = 'ESCO' AND sv.source_name <> 'ESCO') " + + "OR (s.source_type = 'O*NET' AND sv.source_name <> 'O*NET')")); + } + + @Test + void sourceVersionsMatchTheApprovedManifest() { + assertEquals(1L, count( + "SELECT COUNT(*) FROM skill_source_versions " + + "WHERE source_name = 'ESCO' " + + "AND source_version = '1.2.1 (Release v5.7.0, Last update 10/12/2025)' " + + "AND license = 'Commission Decision 2011/833/EU' " + + "AND attribution_url = 'https://esco.ec.europa.eu/en/use-esco/download'")); + assertEquals(1L, count( + "SELECT COUNT(*) FROM skill_source_versions " + + "WHERE source_name = 'O*NET' " + + "AND source_version = '31.0 Database' " + + "AND license = 'CC BY 4.0' " + + "AND attribution_url = 'https://www.onetcenter.org/database.html'")); + assertEquals(2L, count("SELECT COUNT(*) FROM skill_source_versions")); + } + + @Test + void eachIndustryHasAtLeastOneRepresentativeSkill() { + assertTrue(hasSkillInIndustry("INTERNET", "JAVASCRIPT")); + assertTrue(hasSkillInIndustry("FINANCE", "FINANCIAL_ANALYSIS")); + assertTrue(hasSkillInIndustry("EDUCATION", "INSTRUCTIONAL_DESIGN")); + } + + + private static final String EXPECTED_MAPPINGS_RESOURCE = + "skill-cloud/skill-cloud-expected-mappings.csv"; + private static final String EXPECTED_MAPPINGS_HEADER = + "record_type,entity_key,expected_source_type,expected_source_code," + + "expected_element_id,expected_importance,expected_is_proxy"; + + @Test + void expectedMappingFileMatchesSeedAndSourceEvidenceExactly() { + Map expected = loadExpectedMappings(); + Map softwareEvidence = loadEvidence("SOFTWARE"); + Map importanceEvidence = loadEvidence("IMPORTANCE"); + + assertExpectedMappingCounts(expected); + verifyExpectedMappings(expected, softwareEvidence, importanceEvidence); + assertMappingVerifierRejectsTampering(expected); + } + + private Map loadExpectedMappings() { + Map mappings = new LinkedHashMap<>(); + try (InputStream input = new ClassPathResource(EXPECTED_MAPPINGS_RESOURCE).getInputStream(); + BufferedReader reader = new BufferedReader( + new InputStreamReader(input, StandardCharsets.UTF_8))) { + assertEquals(EXPECTED_MAPPINGS_HEADER, reader.readLine(), + "Unexpected expected-mappings header in classpath:" + EXPECTED_MAPPINGS_RESOURCE); + + String line; + int lineNumber = 1; + while ((line = reader.readLine()) != null) { + lineNumber++; + if (line.isBlank()) { + continue; + } + List values = parseCsvLine(line); + assertEquals(7, values.size(), + "Malformed expected-mappings line " + lineNumber + ": " + line); + String recordType = values.get(0); + String entityKey = values.get(1); + if (recordType.isBlank() || entityKey.isBlank()) { + throw expectedMappingFailure(recordType, entityKey, "row", + "non-empty record_type and entity_key", line); + } + ExpectedMapping mapping = new ExpectedMapping( + recordType, + entityKey, + emptyToNull(values.get(2)), + emptyToNull(values.get(3)), + emptyToNull(values.get(4)), + emptyToBigDecimal(values.get(5), recordType, entityKey), + emptyToInteger(values.get(6), recordType, entityKey)); + String key = expectedKey(recordType, entityKey); + ExpectedMapping previous = mappings.putIfAbsent(key, mapping); + if (previous != null) { + throw expectedMappingFailure(recordType, entityKey, "duplicate", + previous, mapping); + } + } + } catch (AssertionError error) { + throw error; + } catch (Exception exception) { + throw new AssertionError( + "Unable to load expected mappings from classpath:" + EXPECTED_MAPPINGS_RESOURCE, + exception); + } + return mappings; + } + + private void assertExpectedMappingCounts(Map mappings) { + Map expectedCounts = Map.of( + "OCCUPATION_SOURCE", 10, + "SKILL_SOURCE", 72, + "RELATION", 139); + Map actualCounts = new LinkedHashMap<>(); + expectedCounts.keySet().forEach(recordType -> actualCounts.put(recordType, 0)); + for (ExpectedMapping mapping : mappings.values()) { + actualCounts.merge(mapping.recordType(), 1, Integer::sum); + } + for (Map.Entry entry : expectedCounts.entrySet()) { + Integer actual = actualCounts.get(entry.getKey()); + if (!entry.getValue().equals(actual)) { + throw expectedMappingFailure(entry.getKey(), "", "count", + entry.getValue(), actual); + } + } + for (Map.Entry entry : actualCounts.entrySet()) { + if (!expectedCounts.containsKey(entry.getKey())) { + throw expectedMappingFailure(entry.getKey(), "", "record_type", + expectedCounts.keySet(), actualCounts.keySet()); + } + } + } + + private void verifyExpectedMappings( + Map expected, + Map softwareEvidence, + Map importanceEvidence) { + Map actual = loadActualMappings(); + assertSameMappingKeys(expected, actual); + + for (ExpectedMapping expectedMapping : expected.values()) { + assertValidExpectedMapping(expectedMapping); + ActualMapping actualMapping = actual.get(expectedKey( + expectedMapping.recordType(), expectedMapping.entityKey())); + if (actualMapping == null) { + throw expectedMappingFailure(expectedMapping.recordType(), expectedMapping.entityKey(), + "entity", "present", "missing"); + } + assertMappingMatches(expectedMapping, actualMapping); + } + + verifySkillEvidence(expected, softwareEvidence); + verifyRelationEvidence(expected, importanceEvidence); + } + + private void assertMappingVerifierRejectsTampering(Map expected) { + List onetSkills = expected.values().stream() + .filter(mapping -> "SKILL_SOURCE".equals(mapping.recordType())) + .filter(mapping -> "O*NET".equals(mapping.sourceType())) + .toList(); + if (onetSkills.size() < 2) { + throw new AssertionError("Falsifiability self-check requires at least two O*NET skills"); + } + ExpectedMapping firstSkill = onetSkills.get(0); + ExpectedMapping secondSkill = onetSkills.get(1); + ExpectedMapping swappedSource = new ExpectedMapping( + firstSkill.recordType(), firstSkill.entityKey(), firstSkill.sourceType(), + secondSkill.sourceCode(), firstSkill.elementId(), + firstSkill.importance(), firstSkill.isProxy()); + assertThrows(AssertionError.class, + () -> assertMappingMatches(swappedSource, actualFrom(firstSkill)), + "Swapping source_code must fail the generic mapping check"); + + ExpectedMapping weightedRelation = expected.values().stream() + .filter(mapping -> "RELATION".equals(mapping.recordType())) + .filter(mapping -> mapping.importance() != null) + .filter(mapping -> mapping.isProxy() != null) + .findFirst() + .orElseThrow(() -> new AssertionError("No weighted relation for self-check")); + ExpectedMapping toggledProxy = new ExpectedMapping( + weightedRelation.recordType(), weightedRelation.entityKey(), weightedRelation.sourceType(), + weightedRelation.sourceCode(), weightedRelation.elementId(), + weightedRelation.importance(), weightedRelation.isProxy() == 0 ? 1 : 0); + assertThrows(AssertionError.class, + () -> assertMappingMatches(toggledProxy, actualFrom(weightedRelation)), + "Toggling expected_is_proxy must fail the generic mapping check"); + + ExpectedMapping tamperedImportance = new ExpectedMapping( + weightedRelation.recordType(), weightedRelation.entityKey(), weightedRelation.sourceType(), + weightedRelation.sourceCode(), weightedRelation.elementId(), + weightedRelation.importance().add(new BigDecimal("0.01")), + weightedRelation.isProxy()); + assertThrows(AssertionError.class, + () -> assertMappingMatches(tamperedImportance, actualFrom(weightedRelation)), + "Tampering expected_importance must fail the generic mapping check"); + } + + private Map loadActualMappings() { + Map mappings = new LinkedHashMap<>(); + + List> occupationRows = jdbcTemplate.queryForList( + "SELECT o.code AS entity_key, sv.source_name AS source_type, o.source_code " + + "FROM skill_occupations o " + + "JOIN skill_source_versions sv ON sv.id = o.source_version_id"); + for (Map row : occupationRows) { + String entityKey = (String) row.get("entity_key"); + putActual(mappings, "OCCUPATION_SOURCE", entityKey, new ActualMapping( + (String) row.get("source_type"), + (String) row.get("source_code"), + null, + null, + null)); + } + + List> skillRows = jdbcTemplate.queryForList( + "SELECT code AS entity_key, source_type, source_code FROM skills"); + for (Map row : skillRows) { + String entityKey = (String) row.get("entity_key"); + putActual(mappings, "SKILL_SOURCE", entityKey, new ActualMapping( + (String) row.get("source_type"), + (String) row.get("source_code"), + null, + null, + null)); + } + + List> relationRows = jdbcTemplate.queryForList( + "SELECT o.code AS occupation_code, s.code AS skill_code, " + + "sv.source_name AS source_type, os.importance_source_code, " + + "os.importance_element_id, os.importance, os.importance_is_proxy " + + "FROM occupation_skills os " + + "JOIN skill_occupations o ON o.id = os.occupation_id " + + "JOIN skills s ON s.id = os.skill_id " + + "JOIN skill_source_versions sv ON sv.id = os.source_version_id"); + for (Map row : relationRows) { + String entityKey = row.get("occupation_code") + "|" + row.get("skill_code"); + putActual(mappings, "RELATION", entityKey, new ActualMapping( + (String) row.get("source_type"), + (String) row.get("importance_source_code"), + (String) row.get("importance_element_id"), + (BigDecimal) row.get("importance"), + toInteger(row.get("importance_is_proxy")))); + } + return mappings; + } + + private void putActual(Map mappings, String recordType, + String entityKey, ActualMapping mapping) { + String key = expectedKey(recordType, entityKey); + ActualMapping previous = mappings.putIfAbsent(key, mapping); + if (previous != null) { + throw expectedMappingFailure(recordType, entityKey, "duplicate", previous, mapping); + } + } + + private Integer toInteger(Object value) { + return value == null ? null : ((Number) value).intValue(); + } + + private void assertSameMappingKeys( + Map expected, Map actual) { + Set expectedKeys = new TreeSet<>(expected.keySet()); + Set actualKeys = new TreeSet<>(actual.keySet()); + if (!expectedKeys.equals(actualKeys)) { + Set missing = new TreeSet<>(expectedKeys); + missing.removeAll(actualKeys); + if (!missing.isEmpty()) { + String key = missing.iterator().next(); + throw expectedMappingFailure(keyPart(key, 0), keyPart(key, 1), + "entity", "present", "missing"); + } + Set extra = new TreeSet<>(actualKeys); + extra.removeAll(expectedKeys); + String key = extra.iterator().next(); + throw expectedMappingFailure(keyPart(key, 0), keyPart(key, 1), + "entity", "absent", "present"); + } + } + + private String keyPart(String key, int index) { + return key.split("\\u0000", -1)[index]; + } + + private void assertValidExpectedMapping(ExpectedMapping mapping) { + String recordType = mapping.recordType(); + String entityKey = mapping.entityKey(); + if ("OCCUPATION_SOURCE".equals(recordType)) { + assertExpectedField(mapping, "expected_source_type", "ESCO", mapping.sourceType()); + assertExpectedNonNull(mapping, "expected_source_code", mapping.sourceCode()); + assertExpectedField(mapping, "expected_element_id", null, mapping.elementId()); + assertExpectedField(mapping, "expected_importance", null, mapping.importance()); + assertExpectedField(mapping, "expected_is_proxy", null, mapping.isProxy()); + return; + } + if ("SKILL_SOURCE".equals(recordType)) { + if (!"ESCO".equals(mapping.sourceType()) && !"O*NET".equals(mapping.sourceType())) { + throw expectedMappingFailure(recordType, entityKey, "expected_source_type", + "ESCO or O*NET", mapping.sourceType()); + } + assertExpectedNonNull(mapping, "expected_source_code", mapping.sourceCode()); + assertExpectedField(mapping, "expected_element_id", null, mapping.elementId()); + assertExpectedField(mapping, "expected_importance", null, mapping.importance()); + assertExpectedField(mapping, "expected_is_proxy", null, mapping.isProxy()); + return; + } + if ("RELATION".equals(recordType)) { + assertExpectedField(mapping, "expected_source_type", "O*NET", mapping.sourceType()); + if (mapping.isProxy() == null) { + throw expectedMappingFailure(recordType, entityKey, "expected_is_proxy", + "0 or 1", null); + } + boolean hasProvenance = mapping.sourceCode() != null + || mapping.elementId() != null + || mapping.importance() != null; + boolean completeProvenance = mapping.sourceCode() != null + && mapping.elementId() != null + && mapping.importance() != null; + if (hasProvenance != completeProvenance) { + throw expectedMappingFailure(recordType, entityKey, "importance_provenance", + "all empty or all present", + mapping.sourceCode() + "|" + mapping.elementId() + "|" + mapping.importance()); + } + if (!completeProvenance && mapping.isProxy() != 0) { + throw expectedMappingFailure(recordType, entityKey, "expected_is_proxy", + 0, mapping.isProxy()); + } + return; + } + throw expectedMappingFailure(recordType, entityKey, "record_type", + "OCCUPATION_SOURCE|SKILL_SOURCE|RELATION", recordType); + } + + private void assertExpectedNonNull( + ExpectedMapping mapping, String field, Object actual) { + if (actual == null) { + throw expectedMappingFailure( + mapping.recordType(), mapping.entityKey(), field, "non-empty", null); + } + } + + private void assertExpectedField( + ExpectedMapping mapping, String field, Object expected, Object actual) { + if (expected == null ? actual != null : !expected.equals(actual)) { + throw expectedMappingFailure( + mapping.recordType(), mapping.entityKey(), field, expected, actual); + } + } + + private void assertMappingMatches(ExpectedMapping expected, ActualMapping actual) { + assertMappingField(expected.recordType(), expected.entityKey(), + "source_type", expected.sourceType(), actual.sourceType()); + assertMappingField(expected.recordType(), expected.entityKey(), + "source_code", expected.sourceCode(), actual.sourceCode()); + assertMappingField(expected.recordType(), expected.entityKey(), + "element_id", expected.elementId(), actual.elementId()); + + if (expected.importance() == null) { + assertMappingField(expected.recordType(), expected.entityKey(), + "importance", null, actual.importance()); + } else if (actual.importance() == null + || actual.importance().subtract(expected.importance()).abs() + .compareTo(IMPORTANCE_TOLERANCE) > 0) { + throw expectedMappingFailure(expected.recordType(), expected.entityKey(), + "importance", expected.importance(), actual.importance()); + } + + assertMappingField(expected.recordType(), expected.entityKey(), + "importance_is_proxy", expected.isProxy(), actual.isProxy()); + } + + private void assertMappingField( + String recordType, String entityKey, String field, Object expected, Object actual) { + if (expected == null ? actual != null : !expected.equals(actual)) { + throw expectedMappingFailure(recordType, entityKey, field, expected, actual); + } + } + + private void verifySkillEvidence( + Map expected, Map softwareEvidence) { + Set expectedSoftwareKeys = new TreeSet<>(); + for (ExpectedMapping mapping : expected.values()) { + if (!"SKILL_SOURCE".equals(mapping.recordType()) || !"O*NET".equals(mapping.sourceType())) { + continue; + } + String[] parts = mapping.sourceCode().split("\\|", -1); + if (parts.length != 3 + || !ONET_SKILL_SOURCE_PATTERN.matcher(mapping.sourceCode()).matches()) { + throw expectedMappingFailure(mapping.recordType(), mapping.entityKey(), + "expected_source_code", "SOC|Workplace Example|Element ID", mapping.sourceCode()); + } + String key = softwareKey(parts[0], parts[1], parts[2]); + EvidenceRow evidence = softwareEvidence.get(key); + if (evidence == null) { + throw expectedMappingFailure(mapping.recordType(), mapping.entityKey(), + "expected_source_code", "present in onet-31-reference.csv", "missing"); + } + if (!"software_skills.csv".equals(evidence.sourceFile())) { + throw expectedMappingFailure(mapping.recordType(), mapping.entityKey(), + "source_file", "software_skills.csv", evidence.sourceFile()); + } + if (evidence.dataValue() != null) { + throw expectedMappingFailure(mapping.recordType(), mapping.entityKey(), + "data_value", null, evidence.dataValue()); + } + expectedSoftwareKeys.add(key); + } + assertEvidenceKeySet("SOFTWARE", expectedSoftwareKeys, softwareEvidence.keySet()); + } + + private void verifyRelationEvidence( + Map expected, Map importanceEvidence) { + Set expectedImportanceKeys = new TreeSet<>(); + for (ExpectedMapping mapping : expected.values()) { + if (!"RELATION".equals(mapping.recordType()) || mapping.importance() == null) { + continue; + } + String key = importanceKey(mapping.sourceCode(), mapping.elementId()); + EvidenceRow evidence = importanceEvidence.get(key); + if (evidence == null) { + throw expectedMappingFailure(mapping.recordType(), mapping.entityKey(), + "importance_source_code", "present in onet-31-reference.csv", "missing"); + } + if (evidence.dataValue() == null + || evidence.dataValue().subtract(mapping.importance()).abs() + .compareTo(IMPORTANCE_TOLERANCE) > 0) { + throw expectedMappingFailure(mapping.recordType(), mapping.entityKey(), + "expected_importance", mapping.importance(), evidence.dataValue()); + } + expectedImportanceKeys.add(key); + } + assertEvidenceKeySet("IMPORTANCE", expectedImportanceKeys, importanceEvidence.keySet()); + } + + private void assertEvidenceKeySet( + String recordType, Set expectedKeys, Set actualKeys) { + Set missing = new TreeSet<>(expectedKeys); + missing.removeAll(actualKeys); + if (!missing.isEmpty()) { + throw expectedMappingFailure(recordType, missing.iterator().next(), + "evidence_key", "present", "missing"); + } + Set extra = new TreeSet<>(actualKeys); + extra.removeAll(expectedKeys); + if (!extra.isEmpty()) { + throw expectedMappingFailure(recordType, extra.iterator().next(), + "evidence_key", "absent", "present"); + } + } + + private ActualMapping actualFrom(ExpectedMapping mapping) { + return new ActualMapping( + mapping.sourceType(), + mapping.sourceCode(), + mapping.elementId(), + mapping.importance(), + mapping.isProxy()); + } + + private String expectedKey(String recordType, String entityKey) { + return recordType + '\u0000' + entityKey; + } + + private String emptyToNull(String value) { + return value == null || value.isBlank() ? null : value; + } + + private BigDecimal emptyToBigDecimal( + String value, String recordType, String entityKey) { + String normalized = emptyToNull(value); + if (normalized == null) { + return null; + } + try { + return new BigDecimal(normalized); + } catch (NumberFormatException exception) { + throw expectedMappingFailure(recordType, entityKey, "expected_importance", + "decimal or empty", value); + } + } + + private Integer emptyToInteger(String value, String recordType, String entityKey) { + String normalized = emptyToNull(value); + if (normalized == null) { + return null; + } + try { + int parsed = Integer.parseInt(normalized); + if (parsed != 0 && parsed != 1) { + throw new NumberFormatException("proxy must be 0 or 1"); + } + return parsed; + } catch (NumberFormatException exception) { + throw expectedMappingFailure(recordType, entityKey, "expected_is_proxy", + "0 or 1", value); + } + } + + private AssertionError expectedMappingFailure( + String recordType, String entityKey, String field, Object expected, Object actual) { + return new AssertionError( + "Expected mapping mismatch: record_type=" + recordType + + ", entity_key=" + entityKey + + ", field=" + field + + ", expected=" + expected + + ", actual=" + actual); + } + + private record ExpectedMapping( + String recordType, + String entityKey, + String sourceType, + String sourceCode, + String elementId, + BigDecimal importance, + Integer isProxy + ) { + } + + private record ActualMapping( + String sourceType, + String sourceCode, + String elementId, + BigDecimal importance, + Integer isProxy + ) { + } + + private Map loadEvidence(String evidenceType) { + Map rows = new LinkedHashMap<>(); + try (InputStream input = new ClassPathResource(EVIDENCE_RESOURCE).getInputStream(); + BufferedReader reader = new BufferedReader( + new InputStreamReader(input, StandardCharsets.UTF_8))) { + assertEquals(EVIDENCE_HEADER, reader.readLine(), + "Unexpected evidence snapshot header in classpath:" + EVIDENCE_RESOURCE); + + String line; + int lineNumber = 1; + while ((line = reader.readLine()) != null) { + lineNumber++; + if (line.isBlank()) { + continue; + } + List values = parseCsvLine(line); + assertEquals(6, values.size(), + "Malformed evidence snapshot line " + lineNumber + ": " + line); + if (!evidenceType.equals(values.get(0))) { + continue; + } + + String sourceSoc = values.get(1); + String elementId = values.get(2); + BigDecimal dataValue = values.get(3).isEmpty() + ? null : new BigDecimal(values.get(3)); + String workplaceExample = values.get(4); + String sourceFile = values.get(5); + + if ("SOFTWARE".equals(evidenceType)) { + assertEquals("software_skills.csv", sourceFile, + "Unexpected SOFTWARE source file at line " + lineNumber); + } else { + assertTrue(IMPORTANCE_SOURCE_FILES.contains(sourceFile), + "Unexpected IMPORTANCE source file at line " + lineNumber + + ": sourceFile=" + sourceFile); + } + + String key = "SOFTWARE".equals(evidenceType) + ? softwareKey(sourceSoc, workplaceExample, elementId) + : importanceKey(sourceSoc, elementId); + EvidenceRow evidence = new EvidenceRow( + evidenceType, sourceSoc, elementId, dataValue, workplaceExample, sourceFile); + EvidenceRow previous = rows.putIfAbsent(key, evidence); + if (previous != null) { + throw new AssertionError("Duplicate evidence row at line " + lineNumber + + ": evidenceType=" + evidenceType + + ", key=" + key + ", previous=" + previous + ", actual=" + evidence); + } + } + } catch (Exception exception) { + throw new AssertionError( + "Unable to load evidence snapshot from classpath:" + EVIDENCE_RESOURCE, exception); + } + return rows; + } + + private List parseCsvLine(String line) { + List values = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean quoted = false; + for (int index = 0; index < line.length(); index++) { + char character = line.charAt(index); + if (character == '"') { + if (quoted && index + 1 < line.length() && line.charAt(index + 1) == '"') { + current.append('"'); + index++; + } else { + quoted = !quoted; + } + } else if (character == ',' && !quoted) { + values.add(current.toString()); + current.setLength(0); + } else { + current.append(character); + } + } + assertFalse(quoted, "Unclosed quoted field in evidence snapshot line: " + line); + values.add(current.toString()); + return values; + } + + private boolean hasSoftware(Map rows, + String sourceSoc, String workplaceExample, String elementId) { + return rows.containsKey(softwareKey(sourceSoc, workplaceExample, elementId)); + } + + private boolean hasImportance(Map rows, + String sourceSoc, String elementId, BigDecimal expectedValue) { + if (expectedValue == null) { + return false; + } + EvidenceRow evidence = rows.get(importanceKey(sourceSoc, elementId)); + return evidence != null + && evidence.dataValue() != null + && evidence.dataValue().subtract(expectedValue).abs() + .compareTo(IMPORTANCE_TOLERANCE) <= 0; + } + + private String softwareKey(String sourceSoc, String workplaceExample, String elementId) { + return sourceSoc + '\u0000' + workplaceExample + '\u0000' + elementId; + } + + private String importanceKey(String sourceSoc, String elementId) { + return sourceSoc + '\u0000' + elementId; + } + + private void assertOnetSkillSource(String skillCode, String expectedSource) { + assertEquals(1L, count( + "SELECT COUNT(*) FROM skills " + + "WHERE code = ? AND source_type = 'O*NET' AND source_code = ?", + skillCode, expectedSource), "Unexpected O*NET source for " + skillCode); + } + + private void assertImportance(String occupationCode, String skillCode, + String sourceCode, String elementId, String expectedValue) { + Map row = jdbcTemplate.queryForMap( + "SELECT os.importance, os.importance_source_code, os.importance_element_id, " + + "os.importance_is_proxy " + + "FROM occupation_skills os " + + "JOIN skill_occupations o ON o.id = os.occupation_id " + + "JOIN skills s ON s.id = os.skill_id " + + "WHERE o.code = ? AND s.code = ?", + occupationCode, skillCode); + assertEquals(new BigDecimal(expectedValue), ((BigDecimal) row.get("importance")).setScale(2)); + assertEquals(sourceCode, row.get("importance_source_code")); + assertEquals(elementId, row.get("importance_element_id")); + assertEquals(1, ((Number) row.get("importance_is_proxy")).intValue()); + } + + private void assertNullImportance(String occupationCode, String skillCode) { + Map row = jdbcTemplate.queryForMap( + "SELECT os.importance, os.importance_source_code, os.importance_element_id, " + + "os.importance_is_proxy " + + "FROM occupation_skills os " + + "JOIN skill_occupations o ON o.id = os.occupation_id " + + "JOIN skills s ON s.id = os.skill_id " + + "WHERE o.code = ? AND s.code = ?", + occupationCode, skillCode); + assertNull(row.get("importance")); + assertNull(row.get("importance_source_code")); + assertNull(row.get("importance_element_id")); + assertEquals(0, ((Number) row.get("importance_is_proxy")).intValue()); + } + + private boolean hasSkillInIndustry(String industryCode, String skillCode) { + return count( + "SELECT COUNT(*) FROM occupation_skills os " + + "JOIN skill_occupations o ON o.id = os.occupation_id " + + "JOIN skill_industries i ON i.id = o.industry_id " + + "JOIN skills s ON s.id = os.skill_id " + + "WHERE i.code = ? AND s.code = ?", + industryCode, skillCode) > 0L; + } + + private long count(String sql, Object... args) { + Long result = jdbcTemplate.queryForObject(sql, Long.class, args); + assertNotNull(result); + return result; + } + + private long number(Object value) { + assertNotNull(value); + return ((Number) value).longValue(); + } + + private record EvidenceRow( + String evidenceType, + String sourceSoc, + String elementId, + BigDecimal dataValue, + String workplaceExample, + String sourceFile + ) { + } +} diff --git a/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyMigrationTest.java b/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyMigrationTest.java index 1e284e9..2bb97e1 100644 --- a/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyMigrationTest.java +++ b/mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyMigrationTest.java @@ -70,7 +70,7 @@ class SkillTaxonomyMigrationTest { assertTableAndColumns("skill_aliases", "id", "skill_id", "alias", "normalized_alias", "created_at"); assertTableAndColumns("occupation_skills", - "id", "occupation_id", "skill_id", "category_id", "importance", + "id", "occupation_id", "skill_id", "category_id", "importance", "importance_source_code", "importance_element_id", "importance_is_proxy", "relationship_type", "source_version_id", "created_at", "updated_at"); assertUniqueIndex("skill_source_versions", "source_name", "source_version"); @@ -137,7 +137,8 @@ class SkillTaxonomyMigrationTest { occupationId, skillId, categoryId, null, "ESSENTIAL", sourceVersionId); Map joined = jdbcTemplate.queryForMap( - "SELECT s.name_zh AS skill_name, c.name_zh AS category_name, os.importance " + + "SELECT s.name_zh AS skill_name, c.name_zh AS category_name, os.importance, " + + "os.importance_source_code, os.importance_element_id, os.importance_is_proxy " + "FROM occupation_skills os " + "JOIN skills s ON s.id = os.skill_id " + "JOIN skill_categories c ON c.id = os.category_id " + @@ -145,13 +146,24 @@ class SkillTaxonomyMigrationTest { assertEquals("Java", joined.get("skill_name")); assertEquals("编程语言", joined.get("category_name")); assertNull(joined.get("importance")); + assertNull(joined.get("importance_source_code")); + assertNull(joined.get("importance_element_id")); + assertEquals(0, ((Number) joined.get("importance_is_proxy")).intValue()); - jdbcTemplate.update("UPDATE occupation_skills SET importance = ? WHERE id = ?", + assertConstraintFailure("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)); - + jdbcTemplate.update("UPDATE occupation_skills SET importance = ?, " + + "importance_source_code = ?, importance_element_id = ?, importance_is_proxy = ? " + + "WHERE id = ?", + new BigDecimal("4.25"), "15-1252.00", "4.A.3.b.1", 1, occupationSkillId); + Map weighted = jdbcTemplate.queryForMap( + "SELECT importance, importance_source_code, importance_element_id, importance_is_proxy " + + "FROM occupation_skills WHERE id = ?", + occupationSkillId); + assertEquals(new BigDecimal("4.25"), weighted.get("importance")); + assertEquals("15-1252.00", weighted.get("importance_source_code")); + assertEquals("4.A.3.b.1", weighted.get("importance_element_id")); + assertEquals(1, ((Number) weighted.get("importance_is_proxy")).intValue()); assertConstraintFailure( "INSERT INTO skill_source_versions " + "(source_name, source_version, license, attribution_url) VALUES (?, ?, ?, ?)", @@ -188,7 +200,9 @@ class SkillTaxonomyMigrationTest { "INSERT INTO skill_aliases (skill_id, alias, normalized_alias) VALUES (?, ?, ?)", secondSkillId, "Java SE alternate", "java se"); - assertTrue(jdbcTemplate.queryForObject( + assertConstraintFailure( + "UPDATE occupation_skills SET importance_is_proxy = ? WHERE id = ?", + 2, occupationSkillId); assertTrue(jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM skill_aliases WHERE skill_id = ? AND normalized_alias = ?", Integer.class, skillId, "java se") == 1); } diff --git a/mydreamjob-server/src/test/resources/skill-cloud/onet-31-reference.csv b/mydreamjob-server/src/test/resources/skill-cloud/onet-31-reference.csv new file mode 100644 index 0000000..2a4e1b2 --- /dev/null +++ b/mydreamjob-server/src/test/resources/skill-cloud/onet-31-reference.csv @@ -0,0 +1,93 @@ +evidence_type,source_soc,element_id,data_value,workplace_example,source_file +SOFTWARE,11-1011.00,2.E.5.b,,Adobe Acrobat,software_skills.csv +SOFTWARE,11-1021.00,2.E.6.f,,Amazon Web Services AWS software,software_skills.csv +SOFTWARE,11-3021.00,2.E.11.a,,Nagios,software_skills.csv +SOFTWARE,13-1199.07,2.E.14.d,,Nmap,software_skills.csv +SOFTWARE,15-1241.01,2.E.11.a,,Wireshark,software_skills.csv +SOFTWARE,15-1252.00,2.E.13.c,,Bash,software_skills.csv +SOFTWARE,15-1252.00,2.E.7.e,,C++,software_skills.csv +SOFTWARE,15-1252.00,2.E.7.h,,Django,software_skills.csv +SOFTWARE,15-1252.00,2.E.10.a,,Docker,software_skills.csv +SOFTWARE,15-1252.00,2.E.5.c,,Git,software_skills.csv +SOFTWARE,15-1252.00,2.E.7.b,,Go,software_skills.csv +SOFTWARE,15-1252.00,2.E.7.e,,Gradle,software_skills.csv +SOFTWARE,15-1252.00,2.E.10.a,,Kubernetes,software_skills.csv +SOFTWARE,15-1252.00,2.E.6.d,,MongoDB,software_skills.csv +SOFTWARE,15-1252.00,2.E.6.d,,Redis,software_skills.csv +SOFTWARE,15-1252.00,2.E.7.f,,SonarQube,software_skills.csv +SOFTWARE,15-1252.00,2.E.10.a,,Spring Boot,software_skills.csv +SOFTWARE,15-1253.00,2.E.7.e,,Apache JMeter,software_skills.csv +SOFTWARE,15-1253.00,2.E.7.f,,JUnit,software_skills.csv +SOFTWARE,15-1253.00,2.E.13.c,,Postman,software_skills.csv +SOFTWARE,15-1253.00,2.E.7.f,,Selenium,software_skills.csv +SOFTWARE,15-1254.00,2.E.7.d,,Figma,software_skills.csv +SOFTWARE,15-1254.00,2.E.7.h,,Node.js,software_skills.csv +SOFTWARE,15-1254.00,2.E.7.h,,React,software_skills.csv +SOFTWARE,15-1254.00,2.E.7.h,,Vue.js,software_skills.csv +SOFTWARE,15-1255.00,2.E.4.g,,Axure RP,software_skills.csv +SOFTWARE,15-1255.00,2.E.7.a,,Chef,software_skills.csv +SOFTWARE,15-1299.07,2.E.7.b,,Apache Kafka,software_skills.csv +SOFTWARE,15-1299.08,2.E.10.a,,GitLab,software_skills.csv +SOFTWARE,15-2051.00,2.E.6.m,,Apache Spark,software_skills.csv +SOFTWARE,15-2051.00,2.E.7.b,,Flask,software_skills.csv +SOFTWARE,15-2051.00,2.E.4.i,,Microsoft Excel,software_skills.csv +SOFTWARE,15-2051.00,2.E.6.m,,Microsoft Power BI,software_skills.csv +SOFTWARE,15-2051.00,2.E.6.f,,NumPy,software_skills.csv +SOFTWARE,15-2051.00,2.E.6.f,,PyTorch,software_skills.csv +SOFTWARE,15-2051.00,2.E.7.b,,Scikit-learn,software_skills.csv +SOFTWARE,15-2051.00,2.E.6.m,,Tableau,software_skills.csv +SOFTWARE,15-2051.00,2.E.9.d,,TensorFlow,software_skills.csv +SOFTWARE,15-2051.00,2.E.6.f,,pandas,software_skills.csv +SOFTWARE,17-2051.02,2.E.10.e,,Google Chrome,software_skills.csv +SOFTWARE,17-2071.00,2.E.7.f,,Debugging software,software_skills.csv +SOFTWARE,19-3092.00,2.E.4.b,,Adobe Photoshop,software_skills.csv +SOFTWARE,25-2022.00,2.E.8.b,,Moodle,software_skills.csv +SOFTWARE,25-9031.00,2.E.8.b,,Articulate Storyline,software_skills.csv +SOFTWARE,27-4012.00,2.E.4.f,,Microsoft PowerPoint,software_skills.csv +SOFTWARE,41-3031.00,2.E.2.d,,Bloomberg Professional,software_skills.csv +SOFTWARE,41-4011.00,2.E.7.b,,Microsoft Azure software,software_skills.csv +IMPORTANCE,13-1082.00,2.C.6,2.52,,knowledge.csv +IMPORTANCE,13-1082.00,4.A.2.a.4,3.94,,work_activities.csv +IMPORTANCE,13-1082.00,4.A.2.b.4,3.73,,work_activities.csv +IMPORTANCE,13-1082.00,4.A.3.b.1,4.45,,work_activities.csv +IMPORTANCE,13-1082.00,4.A.4.a.2,4.73,,work_activities.csv +IMPORTANCE,13-2031.00,2.C.1.c,4.42,,knowledge.csv +IMPORTANCE,13-2031.00,2.C.4.a,4.03,,knowledge.csv +IMPORTANCE,13-2031.00,4.A.2.a.2,4.44,,work_activities.csv +IMPORTANCE,13-2031.00,4.A.2.a.4,4.01,,work_activities.csv +IMPORTANCE,13-2031.00,4.A.3.b.1,4.48,,work_activities.csv +IMPORTANCE,13-2053.00,2.C.4.a,3.27,,knowledge.csv +IMPORTANCE,13-2053.00,4.A.2.a.2,4.13,,work_activities.csv +IMPORTANCE,13-2053.00,4.A.2.a.3,3.90,,work_activities.csv +IMPORTANCE,13-2053.00,4.A.2.a.4,4.43,,work_activities.csv +IMPORTANCE,13-2053.00,4.A.3.b.1,4.41,,work_activities.csv +IMPORTANCE,13-2099.01,2.C.1.c,4.20,,knowledge.csv +IMPORTANCE,13-2099.01,4.A.1.b.3,3.53,,work_activities.csv +IMPORTANCE,13-2099.01,4.A.2.a.2,4.50,,work_activities.csv +IMPORTANCE,13-2099.01,4.A.2.a.3,3.05,,work_activities.csv +IMPORTANCE,13-2099.01,4.A.3.b.1,4.68,,work_activities.csv +IMPORTANCE,15-1244.00,2.C.8.a,2.34,,knowledge.csv +IMPORTANCE,15-1244.00,4.A.1.a.2,3.60,,work_activities.csv +IMPORTANCE,15-1244.00,4.A.2.a.2,4.03,,work_activities.csv +IMPORTANCE,15-1244.00,4.A.3.b.1,4.83,,work_activities.csv +IMPORTANCE,15-1252.00,2.C.8.a,2.57,,knowledge.csv +IMPORTANCE,15-1252.00,4.A.2.a.2,4.38,,work_activities.csv +IMPORTANCE,15-1252.00,4.A.2.a.3,3.94,,work_activities.csv +IMPORTANCE,15-1252.00,4.A.3.b.1,4.61,,work_activities.csv +IMPORTANCE,15-1253.00,4.A.1.b.3,2.94,,work_activities.csv +IMPORTANCE,15-1253.00,4.A.2.a.2,4.38,,work_activities.csv +IMPORTANCE,15-1253.00,4.A.2.a.3,4.24,,work_activities.csv +IMPORTANCE,15-1253.00,4.A.3.b.1,4.94,,work_activities.csv +IMPORTANCE,15-1254.00,4.A.2.a.3,3.33,,work_activities.csv +IMPORTANCE,15-1254.00,4.A.3.b.1,4.88,,work_activities.csv +IMPORTANCE,15-1254.00,4.A.4.a.2,3.88,,work_activities.csv +IMPORTANCE,15-2051.00,2.C.4.a,4.52,,knowledge.csv +IMPORTANCE,15-2051.00,2.C.6,2.28,,knowledge.csv +IMPORTANCE,15-2051.00,4.A.2.a.2,4.71,,work_activities.csv +IMPORTANCE,15-2051.00,4.A.2.a.4,4.83,,work_activities.csv +IMPORTANCE,15-2051.00,4.A.3.b.1,4.75,,work_activities.csv +IMPORTANCE,25-9031.00,2.C.6,4.78,,knowledge.csv +IMPORTANCE,25-9031.00,4.A.2.a.3,3.83,,work_activities.csv +IMPORTANCE,25-9031.00,4.A.2.a.4,4.19,,work_activities.csv +IMPORTANCE,25-9031.00,4.A.2.b.2,3.72,,work_activities.csv +IMPORTANCE,25-9031.00,4.A.3.b.1,4.45,,work_activities.csv diff --git a/mydreamjob-server/src/test/resources/skill-cloud/skill-cloud-expected-mappings.csv b/mydreamjob-server/src/test/resources/skill-cloud/skill-cloud-expected-mappings.csv new file mode 100644 index 0000000..81238cc --- /dev/null +++ b/mydreamjob-server/src/test/resources/skill-cloud/skill-cloud-expected-mappings.csv @@ -0,0 +1,222 @@ +record_type,entity_key,expected_source_type,expected_source_code,expected_element_id,expected_importance,expected_is_proxy +OCCUPATION_SOURCE,FRONTEND_DEVELOPER,ESCO,http://data.europa.eu/esco/occupation/c40a2919-48a9-40ea-b506-1f34f693496d,,, +OCCUPATION_SOURCE,BACKEND_DEVELOPER,ESCO,http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1,,, +OCCUPATION_SOURCE,QA_ENGINEER,ESCO,http://data.europa.eu/esco/occupation/106f79e4-6264-45f1-9e7a-297435cd684b,,, +OCCUPATION_SOURCE,DEVOPS_ENGINEER,ESCO,http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1,,, +OCCUPATION_SOURCE,FINANCIAL_DATA_ANALYST,ESCO,http://data.europa.eu/esco/occupation/8d586ee9-0ab1-4155-a3b0-2ca786c8e48c,,, +OCCUPATION_SOURCE,RISK_MODELING_ENGINEER,ESCO,http://data.europa.eu/esco/occupation/b09acdda-6680-49d1-a411-129610ee3bc0,,, +OCCUPATION_SOURCE,QUANTITATIVE_DEVELOPER,ESCO,http://data.europa.eu/esco/occupation/f2b15a0e-e65a-438a-affb-29b9d50b77d1,,, +OCCUPATION_SOURCE,EDUCATION_PRODUCT_MANAGER,ESCO,http://data.europa.eu/esco/occupation/a79170ad-0b7b-4697-864f-1748359aa10a,,, +OCCUPATION_SOURCE,INSTRUCTIONAL_DESIGNER,ESCO,http://data.europa.eu/esco/occupation/a9c30651-ccbc-4a80-900c-7880615cdf6e,,, +OCCUPATION_SOURCE,EDUCATION_DATA_ANALYST,ESCO,http://data.europa.eu/esco/occupation/d3edb8f8-3a06-47a0-8fb9-9b212c006aa2,,, +SKILL_SOURCE,JAVA,ESCO,http://data.europa.eu/esco/skill/19a8293b-8e95-4de3-983f-77484079c389,,, +SKILL_SOURCE,PYTHON,ESCO,http://data.europa.eu/esco/skill/ccd0a1d9-afda-43d9-b901-96344886e14d,,, +SKILL_SOURCE,JAVASCRIPT,ESCO,http://data.europa.eu/esco/skill/3cd569a2-4f88-4c1e-9995-8dce8c5e51a7,,, +SKILL_SOURCE,TYPESCRIPT,ESCO,http://data.europa.eu/esco/skill/867137fb-ff1b-4ca3-99f3-cb6969aa2c68,,, +SKILL_SOURCE,SQL,ESCO,http://data.europa.eu/esco/skill/598de5b0-5b58-4ea7-8058-a4bc4d18c742,,, +SKILL_SOURCE,CSHARP,ESCO,http://data.europa.eu/esco/skill/4c016b68-4116-468c-9dc6-42710c239e4a,,, +SKILL_SOURCE,HTML,ESCO,http://data.europa.eu/esco/skill/0af062de-eb43-41e9-9b96-249e2cd22d26,,, +SKILL_SOURCE,CSS,ESCO,http://data.europa.eu/esco/skill/e5d1f825-60ed-4bdd-872a-e748c387f777,,, +SKILL_SOURCE,R,ESCO,http://data.europa.eu/esco/skill/51586df8-1c46-4b47-8583-773cb63bf00b,,, +SKILL_SOURCE,MYSQL,ESCO,http://data.europa.eu/esco/skill/4da171e5-779c-4983-a76f-91c16751e99f,,, +SKILL_SOURCE,POSTGRESQL,ESCO,http://data.europa.eu/esco/skill/a8d07b5a-c1a1-42c6-9d53-db9c7a2ca996,,, +SKILL_SOURCE,STATISTICS,ESCO,http://data.europa.eu/esco/skill/7ee4c2ea-b349-4bd2-81a3-ec31475d4833,,, +SKILL_SOURCE,MACHINE_LEARNING,ESCO,http://data.europa.eu/esco/skill/8369c2d6-c100-4cf6-bd83-9668d8678433,,, +SKILL_SOURCE,FINANCIAL_ANALYSIS,ESCO,http://data.europa.eu/esco/skill/99571e68-801f-49af-a897-5f75996642e1,,, +SKILL_SOURCE,RISK_MANAGEMENT,ESCO,http://data.europa.eu/esco/skill/6eff134b-e34f-4d6e-a6e8-5e47cf2228d0,,, +SKILL_SOURCE,INSTRUCTIONAL_DESIGN,ESCO,http://data.europa.eu/esco/skill/e3145bd7-7f35-44ec-a5dc-4417c5c9ab9a,,, +SKILL_SOURCE,CURRICULUM_DEVELOPMENT,ESCO,http://data.europa.eu/esco/skill/142a5319-60cb-42a2-891f-90ba92af18b1,,, +SKILL_SOURCE,LEARNING_MANAGEMENT_SYSTEMS,ESCO,http://data.europa.eu/esco/skill/6c80d53c-d8c9-41fe-998f-091fca208834,,, +SKILL_SOURCE,EDUCATIONAL_TECHNOLOGY,ESCO,http://data.europa.eu/esco/skill/8a6394d1-f44f-4b0d-81d5-7201fc025ba8,,, +SKILL_SOURCE,COMMUNICATION,ESCO,http://data.europa.eu/esco/skill/15d76317-c71a-4fa2-aadc-2ecc34e627b7,,, +SKILL_SOURCE,CRITICAL_THINKING,ESCO,http://data.europa.eu/esco/skill/a9e6718b-b9d8-44c2-b06e-85ceed9d144c,,, +SKILL_SOURCE,COLLABORATION,ESCO,http://data.europa.eu/esco/skill/082967d4-b94c-4305-aa13-35a6aa8c5c7c,,, +SKILL_SOURCE,PROJECT_MANAGEMENT,ESCO,http://data.europa.eu/esco/skill/7111b95d-0ce3-441a-9d92-4c75d05c4388,,, +SKILL_SOURCE,AGILE,ESCO,http://data.europa.eu/esco/skill/dba46f87-0831-49cd-a1c7-340a653c0221,,, +SKILL_SOURCE,JENKINS,ESCO,http://data.europa.eu/esco/skill/f47a1998-0beb-43be-9f46-380aa4d183da,,, +SKILL_SOURCE,REACT,O*NET,15-1254.00|React|2.E.7.h,,, +SKILL_SOURCE,VUE_JS,O*NET,15-1254.00|Vue.js|2.E.7.h,,, +SKILL_SOURCE,NODE_JS,O*NET,15-1254.00|Node.js|2.E.7.h,,, +SKILL_SOURCE,SPRING_BOOT,O*NET,15-1252.00|Spring Boot|2.E.10.a,,, +SKILL_SOURCE,DJANGO,O*NET,15-1252.00|Django|2.E.7.h,,, +SKILL_SOURCE,FLASK,O*NET,15-2051.00|Flask|2.E.7.b,,, +SKILL_SOURCE,GIT,O*NET,15-1252.00|Git|2.E.5.c,,, +SKILL_SOURCE,DOCKER,O*NET,15-1252.00|Docker|2.E.10.a,,, +SKILL_SOURCE,KUBERNETES,O*NET,15-1252.00|Kubernetes|2.E.10.a,,, +SKILL_SOURCE,GRADLE,O*NET,15-1252.00|Gradle|2.E.7.e,,, +SKILL_SOURCE,MONGODB,O*NET,15-1252.00|MongoDB|2.E.6.d,,, +SKILL_SOURCE,REDIS,O*NET,15-1252.00|Redis|2.E.6.d,,, +SKILL_SOURCE,SELENIUM,O*NET,15-1253.00|Selenium|2.E.7.f,,, +SKILL_SOURCE,JUNIT,O*NET,15-1253.00|JUnit|2.E.7.f,,, +SKILL_SOURCE,POSTMAN,O*NET,15-1253.00|Postman|2.E.13.c,,, +SKILL_SOURCE,PANDAS,O*NET,15-2051.00|pandas|2.E.6.f,,, +SKILL_SOURCE,NUMPY,O*NET,15-2051.00|NumPy|2.E.6.f,,, +SKILL_SOURCE,TABLEAU,O*NET,15-2051.00|Tableau|2.E.6.m,,, +SKILL_SOURCE,POWER_BI,O*NET,15-2051.00|Microsoft Power BI|2.E.6.m,,, +SKILL_SOURCE,MICROSOFT_EXCEL,O*NET,15-2051.00|Microsoft Excel|2.E.4.i,,, +SKILL_SOURCE,TENSORFLOW,O*NET,15-2051.00|TensorFlow|2.E.9.d,,, +SKILL_SOURCE,PYTORCH,O*NET,15-2051.00|PyTorch|2.E.6.f,,, +SKILL_SOURCE,SCIKIT_LEARN,O*NET,15-2051.00|Scikit-learn|2.E.7.b,,, +SKILL_SOURCE,APACHE_SPARK,O*NET,15-2051.00|Apache Spark|2.E.6.m,,, +SKILL_SOURCE,FIGMA,O*NET,15-1254.00|Figma|2.E.7.d,,, +SKILL_SOURCE,BASH,O*NET,15-1252.00|Bash|2.E.13.c,,, +SKILL_SOURCE,GO,O*NET,15-1252.00|Go|2.E.7.b,,, +SKILL_SOURCE,CPP,O*NET,15-1252.00|C++|2.E.7.e,,, +SKILL_SOURCE,GOOGLE_CHROME,O*NET,17-2051.02|Google Chrome|2.E.10.e,,, +SKILL_SOURCE,DEBUGGING_SOFTWARE,O*NET,17-2071.00|Debugging software|2.E.7.f,,, +SKILL_SOURCE,APACHE_KAFKA,O*NET,15-1299.07|Apache Kafka|2.E.7.b,,, +SKILL_SOURCE,SONARQUBE,O*NET,15-1252.00|SonarQube|2.E.7.f,,, +SKILL_SOURCE,CHEF,O*NET,15-1255.00|Chef|2.E.7.a,,, +SKILL_SOURCE,GITLAB,O*NET,15-1299.08|GitLab|2.E.10.a,,, +SKILL_SOURCE,NAGIOS,O*NET,11-3021.00|Nagios|2.E.11.a,,, +SKILL_SOURCE,NMAP,O*NET,13-1199.07|Nmap|2.E.14.d,,, +SKILL_SOURCE,BLOOMBERG_PROFESSIONAL,O*NET,41-3031.00|Bloomberg Professional|2.E.2.d,,, +SKILL_SOURCE,MOODLE,O*NET,25-2022.00|Moodle|2.E.8.b,,, +SKILL_SOURCE,ARTICULATE_STORYLINE,O*NET,25-9031.00|Articulate Storyline|2.E.8.b,,, +SKILL_SOURCE,MICROSOFT_POWERPOINT,O*NET,27-4012.00|Microsoft PowerPoint|2.E.4.f,,, +SKILL_SOURCE,AXURE_RP,O*NET,15-1255.00|Axure RP|2.E.4.g,,, +SKILL_SOURCE,ADOBE_PHOTOSHOP,O*NET,19-3092.00|Adobe Photoshop|2.E.4.b,,, +SKILL_SOURCE,APACHE_JMETER,O*NET,15-1253.00|Apache JMeter|2.E.7.e,,, +SKILL_SOURCE,ADOBE_ACROBAT,O*NET,11-1011.00|Adobe Acrobat|2.E.5.b,,, +SKILL_SOURCE,MICROSOFT_AZURE,O*NET,41-4011.00|Microsoft Azure software|2.E.7.b,,, +SKILL_SOURCE,AWS,O*NET,11-1021.00|Amazon Web Services AWS software|2.E.6.f,,, +SKILL_SOURCE,WIRESHARK,O*NET,15-1241.01|Wireshark|2.E.11.a,,, +RELATION,FRONTEND_DEVELOPER|JAVASCRIPT,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|TYPESCRIPT,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|HTML,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|CSS,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|REACT,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|VUE_JS,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|NODE_JS,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|GRADLE,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|GOOGLE_CHROME,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|DEBUGGING_SOFTWARE,O*NET,15-1254.00,4.A.3.b.1,4.88,1 +RELATION,FRONTEND_DEVELOPER|SELENIUM,O*NET,15-1254.00,4.A.2.a.3,3.33,1 +RELATION,FRONTEND_DEVELOPER|JUNIT,O*NET,15-1254.00,4.A.2.a.3,3.33,1 +RELATION,FRONTEND_DEVELOPER|GIT,O*NET,15-1254.00,4.A.4.a.2,3.88,1 +RELATION,FRONTEND_DEVELOPER|AGILE,O*NET,15-1254.00,4.A.4.a.2,3.88,1 +RELATION,BACKEND_DEVELOPER|JAVA,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|PYTHON,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|SPRING_BOOT,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|DJANGO,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|MYSQL,O*NET,15-1252.00,4.A.2.a.2,4.38,1 +RELATION,BACKEND_DEVELOPER|POSTGRESQL,O*NET,15-1252.00,4.A.2.a.2,4.38,1 +RELATION,BACKEND_DEVELOPER|REDIS,O*NET,15-1252.00,4.A.2.a.2,4.38,1 +RELATION,BACKEND_DEVELOPER|APACHE_KAFKA,O*NET,15-1252.00,4.A.2.a.2,4.38,1 +RELATION,BACKEND_DEVELOPER|DOCKER,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|KUBERNETES,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|JUNIT,O*NET,15-1252.00,4.A.2.a.3,3.94,1 +RELATION,BACKEND_DEVELOPER|POSTMAN,O*NET,15-1252.00,4.A.2.a.3,3.94,1 +RELATION,BACKEND_DEVELOPER|GIT,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|GRADLE,O*NET,15-1252.00,4.A.3.b.1,4.61,1 +RELATION,BACKEND_DEVELOPER|NMAP,O*NET,15-1252.00,2.C.8.a,2.57,1 +RELATION,BACKEND_DEVELOPER|SONARQUBE,O*NET,15-1252.00,2.C.8.a,2.57,1 +RELATION,QA_ENGINEER|JAVA,O*NET,15-1253.00,4.A.3.b.1,4.94,1 +RELATION,QA_ENGINEER|PYTHON,O*NET,15-1253.00,4.A.3.b.1,4.94,1 +RELATION,QA_ENGINEER|POSTMAN,O*NET,15-1253.00,4.A.2.a.3,4.24,1 +RELATION,QA_ENGINEER|CRITICAL_THINKING,O*NET,15-1253.00,4.A.2.a.3,4.24,1 +RELATION,QA_ENGINEER|SELENIUM,O*NET,15-1253.00,4.A.3.b.1,4.94,1 +RELATION,QA_ENGINEER|JUNIT,O*NET,15-1253.00,4.A.3.b.1,4.94,1 +RELATION,QA_ENGINEER|SQL,O*NET,15-1253.00,4.A.2.a.2,4.38,1 +RELATION,QA_ENGINEER|MYSQL,O*NET,15-1253.00,4.A.2.a.2,4.38,1 +RELATION,QA_ENGINEER|APACHE_JMETER,O*NET,15-1253.00,4.A.1.b.3,2.94,1 +RELATION,QA_ENGINEER|BASH,O*NET,15-1253.00,4.A.1.b.3,2.94,1 +RELATION,QA_ENGINEER|JENKINS,O*NET,15-1253.00,4.A.3.b.1,4.94,1 +RELATION,QA_ENGINEER|GITLAB,O*NET,15-1253.00,4.A.3.b.1,4.94,1 +RELATION,DEVOPS_ENGINEER|PYTHON,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|BASH,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|AWS,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|MICROSOFT_AZURE,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|DOCKER,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|KUBERNETES,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|JENKINS,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|GITLAB,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|CHEF,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|GIT,O*NET,15-1244.00,4.A.3.b.1,4.83,1 +RELATION,DEVOPS_ENGINEER|NAGIOS,O*NET,15-1244.00,4.A.1.a.2,3.60,1 +RELATION,DEVOPS_ENGINEER|WIRESHARK,O*NET,15-1244.00,4.A.1.a.2,3.60,1 +RELATION,DEVOPS_ENGINEER|NMAP,O*NET,15-1244.00,2.C.8.a,2.34,1 +RELATION,DEVOPS_ENGINEER|SONARQUBE,O*NET,15-1244.00,2.C.8.a,2.34,1 +RELATION,DEVOPS_ENGINEER|MYSQL,O*NET,15-1244.00,4.A.2.a.2,4.03,1 +RELATION,DEVOPS_ENGINEER|POSTGRESQL,O*NET,15-1244.00,4.A.2.a.2,4.03,1 +RELATION,FINANCIAL_DATA_ANALYST|PYTHON,O*NET,13-2031.00,4.A.3.b.1,4.48,1 +RELATION,FINANCIAL_DATA_ANALYST|R,O*NET,13-2031.00,4.A.3.b.1,4.48,1 +RELATION,FINANCIAL_DATA_ANALYST|PANDAS,O*NET,13-2031.00,4.A.2.a.4,4.01,1 +RELATION,FINANCIAL_DATA_ANALYST|TABLEAU,O*NET,13-2031.00,4.A.2.a.4,4.01,1 +RELATION,FINANCIAL_DATA_ANALYST|SQL,O*NET,13-2031.00,4.A.2.a.2,4.44,1 +RELATION,FINANCIAL_DATA_ANALYST|POSTGRESQL,O*NET,13-2031.00,4.A.2.a.2,4.44,1 +RELATION,FINANCIAL_DATA_ANALYST|STATISTICS,O*NET,13-2031.00,2.C.4.a,4.03,1 +RELATION,FINANCIAL_DATA_ANALYST|MACHINE_LEARNING,O*NET,13-2031.00,2.C.4.a,4.03,1 +RELATION,FINANCIAL_DATA_ANALYST|BLOOMBERG_PROFESSIONAL,O*NET,13-2031.00,4.A.3.b.1,4.48,1 +RELATION,FINANCIAL_DATA_ANALYST|MICROSOFT_EXCEL,O*NET,13-2031.00,4.A.3.b.1,4.48,1 +RELATION,FINANCIAL_DATA_ANALYST|FINANCIAL_ANALYSIS,O*NET,13-2031.00,2.C.1.c,4.42,1 +RELATION,FINANCIAL_DATA_ANALYST|RISK_MANAGEMENT,O*NET,13-2031.00,2.C.1.c,4.42,1 +RELATION,FINANCIAL_DATA_ANALYST|GIT,O*NET,13-2031.00,4.A.3.b.1,4.48,1 +RELATION,FINANCIAL_DATA_ANALYST|POWER_BI,O*NET,13-2031.00,4.A.3.b.1,4.48,1 +RELATION,RISK_MODELING_ENGINEER|PYTHON,O*NET,13-2053.00,4.A.3.b.1,4.41,1 +RELATION,RISK_MODELING_ENGINEER|R,O*NET,13-2053.00,4.A.3.b.1,4.41,1 +RELATION,RISK_MODELING_ENGINEER|STATISTICS,O*NET,13-2053.00,2.C.4.a,3.27,1 +RELATION,RISK_MODELING_ENGINEER|MACHINE_LEARNING,O*NET,13-2053.00,2.C.4.a,3.27,1 +RELATION,RISK_MODELING_ENGINEER|RISK_MANAGEMENT,O*NET,13-2053.00,4.A.2.a.4,4.43,1 +RELATION,RISK_MODELING_ENGINEER|CRITICAL_THINKING,O*NET,13-2053.00,4.A.2.a.4,4.43,1 +RELATION,RISK_MODELING_ENGINEER|TENSORFLOW,O*NET,13-2053.00,4.A.3.b.1,4.41,1 +RELATION,RISK_MODELING_ENGINEER|SCIKIT_LEARN,O*NET,13-2053.00,4.A.3.b.1,4.41,1 +RELATION,RISK_MODELING_ENGINEER|SQL,O*NET,13-2053.00,4.A.2.a.2,4.13,1 +RELATION,RISK_MODELING_ENGINEER|APACHE_SPARK,O*NET,13-2053.00,4.A.2.a.2,4.13,1 +RELATION,RISK_MODELING_ENGINEER|JUNIT,O*NET,13-2053.00,4.A.2.a.3,3.90,1 +RELATION,RISK_MODELING_ENGINEER|SONARQUBE,O*NET,13-2053.00,4.A.2.a.3,3.90,1 +RELATION,RISK_MODELING_ENGINEER|DOCKER,O*NET,13-2053.00,4.A.3.b.1,4.41,1 +RELATION,RISK_MODELING_ENGINEER|GIT,O*NET,13-2053.00,4.A.3.b.1,4.41,1 +RELATION,QUANTITATIVE_DEVELOPER|PYTHON,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,QUANTITATIVE_DEVELOPER|CPP,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,QUANTITATIVE_DEVELOPER|GO,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,QUANTITATIVE_DEVELOPER|NUMPY,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,QUANTITATIVE_DEVELOPER|SCIKIT_LEARN,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,QUANTITATIVE_DEVELOPER|BLOOMBERG_PROFESSIONAL,O*NET,13-2099.01,2.C.1.c,4.20,1 +RELATION,QUANTITATIVE_DEVELOPER|FINANCIAL_ANALYSIS,O*NET,13-2099.01,2.C.1.c,4.20,1 +RELATION,QUANTITATIVE_DEVELOPER|POSTGRESQL,O*NET,13-2099.01,4.A.2.a.2,4.50,1 +RELATION,QUANTITATIVE_DEVELOPER|SQL,O*NET,13-2099.01,4.A.2.a.2,4.50,1 +RELATION,QUANTITATIVE_DEVELOPER|APACHE_SPARK,O*NET,13-2099.01,4.A.1.b.3,3.53,1 +RELATION,QUANTITATIVE_DEVELOPER|APACHE_KAFKA,O*NET,13-2099.01,4.A.1.b.3,3.53,1 +RELATION,QUANTITATIVE_DEVELOPER|JUNIT,O*NET,13-2099.01,4.A.2.a.3,3.05,1 +RELATION,QUANTITATIVE_DEVELOPER|APACHE_JMETER,O*NET,13-2099.01,4.A.2.a.3,3.05,1 +RELATION,QUANTITATIVE_DEVELOPER|DOCKER,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,QUANTITATIVE_DEVELOPER|KUBERNETES,O*NET,13-2099.01,4.A.3.b.1,4.68,1 +RELATION,EDUCATION_PRODUCT_MANAGER|INSTRUCTIONAL_DESIGN,O*NET,13-1082.00,2.C.6,2.52,1 +RELATION,EDUCATION_PRODUCT_MANAGER|CURRICULUM_DEVELOPMENT,O*NET,13-1082.00,2.C.6,2.52,1 +RELATION,EDUCATION_PRODUCT_MANAGER|AXURE_RP,O*NET,13-1082.00,4.A.2.a.4,3.94,1 +RELATION,EDUCATION_PRODUCT_MANAGER|POWER_BI,O*NET,13-1082.00,4.A.2.a.4,3.94,1 +RELATION,EDUCATION_PRODUCT_MANAGER|PROJECT_MANAGEMENT,O*NET,13-1082.00,4.A.2.b.4,3.73,1 +RELATION,EDUCATION_PRODUCT_MANAGER|AGILE,O*NET,13-1082.00,4.A.2.b.4,3.73,1 +RELATION,EDUCATION_PRODUCT_MANAGER|TABLEAU,O*NET,13-1082.00,4.A.2.a.4,3.94,1 +RELATION,EDUCATION_PRODUCT_MANAGER|PYTHON,O*NET,13-1082.00,4.A.2.a.4,3.94,1 +RELATION,EDUCATION_PRODUCT_MANAGER|LEARNING_MANAGEMENT_SYSTEMS,O*NET,13-1082.00,4.A.3.b.1,4.45,1 +RELATION,EDUCATION_PRODUCT_MANAGER|MOODLE,O*NET,13-1082.00,4.A.3.b.1,4.45,1 +RELATION,EDUCATION_PRODUCT_MANAGER|GIT,O*NET,13-1082.00,4.A.4.a.2,4.73,1 +RELATION,EDUCATION_PRODUCT_MANAGER|MICROSOFT_EXCEL,O*NET,13-1082.00,4.A.4.a.2,4.73,1 +RELATION,INSTRUCTIONAL_DESIGNER|INSTRUCTIONAL_DESIGN,O*NET,25-9031.00,2.C.6,4.78,1 +RELATION,INSTRUCTIONAL_DESIGNER|CURRICULUM_DEVELOPMENT,O*NET,25-9031.00,2.C.6,4.78,1 +RELATION,INSTRUCTIONAL_DESIGNER|LEARNING_MANAGEMENT_SYSTEMS,O*NET,25-9031.00,4.A.3.b.1,4.45,1 +RELATION,INSTRUCTIONAL_DESIGNER|MOODLE,O*NET,25-9031.00,4.A.3.b.1,4.45,1 +RELATION,INSTRUCTIONAL_DESIGNER|ARTICULATE_STORYLINE,O*NET,25-9031.00,4.A.2.b.2,3.72,1 +RELATION,INSTRUCTIONAL_DESIGNER|FIGMA,O*NET,25-9031.00,4.A.2.b.2,3.72,1 +RELATION,INSTRUCTIONAL_DESIGNER|EDUCATIONAL_TECHNOLOGY,O*NET,25-9031.00,4.A.2.a.3,3.83,1 +RELATION,INSTRUCTIONAL_DESIGNER|CRITICAL_THINKING,O*NET,25-9031.00,4.A.2.a.3,3.83,1 +RELATION,INSTRUCTIONAL_DESIGNER|STATISTICS,O*NET,25-9031.00,4.A.2.a.4,4.19,1 +RELATION,INSTRUCTIONAL_DESIGNER|MICROSOFT_EXCEL,O*NET,25-9031.00,4.A.2.a.4,4.19,1 +RELATION,INSTRUCTIONAL_DESIGNER|ADOBE_ACROBAT,O*NET,25-9031.00,4.A.2.a.3,3.83,1 +RELATION,INSTRUCTIONAL_DESIGNER|COMMUNICATION,O*NET,25-9031.00,4.A.2.a.3,3.83,1 +RELATION,EDUCATION_DATA_ANALYST|PYTHON,O*NET,15-2051.00,4.A.3.b.1,4.75,1 +RELATION,EDUCATION_DATA_ANALYST|R,O*NET,15-2051.00,4.A.3.b.1,4.75,1 +RELATION,EDUCATION_DATA_ANALYST|TABLEAU,O*NET,15-2051.00,4.A.2.a.4,4.83,1 +RELATION,EDUCATION_DATA_ANALYST|POWER_BI,O*NET,15-2051.00,4.A.2.a.4,4.83,1 +RELATION,EDUCATION_DATA_ANALYST|SQL,O*NET,15-2051.00,4.A.2.a.2,4.71,1 +RELATION,EDUCATION_DATA_ANALYST|POSTGRESQL,O*NET,15-2051.00,4.A.2.a.2,4.71,1 +RELATION,EDUCATION_DATA_ANALYST|STATISTICS,O*NET,15-2051.00,2.C.4.a,4.52,1 +RELATION,EDUCATION_DATA_ANALYST|MACHINE_LEARNING,O*NET,15-2051.00,2.C.4.a,4.52,1 +RELATION,EDUCATION_DATA_ANALYST|CURRICULUM_DEVELOPMENT,O*NET,15-2051.00,2.C.6,2.28,1 +RELATION,EDUCATION_DATA_ANALYST|EDUCATIONAL_TECHNOLOGY,O*NET,15-2051.00,2.C.6,2.28,1 +RELATION,EDUCATION_DATA_ANALYST|MICROSOFT_EXCEL,O*NET,15-2051.00,4.A.3.b.1,4.75,1 +RELATION,EDUCATION_DATA_ANALYST|MICROSOFT_POWERPOINT,O*NET,15-2051.00,4.A.3.b.1,4.75,1 +RELATION,FRONTEND_DEVELOPER|FIGMA,O*NET,,,,0 +RELATION,INSTRUCTIONAL_DESIGNER|ADOBE_PHOTOSHOP,O*NET,,,,0