3 * Copyright (C) 2009-2023 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.platform.db.migration.sql;
22 import java.util.ArrayList;
23 import java.util.List;
24 import java.util.stream.Collectors;
25 import javax.annotation.Nullable;
26 import org.sonar.db.dialect.Dialect;
27 import org.sonar.db.dialect.MsSql;
28 import org.sonar.server.platform.db.migration.def.ColumnDef;
30 import static com.google.common.base.Preconditions.checkArgument;
31 import static com.google.common.base.Preconditions.checkState;
32 import static java.util.Objects.requireNonNull;
33 import static org.sonar.server.platform.db.migration.def.Validations.validateTableName;
36 * Creates a new table based on an existing table.
37 * With Oracle, H2 and PSQL it uses the 'CREATE TABLE [...] AS' statement. This is not supported in SQL Server, so we use 'SELECT [...] INTO [new_table] FROM [old_table]'.
38 * Note that indexes are not kept. Constraints are also not kept except for 'NOT NULL' in some dbs and under certain conditions. Some dbs also allow to specify 'NOT NULL'
39 * constraint or even data type when specifying the new table.
40 * For simplicity, we explicitly add NOT NULL constrains with separate statements for all DBs, since it's a fast operation.
42 public class CreateTableAsBuilder {
43 private final Dialect dialect;
44 private final String tableName;
45 private final String fromTableName;
46 private final List<Column> columns = new ArrayList<>();
48 public CreateTableAsBuilder(Dialect dialect, String tableName, String fromTableName) {
49 this.dialect = requireNonNull(dialect, "dialect can't be null");
50 this.tableName = validateTableName(tableName);
51 this.fromTableName = validateTableName(fromTableName);
52 checkArgument(!tableName.equals(fromTableName), "Table names must be different");
55 public CreateTableAsBuilder addColumn(ColumnDef column) {
56 columns.add(new Column(column, null));
60 public CreateTableAsBuilder addColumnWithCast(ColumnDef column, String castFrom) {
61 columns.add(new Column(column, castFrom));
65 public List<String> build() {
66 checkState(!columns.isEmpty(), "Columns need to be specified");
68 List<String> sql = new ArrayList<>();
70 String select = columns.stream().map(this::toSelect).collect(Collectors.joining(", "));
72 if (dialect.getId().equals(MsSql.ID)) {
73 sql.add("SELECT " + select + " INTO " + tableName + " FROM " + fromTableName);
75 StringBuilder sb = new StringBuilder("CREATE TABLE " + tableName + " (");
76 appendColumnNames(sb);
77 sb.append(") AS (SELECT ").append(select).append(" FROM ").append(fromTableName).append(")");
78 sql.add(sb.toString());
81 List<Column> notNullColumns = columns.stream().filter(c -> !c.definition().isNullable()).collect(Collectors.toList());
82 for (Column c : notNullColumns) {
83 sql.addAll(new AlterColumnsBuilder(dialect, tableName).updateColumn(c.definition()).build());
89 private String toSelect(Column column) {
90 if (column.castFrom() == null) {
91 return column.definition().getName();
93 // Example: CAST (metric_id AS VARCHAR(40)) AS metric_uuid
94 return "CAST (" + column.castFrom() + " AS " + column.definition().generateSqlType(dialect) + ") AS " + column.definition().getName();
97 private void appendColumnNames(StringBuilder res) {
98 res.append(columns.stream().map(c -> c.definition().getName()).collect(Collectors.joining(", ")));
101 private static class Column {
102 private final ColumnDef columnDef;
103 private final String castFrom;
105 public Column(ColumnDef columnDef, @Nullable String castFrom) {
106 this.columnDef = columnDef;
107 this.castFrom = castFrom;
110 private ColumnDef definition() {
114 private String castFrom() {