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 org.sonar.db.dialect.Dialect;
26 import org.sonar.db.dialect.PostgreSql;
27 import org.sonar.server.platform.db.migration.def.ColumnDef;
29 import static com.google.common.base.Preconditions.checkArgument;
30 import static java.util.Collections.singletonList;
31 import static java.util.Objects.requireNonNull;
32 import static org.sonar.server.platform.db.migration.def.Validations.validateIndexName;
33 import static org.sonar.server.platform.db.migration.def.Validations.validateTableName;
35 public class CreateIndexBuilder {
37 private static final String COLUMN_CANNOT_BE_NULL = "Column cannot be null";
38 private final List<NullableColumn> columns = new ArrayList<>();
39 private final Dialect dialect;
40 private String tableName;
41 private String indexName;
42 private boolean unique = false;
44 public CreateIndexBuilder(Dialect dialect) {
45 this.dialect = dialect;
49 * Required name of table on which index is created
51 public CreateIndexBuilder setTable(String s) {
57 * Required name of index. Name must be unique among all the tables
60 public CreateIndexBuilder setName(String s) {
66 * By default index is NOT UNIQUE (value {@code false}).
68 public CreateIndexBuilder setUnique(boolean b) {
74 * Add a column to the scope of index. Order of calls to this
75 * method is important and is kept as-is when creating the index.
76 * The attribute used from {@link ColumnDef} is the name.
77 * Other attributes are ignored.
79 public CreateIndexBuilder addColumn(ColumnDef column) {
80 requireNonNull(column, COLUMN_CANNOT_BE_NULL);
81 columns.add(new NullableColumn(column.getName(), column.isNullable()));
86 * Add a column to the scope of index. Order of calls to this
87 * method is important and is kept as-is when creating the index.
89 * @deprecated use {@link CreateIndexBuilder#addColumn(String, boolean) instead}
91 @Deprecated(since = "10.3")
92 public CreateIndexBuilder addColumn(String column) {
93 requireNonNull(column, COLUMN_CANNOT_BE_NULL);
94 columns.add(new NullableColumn(column, false));
98 public CreateIndexBuilder addColumn(String column, boolean isNullable) {
99 requireNonNull(column, COLUMN_CANNOT_BE_NULL);
100 columns.add(new NullableColumn(column, isNullable));
104 public List<String> build() {
105 validateTableName(tableName);
106 validateIndexName(indexName);
107 checkArgument(!columns.isEmpty(), "at least one column must be specified");
108 return singletonList(createSqlStatement());
114 private String createSqlStatement() {
115 StringBuilder sql = new StringBuilder("CREATE ");
117 sql.append("UNIQUE ");
118 if (dialect.supportsNullNotDistinct() && !PostgreSql.ID.equals(dialect.getId())) {
119 sql.append("NULLS NOT DISTINCT ");
122 sql.append("INDEX ");
123 sql.append(indexName);
125 sql.append(tableName);
129 * Oldest versions of postgres don't support NULLS NOT DISTINCT, and their default behavior is NULLS DISTINCT.
130 * To make sure we apply the same constraints as other DB vendors, we use coalesce to default to empty string, to ensure unicity constraint.
131 * Other db vendors are not impacted since they fall back to NULLS NOT DISTINCT by default.
133 if (unique && !dialect.supportsNullNotDistinct() && PostgreSql.ID.equals(dialect.getId())) {
134 sql.append(columns.stream()
135 .map(c -> c.isNullable() ? "COALESCE(%s, '')".formatted(c.name()) : c.name())
136 .collect(Collectors.joining(", ")));
138 sql.append(columns.stream()
139 .map(NullableColumn::name)
140 .collect(Collectors.joining(", ")));
145 if (unique && dialect.supportsNullNotDistinct() && PostgreSql.ID.equals(dialect.getId())) {
146 sql.append(" NULLS NOT DISTINCT");
148 return sql.toString();
151 private record NullableColumn(String name, boolean isNullable) {