OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 142 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
182 ilm 4
 * Copyright 2011-2019 OpenConcerto, by ILM Informatique. All rights reserved.
17 ilm 5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.sql.model;
15
 
65 ilm 16
import org.openconcerto.sql.model.Order.Direction;
17
import org.openconcerto.sql.model.Order.Nulls;
17 ilm 18
import org.openconcerto.sql.model.graph.Path;
73 ilm 19
import org.openconcerto.sql.model.graph.Step;
17 ilm 20
import org.openconcerto.utils.CollectionUtils;
93 ilm 21
import org.openconcerto.utils.Tuple2;
17 ilm 22
import org.openconcerto.utils.cc.ITransformer;
23
 
24
import java.util.ArrayList;
182 ilm 25
import java.util.Arrays;
17 ilm 26
import java.util.Collection;
27
import java.util.Collections;
28
import java.util.HashMap;
29
import java.util.HashSet;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.Set;
33
 
34
/**
35
 * @author ILM Informatique 10 mai 2004
36
 */
37
public final class SQLSelect {
38
 
39
    public static enum ArchiveMode {
40
        UNARCHIVED, ARCHIVED, BOTH
41
    }
42
 
132 ilm 43
    public static enum LockStrength {
44
        NONE, SHARE, UPDATE
45
    }
46
 
17 ilm 47
    public static final ArchiveMode UNARCHIVED = ArchiveMode.UNARCHIVED;
48
    public static final ArchiveMode ARCHIVED = ArchiveMode.ARCHIVED;
49
    public static final ArchiveMode BOTH = ArchiveMode.BOTH;
50
 
51
    // [String], eg : [SITE.ID_SITE, AVG(AGE)]
52
    private final List<String> select;
83 ilm 53
    // names of columns (explicit aliases and field names), e.g. [ID_SITE, null]
54
    private final List<String> selectNames;
55
    // e.g. : [|SITE.ID_SITE|], known fields in this select (addRawSelect)
56
    private final List<FieldRef> selectFields;
17 ilm 57
    private Where where;
58
    private final List<FieldRef> groupBy;
59
    private Where having;
60
    // [String]
61
    private final List<String> order;
62
    private final FromClause from;
63
    // all the tables (and their aliases) in this select
64
    private final AliasedTables declaredTables;
65
    // {String}, aliases not to include in the FROM clause
66
    private final Set<String> joinAliases;
67
    // [String]
68
    private final List<SQLSelectJoin> joins;
69
 
70
    // la politique générale pour l'exclusion des indéfinis
71
    private boolean generalExcludeUndefined;
72
    // [SQLTable => Boolean]
93 ilm 73
    private final Map<SQLTable, Boolean> excludeUndefined;
17 ilm 74
    // null key for general
75
    private final Map<SQLTable, ArchiveMode> archivedPolicy;
76
    // DISTINCT
77
    private boolean distinct;
132 ilm 78
    // how to lock returned rows
79
    private LockStrength lockStrength;
17 ilm 80
    // which tables to wait (avoid SELECT FOR UPDATE/SHARE cannot be applied to the nullable side of
81
    // an outer join)
82
    private final List<String> waitTrxTables;
83
    // number of rows to return
84
    private Integer limit;
93 ilm 85
    // offset from the start
86
    private int offset;
17 ilm 87
 
65 ilm 88
    /**
89
     * Create a new SQLSelect.
90
     *
91
     * @param base the database of the request.
83 ilm 92
     * @deprecated use {@link #SQLSelect(DBSystemRoot, boolean)}
65 ilm 93
     */
93 ilm 94
    @Deprecated
95
    public SQLSelect(final SQLBase base) {
17 ilm 96
        this(base, false);
97
    }
98
 
99
    /**
100
     * Create a new SQLSelect.
101
     *
102
     * @param base the database of the request.
103
     * @param plain whether this request should automatically add a where clause for archived and
104
     *        undefined.
65 ilm 105
     * @deprecated use {@link #SQLSelect(DBSystemRoot, boolean)}
17 ilm 106
     */
93 ilm 107
    @Deprecated
108
    public SQLSelect(final SQLBase base, final boolean plain) {
65 ilm 109
        this(base.getDBSystemRoot(), plain);
110
    }
111
 
112
    public SQLSelect() {
113
        this(false);
114
    }
115
 
93 ilm 116
    public SQLSelect(final boolean plain) {
65 ilm 117
        this((DBSystemRoot) null, plain);
118
    }
119
 
120
    /**
121
     * Create a new SQLSelect.
122
     *
123
     * @param sysRoot the database of the request, can be <code>null</code> (it will come from
124
     *        declared tables).
125
     * @param plain whether this request should automatically add a where clause for archived and
126
     *        undefined.
127
     */
93 ilm 128
    public SQLSelect(final DBSystemRoot sysRoot, final boolean plain) {
17 ilm 129
        this.select = new ArrayList<String>();
83 ilm 130
        this.selectNames = new ArrayList<String>();
131
        this.selectFields = new ArrayList<FieldRef>();
17 ilm 132
        this.where = null;
133
        this.groupBy = new ArrayList<FieldRef>();
134
        this.having = null;
135
        this.order = new ArrayList<String>();
136
        this.from = new FromClause();
65 ilm 137
        this.declaredTables = new AliasedTables(sysRoot);
17 ilm 138
        this.joinAliases = new HashSet<String>();
139
        this.joins = new ArrayList<SQLSelectJoin>();
140
        // false by default cause it slows things down
141
        this.distinct = false;
142
        this.excludeUndefined = new HashMap<SQLTable, Boolean>();
143
        this.archivedPolicy = new HashMap<SQLTable, ArchiveMode>();
132 ilm 144
        // none by default since it requires access rights
145
        this.lockStrength = LockStrength.NONE;
17 ilm 146
        this.waitTrxTables = new ArrayList<String>();
93 ilm 147
        this.limit = null;
148
        this.offset = 0;
17 ilm 149
        if (plain) {
150
            this.generalExcludeUndefined = false;
151
            this.setArchivedPolicy(BOTH);
152
        } else {
153
            this.generalExcludeUndefined = true;
154
            this.setArchivedPolicy(UNARCHIVED);
155
        }
156
        // otherwise getArchiveWhere() fails
157
        assert this.archivedPolicy.containsKey(null);
158
    }
159
 
160
    /**
161
     * Clone un SQLSelect.
162
     *
163
     * @param orig l'instance à cloner.
164
     */
93 ilm 165
    public SQLSelect(final SQLSelect orig) {
17 ilm 166
        // ATTN synch les implémentations des attributs (LinkedHashSet, ...)
167
        this.select = new ArrayList<String>(orig.select);
83 ilm 168
        this.selectNames = new ArrayList<String>(orig.selectNames);
169
        this.selectFields = new ArrayList<FieldRef>(orig.selectFields);
93 ilm 170
        this.where = orig.where;
17 ilm 171
        this.groupBy = new ArrayList<FieldRef>(orig.groupBy);
93 ilm 172
        this.having = orig.having;
17 ilm 173
        this.order = new ArrayList<String>(orig.order);
174
        this.from = new FromClause(orig.from);
175
        this.declaredTables = new AliasedTables(orig.declaredTables);
176
        this.joinAliases = new HashSet<String>(orig.joinAliases);
177
        this.joins = new ArrayList<SQLSelectJoin>(orig.joins);
178
        this.generalExcludeUndefined = orig.generalExcludeUndefined;
179
        this.excludeUndefined = new HashMap<SQLTable, Boolean>(orig.excludeUndefined);
180
        this.archivedPolicy = new HashMap<SQLTable, ArchiveMode>(orig.archivedPolicy);
181
        this.distinct = orig.distinct;
182
 
132 ilm 183
        this.lockStrength = orig.lockStrength;
17 ilm 184
        this.waitTrxTables = new ArrayList<String>(orig.waitTrxTables);
83 ilm 185
        this.limit = orig.limit;
93 ilm 186
        this.offset = orig.offset;
17 ilm 187
    }
188
 
93 ilm 189
    final DBSystemRoot getSystemRoot() {
67 ilm 190
        final DBSystemRoot sysRoot = this.declaredTables.getSysRoot();
191
        if (sysRoot == null)
192
            throw new IllegalStateException("No systemRoot supplied (neither in the constructor nor by adding an item)");
93 ilm 193
        return sysRoot;
65 ilm 194
    }
195
 
93 ilm 196
    public final SQLSystem getSQLSystem() {
197
        return getSystemRoot().getServer().getSQLSystem();
198
    }
199
 
142 ilm 200
    public final SQLSyntax getSyntax() {
201
        return getSystemRoot().getSyntax();
202
    }
203
 
17 ilm 204
    public String asString() {
65 ilm 205
        final SQLSystem sys = this.getSQLSystem();
17 ilm 206
 
207
        final StringBuffer result = new StringBuffer(512);
208
        result.append("SELECT ");
209
        if (this.distinct)
210
            result.append("DISTINCT ");
211
        result.append(CollectionUtils.join(this.select, ", "));
212
 
213
        result.append("\n " + this.from.getSQL());
214
 
215
        // si c'est null, ca marche
216
        Where archive = this.where;
217
        // ne pas exclure les archivés et les indéfinis des joins : SQLSelectJoin does it
21 ilm 218
        final Collection<String> fromAliases = CollectionUtils.substract(this.declaredTables.getAliases(), this.joinAliases);
219
        for (final String alias : fromAliases) {
17 ilm 220
            final SQLTable fromTable = this.declaredTables.getTable(alias);
221
            // on ignore les lignes archivées
222
            archive = Where.and(getArchiveWhere(fromTable, alias), archive);
223
            // on ignore les lignes indéfines
224
            archive = Where.and(getUndefWhere(fromTable, alias), archive);
225
        }
226
        // archive == null si pas d'archive et pas d'undefined
142 ilm 227
        if (archive != null) {
17 ilm 228
            result.append("\n WHERE ");
229
            result.append(archive.getClause());
230
        }
231
        if (!this.groupBy.isEmpty()) {
232
            result.append("\n GROUP BY ");
233
            result.append(CollectionUtils.join(this.groupBy, ", ", new ITransformer<FieldRef, String>() {
234
                @Override
93 ilm 235
                public String transformChecked(final FieldRef input) {
17 ilm 236
                    return input.getFieldRef();
237
                }
238
            }));
239
        }
240
        if (this.having != null) {
241
            result.append("\n HAVING ");
242
            result.append(this.having.getClause());
243
        }
244
        if (!this.order.isEmpty()) {
245
            result.append("\n ORDER BY ");
246
            result.append(CollectionUtils.join(this.order, ", "));
247
        }
93 ilm 248
        // most systems need to specify both
249
        if (this.getLimit() != null || this.getOffset() != 0) {
250
            if (sys == SQLSystem.MSSQL) {
251
                result.append("\nOFFSET ");
252
                result.append(this.getOffset());
253
                result.append(" ROWS");
254
                if (this.getLimit() != null) {
255
                    result.append(" FETCH NEXT ");
256
                    result.append(this.getLimit());
257
                    result.append(" ROWS ONLY");
258
                }
259
            } else {
260
                final Object actualLimit;
261
                if (this.getLimit() != null) {
262
                    actualLimit = this.getLimit();
263
                } else if (sys == SQLSystem.H2) {
264
                    actualLimit = "NULL";
265
                } else if (sys == SQLSystem.POSTGRESQL) {
266
                    actualLimit = "ALL";
267
                } else {
268
                    // From the official MySQL manual
269
                    actualLimit = Integer.MAX_VALUE;
270
                }
271
                result.append("\nLIMIT ");
272
                result.append(actualLimit);
273
                result.append(" OFFSET ");
274
                result.append(this.getOffset());
275
            }
17 ilm 276
        }
277
        // wait for other update trx to finish before selecting
132 ilm 278
        if (this.lockStrength != LockStrength.NONE) {
17 ilm 279
            if (sys.equals(SQLSystem.POSTGRESQL)) {
132 ilm 280
                result.append(this.lockStrength == LockStrength.SHARE ? " FOR SHARE" : " FOR UPDATE");
17 ilm 281
                if (this.waitTrxTables.size() > 0)
282
                    result.append(" OF " + CollectionUtils.join(this.waitTrxTables, ", "));
132 ilm 283
            } else if (sys.equals(SQLSystem.MYSQL)) {
284
                result.append(this.lockStrength == LockStrength.SHARE ? " LOCK IN SHARE MODE" : " FOR UPDATE");
285
            } else if (sys.equals(SQLSystem.H2)) {
286
                result.append(" FOR UPDATE");
287
            } else {
288
                throw new IllegalStateException("Unsupported system : " + sys);
289
            }
17 ilm 290
        }
291
 
292
        return result.toString();
293
    }
294
 
295
    Where getArchiveWhere(final SQLTable table, final String alias) {
296
        final Where res;
297
        // null key is the default
298
        final ArchiveMode m = this.archivedPolicy.containsKey(table) ? this.archivedPolicy.get(table) : this.archivedPolicy.get(null);
299
        assert m != null : "no default policy";
132 ilm 300
        if (m == BOTH) {
301
            res = null;
302
        } else if (table.isArchivable()) {
17 ilm 303
            final Object archiveValue;
304
            if (table.getArchiveField().getType().getJavaType().equals(Boolean.class)) {
305
                archiveValue = m == ARCHIVED;
306
            } else {
307
                archiveValue = m == ARCHIVED ? 1 : 0;
308
            }
309
            res = new Where(this.createRef(alias, table.getArchiveField()), "=", archiveValue);
132 ilm 310
        } else {
311
            // for tables that aren't archivable, either all rows or no rows
312
            res = m == ARCHIVED ? Where.FALSE : null;
313
        }
17 ilm 314
        return res;
315
    }
316
 
317
    Where getUndefWhere(final SQLTable table, final String alias) {
318
        final Where res;
319
        final Boolean exclude = this.excludeUndefined.get(table);
320
        if (table.isRowable() && (exclude == Boolean.TRUE || (exclude == null && this.generalExcludeUndefined))) {
321
            // no need to use NULL_IS_DATA_NEQ since we're in FROM or JOIN and ID cannot be null
322
            res = new Where(this.createRef(alias, table.getKey()), "!=", table.getUndefinedID());
323
        } else
324
            res = null;
325
        return res;
326
    }
327
 
93 ilm 328
    @Override
17 ilm 329
    public String toString() {
330
        return this.asString();
331
    }
332
 
333
    /**
83 ilm 334
     * SQL expressions of the SELECT.
17 ilm 335
     *
83 ilm 336
     * @return a list of expressions used by the SELECT, e.g. "T.*, A.f", "count(*)".
17 ilm 337
     */
338
    public List<String> getSelect() {
83 ilm 339
        return Collections.unmodifiableList(this.select);
17 ilm 340
    }
341
 
342
    /**
83 ilm 343
     * Column names of the SELECT. Should always have the same length and same indexes as the result
344
     * set, i.e. will contain <code>null</code> for computed columns without aliases. But the length
345
     * may not be equal to that of {@link #getSelect()}, e.g. when using
346
     * {@link #addSelectStar(TableRef)} which add one expression but all the fields.
17 ilm 347
     *
83 ilm 348
     * @return a list of column names of the SELECT, <code>null</code> for indexes without any.
17 ilm 349
     */
83 ilm 350
    public List<String> getSelectNames() {
351
        return Collections.unmodifiableList(this.selectNames);
17 ilm 352
    }
353
 
83 ilm 354
    /**
355
     * Fields of the SELECT. Should always have the same length and same indexes as the result set,
356
     * i.e. will contain <code>null</code> for computed columns. But the length may not be equal to
357
     * that of {@link #getSelect()}, e.g. when using {@link #addSelectStar(TableRef)} which add one
358
     * expression but all the fields.
359
     *
360
     * @return a list of fields used by the SELECT, <code>null</code> for indexes without any.
361
     */
362
    public final List<FieldRef> getSelectFields() {
363
        return Collections.unmodifiableList(this.selectFields);
364
    }
365
 
17 ilm 366
    public List<String> getOrder() {
367
        return this.order;
368
    }
369
 
370
    public Where getWhere() {
371
        return this.where;
372
    }
373
 
93 ilm 374
    public final boolean contains(final String alias) {
17 ilm 375
        return this.declaredTables.contains(alias);
376
    }
377
 
378
    /**
379
     * Whether this SELECT already references table (eg by a from or a join). For example, if not
380
     * you can't ORDER BY with a field of that table.
381
     *
382
     * @param table the table to test.
383
     * @return <code>true</code> if table is already in this.
384
     */
93 ilm 385
    public final boolean contains(final SQLTable table) {
17 ilm 386
        return this.contains(table.getName());
387
    }
388
 
93 ilm 389
    private final void addIfNotExist(final TableRef t) {
390
        if (this.declaredTables.add(t, false))
391
            this.from.add(t);
392
    }
393
 
17 ilm 394
    // *** group by / having
395
 
93 ilm 396
    public SQLSelect addGroupBy(final FieldRef f) {
17 ilm 397
        this.groupBy.add(f);
398
        return this;
399
    }
400
 
93 ilm 401
    public SQLSelect setHaving(final Where w) {
17 ilm 402
        this.having = w;
403
        return this;
404
    }
405
 
406
    // *** order by
407
 
408
    /**
409
     * Ajoute un ORDER BY.
410
     *
411
     * @param t a table alias.
412
     * @return this.
413
     * @throws IllegalArgumentException si t n'est pas ordonné.
414
     * @throws IllegalStateException si t n'est pas dans cette requete.
415
     * @see SQLTable#isOrdered()
416
     */
93 ilm 417
    public SQLSelect addOrder(final String t) {
65 ilm 418
        return this.addOrder(this.getTableRef(t));
17 ilm 419
    }
420
 
93 ilm 421
    public SQLSelect addOrder(final TableRef t) {
65 ilm 422
        return this.addOrder(t, true);
17 ilm 423
    }
424
 
65 ilm 425
    /**
142 ilm 426
     * Add an ORDER BY for the passed table.
65 ilm 427
     *
428
     * @param t the table.
429
     * @param fieldMustExist if <code>true</code> then <code>t</code> must be
142 ilm 430
     *        {@link SQLTable#isOrdered() ordered} or have a {@link SQLTable#isRowable() numeric
431
     *        primary key}.
65 ilm 432
     * @return this.
142 ilm 433
     * @throws IllegalArgumentException if <code>t</code> has no usable order field and
434
     *         <code>mustExist</code> is <code>true</code>.
65 ilm 435
     */
93 ilm 436
    public SQLSelect addOrder(final TableRef t, final boolean fieldMustExist) {
65 ilm 437
        final SQLField orderField = t.getTable().getOrderField();
438
        if (orderField != null)
439
            this.addFieldOrder(t.getField(orderField.getName()));
142 ilm 440
        else if (t.getTable().isRowable())
441
            this.addFieldOrder(t.getKey());
65 ilm 442
        else if (fieldMustExist)
443
            throw new IllegalArgumentException("table is not ordered : " + t);
444
        return this;
445
    }
446
 
93 ilm 447
    public SQLSelect addFieldOrder(final FieldRef fieldRef) {
65 ilm 448
        return this.addFieldOrder(fieldRef, Order.asc());
449
    }
450
 
93 ilm 451
    public SQLSelect addFieldOrder(final FieldRef fieldRef, final Direction dir) {
65 ilm 452
        return this.addFieldOrder(fieldRef, dir, null);
453
    }
454
 
93 ilm 455
    public SQLSelect addFieldOrder(final FieldRef fieldRef, final Direction dir, final Nulls nulls) {
17 ilm 456
        // with Derby if you ORDER BY w/o mentioning the field in the select clause
457
        // you can't get the table names of columns in a result set.
67 ilm 458
        if (fieldRef.getField().getServer().getSQLSystem().equals(SQLSystem.DERBY))
17 ilm 459
            this.addSelect(fieldRef);
460
 
65 ilm 461
        return this.addRawOrder(fieldRef.getFieldRef() + dir.getSQL() + (nulls == null ? "" : nulls.getSQL()));
17 ilm 462
    }
463
 
464
    /**
465
     * Add an ORDER BY that is not an ORDER field.
466
     *
467
     * @param selectItem an item that appears in the select, either a field reference or an alias.
468
     * @return this.
469
     */
93 ilm 470
    public SQLSelect addRawOrder(final String selectItem) {
17 ilm 471
        this.order.add(selectItem);
472
        return this;
473
    }
474
 
475
    public SQLSelect clearOrder() {
476
        this.order.clear();
477
        return this;
478
    }
479
 
480
    /**
481
     * Ajoute un ORDER BY. Ne fais rien si t n'est pas ordonné.
482
     *
483
     * @param t la table.
484
     * @return this.
485
     * @throws IllegalStateException si t n'est pas dans cette requete.
486
     */
93 ilm 487
    public SQLSelect addOrderSilent(final String t) {
65 ilm 488
        return this.addOrder(this.getTableRef(t), false);
17 ilm 489
    }
490
 
491
    // *** select
492
 
493
    /**
494
     * Ajoute un champ au SELECT.
495
     *
496
     * @param f le champ à ajouter.
497
     * @return this pour pouvoir chaîner.
498
     */
93 ilm 499
    public SQLSelect addSelect(final FieldRef f) {
17 ilm 500
        return this.addSelect(f, null);
501
    }
502
 
182 ilm 503
    public final SQLSelect addAllSelect(final FieldRef... s) {
504
        return this.addAllSelect(Arrays.asList(s));
505
    }
506
 
17 ilm 507
    /**
508
     * Permet d'ajouter plusieurs champs.
509
     *
21 ilm 510
     * @param s une collection de FieldRef.
17 ilm 511
     * @return this pour pouvoir chaîner.
512
     */
93 ilm 513
    public SQLSelect addAllSelect(final Collection<? extends FieldRef> s) {
21 ilm 514
        for (final FieldRef element : s) {
515
            this.addSelect(element);
17 ilm 516
        }
517
        return this;
518
    }
519
 
520
    /**
521
     * Permet d'ajouter plusieurs champs d'une même table sans avoir à les préfixer.
522
     *
523
     * @param t la table.
524
     * @param s une collection de nom de champs, eg "NOM".
525
     * @return this pour pouvoir chaîner.
526
     */
93 ilm 527
    public SQLSelect addAllSelect(final TableRef t, final Collection<String> s) {
21 ilm 528
        for (final String fieldName : s) {
17 ilm 529
            this.addSelect(t.getField(fieldName));
530
        }
531
        return this;
532
    }
533
 
534
    /**
535
     * Ajoute une fonction d'un champ au SELECT.
536
     *
537
     * @param f le champ, eg "PERSON.AGE".
538
     * @param function la fonction, eg "AVG".
539
     * @return this pour pouvoir chaîner.
540
     */
93 ilm 541
    public SQLSelect addSelect(final FieldRef f, final String function) {
17 ilm 542
        return this.addSelect(f, function, null);
543
    }
544
 
93 ilm 545
    public SQLSelect addSelect(final FieldRef f, final String function, final String alias) {
83 ilm 546
        final String defaultAlias;
17 ilm 547
        String s = f.getFieldRef();
548
        if (function != null) {
549
            s = function + "(" + s + ")";
83 ilm 550
            defaultAlias = function;
551
        } else {
552
            defaultAlias = f.getField().getName();
17 ilm 553
        }
83 ilm 554
        return this.addRawSelect(f, s, alias, defaultAlias);
17 ilm 555
    }
556
 
557
    /**
558
     * To add an item that is not a field.
559
     *
83 ilm 560
     * @param expr any legal exp in a SELECT statement (e.g. a constant, a complex function, etc).
17 ilm 561
     * @param alias a name for the expression, may be <code>null</code>.
562
     * @return this.
563
     */
93 ilm 564
    public SQLSelect addRawSelect(final String expr, final String alias) {
83 ilm 565
        return this.addRawSelect(null, expr, alias, null);
566
    }
567
 
568
    // private since we can't check that f is used in expr
569
    // defaultName only used if alias is null
93 ilm 570
    private SQLSelect addRawSelect(final FieldRef f, String expr, final String alias, final String defaultName) {
17 ilm 571
        if (alias != null) {
21 ilm 572
            expr += " as " + SQLBase.quoteIdentifier(alias);
17 ilm 573
        }
574
        this.select.add(expr);
83 ilm 575
        if (f != null)
93 ilm 576
            this.addIfNotExist(f.getTableRef());
83 ilm 577
        this.selectFields.add(f);
578
        this.selectNames.add(alias != null ? alias : defaultName);
17 ilm 579
        return this;
580
    }
581
 
582
    /**
583
     * Ajoute une fonction prenant * comme paramètre.
584
     *
585
     * @param function la fonction, eg "COUNT".
586
     * @return this pour pouvoir chaîner.
587
     */
93 ilm 588
    public SQLSelect addSelectFunctionStar(final String function) {
17 ilm 589
        return this.addRawSelect(function + "(*)", null);
590
    }
591
 
93 ilm 592
    public SQLSelect addSelectStar(final TableRef table) {
73 ilm 593
        this.select.add(SQLBase.quoteIdentifier(table.getAlias()) + ".*");
93 ilm 594
        this.addIfNotExist(table);
83 ilm 595
        final List<SQLField> allFields = table.getTable().getOrderedFields();
596
        this.selectFields.addAll(allFields);
597
        for (final SQLField f : allFields)
598
            this.selectNames.add(f.getName());
17 ilm 599
        return this;
600
    }
601
 
93 ilm 602
    public SQLSelect clearSelect() {
603
        this.select.clear();
604
        this.selectFields.clear();
605
        this.selectNames.clear();
606
        return this;
607
    }
608
 
17 ilm 609
    // *** from
610
 
93 ilm 611
    public SQLSelect addFrom(final SQLTable table, final String alias) {
612
        return this.addFrom(AliasedTable.getTableRef(table, alias));
17 ilm 613
    }
614
 
615
    /**
616
     * Explicitely add a table to the from clause. Rarely needed since tables are auto added by
617
     * addSelect(), setWhere() and addJoin().
618
     *
65 ilm 619
     * @param t the table to add.
17 ilm 620
     * @return this.
621
     */
93 ilm 622
    public SQLSelect addFrom(final TableRef t) {
623
        this.addIfNotExist(t);
17 ilm 624
        return this;
625
    }
626
 
627
    // *** where
628
 
629
    /**
630
     * Change la clause where de cette requete.
631
     *
632
     * @param w la nouvelle clause, <code>null</code> pour aucune clause.
633
     * @return this.
634
     */
93 ilm 635
    public SQLSelect setWhere(final Where w) {
17 ilm 636
        this.where = w;
637
        // FIXME si where était non null alors on a ajouté des tables dans FROM
638
        // qui ne sont peut être plus utiles
639
        // une solution : ne calculer le from que dans asString() => marche pas car on s'en
640
        // sert dans addOrder
67 ilm 641
        if (w != null) {
21 ilm 642
            for (final FieldRef f : w.getFields()) {
93 ilm 643
                this.addIfNotExist(f.getTableRef());
17 ilm 644
            }
645
        }
646
        return this;
647
    }
648
 
93 ilm 649
    public SQLSelect setWhere(final FieldRef field, final String op, final int i) {
65 ilm 650
        return this.setWhere(new Where(field, op, i));
17 ilm 651
    }
652
 
653
    /**
654
     * Ajoute le Where passé à celui de ce select.
655
     *
656
     * @param w le Where à ajouter.
657
     * @return this.
658
     */
93 ilm 659
    public SQLSelect andWhere(final Where w) {
17 ilm 660
        return this.setWhere(Where.and(this.getWhere(), w));
661
    }
662
 
663
    // *** join
664
 
93 ilm 665
    /**
666
     * Add a join to this SELECT.
667
     *
668
     * @param joinType can be INNER, LEFT or RIGHT.
669
     * @param existingAlias an alias for a table already in this select, can be <code>null</code>.
670
     * @param s how to join a new table, i.e. the {@link Step#getTo() destination} will be added.
671
     * @param joinedAlias the alias of the new table, can be <code>null</code>.
672
     * @return the added join.
673
     */
674
    public SQLSelectJoin addJoin(final String joinType, final String existingAlias, final Step s, final String joinedAlias) {
675
        final TableRef existingTable = this.getTableRef(existingAlias != null ? existingAlias : s.getFrom().getName());
676
        final TableRef joinedTable = new AliasedTable(s.getTo(), joinedAlias);
677
        return this.addJoin(new SQLSelectJoin(this, joinType, existingTable, s, joinedTable));
678
    }
679
 
17 ilm 680
    // simple joins (with foreign field)
681
 
682
    /**
683
     * Add a join to this SELECT. Eg if <code>f</code> is |BATIMENT.ID_SITE|, then "join SITE on
684
     * BATIMENT.ID_SITE = SITE.ID" will be added.
685
     *
686
     * @param joinType can be INNER, LEFT or RIGHT.
687
     * @param f a foreign key, eg |BATIMENT.ID_SITE|.
688
     * @return the added join.
689
     */
93 ilm 690
    public SQLSelectJoin addJoin(final String joinType, final FieldRef f) {
17 ilm 691
        return this.addJoin(joinType, f, null);
692
    }
693
 
694
    /**
695
     * Add a join to this SELECT. Eg if <code>f</code> is bat.ID_SITE and <code>alias</code> is "s",
696
     * then "join SITE s on bat.ID_SITE = s.ID" will be added.
697
     *
698
     * @param joinType can be INNER, LEFT or RIGHT.
699
     * @param f a foreign key, eg obs.ID_ARTICLE_2.
700
     * @param alias the alias for joined table, can be <code>null</code>, eg "art2".
701
     * @return the added join.
702
     */
93 ilm 703
    public SQLSelectJoin addJoin(final String joinType, final FieldRef f, final String alias) {
704
        final Step s = Step.create(f.getField(), org.openconcerto.sql.model.graph.Link.Direction.FOREIGN);
705
        return this.addJoin(joinType, f.getAlias(), s, alias);
17 ilm 706
    }
707
 
708
    // arbitrary joins
709
 
710
    /**
711
     * Add a join to this SELECT, inferring the joined table from the where.
712
     *
713
     * @param joinType can be INNER, LEFT or RIGHT.
714
     * @param w the where joining the new table.
715
     * @return the added join.
716
     * @throws IllegalArgumentException if <code>w</code> hasn't exactly one table not yet
717
     *         {@link #contains(String) contained} in this.
718
     */
93 ilm 719
    public SQLSelectJoin addJoin(final String joinType, final Where w) {
17 ilm 720
        final Set<AliasedTable> tables = new HashSet<AliasedTable>();
721
        for (final FieldRef f : w.getFields()) {
722
            if (!this.contains(f.getAlias())) {
93 ilm 723
                // since it's a Set, use same class (i.e. SQLTable.equals(AliasedTable) always
724
                // return false)
17 ilm 725
                tables.add(new AliasedTable(f.getField().getTable(), f.getAlias()));
726
            }
727
        }
728
        if (tables.size() == 0)
729
            throw new IllegalArgumentException("No tables to add in " + w);
730
        if (tables.size() > 1)
731
            throw new IllegalArgumentException("More than one table to add (" + tables + ") in " + w);
732
        final AliasedTable joinedTable = tables.iterator().next();
93 ilm 733
        return addJoin(joinType, joinedTable, w);
17 ilm 734
    }
735
 
93 ilm 736
    public SQLSelectJoin addJoin(final String joinType, final TableRef joinedTable, final Where w) {
737
        // try to parse the where to find a Step
738
        final Tuple2<FieldRef, TableRef> parsed = SQLSelectJoin.parse(w);
739
        final FieldRef foreignFieldParsed = parsed.get0();
740
        final Step s;
741
        final TableRef existingTable;
742
        if (foreignFieldParsed == null) {
743
            s = null;
744
            existingTable = null;
745
        } else {
746
            final TableRef srcTableParsed = foreignFieldParsed.getTableRef();
747
            final TableRef destTableParsed = parsed.get1();
748
            if (AliasedTable.equals(destTableParsed, joinedTable)) {
749
                existingTable = srcTableParsed;
750
                s = Step.create(foreignFieldParsed.getField(), org.openconcerto.sql.model.graph.Link.Direction.FOREIGN);
751
            } else if (AliasedTable.equals(srcTableParsed, joinedTable)) {
752
                existingTable = destTableParsed;
753
                s = Step.create(foreignFieldParsed.getField(), org.openconcerto.sql.model.graph.Link.Direction.REFERENT);
754
            } else {
755
                throw new IllegalArgumentException("Joined table " + joinedTable + " isn't referenced in " + w);
756
            }
757
        }
17 ilm 758
 
93 ilm 759
        return this.addJoin(new SQLSelectJoin(this, joinType, joinedTable, w, s, existingTable));
17 ilm 760
    }
761
 
762
    /**
763
     * Add a join that goes backward through a foreign key, eg LEFT JOIN "KD_2006"."BATIMENT" "bat"
764
     * on "s"."ID" = "bat"."ID_SITE".
765
     *
766
     * @param joinType can be INNER, LEFT or RIGHT.
767
     * @param joinAlias the alias for the joined table, must not exist, eg "bat".
768
     * @param ff the foreign field, eg |BATIMENT.ID_SITE|.
769
     * @param foreignTableAlias the alias for the foreign table, must exist, eg "sit" or
770
     *        <code>null</code> for "SITE".
771
     * @return the added join.
772
     */
93 ilm 773
    public SQLSelectJoin addBackwardJoin(final String joinType, final String joinAlias, final SQLField ff, final String foreignTableAlias) {
65 ilm 774
        return this.addBackwardJoin(joinType, new AliasedField(ff, joinAlias), foreignTableAlias);
775
    }
776
 
777
    /**
778
     * Add a join that goes backward through a foreign key, eg LEFT JOIN "KD_2006"."BATIMENT" "bat"
779
     * on "s"."ID" = "bat"."ID_SITE".
780
     *
781
     * @param joinType can be INNER, LEFT or RIGHT.
782
     * @param ff the foreign field, the alias must not exist, e.g. bat.ID_SITE.
783
     * @param foreignTableAlias the alias for the foreign table, must exist, e.g. "sit" or
784
     *        <code>null</code> for "SITE".
785
     * @return the added join.
786
     */
93 ilm 787
    public SQLSelectJoin addBackwardJoin(final String joinType, final FieldRef ff, final String foreignTableAlias) {
788
        final Step s = Step.create(ff.getField(), org.openconcerto.sql.model.graph.Link.Direction.REFERENT);
789
        return this.addJoin(joinType, foreignTableAlias, s, ff.getAlias());
17 ilm 790
    }
791
 
93 ilm 792
    private final SQLSelectJoin addJoin(final SQLSelectJoin j) {
17 ilm 793
        // first check if the joined table is not already in this from
93 ilm 794
        // avoid this (where the 2nd line already added MOUVEMENT) :
795
        // sel.addSelect(tableEcriture.getField("NOM"));
796
        // sel.addSelect(tableMouvement.getField("NUMERO"));
797
        // sel.addJoin("LEFT", "ECRITURE.ID_MOUVEMENT");
798
        final boolean added = this.declaredTables.add(j.getJoinedTable(), true);
799
        // since we passed mustBeNew=true
800
        assert added;
17 ilm 801
        this.from.add(j);
802
        this.joinAliases.add(j.getAlias());
803
        this.joins.add(j);
804
        return j;
805
    }
806
 
93 ilm 807
    // ATTN doesn't check if the join is referenced
808
    private final void removeJoin(final SQLSelectJoin j) {
809
        if (this.joins.remove(j)) {
810
            final boolean removed = this.declaredTables.remove(j.getJoinedTable());
811
            assert removed;
812
            this.from.remove(j);
813
            this.joinAliases.remove(j.getAlias());
814
        }
815
    }
816
 
17 ilm 817
    public final List<SQLSelectJoin> getJoins() {
818
        return Collections.unmodifiableList(this.joins);
819
    }
820
 
821
    /**
822
     * Get the join going through <code>ff</code>, regardless of its alias.
823
     *
824
     * @param ff a foreign field, eg |BATIMENT.ID_SITE|.
93 ilm 825
     * @return the corresponding join or <code>null</code> if not exactly one is found, e.g. LEFT
826
     *         JOIN "test"."SITE" "s" on "bat"."ID_SITE"="s"."ID"
17 ilm 827
     */
93 ilm 828
    public final SQLSelectJoin getJoinFromField(final SQLField ff) {
829
        return CollectionUtils.getSole(getJoinsFromField(ff));
830
    }
831
 
832
    public final List<SQLSelectJoin> getJoinsFromField(final SQLField ff) {
833
        final List<SQLSelectJoin> res = new ArrayList<SQLSelectJoin>();
17 ilm 834
        for (final SQLSelectJoin j : this.joins) {
93 ilm 835
            final Step s = j.getStep();
836
            if (s != null && ff.equals(s.getSingleField())) {
837
                res.add(j);
17 ilm 838
            }
839
        }
93 ilm 840
        return res;
17 ilm 841
    }
842
 
843
    /**
844
     * The first join adding the passed table.
845
     *
846
     * @param t the table to search for, e.g. /LOCAL/.
847
     * @return the first matching join or <code>null</code> if none found, eg LEFT JOIN
848
     *         "test"."LOCAL" "l" on "r"."ID_LOCAL"="l"."ID"
849
     */
93 ilm 850
    public final SQLSelectJoin findJoinAdding(final SQLTable t) {
17 ilm 851
        for (final SQLSelectJoin j : this.joins) {
852
            if (j.getJoinedTable().getTable().equals(t)) {
853
                return j;
854
            }
855
        }
856
        return null;
857
    }
858
 
859
    /**
860
     * The join adding the passed table alias.
861
     *
862
     * @param alias a table alias, e.g. "l".
863
     * @return the matching join or <code>null</code> if none found, eg LEFT JOIN "test"."LOCAL" "l"
864
     *         on "r"."ID_LOCAL"="l"."ID"
865
     */
866
    public final SQLSelectJoin getJoinAdding(final String alias) {
867
        for (final SQLSelectJoin j : this.joins) {
868
            if (j.getAlias().equals(alias)) {
869
                return j;
870
            }
871
        }
872
        return null;
873
    }
874
 
875
    /**
93 ilm 876
     * Get the join going through <code>ff</code>, matching its alias but regardless of its
877
     * direction.
17 ilm 878
     *
879
     * @param ff a foreign field, eg |BATIMENT.ID_SITE|.
93 ilm 880
     * @return the corresponding join or <code>null</code> if not exactly one is found, eg
881
     *         <code>null</code> if this only contains LEFT JOIN "test"."SITE" "s" on
882
     *         "bat"."ID_SITE"="s"."ID" or if it contains
883
     *
884
     *         <pre>
885
     *         LEFT JOIN "test"."SITE" s1 on "BATIMENT"."ID_SITE" = s1."ID" and s1."FOO"
886
     *         LEFT JOIN "test"."SITE" s2 on "BATIMENT"."ID_SITE" = s2."ID" and s2."BAR"
132 ilm 887
     *         </pre>
17 ilm 888
     */
93 ilm 889
    public final SQLSelectJoin getJoin(final FieldRef ff) {
890
        return this.getJoin(ff, null);
17 ilm 891
    }
892
 
93 ilm 893
    public final SQLSelectJoin getJoin(final FieldRef ff, final String foreignAlias) {
894
        final Step s = Step.create(ff.getField(), org.openconcerto.sql.model.graph.Link.Direction.FOREIGN);
895
        final List<SQLSelectJoin> res = new ArrayList<SQLSelectJoin>();
896
        res.addAll(this.getJoins(ff.getAlias(), s, foreignAlias));
897
        res.addAll(this.getJoins(foreignAlias, s.reverse(), ff.getAlias()));
898
 
899
        // if we specify both aliases there can't be more than one join
900
        if (foreignAlias != null && res.size() > 1)
901
            throw new IllegalStateException("More than one join matched " + ff + " and " + foreignAlias + " :\n" + CollectionUtils.join(res, "\n"));
902
        return CollectionUtils.getSole(res);
903
    }
904
 
905
    /**
906
     * Get the JOIN matching the passed step.
907
     *
908
     * @param fromAlias the alias for the source of the step, <code>null</code> match any alias.
909
     * @param s the {@link SQLSelectJoin#getStep() step}, cannot be <code>null</code>.
910
     * @param joinedAlias the alias for the destination of the step, i.e. the
911
     *        {@link SQLSelectJoin#getJoinedTable() added table}, <code>null</code> match any alias.
912
     * @return the matching joins.
913
     */
914
    public final List<SQLSelectJoin> getJoins(final String fromAlias, final Step s, final String joinedAlias) {
915
        if (s == null)
916
            throw new NullPointerException("Null step");
917
        final List<SQLSelectJoin> res = new ArrayList<SQLSelectJoin>();
17 ilm 918
        for (final SQLSelectJoin j : this.joins) {
93 ilm 919
            final Step joinStep = j.getStep();
920
            if (s.equals(joinStep) && (fromAlias == null || j.getExistingTable().getAlias().equals(fromAlias)) && (joinedAlias == null || j.getAlias().equals(joinedAlias))) {
921
                res.add(j);
17 ilm 922
            }
923
        }
93 ilm 924
        return res;
17 ilm 925
    }
926
 
927
    /**
928
     * Assure that there's a path from <code>tableAlias</code> through <code>p</code>, adding the
929
     * missing joins.
930
     *
931
     * @param tableAlias the table at the start, eg "loc".
932
     * @param p the path that must be added, eg LOCAL-BATIMENT-SITE.
933
     * @return the alias of the last table of the path, "sit".
934
     */
93 ilm 935
    public TableRef assurePath(final String tableAlias, final Path p) {
17 ilm 936
        return this.followPath(tableAlias, p, true);
937
    }
938
 
93 ilm 939
    public TableRef followPath(final String tableAlias, final Path p) {
17 ilm 940
        return this.followPath(tableAlias, p, false);
941
    }
942
 
943
    /**
944
     * Return the alias at the end of the passed path.
945
     *
946
     * @param tableAlias the table at the start, eg "loc".
947
     * @param p the path to follow, eg LOCAL-BATIMENT-SITE.
948
     * @param create <code>true</code> if missing joins should be created.
949
     * @return the alias of the last table of the path or <code>null</code>, eg "sit".
950
     */
93 ilm 951
    public TableRef followPath(final String tableAlias, final Path p, final boolean create) {
65 ilm 952
        final TableRef firstTableRef = this.getTableRef(tableAlias);
953
        final SQLTable firstTable = firstTableRef.getTable();
17 ilm 954
        if (!p.getFirst().equals(firstTable) && !p.getLast().equals(firstTable))
955
            throw new IllegalArgumentException("neither ends of " + p + " is " + firstTable);
956
        else if (!p.getFirst().equals(firstTable))
957
            return followPath(tableAlias, p.reverse(), create);
958
 
65 ilm 959
        TableRef current = firstTableRef;
17 ilm 960
        for (int i = 0; i < p.length(); i++) {
73 ilm 961
            final Step step = p.getStep(i);
93 ilm 962
            final List<SQLSelectJoin> joins = this.getJoins(current.getAlias(), step, null);
963
            if (joins.size() > 1)
964
                throw new IllegalStateException("More than one join from " + current + " through " + step + " : " + joins);
965
            if (joins.size() == 1) {
966
                current = joins.get(0).getJoinedTable();
967
            } else if (create) {
17 ilm 968
                // we must add a join
65 ilm 969
                final String uniqAlias = getUniqueAlias("assurePath_" + i);
93 ilm 970
                final SQLSelectJoin createdJoin = this.addJoin("LEFT", current.getAlias(), step, uniqAlias);
65 ilm 971
                current = createdJoin.getJoinedTable();
93 ilm 972
            } else {
17 ilm 973
                return null;
93 ilm 974
            }
17 ilm 975
        }
976
 
977
        return current;
978
    }
979
 
142 ilm 980
    public final FieldRef followFieldPath(final IFieldPath fp) {
981
        return this.followFieldPath(fp.getPath().getFirst().getAlias(), fp);
982
    }
983
 
984
    public final FieldRef followFieldPath(final String tableAlias, final IFieldPath fp) {
985
        return this.followPath(tableAlias, fp.getPath()).getField(fp.getFieldName());
986
    }
987
 
17 ilm 988
    public boolean isExcludeUndefined() {
989
        return this.generalExcludeUndefined;
990
    }
991
 
93 ilm 992
    public void setExcludeUndefined(final boolean excludeUndefined) {
17 ilm 993
        this.generalExcludeUndefined = excludeUndefined;
994
    }
995
 
93 ilm 996
    public void setExcludeUndefined(final boolean exclude, final SQLTable table) {
17 ilm 997
        this.excludeUndefined.put(table, Boolean.valueOf(exclude));
998
    }
999
 
93 ilm 1000
    public void setArchivedPolicy(final ArchiveMode policy) {
17 ilm 1001
        this.setArchivedPolicy(null, policy);
1002
    }
1003
 
93 ilm 1004
    public void setArchivedPolicy(final SQLTable t, final ArchiveMode policy) {
17 ilm 1005
        this.archivedPolicy.put(t, policy);
1006
    }
1007
 
93 ilm 1008
    public final void setDistinct(final boolean distinct) {
17 ilm 1009
        this.distinct = distinct;
1010
    }
1011
 
1012
    /**
132 ilm 1013
     * Whether this SELECT should wait until all current transactions are complete. This prevent a
17 ilm 1014
     * SELECT following an UPDATE from seeing rows as they were before. NOTE that this may conflict
1015
     * with other clauses (GROUP BY, DISTINCT, etc.).
1016
     *
1017
     * @param waitTrx <code>true</code> if this select should wait.
132 ilm 1018
     * @deprecated use {@link #setLockStrength(LockStrength)}
17 ilm 1019
     */
93 ilm 1020
    public void setWaitPreviousWriteTX(final boolean waitTrx) {
132 ilm 1021
        this.setLockStrength(waitTrx ? LockStrength.SHARE : LockStrength.NONE);
17 ilm 1022
    }
1023
 
132 ilm 1024
    /**
1025
     * Set the lock strength for the returned rows. NOTE : this is a minimum, e.g. H2 only supports
1026
     * {@link LockStrength#UPDATE}.
1027
     *
1028
     * @param l the new lock strength.
1029
     * @throws IllegalArgumentException if the {@link #getSQLSystem() system} doesn't support locks.
1030
     */
1031
    public final void setLockStrength(final LockStrength l) throws IllegalArgumentException {
1032
        final SQLSystem sys = getSQLSystem();
1033
        if (l != LockStrength.NONE && sys != SQLSystem.POSTGRESQL && sys != SQLSystem.MYSQL && sys != SQLSystem.H2)
1034
            throw new IllegalArgumentException("This system doesn't support locks : " + sys);
1035
        this.lockStrength = l;
1036
    }
1037
 
1038
    public final LockStrength getLockStrength() {
1039
        return this.lockStrength;
1040
    }
1041
 
1042
    public void addLockedTable(final String table) {
1043
        if (this.getLockStrength() == LockStrength.NONE)
1044
            this.setLockStrength(LockStrength.SHARE);
17 ilm 1045
        this.waitTrxTables.add(SQLBase.quoteIdentifier(table));
1046
    }
1047
 
1048
    /**
1049
     * Set the maximum number of rows to return.
1050
     *
1051
     * @param limit the number of rows, <code>null</code> meaning no limit
1052
     * @return this.
1053
     */
1054
    public SQLSelect setLimit(final Integer limit) {
1055
        this.limit = limit;
1056
        return this;
1057
    }
1058
 
1059
    public final Integer getLimit() {
1060
        return this.limit;
1061
    }
1062
 
93 ilm 1063
    /**
1064
     * Set the number of rows to skip. NOTE: many systems require an ORDER BY, but even if some
1065
     * don't you should use one to get consistent results.
1066
     *
1067
     * @param offset number of rows to skip, <code>0</code> meaning don't skip any.
1068
     * @return this.
1069
     */
1070
    public SQLSelect setOffset(final int offset) {
1071
        if (offset < 0)
1072
            throw new IllegalArgumentException("Negative offset : " + offset);
1073
        this.offset = offset;
1074
        return this;
1075
    }
1076
 
1077
    public int getOffset() {
1078
        return this.offset;
1079
    }
1080
 
17 ilm 1081
    @Override
93 ilm 1082
    public boolean equals(final Object o) {
17 ilm 1083
        if (o instanceof SQLSelect)
1084
            // MAYBE use instance variables
1085
            return this.asString().equals(((SQLSelect) o).asString());
1086
        else
1087
            return false;
1088
    }
1089
 
1090
    @Override
1091
    public int hashCode() {
1092
        // don't use asString() which is more CPU intensive
1093
        return this.select.hashCode() + this.from.getSQL().hashCode() + (this.where == null ? 0 : this.where.hashCode());
1094
    }
1095
 
1096
    /**
132 ilm 1097
     * This method will return a query for counting rows of this SELECT.
1098
     *
1099
     * @return a query returning a single number, never <code>null</code>.
1100
     */
1101
    public final String getForRowCount() {
1102
        if (this.getLimit() != null && this.getLimit().intValue() == 0)
1103
            return "SELECT 0";
1104
 
1105
        final SQLSelect res = new SQLSelect(this);
1106
        if (res.clearForRowCount(true)) {
1107
            return "select count(*) from (" + res.asString() + ") subq";
1108
        } else {
1109
            return res.asString();
1110
        }
1111
    }
1112
 
1113
    /**
93 ilm 1114
     * This method will replace the expressions in the {@link #getSelect()} by <code>count(*)</code>
1115
     * , further it will remove any items not useful for counting rows. This includes
1116
     * {@link #getOrder()} and left joins.
132 ilm 1117
     *
1118
     * @throws IllegalStateException if this has GROUP BY, HAVING, OFFSET or LIMIT
1119
     * @see #getForRowCount()
93 ilm 1120
     */
132 ilm 1121
    public final void clearForRowCount() throws IllegalStateException {
1122
        this.clearForRowCount(false);
1123
    }
1124
 
1125
    private final boolean clearForRowCount(final boolean allowSubquery) throws IllegalStateException {
1126
        final boolean hasGroupByOrHaving = !this.groupBy.isEmpty() || this.having != null;
1127
        if (!allowSubquery && hasGroupByOrHaving)
93 ilm 1128
            throw new IllegalStateException("Group by present");
132 ilm 1129
        final boolean hasOffsetOrLimit = this.getOffset() > 0 || this.getLimit() != null;
1130
        if (!allowSubquery && hasOffsetOrLimit)
1131
            throw new IllegalStateException("Offset or limit present");
93 ilm 1132
 
1133
        this.clearSelect();
1134
        // not needed and some systems require the used fields to be in the select
1135
        this.clearOrder();
1136
        // some systems don't support aggregate functions (e.g. count) with this
1137
        this.setWaitPreviousWriteTX(false);
1138
 
1139
        final Set<String> requiredAliases;
1140
        if (this.getWhere() == null) {
1141
            requiredAliases = Collections.emptySet();
1142
        } else {
1143
            requiredAliases = new HashSet<String>();
1144
            for (final FieldRef f : this.getWhere().getFields()) {
1145
                requiredAliases.add(f.getTableRef().getAlias());
1146
            }
1147
        }
1148
        for (final SQLSelectJoin j : new ArrayList<SQLSelectJoin>(this.joins)) {
1149
            if (j.getJoinType().equalsIgnoreCase("left") && !requiredAliases.contains(j.getAlias()))
1150
                this.removeJoin(j);
1151
        }
1152
 
132 ilm 1153
        if (hasGroupByOrHaving || hasOffsetOrLimit) {
1154
            assert allowSubquery;
1155
            this.addRawSelect("1", null);
1156
            return true;
1157
        } else {
1158
            this.addSelectFunctionStar("count");
1159
            return false;
1160
        }
93 ilm 1161
    }
1162
 
1163
    public final Map<String, TableRef> getTableRefs() {
1164
        return this.declaredTables.getMap();
1165
    }
1166
 
1167
    /**
17 ilm 1168
     * Returns the table designated in this select by name.
1169
     *
1170
     * @param name a table name or an alias, eg "OBSERVATION" or "art2".
1171
     * @return the table named <code>name</code>.
1172
     * @throws IllegalArgumentException if <code>name</code> is unknown to this select.
1173
     */
93 ilm 1174
    public final SQLTable getTable(final String name) {
65 ilm 1175
        return this.getTableRef(name).getTable();
17 ilm 1176
    }
1177
 
93 ilm 1178
    public final TableRef getTableRef(final String alias) {
65 ilm 1179
        final TableRef res = this.declaredTables.getAliasedTable(alias);
1180
        if (res == null)
1181
            throw new IllegalArgumentException("alias not in this select : " + alias);
1182
        return res;
1183
    }
1184
 
17 ilm 1185
    /**
1186
     * Return the alias for the passed table.
1187
     *
1188
     * @param t a table.
1189
     * @return the alias for <code>t</code>, or <code>null</code> if <code>t</code> is not exactly
1190
     *         once in this.
1191
     */
93 ilm 1192
    public final TableRef getAlias(final SQLTable t) {
17 ilm 1193
        return this.declaredTables.getAlias(t);
1194
    }
1195
 
93 ilm 1196
    public final List<TableRef> getAliases(final SQLTable t) {
17 ilm 1197
        return this.declaredTables.getAliases(t);
1198
    }
1199
 
93 ilm 1200
    public final FieldRef getAlias(final SQLField f) {
17 ilm 1201
        return this.getAlias(f.getTable()).getField(f.getName());
1202
    }
1203
 
1204
    /**
1205
     * See http://www.postgresql.org/docs/8.2/interactive/sql-syntax-lexical.html#SQL-SYNTAX-
1206
     * IDENTIFIERS
1207
     */
1208
    static final int maxAliasLength = 63;
1209
 
1210
    /**
1211
     * Return an unused alias in this select.
1212
     *
1213
     * @param seed the wanted name, eg "tableAlias".
1214
     * @return a unique alias with the maximum possible of <code>seed</code>, eg "tableAl_1234".
1215
     */
1216
    public final String getUniqueAlias(String seed) {
1217
        if (seed.length() > maxAliasLength)
1218
            seed = seed.substring(0, maxAliasLength);
1219
 
1220
        if (!this.contains(seed)) {
1221
            return seed;
1222
        } else {
67 ilm 1223
            long time = 1;
17 ilm 1224
            for (int i = 0; i < 50; i++) {
1225
                final String res;
1226
                final String cat = seed + "_" + time;
1227
                if (cat.length() > maxAliasLength)
1228
                    res = seed.substring(0, seed.length() - (cat.length() - maxAliasLength)) + "_" + time;
1229
                else
1230
                    res = cat;
1231
                if (!this.contains(res))
1232
                    return res;
1233
                else
1234
                    time += 1;
1235
            }
1236
            // quit
1237
            return null;
1238
        }
1239
    }
1240
 
93 ilm 1241
    private final FieldRef createRef(final String alias, final SQLField f) {
17 ilm 1242
        return createRef(alias, f, true);
1243
    }
1244
 
1245
    /**
1246
     * Creates a FieldRef from the passed alias and field.
1247
     *
1248
     * @param alias the table alias, eg "obs".
1249
     * @param f the field, eg |OBSERVATION.ID_TENSION|.
1250
     * @param mustExist if the table name/alias must already exist in this select.
1251
     * @return the corresponding FieldRef.
1252
     * @throws IllegalArgumentException if <code>mustExist</code> is <code>true</code> and this does
1253
     *         not contain alias.
1254
     */
93 ilm 1255
    private final FieldRef createRef(final String alias, final SQLField f, final boolean mustExist) {
17 ilm 1256
        if (mustExist && !this.contains(alias))
1257
            throw new IllegalArgumentException("unknown alias " + alias);
1258
        return new AliasedField(f, alias);
1259
    }
1260
 
1261
    /**
1262
     * Return all fields known to this instance. NOTE the fields used in ORDER BY are not returned.
1263
     *
1264
     * @return all fields known to this instance.
1265
     */
1266
    public final Set<SQLField> getFields() {
83 ilm 1267
        final Set<SQLField> res = new HashSet<SQLField>(this.getSelectFields().size());
1268
        for (final FieldRef f : this.getSelectFields()) {
1269
            if (f != null)
1270
                res.add(f.getField());
1271
        }
17 ilm 1272
        for (final SQLSelectJoin j : getJoins())
1273
            res.addAll(getFields(j.getWhere()));
1274
        res.addAll(getFields(this.getWhere()));
1275
        for (final FieldRef gb : this.groupBy)
1276
            res.add(gb.getField());
1277
        res.addAll(getFields(this.having));
1278
        // MAYBE add order
1279
 
1280
        return res;
1281
    }
1282
 
93 ilm 1283
    private static final Set<SQLField> getFields(final Where w) {
17 ilm 1284
        if (w != null) {
1285
            final Set<SQLField> res = new HashSet<SQLField>();
1286
            for (final FieldRef v : w.getFields())
1287
                res.add(v.getField());
1288
            return res;
1289
        } else
1290
            return Collections.emptySet();
1291
    }
1292
 
1293
}