001/*
002 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the Eclipse Public License v1.0
006 * which accompanies this distribution, and is available at
007 * http://www.eclipse.org/legal/epl-v10.html
008 *
009 * Contributors:
010 *     Florent Guillaume
011 */
012package org.nuxeo.ecm.core.storage.sql.jdbc;
013
014import java.io.IOException;
015import java.io.Serializable;
016import java.sql.ResultSet;
017import java.sql.SQLException;
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Set;
028
029import org.nuxeo.common.utils.StringUtils;
030import org.nuxeo.ecm.core.api.NuxeoException;
031import org.nuxeo.ecm.core.query.sql.NXQL;
032import org.nuxeo.ecm.core.storage.FulltextConfiguration;
033import org.nuxeo.ecm.core.storage.sql.ColumnType;
034import org.nuxeo.ecm.core.storage.sql.Mapper;
035import org.nuxeo.ecm.core.storage.sql.Model;
036import org.nuxeo.ecm.core.storage.sql.RepositoryDescriptor;
037import org.nuxeo.ecm.core.storage.sql.Selection;
038import org.nuxeo.ecm.core.storage.sql.SelectionType;
039import org.nuxeo.ecm.core.storage.sql.jdbc.db.Column;
040import org.nuxeo.ecm.core.storage.sql.jdbc.db.Database;
041import org.nuxeo.ecm.core.storage.sql.jdbc.db.Delete;
042import org.nuxeo.ecm.core.storage.sql.jdbc.db.Insert;
043import org.nuxeo.ecm.core.storage.sql.jdbc.db.Join;
044import org.nuxeo.ecm.core.storage.sql.jdbc.db.Select;
045import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table;
046import org.nuxeo.ecm.core.storage.sql.jdbc.db.Update;
047import org.nuxeo.ecm.core.storage.sql.jdbc.db.Table.IndexType;
048import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect;
049import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.SQLStatement;
050
051/**
052 * This singleton generates and holds the actual SQL DDL and DML statements for the operations needed by the
053 * {@link Mapper}, given a {@link Model}.
054 * <p>
055 * It is specific to one SQL dialect.
056 */
057public class SQLInfo {
058
059    private static final String ORDER_DESC = "DESC";
060
061    private static final String ORDER_ASC = "ASC";
062
063    public final Database database;
064
065    public final Dialect dialect;
066
067    public final boolean softDeleteEnabled;
068
069    public final boolean proxiesEnabled;
070
071    private final Model model;
072
073    private String selectRootIdSql;
074
075    private Column selectRootIdWhatColumn;
076
077    private final Map<String, String> insertSqlMap; // statement
078
079    private final Map<String, List<Column>> insertColumnsMap;
080
081    private final Map<String, String> deleteSqlMap; // statement
082
083    private Map<SelectionType, SQLInfoSelection> selections;
084
085    private String selectChildrenIdsAndTypesSql;
086
087    private String selectComplexChildrenIdsAndTypesSql;
088
089    private List<Column> selectChildrenIdsAndTypesWhatColumns;
090
091    private String selectDescendantsInfoSql;
092
093    private List<Column> selectDescendantsInfoWhatColumns;
094
095    private final Map<String, String> copySqlMap;
096
097    private final Map<String, Column> copyIdColumnMap;
098
099    protected final Map<String, SQLInfoSelect> selectFragmentById;
100
101    protected String createClusterNodeSql;
102
103    protected List<Column> createClusterNodeColumns;
104
105    protected String deleteClusterNodeSql;
106
107    protected Column deleteClusterNodeColumn;
108
109    protected String deleteClusterInvalsSql;
110
111    protected Column deleteClusterInvalsColumn;
112
113    protected List<Column> clusterInvalidationsColumns;
114
115    protected Map<String, List<SQLStatement>> sqlStatements;
116
117    protected Map<String, Serializable> sqlStatementsProperties;
118
119    protected List<String> getBinariesSql;
120
121    protected List<Column> getBinariesColumns;
122
123    /**
124     * Generates and holds the needed SQL statements given a {@link Model} and a {@link Dialect}.
125     *
126     * @param model the model
127     * @param dialect the SQL dialect
128     */
129    public SQLInfo(Model model, Dialect dialect) {
130        this.model = model;
131        this.dialect = dialect;
132        RepositoryDescriptor repositoryDescriptor = model.getRepositoryDescriptor();
133        softDeleteEnabled = repositoryDescriptor.getSoftDeleteEnabled();
134        proxiesEnabled = repositoryDescriptor.getProxiesEnabled();
135
136        database = new Database(dialect);
137
138        selectRootIdSql = null;
139        selectRootIdWhatColumn = null;
140
141        selectFragmentById = new HashMap<String, SQLInfoSelect>();
142
143        selections = new HashMap<SelectionType, SQLInfoSelection>();
144
145        selectChildrenIdsAndTypesSql = null;
146        selectChildrenIdsAndTypesWhatColumns = null;
147        selectComplexChildrenIdsAndTypesSql = null;
148
149        insertSqlMap = new HashMap<String, String>();
150        insertColumnsMap = new HashMap<String, List<Column>>();
151
152        deleteSqlMap = new HashMap<String, String>();
153
154        copySqlMap = new HashMap<String, String>();
155        copyIdColumnMap = new HashMap<String, Column>();
156
157        getBinariesSql = new ArrayList<String>(1);
158        getBinariesColumns = new ArrayList<Column>(1);
159
160        initSQL();
161        initSelections();
162
163        try {
164            initSQLStatements(JDBCMapper.testProps, repositoryDescriptor.sqlInitFiles);
165        } catch (IOException e) {
166            throw new NuxeoException(e);
167        }
168    }
169
170    public Database getDatabase() {
171        return database;
172    }
173
174    // ----- select -----
175
176    public String getSelectRootIdSql() {
177        return selectRootIdSql;
178    }
179
180    public Column getSelectRootIdWhatColumn() {
181        return selectRootIdWhatColumn;
182    }
183
184    public String getInsertRootIdSql() {
185        return insertSqlMap.get(model.REPOINFO_TABLE_NAME);
186    }
187
188    public List<Column> getInsertRootIdColumns() {
189        return insertColumnsMap.get(model.REPOINFO_TABLE_NAME);
190    }
191
192    public SQLInfoSelection getSelection(SelectionType type) {
193        return selections.get(type);
194
195    }
196
197    public String getSelectChildrenIdsAndTypesSql(boolean onlyComplex) {
198        return onlyComplex ? selectComplexChildrenIdsAndTypesSql : selectChildrenIdsAndTypesSql;
199    }
200
201    public List<Column> getSelectChildrenIdsAndTypesWhatColumns() {
202        return selectChildrenIdsAndTypesWhatColumns;
203    }
204
205    public String getSelectDescendantsInfoSql() {
206        return selectDescendantsInfoSql;
207    }
208
209    public List<Column> getSelectDescendantsInfoWhatColumns() {
210        return selectDescendantsInfoWhatColumns;
211    }
212
213    // ----- cluster -----
214
215    public String getCreateClusterNodeSql() {
216        return createClusterNodeSql;
217    }
218
219    public List<Column> getCreateClusterNodeColumns() {
220        return createClusterNodeColumns;
221    }
222
223    public String getDeleteClusterNodeSql() {
224        return deleteClusterNodeSql;
225    }
226
227    public Column getDeleteClusterNodeColumn() {
228        return deleteClusterNodeColumn;
229    }
230
231    public String getDeleteClusterInvalsSql() {
232        return deleteClusterInvalsSql;
233    }
234
235    public Column getDeleteClusterInvalsColumn() {
236        return deleteClusterInvalsColumn;
237    }
238
239    public int getClusterNodeIdType() {
240        return dialect.getJDBCTypeAndString(ColumnType.CLUSTERNODE).jdbcType;
241    }
242
243    public List<Column> getClusterInvalidationsColumns() {
244        return clusterInvalidationsColumns;
245    }
246
247    // ----- insert -----
248
249    /**
250     * Returns the SQL {@code INSERT} to add a row. The columns that represent sequences that are implicitly
251     * auto-incremented aren't included.
252     *
253     * @param tableName the table name
254     * @return the SQL {@code INSERT} statement
255     */
256    public String getInsertSql(String tableName) {
257        return insertSqlMap.get(tableName);
258    }
259
260    /**
261     * Returns the list of columns to use for an {@INSERT} statement {@link #getInsertSql}.
262     *
263     * @param tableName the table name
264     * @return the list of columns
265     */
266    public List<Column> getInsertColumns(String tableName) {
267        return insertColumnsMap.get(tableName);
268    }
269
270    // -----
271
272    /**
273     * Returns the clause used to match a given row by id in the given table.
274     * <p>
275     * Takes into account soft deletes.
276     *
277     * @param tableName the table name
278     * @return the clause, like {@code table.id = ?}
279     */
280    public String getIdEqualsClause(String tableName) {
281        return database.getTable(tableName).getColumn(model.MAIN_KEY).getQuotedName() + " = ?"
282                + getSoftDeleteClause(tableName);
283    }
284
285    /**
286     * Returns {@code AND isdeleted IS NULL} if this is the hierarchy table and soft delete is activated.
287     *
288     * @param tableName the table name
289     * @return the clause
290     */
291    public String getSoftDeleteClause(String tableName) {
292        if (model.HIER_TABLE_NAME.equals(tableName) && softDeleteEnabled) {
293            return " AND " + getSoftDeleteClause();
294        } else {
295            return "";
296        }
297    }
298
299    /**
300     * Returns null or {@code AND isdeleted IS NULL} if soft delete is activated.
301     *
302     * @return the clause, or null
303     */
304    public String getSoftDeleteClause() {
305        if (softDeleteEnabled) {
306            return database.getTable(model.HIER_TABLE_NAME).getColumn(model.MAIN_IS_DELETED_KEY).getFullQuotedName()
307                    + " IS NULL";
308        } else {
309            return null;
310        }
311    }
312
313    // ----- update -----
314
315    // TODO these two methods are redundant with one another
316
317    public SQLInfoSelect getUpdateById(String tableName, Collection<String> keys, Set<String> deltas) {
318        Table table = database.getTable(tableName);
319        List<Column> columns = new LinkedList<Column>();
320        for (String key : keys) {
321            columns.add(table.getColumn(key));
322        }
323        Update update = new Update(table);
324        update.setUpdatedColumns(columns, deltas);
325        update.setWhere(getIdEqualsClause(tableName));
326        columns.add(table.getColumn(model.MAIN_KEY));
327        return new SQLInfoSelect(update.getStatement(), columns, null, null);
328    }
329
330    public Update getUpdateByIdForKeys(String tableName, List<String> keys) {
331        Table table = database.getTable(tableName);
332        List<Column> columns = new LinkedList<Column>();
333        for (String key : keys) {
334            columns.add(table.getColumn(key));
335        }
336        Update update = new Update(table);
337        update.setUpdatedColumns(columns);
338        update.setWhere(getIdEqualsClause(tableName));
339        return update;
340    }
341
342    /**
343     * Select by ids for all values of several fragments.
344     */
345    public SQLInfoSelect getSelectFragmentsByIds(String tableName, int nids) {
346        return getSelectFragmentsByIds(tableName, nids, null, null);
347    }
348
349    /**
350     * Select by ids for all values of several fragments (maybe ordered along columns -- for collection fragments
351     * retrieval).
352     */
353    public SQLInfoSelect getSelectFragmentsByIds(String tableName, int nids, String[] orderBys, Set<String> skipColumns) {
354        Table table = database.getTable(tableName);
355        List<Column> whatColumns = new LinkedList<Column>();
356        List<String> whats = new LinkedList<String>();
357        List<Column> opaqueColumns = new LinkedList<Column>();
358        for (Column column : table.getColumns()) {
359            if (column.isOpaque()) {
360                opaqueColumns.add(column);
361            } else if (skipColumns == null || !skipColumns.contains(column.getKey())) {
362                whatColumns.add(column);
363                whats.add(column.getQuotedName());
364            }
365        }
366        Column whereColumn = table.getColumn(model.MAIN_KEY);
367        StringBuilder wherebuf = new StringBuilder(whereColumn.getQuotedName());
368        wherebuf.append(" IN (");
369        for (int i = 0; i < nids; i++) {
370            if (i != 0) {
371                wherebuf.append(", ");
372            }
373            wherebuf.append('?');
374        }
375        wherebuf.append(')');
376        wherebuf.append(getSoftDeleteClause(tableName));
377        Select select = new Select(table);
378        select.setWhat(StringUtils.join(whats, ", "));
379        select.setFrom(table.getQuotedName());
380        select.setWhere(wherebuf.toString());
381        if (orderBys != null) {
382            List<String> orders = new LinkedList<String>();
383            for (String orderBy : orderBys) {
384                orders.add(table.getColumn(orderBy).getQuotedName());
385            }
386            select.setOrderBy(StringUtils.join(orders, ", "));
387        }
388        return new SQLInfoSelect(select.getStatement(), whatColumns, Collections.singletonList(whereColumn),
389                opaqueColumns.isEmpty() ? null : opaqueColumns);
390    }
391
392    /**
393     * Select all ancestors ids for several fragments.
394     * <p>
395     * Fast alternative to the slowest iterative {@link #getSelectParentIds}.
396     *
397     * @return null if it's not possible in one call in this dialect
398     */
399    public SQLInfoSelect getSelectAncestorsIds() {
400        String sql = dialect.getAncestorsIdsSql();
401        if (sql == null) {
402            return null;
403        }
404        Table table = database.getTable(model.HIER_TABLE_NAME);
405        Column mainColumn = table.getColumn(model.MAIN_KEY);
406        // no soft-delete check needed, as ancestors of a non-deleted doc
407        // aren't deleted either
408        return new SQLInfoSelect(sql, Collections.singletonList(mainColumn), null, null);
409    }
410
411    /**
412     * Select parentid by ids for all values of several fragments.
413     */
414    public SQLInfoSelect getSelectParentIds(int nids) {
415        Table table = database.getTable(model.HIER_TABLE_NAME);
416        Column whatColumn = table.getColumn(model.HIER_PARENT_KEY);
417        Column whereColumn = table.getColumn(model.MAIN_KEY);
418        StringBuilder wherebuf = new StringBuilder(whereColumn.getQuotedName());
419        wherebuf.append(" IN (");
420        for (int i = 0; i < nids; i++) {
421            if (i != 0) {
422                wherebuf.append(", ");
423            }
424            wherebuf.append('?');
425        }
426        wherebuf.append(')');
427        wherebuf.append(getSoftDeleteClause(model.HIER_TABLE_NAME));
428        Select select = new Select(table);
429        select.setWhat("DISTINCT " + whatColumn.getQuotedName());
430        select.setFrom(table.getQuotedName());
431        select.setWhere(wherebuf.toString());
432        return new SQLInfoSelect(select.getStatement(), Collections.singletonList(whatColumn),
433                Collections.singletonList(whereColumn), null);
434    }
435
436    /**
437     * Selects all children (not complex) for several parent ids.
438     */
439    public SQLInfoSelect getSelectChildrenNodeInfos(int nids) {
440        Table hierTable = database.getTable(model.HIER_TABLE_NAME);
441        Column mainColumn = hierTable.getColumn(model.MAIN_KEY);
442        List<Column> whatColumns = new ArrayList<>();
443        whatColumns.add(mainColumn);
444        whatColumns.add(hierTable.getColumn(model.HIER_PARENT_KEY));
445        whatColumns.add(hierTable.getColumn(model.MAIN_PRIMARY_TYPE_KEY));
446        Table proxyTable = null;
447        if (proxiesEnabled) {
448            proxyTable = database.getTable(model.PROXY_TABLE_NAME);
449            whatColumns.add(proxyTable.getColumn(model.PROXY_TARGET_KEY));
450            whatColumns.add(proxyTable.getColumn(model.PROXY_VERSIONABLE_KEY));
451        }
452        List<String> selectWhats = new ArrayList<>(whatColumns.size());
453        for (Column col : whatColumns) {
454            selectWhats.add(col.getFullQuotedName());
455        }
456        Select select = new Select(null);
457        select.setWhat(StringUtils.join(selectWhats, ", "));
458        String from = hierTable.getQuotedName();
459        if (proxiesEnabled) {
460            from += " LEFT JOIN " + proxyTable.getQuotedName() + " ON " + mainColumn.getFullQuotedName() + " = "
461                    + proxyTable.getColumn(model.MAIN_KEY).getFullQuotedName();
462        }
463        select.setFrom(from);
464        Column whereColumn = hierTable.getColumn(model.HIER_PARENT_KEY);
465        StringBuilder wherebuf = new StringBuilder(whereColumn.getFullQuotedName());
466        if (nids == 1) {
467            wherebuf.append(" = ?");
468        } else {
469            wherebuf.append(" IN (");
470            for (int i = 0; i < nids; i++) {
471                if (i != 0) {
472                    wherebuf.append(", ");
473                }
474                wherebuf.append('?');
475            }
476            wherebuf.append(')');
477        }
478        wherebuf.append(" AND ");
479        wherebuf.append(hierTable.getColumn(model.HIER_CHILD_ISPROPERTY_KEY).getFullQuotedName());
480        wherebuf.append(" = " + dialect.toBooleanValueString(false)); // not complex
481        wherebuf.append(getSoftDeleteClause(model.HIER_TABLE_NAME));
482        select.setWhere(wherebuf.toString());
483        return new SQLInfoSelect(select.getStatement(), whatColumns, Collections.singletonList(whereColumn), null);
484    }
485
486    // ----- delete -----
487
488    /**
489     * Returns the SQL {@code DELETE} to delete a row. The primary key columns are free parameters.
490     *
491     * @param tableName the table name
492     * @return the SQL {@code DELETE} statement
493     */
494    public String getDeleteSql(String tableName) {
495        return deleteSqlMap.get(tableName);
496    }
497
498    /**
499     * Returns the SQL {@code DELETE} to delete several rows. The primary key columns are free parameters.
500     *
501     * @param tableName the table name
502     * @param n the number of rows to delete
503     * @return the SQL {@code DELETE} statement with a {@code IN} for the keys
504     */
505    public String getDeleteSql(String tableName, int n) {
506        Table table = database.getTable(tableName);
507        Delete delete = new Delete(table);
508        String where = null;
509        for (Column column : table.getColumns()) {
510            if (column.getKey().equals(model.MAIN_KEY)) {
511                StringBuilder buf = new StringBuilder();
512                buf.append(column.getQuotedName());
513                if (n == 1) {
514                    buf.append(" = ?");
515                } else {
516                    buf.append(" IN (");
517                    for (int i = 0; i < n; i++) {
518                        if (i > 0) {
519                            buf.append(", ");
520                        }
521                        buf.append("?");
522                    }
523                    buf.append(")");
524                }
525                where = buf.toString();
526            }
527        }
528        delete.setWhere(where);
529        return delete.getStatement();
530    }
531
532    /**
533     * Returns the SQL to soft-delete several rows. The array of ids and the time are free parameters.
534     *
535     * @return the SQL statement
536     */
537    public String getSoftDeleteSql() {
538        return dialect.getSoftDeleteSql();
539    }
540
541    /**
542     * Returns the SQL to clean (hard-delete) soft-deleted rows. The max and beforeTime are free parameters.
543     *
544     * @return the SQL statement
545     */
546    public String getSoftDeleteCleanupSql() {
547        return dialect.getSoftDeleteCleanupSql();
548    }
549
550    // ----- copy -----
551
552    public SQLInfoSelect getCopyHier(boolean explicitName, boolean resetVersion) {
553        Table table = database.getTable(model.HIER_TABLE_NAME);
554        Collection<Column> columns = table.getColumns();
555        List<String> selectWhats = new ArrayList<String>(columns.size());
556        List<Column> selectWhatColumns = new ArrayList<Column>(5);
557        Insert insert = new Insert(table);
558        for (Column column : columns) {
559            if (column.isIdentity()) {
560                // identity column is never copied
561                continue;
562            }
563            insert.addColumn(column);
564            String quotedName = column.getQuotedName();
565            String key = column.getKey();
566            if (key.equals(model.MAIN_KEY) //
567                    || key.equals(model.HIER_PARENT_KEY) //
568                    || key.equals(model.MAIN_BASE_VERSION_KEY) //
569                    || key.equals(model.MAIN_CHECKED_IN_KEY) //
570                    || (key.equals(model.MAIN_MINOR_VERSION_KEY) && resetVersion) //
571                    || (key.equals(model.MAIN_MAJOR_VERSION_KEY) && resetVersion) //
572                    || (key.equals(model.HIER_CHILD_NAME_KEY) && explicitName)) {
573                // explicit value set
574                selectWhats.add("?");
575                selectWhatColumns.add(column);
576            } else {
577                // otherwise copy value
578                selectWhats.add(quotedName);
579            }
580        }
581        Column whereColumn = table.getColumn(model.MAIN_KEY);
582        Select select = new Select(null);
583        select.setFrom(table.getQuotedName());
584        select.setWhat(StringUtils.join(selectWhats, ", "));
585        select.setWhere(whereColumn.getQuotedName() + " = ?");
586        insert.setValues(select.getStatement());
587        String sql = insert.getStatement();
588        return new SQLInfoSelect(sql, selectWhatColumns, Collections.singletonList(whereColumn), null);
589    }
590
591    public String getCopySql(String tableName) {
592        return copySqlMap.get(tableName);
593    }
594
595    public Column getCopyIdColumn(String tableName) {
596        return copyIdColumnMap.get(tableName);
597    }
598
599    // ----- prepare everything -----
600
601    /**
602     * Creates all the sql from the models.
603     */
604    protected void initSQL() {
605
606        // structural tables
607        if (model.getRepositoryDescriptor().getClusteringEnabled()) {
608            if (!dialect.isClusteringSupported()) {
609                throw new NuxeoException("Clustering not supported for " + dialect.getClass().getSimpleName());
610            }
611            initClusterSQL();
612        }
613        initHierarchySQL();
614        initRepositorySQL();
615        if (dialect.supportsAncestorsTable()) {
616            initAncestorsSQL();
617        }
618
619        for (String tableName : model.getFragmentNames()) {
620            if (tableName.equals(model.HIER_TABLE_NAME)) {
621                continue;
622            }
623            initFragmentSQL(tableName);
624        }
625
626        /*
627         * versions
628         */
629
630        Table hierTable = database.getTable(model.HIER_TABLE_NAME);
631        Table versionTable = database.getTable(model.VERSION_TABLE_NAME);
632        hierTable.addIndex(model.MAIN_IS_VERSION_KEY);
633        versionTable.addIndex(model.VERSION_VERSIONABLE_KEY);
634        // don't index series+label, a simple label scan will suffice
635
636        /*
637         * proxies
638         */
639
640        if (proxiesEnabled) {
641            Table proxyTable = database.getTable(model.PROXY_TABLE_NAME);
642            proxyTable.addIndex(model.PROXY_VERSIONABLE_KEY);
643            proxyTable.addIndex(model.PROXY_TARGET_KEY);
644        }
645
646        initSelectDescendantsSQL();
647
648        /*
649         * fulltext
650         */
651        if (!model.getRepositoryDescriptor().getFulltextSearchDisabled()) {
652            Table table = database.getTable(model.FULLTEXT_TABLE_NAME);
653            FulltextConfiguration fulltextConfiguration = model.getFulltextConfiguration();
654            if (fulltextConfiguration.indexNames.size() > 1 && !dialect.supportsMultipleFulltextIndexes()) {
655                String msg = String.format("SQL database supports only one fulltext index, but %d are configured: %s",
656                        fulltextConfiguration.indexNames.size(), fulltextConfiguration.indexNames);
657                throw new NuxeoException(msg);
658            }
659            for (String indexName : fulltextConfiguration.indexNames) {
660                String suffix = model.getFulltextIndexSuffix(indexName);
661                int ftic = dialect.getFulltextIndexedColumns();
662                if (ftic == 1) {
663                    table.addIndex(indexName, IndexType.FULLTEXT, model.FULLTEXT_FULLTEXT_KEY + suffix);
664                } else if (ftic == 2) {
665                    table.addIndex(indexName, IndexType.FULLTEXT, model.FULLTEXT_SIMPLETEXT_KEY + suffix,
666                            model.FULLTEXT_BINARYTEXT_KEY + suffix);
667                }
668            }
669        }
670
671        /*
672         * binary columns for GC
673         */
674        for (Entry<String, List<String>> e : model.getBinaryPropertyInfos().entrySet()) {
675            String tableName = e.getKey();
676            Table table = database.getTable(tableName);
677            for (String key : e.getValue()) {
678                Select select = new Select(table);
679                Column col = table.getColumn(key); // key = name for now
680                select.setWhat("DISTINCT " + col.getQuotedName());
681                select.setFrom(table.getQuotedName());
682                getBinariesSql.add(select.getStatement());
683                // in the result column we want the digest, not the binary
684                Column resCol = new Column(table, null, ColumnType.STRING, null);
685                getBinariesColumns.add(resCol);
686            }
687        }
688    }
689
690    protected void initClusterSQL() {
691        TableMaker maker = new TableMaker(model.CLUSTER_NODES_TABLE_NAME);
692        maker.newColumn(model.CLUSTER_NODES_NODEID_KEY, ColumnType.CLUSTERNODE);
693        maker.newColumn(model.CLUSTER_NODES_CREATED_KEY, ColumnType.TIMESTAMP);
694        maker.postProcessClusterNodes();
695
696        maker = new TableMaker(model.CLUSTER_INVALS_TABLE_NAME);
697        maker.newColumn(model.CLUSTER_INVALS_NODEID_KEY, ColumnType.CLUSTERNODE);
698        maker.newColumn(model.CLUSTER_INVALS_ID_KEY, ColumnType.NODEVAL);
699        maker.newColumn(model.CLUSTER_INVALS_FRAGMENTS_KEY, ColumnType.CLUSTERFRAGS);
700        maker.newColumn(model.CLUSTER_INVALS_KIND_KEY, ColumnType.TINYINT);
701        maker.table.addIndex(model.CLUSTER_INVALS_NODEID_KEY);
702        maker.postProcessClusterInvalidations();
703    }
704
705    /**
706     * Creates the SQL for the table holding global repository information. This includes the id of the hierarchy root
707     * node.
708     */
709    protected void initRepositorySQL() {
710        TableMaker maker = new TableMaker(model.REPOINFO_TABLE_NAME);
711        maker.newColumn(model.MAIN_KEY, ColumnType.NODEIDFK);
712        maker.newColumn(model.REPOINFO_REPONAME_KEY, ColumnType.SYSNAME);
713        maker.postProcessRepository();
714    }
715
716    /**
717     * Creates the SQL for the table holding hierarchy information.
718     */
719    protected void initHierarchySQL() {
720        TableMaker maker = new TableMaker(model.HIER_TABLE_NAME);
721        // if (separateMainTable)
722        // maker.newColumn(model.MAIN_KEY, ColumnType.NODEIDFK);
723        maker.newColumn(model.MAIN_KEY, ColumnType.NODEID);
724        Column column = maker.newColumn(model.HIER_PARENT_KEY, ColumnType.NODEIDFKNULL);
725        maker.newColumn(model.HIER_CHILD_POS_KEY, ColumnType.INTEGER);
726        maker.newColumn(model.HIER_CHILD_NAME_KEY, ColumnType.STRING);
727        maker.newColumn(model.HIER_CHILD_ISPROPERTY_KEY, ColumnType.BOOLEAN); // notnull
728        // if (!separateMainTable)
729        maker.newFragmentFields();
730        maker.postProcess();
731        maker.postProcessHierarchy();
732        // if (!separateMainTable)
733        // maker.postProcessIdGeneration();
734
735        maker.table.addIndex(model.HIER_PARENT_KEY);
736        maker.table.addIndex(model.HIER_PARENT_KEY, model.HIER_CHILD_NAME_KEY);
737        // don't index parent+name+isprop, a simple isprop scan will suffice
738        maker.table.addIndex(model.MAIN_PRIMARY_TYPE_KEY);
739
740        if (model.getRepositoryDescriptor().getSoftDeleteEnabled()) {
741            maker.table.addIndex(model.MAIN_IS_DELETED_KEY);
742        }
743    }
744
745    protected void initSelectDescendantsSQL() {
746        Table hierTable = database.getTable(model.HIER_TABLE_NAME);
747        Table proxyTable = null;
748        if (proxiesEnabled) {
749            proxyTable = database.getTable(model.PROXY_TABLE_NAME);
750        }
751        Column mainColumn = hierTable.getColumn(model.MAIN_KEY);
752        List<Column> whatCols = new ArrayList<Column>(Arrays.asList(mainColumn,
753                hierTable.getColumn(model.HIER_PARENT_KEY), hierTable.getColumn(model.MAIN_PRIMARY_TYPE_KEY),
754                hierTable.getColumn(model.HIER_CHILD_ISPROPERTY_KEY)));
755        if (proxiesEnabled) {
756            whatCols.add(proxyTable.getColumn(model.PROXY_VERSIONABLE_KEY));
757            whatCols.add(proxyTable.getColumn(model.PROXY_TARGET_KEY));
758        }
759        // no mixins, not used to decide if we have a version or proxy
760        List<String> whats = new ArrayList<String>(6);
761        for (Column col : whatCols) {
762            whats.add(col.getFullQuotedName());
763        }
764        Select select = new Select(null);
765        select.setWhat(StringUtils.join(whats, ", "));
766        String from = hierTable.getQuotedName();
767        if (proxiesEnabled) {
768            from += " LEFT JOIN " + proxyTable.getQuotedName() + " ON " + mainColumn.getFullQuotedName() + " = "
769                    + proxyTable.getColumn(model.MAIN_KEY).getFullQuotedName();
770        }
771        select.setFrom(from);
772        String where = dialect.getInTreeSql(mainColumn.getFullQuotedName(), null);
773        where += getSoftDeleteClause(model.HIER_TABLE_NAME);
774        select.setWhere(where);
775        selectDescendantsInfoSql = select.getStatement();
776        selectDescendantsInfoWhatColumns = whatCols;
777    }
778
779    /**
780     * Creates the SQL for the table holding ancestors information.
781     * <p>
782     * This table holds trigger-updated information extracted from the recursive parent-child relationship in the
783     * hierarchy table.
784     */
785    protected void initAncestorsSQL() {
786        TableMaker maker = new TableMaker(model.ANCESTORS_TABLE_NAME);
787        maker.newColumn(model.MAIN_KEY, ColumnType.NODEIDFKMUL);
788        maker.newColumn(model.ANCESTORS_ANCESTOR_KEY, ColumnType.NODEARRAY);
789    }
790
791    /**
792     * Creates the SQL for one fragment (simple or collection).
793     */
794    protected void initFragmentSQL(String tableName) {
795        TableMaker maker = new TableMaker(tableName);
796        ColumnType type;
797        if (tableName.equals(model.HIER_TABLE_NAME)) {
798            type = ColumnType.NODEID;
799        } else if (tableName.equals(model.LOCK_TABLE_NAME)) {
800            type = ColumnType.NODEIDPK; // no foreign key to hierarchy
801        } else if (model.isCollectionFragment(tableName)) {
802            type = ColumnType.NODEIDFKMUL;
803        } else {
804            type = ColumnType.NODEIDFK;
805        }
806        maker.newColumn(model.MAIN_KEY, type);
807        maker.newFragmentFields();
808        maker.postProcess();
809        // if (isMain)
810        // maker.postProcessIdGeneration();
811    }
812
813    protected void initSelections() {
814        for (SelectionType selType : SelectionType.values()) {
815            if (!proxiesEnabled && selType.tableName.equals(Model.PROXY_TABLE_NAME)) {
816                continue;
817            }
818            selections.put(selType, new SQLInfoSelection(selType));
819        }
820    }
821
822    // ----- prepare one table -----
823
824    protected class TableMaker {
825
826        private final String tableName;
827
828        private final Table table;
829
830        private final String orderBy;
831
832        protected TableMaker(String tableName) {
833            this.tableName = tableName;
834            table = database.addTable(tableName);
835            orderBy = model.getCollectionOrderBy(tableName);
836        }
837
838        protected void newFragmentFields() {
839            Map<String, ColumnType> keysType = model.getFragmentKeysType(tableName);
840            for (Entry<String, ColumnType> entry : keysType.entrySet()) {
841                newColumn(entry.getKey(), entry.getValue());
842            }
843        }
844
845        protected Column newColumn(String key, ColumnType type) {
846            String columnName = key;
847            Column column = table.addColumn(columnName, type, key, model);
848            if (type == ColumnType.NODEID) {
849                // column.setIdentity(true); if idGenPolicy identity
850                column.setNullable(false);
851                column.setPrimary(true);
852            }
853            if (type == ColumnType.NODEIDFK || type == ColumnType.NODEIDPK) {
854                column.setNullable(false);
855                column.setPrimary(true);
856            }
857            if (type == ColumnType.NODEIDFKMUL) {
858                column.setNullable(false);
859                table.addIndex(key);
860            }
861            if (type == ColumnType.NODEIDFK || type == ColumnType.NODEIDFKNP || type == ColumnType.NODEIDFKNULL
862                    || type == ColumnType.NODEIDFKMUL) {
863                column.setReferences(database.getTable(model.HIER_TABLE_NAME), model.MAIN_KEY);
864            }
865            return column;
866        }
867
868        // ----------------------- post processing -----------------------
869
870        protected void postProcessClusterNodes() {
871            Collection<Column> columns = table.getColumns();
872            List<Column> insertColumns = new ArrayList<Column>(columns.size());
873            Insert insert = new Insert(table);
874            for (Column column : columns) {
875                insertColumns.add(column);
876                insert.addColumn(column);
877            }
878            createClusterNodeSql = insert.getStatement();
879            createClusterNodeColumns = new ArrayList<>(columns);
880
881            Delete delete = new Delete(table);
882            Column column = table.getColumn(model.CLUSTER_NODES_NODEID_KEY);
883            delete.setWhere(column.getQuotedName() + " = ?");
884            deleteClusterNodeSql = delete.getStatement();
885            deleteClusterNodeColumn = column;
886        }
887
888        protected void postProcessClusterInvalidations() {
889            clusterInvalidationsColumns = Arrays.asList(table.getColumn(model.CLUSTER_INVALS_NODEID_KEY),
890                    table.getColumn(model.CLUSTER_INVALS_ID_KEY), table.getColumn(model.CLUSTER_INVALS_FRAGMENTS_KEY),
891                    table.getColumn(model.CLUSTER_INVALS_KIND_KEY));
892
893            Delete delete = new Delete(table);
894            Column column = table.getColumn(model.CLUSTER_INVALS_NODEID_KEY);
895            delete.setWhere(column.getQuotedName() + " = ?");
896            deleteClusterInvalsSql = delete.getStatement();
897            deleteClusterInvalsColumn = column;
898        }
899
900        protected void postProcessRepository() {
901            postProcessRootIdSelect();
902            postProcessInsert();
903        }
904
905        protected void postProcessRootIdSelect() {
906            String what = null;
907            String where = null;
908            for (Column column : table.getColumns()) {
909                String key = column.getKey();
910                String qname = column.getQuotedName();
911                if (key.equals(model.MAIN_KEY)) {
912                    what = qname;
913                    selectRootIdWhatColumn = column;
914                } else if (key.equals(model.REPOINFO_REPONAME_KEY)) {
915                    where = qname + " = ?";
916                } else {
917                    throw new RuntimeException(column.toString());
918                }
919            }
920            Select select = new Select(table);
921            select.setWhat(what);
922            select.setFrom(table.getQuotedName());
923            select.setWhere(where);
924            selectRootIdSql = select.getStatement();
925        }
926
927        /**
928         * Precompute what we can from the information available for a regular schema table, or a collection table.
929         */
930        protected void postProcess() {
931            postProcessSelectById();
932            postProcessInsert();
933            postProcessDelete();
934            postProcessCopy();
935        }
936
937        /**
938         * Additional SQL for the hierarchy table.
939         */
940        protected void postProcessHierarchy() {
941            postProcessSelectChildrenIdsAndTypes();
942        }
943
944        protected void postProcessSelectById() {
945            String[] orderBys = orderBy == null ? NO_ORDER_BY : new String[] { orderBy, ORDER_ASC };
946            SQLInfoSelect select = makeSelect(table, orderBys, model.MAIN_KEY);
947            selectFragmentById.put(tableName, select);
948        }
949
950        protected void postProcessSelectChildrenIdsAndTypes() {
951            List<Column> whatColumns = new ArrayList<Column>(2);
952            List<String> whats = new ArrayList<String>(2);
953            Column column = table.getColumn(model.MAIN_KEY);
954            whatColumns.add(column);
955            whats.add(column.getQuotedName());
956            column = table.getColumn(model.MAIN_PRIMARY_TYPE_KEY);
957            whatColumns.add(column);
958            whats.add(column.getQuotedName());
959            column = table.getColumn(model.MAIN_MIXIN_TYPES_KEY);
960            whatColumns.add(column);
961            whats.add(column.getQuotedName());
962            Select select = new Select(table);
963            select.setWhat(StringUtils.join(whats, ", "));
964            select.setFrom(table.getQuotedName());
965            String where = table.getColumn(model.HIER_PARENT_KEY).getQuotedName() + " = ?"
966                    + getSoftDeleteClause(tableName);
967            select.setWhere(where);
968            selectChildrenIdsAndTypesSql = select.getStatement();
969            selectChildrenIdsAndTypesWhatColumns = whatColumns;
970            // now only complex properties
971            where += " AND " + table.getColumn(model.HIER_CHILD_ISPROPERTY_KEY).getQuotedName() + " = "
972                    + dialect.toBooleanValueString(true);
973            select.setWhere(where);
974            selectComplexChildrenIdsAndTypesSql = select.getStatement();
975        }
976
977        // TODO optimize multiple inserts into one statement for collections
978        protected void postProcessInsert() {
979            // insert (implicitly auto-generated sequences not included)
980            Collection<Column> columns = table.getColumns();
981            List<Column> insertColumns = new ArrayList<Column>(columns.size());
982            Insert insert = new Insert(table);
983            for (Column column : columns) {
984                if (column.isIdentity()) {
985                    // identity column is never inserted
986                    continue;
987                }
988                insertColumns.add(column);
989                insert.addColumn(column);
990            }
991            insertSqlMap.put(tableName, insert.getStatement());
992            insertColumnsMap.put(tableName, insertColumns);
993        }
994
995        protected void postProcessDelete() {
996            Delete delete = new Delete(table);
997            List<String> wheres = new LinkedList<String>();
998            for (Column column : table.getColumns()) {
999                if (column.getKey().equals(model.MAIN_KEY)) {
1000                    wheres.add(column.getQuotedName() + " = ?");
1001                }
1002            }
1003            delete.setWhere(StringUtils.join(wheres, " AND "));
1004            deleteSqlMap.put(tableName, delete.getStatement());
1005        }
1006
1007        // copy of a fragment
1008        // INSERT INTO foo (id, x, y) SELECT ?, x, y FROM foo WHERE id = ?
1009        protected void postProcessCopy() {
1010            Collection<Column> columns = table.getColumns();
1011            List<String> selectWhats = new ArrayList<String>(columns.size());
1012            Column copyIdColumn = table.getColumn(model.MAIN_KEY);
1013            Insert insert = new Insert(table);
1014            for (Column column : columns) {
1015                if (column.isIdentity()) {
1016                    // identity column is never copied
1017                    continue;
1018                }
1019                insert.addColumn(column);
1020                if (column == copyIdColumn) {
1021                    // explicit value
1022                    selectWhats.add("?");
1023                } else {
1024                    // otherwise copy value
1025                    selectWhats.add(column.getQuotedName());
1026                }
1027            }
1028            Select select = new Select(table);
1029            select.setWhat(StringUtils.join(selectWhats, ", "));
1030            select.setFrom(table.getQuotedName());
1031            select.setWhere(copyIdColumn.getQuotedName() + " = ?");
1032            insert.setValues(select.getStatement());
1033            copySqlMap.put(tableName, insert.getStatement());
1034            copyIdColumnMap.put(tableName, copyIdColumn);
1035        }
1036
1037    }
1038
1039    public static class SQLInfoSelect {
1040
1041        public final String sql;
1042
1043        public final List<Column> whatColumns;
1044
1045        public final MapMaker mapMaker;
1046
1047        public final List<Column> whereColumns;
1048
1049        public final List<Column> opaqueColumns;
1050
1051        /**
1052         * Standard select for given columns.
1053         */
1054        public SQLInfoSelect(String sql, List<Column> whatColumns, List<Column> whereColumns, List<Column> opaqueColumns) {
1055            this(sql, whatColumns, null, whereColumns, opaqueColumns);
1056        }
1057
1058        /**
1059         * Select where some column keys may be aliased, and some columns may be computed. The {@link MapMaker} is used
1060         * by the queryAndFetch() method.
1061         */
1062        public SQLInfoSelect(String sql, MapMaker mapMaker) {
1063            this(sql, null, mapMaker, null, null);
1064        }
1065
1066        public SQLInfoSelect(String sql, List<Column> whatColumns, MapMaker mapMaker, List<Column> whereColumns,
1067                List<Column> opaqueColumns) {
1068            this.sql = sql;
1069            this.whatColumns = whatColumns;
1070            this.mapMaker = mapMaker;
1071            this.whereColumns = whereColumns == null ? null : new ArrayList<Column>(whereColumns);
1072            this.opaqueColumns = opaqueColumns == null ? null : new ArrayList<Column>(opaqueColumns);
1073        }
1074    }
1075
1076    /**
1077     * Info about how to do the query to get a {@link Selection}.
1078     */
1079    public class SQLInfoSelection {
1080
1081        public final SelectionType type;
1082
1083        public final SQLInfoSelect selectAll;
1084
1085        public final SQLInfoSelect selectFiltered;
1086
1087        public SQLInfoSelection(SelectionType selType) {
1088            this.type = selType;
1089            Table table = database.getTable(selType.tableName);
1090            SQLInfoSelect selectAll;
1091            SQLInfoSelect selectFiltered;
1092            String from = table.getQuotedName();
1093            List<String> clauses;
1094            if (selType.tableName.equals(model.HIER_TABLE_NAME)) {
1095                // clause already added by makeSelect
1096                clauses = null;
1097            } else {
1098                Table hierTable = database.getTable(model.HIER_TABLE_NAME);
1099                Join join = new Join(Join.INNER, hierTable.getQuotedName(), null, null,
1100                        hierTable.getColumn(model.MAIN_KEY), table.getColumn(model.MAIN_KEY));
1101                from += join.toSql(dialect);
1102                String clause = getSoftDeleteClause();
1103                clauses = clause == null ? null : Collections.singletonList(clause);
1104            }
1105            if (selType.criterionKey == null) {
1106                selectAll = makeSelect(table, from, clauses, NO_ORDER_BY, selType.selKey);
1107                selectFiltered = makeSelect(table, from, clauses, NO_ORDER_BY, selType.selKey, selType.filterKey);
1108            } else {
1109                selectAll = makeSelect(table, from, clauses, NO_ORDER_BY, selType.selKey, selType.criterionKey);
1110                selectFiltered = makeSelect(table, from, clauses, NO_ORDER_BY, selType.selKey, selType.filterKey,
1111                        selType.criterionKey);
1112            }
1113            this.selectAll = selectAll;
1114            this.selectFiltered = selectFiltered;
1115        }
1116    }
1117
1118    /**
1119     * Knows how to build a result map for a row given a {@link ResultSet}. This abstraction may be used to compute some
1120     * values on the fly.
1121     */
1122    public interface MapMaker {
1123        Map<String, Serializable> makeMap(ResultSet rs) throws SQLException;
1124    }
1125
1126    /**
1127     * Builds the map from a result set given a list of columns and column keys.
1128     */
1129    public static class ColumnMapMaker implements MapMaker {
1130        public final List<Column> columns;
1131
1132        public final List<String> keys;
1133
1134        public ColumnMapMaker(List<Column> columns) {
1135            this.columns = columns;
1136            keys = new ArrayList<String>(columns.size());
1137            for (Column column : columns) {
1138                keys.add(column.getKey());
1139            }
1140        }
1141
1142        public ColumnMapMaker(List<Column> columns, List<String> keys) {
1143            this.columns = columns;
1144            this.keys = keys;
1145        }
1146
1147        @Override
1148        public Map<String, Serializable> makeMap(ResultSet rs) throws SQLException {
1149            Map<String, Serializable> map = new HashMap<String, Serializable>();
1150            int i = 1;
1151            for (Column column : columns) {
1152                String key = keys.get(i - 1);
1153                Serializable value = column.getFromResultSet(rs, i++);
1154                if (NXQL.ECM_UUID.equals(key) || NXQL.ECM_PARENTID.equals(key)) {
1155                    value = String.valueOf(value); // idToString
1156                }
1157                map.put(key, value);
1158            }
1159            return map;
1160        }
1161    }
1162
1163    private static String[] NO_ORDER_BY = new String[0];
1164
1165    /**
1166     * Basic SELECT x, y, z FROM table WHERE a = ? AND b = ?
1167     * <p>
1168     * with optional ORDER BY x, y DESC
1169     */
1170    public SQLInfoSelect makeSelect(Table table, String[] orderBys, String... freeColumns) {
1171        return makeSelect(table, null, null, orderBys, freeColumns);
1172    }
1173
1174    /**
1175     * Same as above but the FROM can be passed in, to allow JOINs.
1176     */
1177    public SQLInfoSelect makeSelect(Table table, String from, List<String> clauses, String[] orderBys,
1178            String... freeColumns) {
1179        boolean fullQuotedName = from != null;
1180        List<String> freeColumnsList = Arrays.asList(freeColumns);
1181        List<Column> whatColumns = new LinkedList<Column>();
1182        List<Column> whereColumns = new LinkedList<Column>();
1183        List<Column> opaqueColumns = new LinkedList<Column>();
1184        List<String> whats = new LinkedList<String>();
1185        List<String> wheres = new LinkedList<String>();
1186        for (Column column : table.getColumns()) {
1187            String qname = fullQuotedName ? column.getFullQuotedName() : column.getQuotedName();
1188            if (freeColumnsList.contains(column.getKey())) {
1189                whereColumns.add(column);
1190                wheres.add(qname + " = ?");
1191            } else if (column.isOpaque()) {
1192                opaqueColumns.add(column);
1193            } else {
1194                whatColumns.add(column);
1195                whats.add(qname);
1196            }
1197        }
1198        if (whats.isEmpty()) {
1199            // only opaque columns, don't generate an illegal SELECT
1200            whats.add(table.getColumn(Model.MAIN_KEY).getQuotedName());
1201        }
1202        if (clauses != null) {
1203            wheres.addAll(clauses);
1204        }
1205        Select select = new Select(table);
1206        select.setWhat(StringUtils.join(whats, ", "));
1207        if (from == null) {
1208            from = table.getQuotedName();
1209        }
1210        select.setFrom(from);
1211        String where = StringUtils.join(wheres, " AND ") + getSoftDeleteClause(table.getKey());
1212        select.setWhere(where);
1213        List<String> orders = new LinkedList<String>();
1214        for (int i = 0; i < orderBys.length; i++) {
1215            String name = orderBys[i++];
1216            String ascdesc = orderBys[i].equals(ORDER_DESC) ? " " + ORDER_DESC : "";
1217            Column col = table.getColumn(name);
1218            String qcol = fullQuotedName ? col.getFullQuotedName() : col.getQuotedName();
1219            orders.add(qcol + ascdesc);
1220        }
1221        select.setOrderBy(StringUtils.join(orders, ", "));
1222        return new SQLInfoSelect(select.getStatement(), whatColumns, whereColumns, opaqueColumns.isEmpty() ? null
1223                : opaqueColumns);
1224    }
1225
1226    public void initSQLStatements(Map<String, Serializable> testProps, List<String> sqlInitFiles) throws IOException {
1227        sqlStatements = new HashMap<String, List<SQLStatement>>();
1228        SQLStatement.read(dialect.getSQLStatementsFilename(), sqlStatements);
1229        if (sqlInitFiles != null) {
1230            for (String filename : sqlInitFiles) {
1231                SQLStatement.read(filename, sqlStatements);
1232            }
1233        }
1234        if (!testProps.isEmpty()) {
1235            SQLStatement.read(dialect.getTestSQLStatementsFilename(), sqlStatements);
1236        }
1237        sqlStatementsProperties = dialect.getSQLStatementsProperties(model, database);
1238        if (!testProps.isEmpty()) {
1239            sqlStatementsProperties.putAll(testProps);
1240        }
1241    }
1242
1243    /**
1244     * Executes the SQL statements for the given category.
1245     */
1246    public void executeSQLStatements(String category, JDBCConnection jdbc) throws SQLException {
1247        List<SQLStatement> statements = sqlStatements.get(category);
1248        if (statements != null) {
1249            SQLStatement.execute(statements, sqlStatementsProperties, jdbc);
1250        }
1251    }
1252
1253    public int getMaximumArgsForIn() {
1254        return dialect.getMaximumArgsForIn();
1255    }
1256
1257}