001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006package org.fcrepo.search.impl;
007
008import static java.time.format.DateTimeFormatter.ISO_INSTANT;
009import static java.util.stream.Collectors.toList;
010import static org.fcrepo.common.db.DbPlatform.POSTGRESQL;
011import static org.fcrepo.search.api.Condition.Field.CONTENT_SIZE;
012import static org.fcrepo.search.api.Condition.Field.FEDORA_ID;
013import static org.fcrepo.search.api.Condition.Field.MIME_TYPE;
014import static org.fcrepo.search.api.Condition.Field.RDF_TYPE;
015
016import java.net.URI;
017import java.sql.ResultSet;
018import java.sql.SQLException;
019import java.sql.Timestamp;
020import java.sql.Types;
021import java.time.Instant;
022import java.time.temporal.ChronoUnit;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.concurrent.ConcurrentHashMap;
032
033import javax.annotation.PostConstruct;
034import javax.inject.Inject;
035import javax.sql.DataSource;
036
037import com.google.common.collect.Sets;
038import org.fcrepo.common.db.DbPlatform;
039import org.fcrepo.kernel.api.Transaction;
040import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
041import org.fcrepo.kernel.api.identifiers.FedoraId;
042import org.fcrepo.kernel.api.models.ResourceFactory;
043import org.fcrepo.kernel.api.models.ResourceHeaders;
044import org.fcrepo.search.api.Condition;
045import org.fcrepo.search.api.InvalidQueryException;
046import org.fcrepo.search.api.PaginationInfo;
047import org.fcrepo.search.api.SearchIndex;
048import org.fcrepo.search.api.SearchParameters;
049import org.fcrepo.search.api.SearchResult;
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052import org.springframework.dao.DuplicateKeyException;
053import org.springframework.jdbc.core.RowMapper;
054import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
055import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
056import org.springframework.stereotype.Component;
057import org.springframework.transaction.annotation.Propagation;
058import org.springframework.transaction.annotation.Transactional;
059
060/**
061 * An implementation of the {@link SearchIndex}
062 *
063 * @author dbernstein
064 * @author whikloj
065 */
066@Component("searchIndexImpl")
067public class DbSearchIndexImpl implements SearchIndex {
068    private static final Logger LOGGER = LoggerFactory.getLogger(DbSearchIndexImpl.class);
069
070    private static final String TRANSACTION_ID_COLUMN = "transaction_id";
071    private static final String SIMPLE_SEARCH_TABLE = "simple_search";
072    private static final String SIMPLE_SEARCH_TRANSACTIONS_TABLE = "simple_search_transactions";
073    private static final String SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE = "search_resource_rdf_type_transactions";
074    public static final String SEARCH_RESOURCE_RDF_TYPE_TABLE = "search_resource_rdf_type";
075    public static final String SEARCH_RDF_TYPE_TABLE = "search_rdf_type";
076
077    private static final String FEDORA_ID_COLUMN = "fedora_id";
078    private static final String MODIFIED_COLUMN = "modified";
079    private static final String CREATED_COLUMN = "created";
080    private static final String CONTENT_SIZE_COLUMN = "content_size";
081    private static final String MIME_TYPE_COLUMN = "mime_type";
082    private static final String RESOURCE_ID_COLUMN = "resource_id";
083    public static final String RDF_TYPE_ID_COLUMN = "rdf_type_id";
084    public static final String ID_COLUMN = "id";
085    private static final String OPERATION_COLUMN = "operation";
086    private static final String RDF_TYPE_URI_COLUMN = "rdf_type_uri";
087
088    private static final String FEDORA_ID_PARAM = "fedora_id";
089    private static final String RESOURCE_ID_PARAM = "resource_id";
090    private static final String RDF_TYPE_ID_PARAM = "rdf_type_id";
091    private static final String MODIFIED_PARAM = "modified";
092    private static final String CONTENT_SIZE_PARAM = "content_size";
093    private static final String MIME_TYPE_PARAM = "mime_type";
094    private static final String CREATED_PARAM = "created";
095    public static final String RDF_TYPE_URI_PARAM = "rdf_type_uri";
096    public static final String RESOURCE_SEARCH_ID_PARAM = "resource_search_id";
097
098    public static final String TRANSACTION_ID_PARAM = "transaction_id";
099    private static final String OPERATION_PARAM = "operation";
100
101    private static final String RDF_TYPE_FILTER_SUB_TABLE = ", (SELECT rrt." + RESOURCE_ID_COLUMN + " from " +
102            SEARCH_RESOURCE_RDF_TYPE_TABLE + " rrt, " +
103            SEARCH_RDF_TYPE_TABLE + " rt, " + SIMPLE_SEARCH_TABLE + " s WHERE rrt.rdf_type_id = rt.id and s.id = " +
104            "rrt.resource_id and rt." + RDF_TYPE_URI_COLUMN + " like :" + RDF_TYPE_URI_PARAM +
105            " group by rrt." + RESOURCE_ID_COLUMN + ") r_filter";
106    private static final String RDF_TYPES_SUB_TABLE = ", (SELECT rrt.resource_id,  group_concat_function as rdf_type " +
107            " from " + SEARCH_RESOURCE_RDF_TYPE_TABLE + " rrt, " +
108            "search_rdf_type rt ," + SIMPLE_SEARCH_TABLE + " s " +
109            "WHERE rrt.rdf_type_id = rt.id group by rrt.resource_id) r ";
110
111    private static final String POSTGRES_GROUP_CONCAT_FUNCTION = "STRING_AGG(b.rdf_type_uri, ',')";
112    private static final String DEFAULT_GROUP_CONCAT_FUNCTION = "GROUP_CONCAT(distinct b.rdf_type_uri " +
113            "ORDER BY b.rdf_type_uri ASC SEPARATOR ',')";
114
115    private static final String UPSERT_SIMPLE_SEARCH_TRANSACTION_H2 =
116            "MERGE INTO " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " +
117                    CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," +
118                    FEDORA_ID_COLUMN + "," + OPERATION_COLUMN + ", " + TRANSACTION_ID_COLUMN +
119                    ") KEY (" + FEDORA_ID_COLUMN + ", " + TRANSACTION_ID_COLUMN + ") VALUES ( :" + MODIFIED_PARAM +
120                    ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," +
121                    ":" + FEDORA_ID_PARAM + ", :" + OPERATION_PARAM + ", :" + TRANSACTION_ID_PARAM + ")";
122
123    private static final String UPSERT_SIMPLE_SEARCH_H2 =
124            "MERGE INTO " + SIMPLE_SEARCH_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " +
125                    CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," +
126                    FEDORA_ID_COLUMN + ") KEY (" + FEDORA_ID_COLUMN + ") VALUES ( :" + MODIFIED_PARAM +
127                    ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," +
128                    ":" + FEDORA_ID_PARAM + ")";
129
130    private static final String UPSERT_SIMPLE_SEARCH_TRANSACTION_MYSQL_MARIA =
131            "INSERT INTO " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " +
132                    CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," +
133                    FEDORA_ID_COLUMN + "," + OPERATION_COLUMN + ", " + TRANSACTION_ID_COLUMN +
134                    ")  VALUES ( :" + MODIFIED_PARAM + ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM +
135                    ", :" + MIME_TYPE_PARAM + "," + ":" + FEDORA_ID_PARAM + ", :" + OPERATION_PARAM +
136                    ", :" + TRANSACTION_ID_PARAM + ") ON DUPLICATE KEY " +
137                    "UPDATE " + MODIFIED_COLUMN + " = VALUES(" + MODIFIED_COLUMN + "), " +
138                    CREATED_COLUMN + "= VALUES(" + CREATED_COLUMN + ")," +
139                    CONTENT_SIZE_COLUMN + "= VALUES(" + CONTENT_SIZE_COLUMN + ")," +
140                    MIME_TYPE_COLUMN + "= VALUES(" + MIME_TYPE_COLUMN + ")," +
141                    OPERATION_COLUMN + "= VALUES(" + OPERATION_COLUMN + ")";
142
143    private static final String UPSERT_SIMPLE_SEARCH_MYSQL_MARIA =
144            "INSERT INTO " + SIMPLE_SEARCH_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " +
145                    CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," +
146                    FEDORA_ID_COLUMN + ")  VALUES ( :" + MODIFIED_PARAM + ", :" + CREATED_PARAM +
147                    ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," + ":" + FEDORA_ID_PARAM + ") " +
148                    "ON DUPLICATE KEY UPDATE " + MODIFIED_COLUMN + " = VALUES(" + MODIFIED_COLUMN + "), " +
149                    CREATED_COLUMN + "= VALUES(" + CREATED_COLUMN + ")," +
150                    CONTENT_SIZE_COLUMN + "= VALUES(" + CONTENT_SIZE_COLUMN + ")," +
151                    MIME_TYPE_COLUMN + "= VALUES(" + MIME_TYPE_COLUMN + ")";
152
153    private static final String UPSERT_SIMPLE_SEARCH_TRANSACTION_POSTGRESQL =
154            "INSERT INTO " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " +
155                    CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," +
156                    FEDORA_ID_COLUMN + "," + OPERATION_COLUMN + ", " + TRANSACTION_ID_COLUMN +
157                    ")  VALUES ( :" + MODIFIED_PARAM +
158                    ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," +
159                    ":" + FEDORA_ID_PARAM + ", :" + OPERATION_PARAM + ", :" + TRANSACTION_ID_PARAM + ") ON CONFLICT " +
160                    "( " + FEDORA_ID_COLUMN + ", " + TRANSACTION_ID_COLUMN + ") " +
161                    "DO UPDATE SET " + MODIFIED_COLUMN + " = EXCLUDED." + MODIFIED_COLUMN + ", " +
162                    CREATED_COLUMN + " = EXCLUDED." + CREATED_COLUMN + ", " +
163                    CONTENT_SIZE_COLUMN + " = EXCLUDED." + CONTENT_SIZE_COLUMN + ", " +
164                    MIME_TYPE_COLUMN + " = EXCLUDED." + MIME_TYPE_COLUMN + ", " +
165                    OPERATION_COLUMN + " = EXCLUDED." + OPERATION_COLUMN;
166
167    private static final String UPSERT_SIMPLE_SEARCH_POSTGRESQL =
168            "INSERT INTO " + SIMPLE_SEARCH_TABLE + " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " +
169                    CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + "," +
170                    FEDORA_ID_COLUMN + ")  VALUES ( :" + MODIFIED_PARAM +
171                    ", :" + CREATED_PARAM + ", :" + CONTENT_SIZE_PARAM + ", :" + MIME_TYPE_PARAM + "," +
172                    ":" + FEDORA_ID_PARAM + ") ON CONFLICT ( " + FEDORA_ID_COLUMN + ") " +
173                    "DO UPDATE SET " + MODIFIED_COLUMN + " = EXCLUDED." + MODIFIED_COLUMN + ", " +
174                    CREATED_COLUMN + " = EXCLUDED." + CREATED_COLUMN + ", " +
175                    CONTENT_SIZE_COLUMN + " = EXCLUDED." + CONTENT_SIZE_COLUMN + ", " +
176                    MIME_TYPE_COLUMN + " = EXCLUDED." + MIME_TYPE_COLUMN;
177
178    private static final String UPSERT_COMMIT_SIMPLE_SEARCH_H2 =
179            "MERGE INTO " + SIMPLE_SEARCH_TABLE +
180                    " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + CONTENT_SIZE_COLUMN + "," +
181                    MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN +
182                    ") KEY (" + FEDORA_ID_COLUMN + ") SELECT " + MODIFIED_COLUMN + ", " + CREATED_COLUMN +
183                    ", " + CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + " FROM " +
184                    SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + "= :" +
185                    TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + "='add'";
186
187    private static final String UPSERT_COMMIT_SIMPLE_SEARCH_MYSQL_MARIA = "INSERT INTO " + SIMPLE_SEARCH_TABLE +
188            " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + CONTENT_SIZE_COLUMN + "," +
189            MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN +
190            ") SELECT " + MODIFIED_COLUMN + ", " + CREATED_COLUMN +
191            ", " + CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + " FROM " +
192            SIMPLE_SEARCH_TRANSACTIONS_TABLE + " a WHERE " + TRANSACTION_ID_COLUMN + "= :" +
193            TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + "='add' " +
194            "ON DUPLICATE KEY UPDATE " + MODIFIED_COLUMN + " = a." + MODIFIED_COLUMN + ", " +
195            CREATED_COLUMN + " = a." + CREATED_COLUMN + ", " +
196            CONTENT_SIZE_COLUMN + " = a." + CONTENT_SIZE_COLUMN + ", " +
197            MIME_TYPE_COLUMN + " = a." + MIME_TYPE_COLUMN;
198
199    private static final String UPSERT_COMMIT_SIMPLE_SEARCH_POSTGRESQL = "INSERT INTO " + SIMPLE_SEARCH_TABLE +
200            " (" + MODIFIED_COLUMN + "," + CREATED_COLUMN + ", " + CONTENT_SIZE_COLUMN + "," +
201            MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN +
202            ") SELECT " + MODIFIED_COLUMN + ", " + CREATED_COLUMN +
203            ", " + CONTENT_SIZE_COLUMN + "," + MIME_TYPE_COLUMN + ", " + FEDORA_ID_COLUMN + " FROM " +
204            SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + "= :" +
205            TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + "='add' ON CONFLICT (" + FEDORA_ID_COLUMN + ") " +
206            "DO UPDATE SET " + MODIFIED_COLUMN + " = EXCLUDED." + MODIFIED_COLUMN + ", " +
207            CREATED_COLUMN + " = EXCLUDED." + CREATED_COLUMN + ", " +
208            CONTENT_SIZE_COLUMN + " = EXCLUDED." + CONTENT_SIZE_COLUMN + ", " +
209            MIME_TYPE_COLUMN + " = EXCLUDED." + MIME_TYPE_COLUMN;
210
211    private static final String COMMIT_RDF_TYPES =
212            "INSERT INTO " + SEARCH_RDF_TYPE_TABLE + " (" + RDF_TYPE_URI_COLUMN + ")" +
213                    " SELECT distinct " + RDF_TYPE_URI_COLUMN + " FROM " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE +
214                    " WHERE " + TRANSACTION_ID_COLUMN + "= :" + TRANSACTION_ID_PARAM + " AND " + RDF_TYPE_URI_COLUMN +
215                    " NOT IN (SELECT " + RDF_TYPE_URI_PARAM + " FROM " + SEARCH_RDF_TYPE_TABLE + ")";
216
217    private static final String INSERT_RDF_TYPE =
218            "INSERT INTO " + SEARCH_RDF_TYPE_TABLE + " (" + RDF_TYPE_URI_COLUMN + ")" +
219                    " VALUES (:" + RDF_TYPE_URI_PARAM + ")";
220
221    private static final String INSERT_RDF_TYPE_POSTGRES =
222            "INSERT INTO " + SEARCH_RDF_TYPE_TABLE + " (" + RDF_TYPE_URI_COLUMN + ")" +
223                    " VALUES (:" + RDF_TYPE_URI_PARAM + ")" +
224                    " ON CONFLICT (" + RDF_TYPE_URI_COLUMN + ") DO NOTHING";
225
226    private static final String COMMIT_RDF_TYPE_ASSOCIATIONS =
227            "INSERT INTO " + SEARCH_RESOURCE_RDF_TYPE_TABLE +
228                    " (" + RESOURCE_ID_COLUMN + "," + RDF_TYPE_ID_COLUMN + ")" +
229                    " SELECT a." + ID_COLUMN + ", b." + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + " a, " +
230                    SEARCH_RDF_TYPE_TABLE + " b, " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " c WHERE c." +
231                    TRANSACTION_ID_COLUMN + "= :" + TRANSACTION_ID_PARAM + " AND b." + RDF_TYPE_URI_COLUMN +
232                    "= c." + RDF_TYPE_URI_COLUMN + " AND c." + FEDORA_ID_COLUMN + " = a." + FEDORA_ID_COLUMN +
233                    " GROUP BY a." + ID_COLUMN + ", b." + ID_COLUMN;
234
235    private static final String COMMIT_DELETE_RESOURCES_IN_TRANSACTION =
236            "DELETE FROM " + SIMPLE_SEARCH_TABLE + " WHERE " + FEDORA_ID_COLUMN + " IN (SELECT  " + FEDORA_ID_COLUMN +
237                    " FROM " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = " +
238                    ":" + TRANSACTION_ID_PARAM + " AND " + OPERATION_COLUMN + " = 'delete')";
239
240    private static final String COMMIT_DELETE_RDF_TYPE_ASSOCIATIONS =
241            "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TABLE + " where " +
242                    RESOURCE_ID_COLUMN + " in (SELECT a." + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + " a, " +
243                    SIMPLE_SEARCH_TRANSACTIONS_TABLE + " b " +
244                    " WHERE a." + FEDORA_ID_COLUMN + "= b." + FEDORA_ID_COLUMN + " AND b." + TRANSACTION_ID_COLUMN +
245                    "= :" + TRANSACTION_ID_PARAM + ")";
246
247    private static final String DELETE_TRANSACTION =
248            "DELETE FROM " + SIMPLE_SEARCH_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :" +
249                    TRANSACTION_ID_PARAM;
250
251    private static final String DELETE_RESOURCE_FROM_SEARCH =
252            "DELETE FROM " + SIMPLE_SEARCH_TABLE + " WHERE " + FEDORA_ID_COLUMN + " = :" +
253                    FEDORA_ID_PARAM;
254
255    private static final String DELETE_RDF_TYPE_ASSOCIATIONS_IN_TRANSACTION =
256            "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " WHERE " + TRANSACTION_ID_COLUMN + " = :" +
257                    TRANSACTION_ID_PARAM;
258
259
260    private static final Map<DbPlatform, String> DIRECT_UPSERT_MAPPING = Map.of(
261            DbPlatform.H2, UPSERT_SIMPLE_SEARCH_H2,
262            DbPlatform.MYSQL, UPSERT_SIMPLE_SEARCH_MYSQL_MARIA,
263            DbPlatform.MARIADB, UPSERT_SIMPLE_SEARCH_MYSQL_MARIA,
264            DbPlatform.POSTGRESQL, UPSERT_SIMPLE_SEARCH_POSTGRESQL
265    );
266
267    private static final Map<DbPlatform, String> TRANSACTION_UPSERT_MAPPING = Map.of(
268            DbPlatform.H2, UPSERT_SIMPLE_SEARCH_TRANSACTION_H2,
269            DbPlatform.MYSQL, UPSERT_SIMPLE_SEARCH_TRANSACTION_MYSQL_MARIA,
270            DbPlatform.MARIADB, UPSERT_SIMPLE_SEARCH_TRANSACTION_MYSQL_MARIA,
271            DbPlatform.POSTGRESQL, UPSERT_SIMPLE_SEARCH_TRANSACTION_POSTGRESQL
272    );
273
274    private static final Map<DbPlatform, String> UPSERT_COMMIT_MAPPING = Map.of(
275            DbPlatform.H2, UPSERT_COMMIT_SIMPLE_SEARCH_H2,
276            DbPlatform.MYSQL, UPSERT_COMMIT_SIMPLE_SEARCH_MYSQL_MARIA,
277            DbPlatform.MARIADB, UPSERT_COMMIT_SIMPLE_SEARCH_MYSQL_MARIA,
278            DbPlatform.POSTGRESQL, UPSERT_COMMIT_SIMPLE_SEARCH_POSTGRESQL
279    );
280
281    /*
282     * Insert an association between a RDF type and a resource.
283     */
284    private static final String INSERT_RDF_TYPE_ASSOC_IN_TRANSACTION = "INSERT INTO " +
285            SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " (" + FEDORA_ID_COLUMN + ", " + RDF_TYPE_URI_COLUMN + ", " +
286            TRANSACTION_ID_COLUMN + ") VALUES (:" + FEDORA_ID_PARAM + ", :" + RDF_TYPE_URI_PARAM + ", :" +
287            TRANSACTION_ID_PARAM + ")";
288
289    private static final String SELECT_RESOURCE_SEARCH_ID = "SELECT " + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE +
290            " WHERE " + FEDORA_ID_COLUMN + " = :" + FEDORA_ID_PARAM;
291
292    private static final String SELECT_RDF_TYPE_ID = "SELECT " + ID_COLUMN + " FROM " + SEARCH_RDF_TYPE_TABLE +
293            " WHERE " + RDF_TYPE_URI_COLUMN + "= :" + RDF_TYPE_URI_PARAM;
294
295    private static final String INSERT_RDF_TYPE_ASSOC = "INSERT INTO " + SEARCH_RESOURCE_RDF_TYPE_TABLE +
296            " (" + RESOURCE_ID_COLUMN + ", " + RDF_TYPE_ID_COLUMN + ")" +
297            " VALUES (:" + RESOURCE_SEARCH_ID_PARAM + ", :" + RDF_TYPE_ID_PARAM + ")";
298
299    private static final String DELETE_RESOURCE_TYPE_ASSOCIATIONS_IN_TRANSACTION =
300            "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE + " WHERE " +
301                    FEDORA_ID_COLUMN + "= :" + FEDORA_ID_PARAM + " AND " + TRANSACTION_ID_COLUMN + "= :" +
302                    TRANSACTION_ID_PARAM;
303
304    private static final String DELETE_RDF_TYPE_ASSOCIATIONS =
305            "DELETE FROM " + SEARCH_RESOURCE_RDF_TYPE_TABLE + " WHERE " + RESOURCE_ID_COLUMN +
306                    " = (SELECT " + ID_COLUMN + " FROM " + SIMPLE_SEARCH_TABLE + " WHERE " +
307                    FEDORA_ID_COLUMN + " = :" + FEDORA_ID_PARAM + ")";
308
309    private static final List<String> COUNT_QUERY_COLUMNS = Arrays.asList("count(0) as count");
310
311    @Inject
312    private DataSource dataSource;
313
314    private NamedParameterJdbcTemplate jdbcTemplate;
315
316    @Inject
317    private ResourceFactory resourceFactory;
318
319    private DbPlatform dbPlatForm;
320
321    private final Map<URI, Long> rdfTypeIdCache;
322
323    /**
324     * Setup database table and connection
325     */
326    @PostConstruct
327    public void setup() {
328        this.dbPlatForm = DbPlatform.fromDataSource(this.dataSource);
329        this.jdbcTemplate = new NamedParameterJdbcTemplate(this.dataSource);
330    }
331
332    public DbSearchIndexImpl() {
333        this.rdfTypeIdCache = new ConcurrentHashMap<>();
334    }
335
336    @Override
337    public SearchResult doSearch(final SearchParameters parameters) throws InvalidQueryException {
338        //translate parameters into a SQL query
339        final MapSqlParameterSource parameterSource = new MapSqlParameterSource();
340        final var fields = parameters.getFields().stream().map(Condition.Field::toString).collect(toList());
341        final var selectQuery = createSearchQuery(parameters, parameterSource, fields, false);
342        final RowMapper<Map<String, Object>> rowMapper = createRowMapper(fields);
343
344        Integer totalResults = -1;
345        if (parameters.isIncludeTotalResultCount()) {
346            final var countQuery = createSearchQuery(parameters, parameterSource, Collections.emptyList(), true);
347            LOGGER.debug("countQuery={}, parameterSource={}", countQuery, parameterSource);
348            totalResults = jdbcTemplate.queryForObject(countQuery.toString(), parameterSource, Integer.class);
349        }
350
351        final var selectQueryStr = selectQuery.toString();
352        LOGGER.debug("selectQueryStr={}, parameterSource={}", selectQueryStr, parameterSource);
353
354        final List<Map<String, Object>> items = jdbcTemplate.query(selectQueryStr, parameterSource, rowMapper);
355        final var pagination = new PaginationInfo(parameters.getMaxResults(), parameters.getOffset(),
356                (totalResults != null ? totalResults : 0));
357        LOGGER.debug("Search query with parameters: {} - {}", selectQuery.toString(), parameters);
358        return new SearchResult(items, pagination);
359    }
360
361    private RowMapper<Map<String, Object>> createRowMapper(final List<String> fields) {
362        return new RowMapper<Map<String, Object>>() {
363            @Override
364            public Map<String, Object> mapRow(final ResultSet rs, final int rowNum) throws SQLException {
365                final Map<String, Object> map = new HashMap<>();
366                for (final String fieldStr : fields) {
367                    var value = rs.getObject(fieldStr);
368                    if (value instanceof Timestamp) {
369                        //format as iso instant if timestamp
370                        value = ISO_INSTANT.format(Instant.ofEpochMilli(((Timestamp) value).getTime()));
371                    } else if (fieldStr.equals(RDF_TYPE.toString())) {
372                        //convert the comma-separate string to an array for rdf_type
373                        value = value.toString().split(",");
374                    }
375                    map.put(fieldStr, value);
376                }
377                return map;
378            }
379        };
380    }
381
382    private StringBuilder createSearchQuery(final SearchParameters parameters,
383                                            final MapSqlParameterSource parameterSource,
384                                            final List<String> selectedFields, final boolean isCountQuery)
385            throws InvalidQueryException {
386
387        final var queryFields = new ArrayList<>(selectedFields);
388        final var fedoraIdStr = FEDORA_ID.toString();
389
390        if (!isCountQuery) {
391            if (!queryFields.contains(fedoraIdStr)) {
392                queryFields.add(0,fedoraIdStr);
393            }
394            queryFields.add(0,"id");
395        } else {
396            queryFields.add("count(0)");
397        }
398
399        final var whereClauses = new ArrayList<String>();
400        final var conditions = parameters.getConditions();
401        final var fields = new ArrayList<String>(queryFields);
402        final var rdfTypeConditionValue =
403                conditions.stream().filter(c -> c.getField().equals(RDF_TYPE)).findFirst().orElse(null);
404        final var returnRdfType = fields.stream().anyMatch(x -> x.equals(RDF_TYPE.toString()));
405        final var returnFields = fields.stream().filter(x -> !x.equals(RDF_TYPE.toString())).collect(toList());
406
407        final var sql = new StringBuilder("")
408                .append("SELECT ")
409                .append(String.join(",", returnFields));
410        sql.append(" FROM ")
411                .append(SIMPLE_SEARCH_TABLE).append(" s ");
412
413        if (rdfTypeConditionValue != null) {
414            final var rdfTypeOperator = rdfTypeConditionValue.getObject().contains("*") ? " LIKE " : " = ";
415            sql.append(", (SELECT ").append(RESOURCE_ID_COLUMN).append(" FROM ")
416                    .append(SEARCH_RESOURCE_RDF_TYPE_TABLE).append(" WHERE ")
417                    .append(RDF_TYPE_ID_COLUMN).append(" IN (").append("SELECT ID FROM ").append(SEARCH_RDF_TYPE_TABLE)
418                    .append(" WHERE ").append(RDF_TYPE_URI_COLUMN).append(rdfTypeOperator)
419                    .append(":").append(RDF_TYPE_URI_PARAM).append(")) rdf_type_filter ");
420            whereClauses.add("rdf_type_filter.resource_id = s.id");
421            addRdfTypeParam(parameterSource, conditions);
422        }
423
424        for (int i = 0; i < conditions.size(); i++) {
425            addWhereClause(i, parameterSource, whereClauses, conditions.get(i));
426        }
427
428        if (!whereClauses.isEmpty()) {
429            sql.append(" WHERE ");
430            for (final var it = whereClauses.iterator(); it.hasNext(); ) {
431                sql.append(it.next());
432                if (it.hasNext()) {
433                    sql.append(" AND ");
434                }
435            }
436        }
437
438        if (isCountQuery) {
439            return sql;
440        }
441
442        if (parameters.getOrderBy() != null) {
443            //add order by limit and offset to selectquery.
444            sql.append(" ORDER BY ").append(parameters.getOrderBy()).append(" ").append(parameters.getOrder());
445        }
446
447        sql.append(" LIMIT :limit OFFSET :offset");
448        parameterSource.addValue("limit", parameters.getMaxResults());
449        parameterSource.addValue("offset", parameters.getOffset());
450
451        if (!returnRdfType) {
452            return sql;
453        } else {
454            final var rdfTypeWrapperSql = new StringBuilder();
455            rdfTypeWrapperSql.append("SELECT a.*, ")
456                    .append(isPostgres() ? POSTGRES_GROUP_CONCAT_FUNCTION : DEFAULT_GROUP_CONCAT_FUNCTION)
457                    .append(" as rdf_type")
458                    .append(" FROM ")
459                    .append("(").append(sql).append(") a, ")
460                    .append("(SELECT rrt.resource_id , rt.rdf_type_uri FROM search_resource_rdf_type rrt, " +
461                            "search_rdf_type rt WHERE  rrt.rdf_type_id = rt.id) b ")
462                    .append("WHERE a.id = b.resource_id GROUP BY ").append(String.join(",", returnFields));
463
464            if (parameters.getOrderBy() != null) {
465                //add order by limit and offset to selectquery.
466                rdfTypeWrapperSql.append(" ORDER BY ").append(parameters.getOrderBy()).append(" ")
467                        .append(parameters.getOrder());
468            }
469
470            return rdfTypeWrapperSql;
471        }
472    }
473
474    private void addRdfTypeParam(final MapSqlParameterSource parameterSource, final List<Condition> conditions) {
475        var rdfTypeUriParamValue = "*";
476        for (final Condition condition : conditions) {
477            if (condition.getField().equals(RDF_TYPE)) {
478                rdfTypeUriParamValue = condition.getObject();
479                break;
480            }
481        }
482        parameterSource.addValue(RDF_TYPE_URI_PARAM, convertToSqlLikeWildcard(rdfTypeUriParamValue));
483    }
484
485    private void addWhereClause(final int paramCount, final MapSqlParameterSource parameterSource,
486                                final List<String> whereClauses,
487                                final Condition condition) throws InvalidQueryException {
488        final var field = condition.getField();
489        final var operation = condition.getOperator();
490        var object = condition.getObject();
491        final var paramName = "param" + paramCount;
492        if ((field.equals(FEDORA_ID) || field.equals(MIME_TYPE)) &&
493                condition.getOperator().equals(Condition.Operator.EQ)) {
494            if (!object.equals("*")) {
495                final String whereClause;
496                if (object.contains("*")) {
497                    object = convertToSqlLikeWildcard(object);
498                    if (object.contains("_")) {
499                        object = object.replaceAll("_", "\\\\_");
500                    }
501                    whereClause = field + " like :" + paramName;
502                } else {
503                    whereClause = field + " = :" + paramName;
504                }
505
506                whereClauses.add("s." +  whereClause);
507                parameterSource.addValue(paramName, object);
508            }
509        } else if (field.equals(Condition.Field.CREATED) || field.equals(Condition.Field.MODIFIED)) {
510            //parse date
511            try {
512                final var instant = InstantParser.parse(object);
513                whereClauses.add("s." + field + " " + operation.getStringValue() + " :" + paramName);
514                parameterSource.addValue(paramName, new Timestamp(instant.toEpochMilli()), Types.TIMESTAMP);
515            } catch (final Exception ex) {
516                throw new InvalidQueryException(ex.getMessage());
517            }
518        } else if (field.equals(CONTENT_SIZE)) {
519            try {
520                whereClauses.add(field + " " + operation.getStringValue() +
521                        " :" + paramName);
522                parameterSource.addValue(paramName, Long.parseLong(object), Types.INTEGER);
523            } catch (final Exception ex) {
524                throw new InvalidQueryException(ex.getMessage());
525            }
526        } else if (field.equals(RDF_TYPE) && condition.getOperator().equals(Condition.Operator.EQ) ) {
527           //allowed but no where clause added here.
528        } else {
529            throw new InvalidQueryException("Condition not supported: \"" + condition + "\"");
530        }
531    }
532
533    private String convertToSqlLikeWildcard(final String value) {
534        return value.replace("*", "%");
535    }
536
537    @Override
538    public void addUpdateIndex(final Transaction transaction, final ResourceHeaders resourceHeaders) {
539        final var fedoraId = resourceHeaders.getId();
540        if (fedoraId.isAcl() || fedoraId.isMemento()) {
541            LOGGER.debug("The search index does not include acls or mementos. Ignoring resource {}",
542                    fedoraId.getFullId());
543            return;
544        }
545        LOGGER.debug("Updating search index for {}", fedoraId);
546        transaction.doInTx(() -> {
547            if (!transaction.isShortLived()) {
548                doUpsertWithTransaction(transaction, resourceHeaders, fedoraId);
549            } else {
550                doDirectUpsert(transaction, resourceHeaders, fedoraId);
551            }
552        });
553
554    }
555
556    private void doDirectUpsert(final Transaction transaction, final ResourceHeaders resourceHeaders,
557                                final FedoraId fedoraId) {
558        final var fullId = fedoraId.getFullId();
559        try {
560            final var fedoraResource = resourceFactory.getResource(transaction, fedoraId);
561            doUpsertIntoSimpleSearch(fedoraId, resourceHeaders);
562            final var rdfTypes = new ArrayList<>(Sets.newHashSet(fedoraResource.getTypes()));
563            final var newTypes = insertRdfTypes(rdfTypes);
564            deleteRdfTypeAssociations(fedoraId);
565            insertRdfTypeAssociations(rdfTypes, newTypes, fedoraId);
566        } catch (final Exception e) {
567            throw new RepositoryRuntimeException("Failed add/updated the search index for : " + fullId, e);
568        }
569    }
570
571    /**
572     * Adds the list of RDF types to the db, if they aren't already there, and returns a set of types that were
573     * actually added.
574     *
575     * @param rdfTypes the types to attempt to add
576     * @return the types that were added
577     */
578    private Set<URI> insertRdfTypes(final List<URI> rdfTypes) {
579        final var addTypes = new HashSet<URI>();
580
581        rdfTypes.stream()
582                .filter(rdfType -> !rdfTypeIdCache.containsKey(rdfType))
583                .forEach(rdfType -> {
584                    try {
585                        final var params = new MapSqlParameterSource();
586                        params.addValue(RDF_TYPE_URI_PARAM, rdfType.toString());
587                        if (isPostgres()) {
588                            // weirdly, postgres spoils the entire tx on duplicate keys and must be handled differently
589                            jdbcTemplate.update(INSERT_RDF_TYPE_POSTGRES, params);
590                        } else {
591                            jdbcTemplate.update(INSERT_RDF_TYPE, params);
592                        }
593
594                        addTypes.add(rdfType);
595                    } catch (DuplicateKeyException e) {
596                        // ignore duplicate keys
597                    }
598                });
599
600        return addTypes;
601    }
602
603    private void doUpsertWithTransaction(final Transaction transaction, final ResourceHeaders resourceHeaders,
604                                         final FedoraId fedoraId) {
605        final var fullId = fedoraId.getFullId();
606        try {
607            final var txId = transaction.getId();
608            final var fedoraResource = resourceFactory.getResource(transaction, fedoraId);
609            doUpsertIntoTransactionTables(txId, fedoraId, resourceHeaders, "add");
610            // add rdf type associations to the rdf type association table
611            final var rdfTypes = Sets.newHashSet(fedoraResource.getTypes());
612            insertRdfTypeAssociationsInTransaction(rdfTypes, txId, fedoraId);
613        } catch (final Exception e) {
614            throw new RepositoryRuntimeException("Failed add/updated the search index for : " + fullId, e);
615        }
616    }
617
618    /**
619     * Do the upsert action to the transaction table.
620     *
621     * @param txId            the transaction id
622     * @param fedoraId        the resourceId
623     * @param resourceHeaders the resources headers
624     * @param operation       the operation to perform.
625     */
626    private void doUpsertIntoTransactionTables(final String txId, final FedoraId fedoraId,
627                                               final ResourceHeaders resourceHeaders, final String operation) {
628        var mimetype = "";
629        long contentSize = 0;
630        var modified = Instant.now();
631        var created = Instant.now();
632        if (resourceHeaders != null) {
633            contentSize = resourceHeaders.getContentSize();
634            mimetype = resourceHeaders.getMimeType();
635            modified = resourceHeaders.getLastModifiedDate();
636            created = resourceHeaders.getCreatedDate();
637        }
638
639        final var params = new MapSqlParameterSource();
640        params.addValue(FEDORA_ID_PARAM, fedoraId.getFullId());
641        params.addValue(MIME_TYPE_PARAM, mimetype);
642        params.addValue(CONTENT_SIZE_PARAM, contentSize);
643        params.addValue(CREATED_PARAM, formatInstant(created));
644        params.addValue(MODIFIED_PARAM, formatInstant(modified));
645        params.addValue(OPERATION_PARAM, operation);
646        params.addValue(TRANSACTION_ID_PARAM, txId);
647        jdbcTemplate.update(TRANSACTION_UPSERT_MAPPING.get(dbPlatForm), params);
648    }
649
650    /**
651     * Do direct upsert into simpl search table.
652     *
653     * @param fedoraId        the resourceId
654     * @param resourceHeaders the resources headers
655     */
656    private void doUpsertIntoSimpleSearch(final FedoraId fedoraId,
657                                          final ResourceHeaders resourceHeaders) {
658        var mimetype = "";
659        long contentSize = 0;
660        var modified = Instant.now();
661        var created = Instant.now();
662        if (resourceHeaders != null) {
663            contentSize = resourceHeaders.getContentSize();
664            mimetype = resourceHeaders.getMimeType();
665            modified = resourceHeaders.getLastModifiedDate();
666            created = resourceHeaders.getCreatedDate();
667        }
668
669        final var params = new MapSqlParameterSource();
670        params.addValue(FEDORA_ID_PARAM, fedoraId.getFullId());
671        params.addValue(MIME_TYPE_PARAM, mimetype);
672        params.addValue(CONTENT_SIZE_PARAM, contentSize);
673        params.addValue(CREATED_PARAM, formatInstant(created));
674        params.addValue(MODIFIED_PARAM, formatInstant(modified));
675        jdbcTemplate.update(DIRECT_UPSERT_MAPPING.get(dbPlatForm), params);
676    }
677
678    private Timestamp formatInstant(final Instant instant) {
679        if (instant == null) {
680            return null;
681        }
682        return Timestamp.from(instant.truncatedTo(ChronoUnit.MILLIS));
683    }
684
685    private void insertRdfTypeAssociationsInTransaction(final Set<URI> rdfTypes,
686                                                        final String txId,
687                                                        final FedoraId fedoraId) {
688        //remove and add type associations for the fedora id.
689        final List<MapSqlParameterSource> parameterSourcesList = new ArrayList<>(rdfTypes.size());
690        final var parameterSource = new MapSqlParameterSource();
691        parameterSource.addValue(TRANSACTION_ID_PARAM, txId);
692        parameterSource.addValue(FEDORA_ID_PARAM, fedoraId.getFullId());
693        jdbcTemplate.update(DELETE_RESOURCE_TYPE_ASSOCIATIONS_IN_TRANSACTION, parameterSource);
694
695        for (final var rdfType : rdfTypes) {
696            final var assocParams = new MapSqlParameterSource();
697            assocParams.addValue(TRANSACTION_ID_PARAM, txId);
698            assocParams.addValue(FEDORA_ID_PARAM, fedoraId.getFullId());
699            assocParams.addValue(RDF_TYPE_URI_PARAM, rdfType.toString());
700            parameterSourcesList.add(assocParams);
701        }
702        final MapSqlParameterSource[] psArray = parameterSourcesList.toArray(new MapSqlParameterSource[0]);
703        jdbcTemplate.batchUpdate(INSERT_RDF_TYPE_ASSOC_IN_TRANSACTION, psArray);
704    }
705
706    private void deleteRdfTypeAssociations(final FedoraId fedoraId) {
707        final var deleteParams = new MapSqlParameterSource();
708        deleteParams.addValue(FEDORA_ID_PARAM, fedoraId.getFullId());
709        jdbcTemplate.update(DELETE_RDF_TYPE_ASSOCIATIONS,
710                deleteParams);
711    }
712
713    private void insertRdfTypeAssociations(final List<URI> rdfTypes,
714                                           final Set<URI> newTypes,
715                                           final FedoraId fedoraId) {
716        //add rdf type associations
717
718        final var resourceSearchId = jdbcTemplate.queryForObject(
719                SELECT_RESOURCE_SEARCH_ID,
720                Map.of(FEDORA_ID_PARAM, fedoraId.getFullId()), Long.class);
721
722        final List<MapSqlParameterSource> parameterSourcesList = new ArrayList<>();
723        for (final var rdfType : rdfTypes) {
724            final Long rdfTypeId;
725            if (newTypes.contains(rdfType)) {
726                // The cache MUST NOT be used when the current TX created the record as it will not be committed yet
727                // and it will break other transactions.
728                rdfTypeId = getRdfTypeIdDirect(rdfType);
729            } else {
730                rdfTypeId = getRdfTypeIdCached(rdfType);
731            }
732
733            final var assocParams = new MapSqlParameterSource();
734            assocParams.addValue(RESOURCE_SEARCH_ID_PARAM, resourceSearchId);
735            assocParams.addValue(RDF_TYPE_ID_PARAM, rdfTypeId);
736            parameterSourcesList.add(assocParams);
737        }
738
739        final MapSqlParameterSource[] psArray = parameterSourcesList.toArray(new MapSqlParameterSource[0]);
740        jdbcTemplate.batchUpdate(INSERT_RDF_TYPE_ASSOC, psArray);
741    }
742
743    private Long getRdfTypeIdCached(final URI rdfType) {
744        return rdfTypeIdCache.computeIfAbsent(rdfType, this::getRdfTypeIdDirect);
745    }
746
747    private Long getRdfTypeIdDirect(final URI rdfType) {
748        return jdbcTemplate.queryForObject(
749                SELECT_RDF_TYPE_ID,
750                Map.of(RDF_TYPE_URI_PARAM, rdfType.toString()), Long.class);
751    }
752
753    @Override
754    public void removeFromIndex(final Transaction transaction, final FedoraId fedoraId) {
755        transaction.doInTx(() -> {
756            if (!transaction.isShortLived()) {
757                try {
758                    doUpsertIntoTransactionTables(transaction.getId(), fedoraId, null, "delete");
759                } catch (final Exception e) {
760                    throw new RepositoryRuntimeException("Failed to remove " + fedoraId + " from search index", e);
761                }
762            } else {
763                doDirectRemove(fedoraId);
764            }
765        });
766    }
767
768    private void doDirectRemove(final FedoraId fedoraId) {
769        deleteRdfTypeAssociations(fedoraId);
770        deleteResource(fedoraId);
771    }
772
773    private void deleteResource(final FedoraId fedoraId) {
774        final var params = new MapSqlParameterSource();
775        params.addValue(FEDORA_ID_PARAM, fedoraId.getFullId());
776        jdbcTemplate.update(DELETE_RESOURCE_FROM_SEARCH, params);
777    }
778
779    @Override
780    public void reset() {
781        rdfTypeIdCache.clear();
782
783        try (final var conn = this.dataSource.getConnection();
784             final var statement = conn.createStatement()) {
785            for (final var sql : toggleForeignKeyChecks(false)) {
786                statement.addBatch(sql);
787            }
788            statement.addBatch(truncateTable(SEARCH_RESOURCE_RDF_TYPE_TABLE));
789            statement.addBatch(truncateTable(SEARCH_RDF_TYPE_TABLE));
790            statement.addBatch(truncateTable(SIMPLE_SEARCH_TABLE));
791            statement.addBatch(truncateTable(SEARCH_RESOURCE_RDF_TYPE_TRANSACTIONS_TABLE));
792            statement.addBatch(truncateTable(SIMPLE_SEARCH_TRANSACTIONS_TABLE));
793            for (final var sql : toggleForeignKeyChecks(true)) {
794                statement.addBatch(sql);
795            }
796            statement.executeBatch();
797        } catch (final SQLException e) {
798            throw new RepositoryRuntimeException("Failed to truncate search index tables", e);
799        }
800    }
801
802    @Override
803    public void commitTransaction(final Transaction tx) {
804        if (!tx.isShortLived()) {
805            tx.ensureCommitting();
806            final var txId = tx.getId();
807            try {
808                final MapSqlParameterSource parameterSource = new MapSqlParameterSource();
809                parameterSource.addValue(TRANSACTION_ID_PARAM, txId);
810                final int deletedAssociations = jdbcTemplate.update(COMMIT_DELETE_RDF_TYPE_ASSOCIATIONS,
811                        parameterSource);
812                final int deletedResources = jdbcTemplate.update(COMMIT_DELETE_RESOURCES_IN_TRANSACTION,
813                        parameterSource);
814                final int addedRdfTypes = jdbcTemplate.update(COMMIT_RDF_TYPES, parameterSource);
815                final int addedResources = jdbcTemplate.update(UPSERT_COMMIT_MAPPING.get(dbPlatForm),
816                        parameterSource);
817                final int addRdfTypeAssociations = jdbcTemplate.update(COMMIT_RDF_TYPE_ASSOCIATIONS, parameterSource);
818                cleanupTransaction(txId);
819                LOGGER.debug("Commit of tx {} complete with {} resource adds, {} resource associations adds, " +
820                                "{} rdf types adds{},  resource deletes, {} resource/rdf type associations deletes",
821                        txId, addedResources, addRdfTypeAssociations, addedRdfTypes, deletedResources,
822                        deletedAssociations);
823            } catch (final Exception e) {
824                LOGGER.warn("Unable to commit search index transaction {}: {}", txId, e.getMessage());
825                throw new RepositoryRuntimeException("Unable to commit search index transaction", e);
826            }
827        }
828    }
829
830    @Transactional(propagation = Propagation.NOT_SUPPORTED)
831    @Override
832    public void rollbackTransaction(final Transaction tx) {
833        if (!tx.isShortLived()) {
834            cleanupTransaction(tx.getId());
835        }
836    }
837
838    private void cleanupTransaction(final String txId) {
839        final MapSqlParameterSource parameterSource = new MapSqlParameterSource();
840        parameterSource.addValue(TRANSACTION_ID_PARAM, txId);
841        jdbcTemplate.update(DELETE_TRANSACTION, parameterSource);
842        jdbcTemplate.update(DELETE_RDF_TYPE_ASSOCIATIONS_IN_TRANSACTION, parameterSource);
843        LOGGER.debug("Transaction data has been removed from the search transaction tables for txId={} ", txId);
844    }
845
846    private List<String> toggleForeignKeyChecks(final boolean enable) {
847
848        if (isPostgres()) {
849            return Collections.emptyList();
850        } else {
851            return List.of("SET FOREIGN_KEY_CHECKS = " + (enable ? 1 : 0) + ";");
852        }
853    }
854
855    private boolean isPostgres() {
856        return dbPlatForm.equals(POSTGRESQL);
857    }
858
859    private String truncateTable(final String tableName) {
860        final var addCascade = isPostgres();
861        return "TRUNCATE TABLE " + tableName + (addCascade ? " CASCADE" : "") + ";";
862    }
863
864}