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