001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.migrate.util; 021 022import jakarta.annotation.Nonnull; 023import org.apache.commons.lang3.StringUtils; 024import org.apache.commons.lang3.Validate; 025 026import java.util.Arrays; 027import java.util.List; 028import java.util.Locale; 029import java.util.Optional; 030import java.util.regex.Matcher; 031import java.util.regex.Pattern; 032import java.util.stream.Collectors; 033 034public class SqlUtil { 035 036 /** 037 * This regex parses out the PK of a Postgres CREATE TABLE statement. To work on it, 038 * import it into <a href="https://regex101.com">https://regex101.com</a>. The 039 * raw Regex is: 040 * <pre> 041 * create table ([a-zA-Z0-9_]+).*(\s|[a-zA-Z0-9,()_])+?primary key\s+\(([a-zA-Z_, ]+)\).* 042 * </pre> 043 * A sample testing value is: 044 * <pre> 045 * create table HFJ_IDX_CMB_TOK_NU ( 046 * PID bigint not null, 047 * PARTITION_ID integer not null, 048 * PARTITION_DATE date, 049 * HASH_COMPLETE bigint not null, 050 * IDX_STRING varchar(500) not null, 051 * RES_ID bigint, 052 * primary key (PID, PARTITION_ID) 053 * ); 054 * </pre> 055 */ 056 private static final Pattern CREATE_TABLE = Pattern.compile( 057 "create table ([a-zA-Z0-9_]+).*(\\s|[a-zA-Z0-9,()_])+?primary key\\s+\\(([a-zA-Z_, ]+)\\).*", 058 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); 059 060 /** 061 * This regex parses out the PK of a Postgres ALTER TABLE..ADD CONSTRAINT statement. 062 * To work on it, import it into <a href="https://regex101.com">https://regex101.com</a>. 063 * The raw Regex is: 064 * <pre> 065 * alter table\s+(if exists)?\s+(\w+)\s+add constraint\s+(\w+)\s+foreign key \(([a-zA-Z_, ]+)\)\s+references (\w+).* 066 * </pre> 067 * A sample testing value is: 068 * <pre> 069 * alter table if exists MPI_LINK 070 * add constraint FK_EMPI_LINK_GOLDEN_RESOURCE 071 * foreign key (GOLDEN_RESOURCE_PID, GOLDEN_RESOURCE_PARTITION_ID) 072 * references HFJ_RESOURCE; 073 * </pre> 074 */ 075 private static final Pattern ALTER_TABLE_ADD_CONSTRAINT_FOREIGN_KEY = Pattern.compile( 076 "alter table\\s+(if exists)?\\s+(\\w+)\\s+add constraint\\s+(\\w+)\\s+foreign key \\(([a-zA-Z_, ]+)\\)\\s+references (\\w+).*", 077 Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); 078 079 /** 080 * Non instantiable 081 */ 082 private SqlUtil() { 083 // nothing 084 } 085 086 @Nonnull 087 public static List<String> splitSqlFileIntoStatements(String theSql) { 088 String sqlWithoutComments = Arrays.stream(theSql.split("\n")) 089 .filter(t -> !t.trim().startsWith("--")) 090 .collect(Collectors.joining("\n")); 091 092 return Arrays.stream(sqlWithoutComments.split(";")) 093 .filter(StringUtils::isNotBlank) 094 .map(StringUtils::trim) 095 .collect(Collectors.toList()); 096 } 097 098 /** 099 * Accepts a SQL statement and parses it as a SQL <code>CREATE TABLE</code> 100 * statement, returning details about the table name and primary key. 101 * <b>This method has only been tested for Postgresql DDL format!</b> 102 * 103 * @param theStatement A single SQL statement 104 * @return Returns details about the table name and PK columns if the SQL statement 105 * contains a valid CREATE TABLE statement, returns {@literal null} 106 * otherwise. 107 */ 108 @Nonnull 109 public static Optional<CreateTablePrimaryKey> parseCreateTableStatementPrimaryKey(String theStatement) { 110 Matcher matcher = CREATE_TABLE.matcher(theStatement); 111 if (matcher.find()) { 112 String tableName = matcher.group(1).toUpperCase(Locale.US); 113 String primaryKeyColumnsString = matcher.group(3); 114 List<String> primaryKeyColumns = splitCommaSeparatedList(primaryKeyColumnsString); 115 return Optional.of(new CreateTablePrimaryKey(tableName, primaryKeyColumns)); 116 } 117 return Optional.empty(); 118 } 119 120 /** 121 * Accepts a DDL statement containing 122 * <code>ALTER TABLE [IF EXISTS]? table_name ADD CONSTRAINT constraint_name FOREIGN KEY (column_list)</code> 123 * and returns the parsed details. 124 */ 125 @Nonnull 126 public static Optional<AlterTableAddConstraint> parseAlterTableAddConstraintConstraintForeignKey( 127 String theStatement) { 128 Matcher matcher = ALTER_TABLE_ADD_CONSTRAINT_FOREIGN_KEY.matcher(theStatement); 129 if (matcher.find()) { 130 String tableName = matcher.group(2); 131 String constraintName = matcher.group(3); 132 String columnsString = matcher.group(4); 133 String references = matcher.group(5); 134 List<String> columns = splitCommaSeparatedList(columnsString); 135 136 return Optional.of(new AlterTableAddConstraint(tableName, constraintName, columns, references)); 137 } 138 return Optional.empty(); 139 } 140 141 @Nonnull 142 private static List<String> splitCommaSeparatedList(String primaryKeyColumnsString) { 143 return Arrays.asList(StringUtils.split(primaryKeyColumnsString, ", ")); 144 } 145 146 public static class CreateTablePrimaryKey { 147 private final String myTableName; 148 private final List<String> myPrimaryKeyColumns; 149 150 public CreateTablePrimaryKey(String theTableName, List<String> thePrimaryKeyColumns) { 151 myTableName = theTableName; 152 myPrimaryKeyColumns = thePrimaryKeyColumns; 153 } 154 155 public List<String> getPrimaryKeyColumns() { 156 return myPrimaryKeyColumns; 157 } 158 159 public String getTableName() { 160 return myTableName; 161 } 162 } 163 164 public static class AlterTableAddConstraint { 165 private final String myConstraintName; 166 private final List<String> myColumns; 167 private final String myTableName; 168 private final String myReferences; 169 170 public AlterTableAddConstraint( 171 String theTableName, String theConstraintName, List<String> theColumns, String theReferences) { 172 Validate.isTrue(theTableName.matches("^[a-zA-Z0-9_]+$"), "Invalid table name '%s'", theTableName); 173 Validate.isTrue( 174 theConstraintName.matches("^[a-zA-Z0-9_]+$"), "Invalid constraint name '%s'", theConstraintName); 175 Validate.isTrue(theReferences.matches("^[a-zA-Z0-9_]+$"), "Invalid reference '%s'", theReferences); 176 Validate.isTrue(!theColumns.isEmpty(), "Invalid columns '%s'", theColumns); 177 Validate.isTrue( 178 theColumns.stream() 179 .map(t -> t.matches("^[a-zA-Z0-9_]+$")) 180 .filter(t -> t) 181 .count() 182 == theColumns.size(), 183 "Invalid columns '%s'", 184 theColumns); 185 myTableName = theTableName; 186 myConstraintName = theConstraintName; 187 myColumns = theColumns; 188 myReferences = theReferences; 189 } 190 191 public String getReferences() { 192 return myReferences; 193 } 194 195 public List<String> getColumns() { 196 return myColumns; 197 } 198 199 public String getTableName() { 200 return myTableName; 201 } 202 203 public String getConstraintName() { 204 return myConstraintName; 205 } 206 } 207}