View Javadoc
1   /*
2   Copyright (c) 2008 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess.impl.query;
18  
19  import java.io.IOException;
20  import java.lang.System.Logger;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collection;
24  import java.util.Iterator;
25  import java.util.List;
26  
27  import com.healthmarketscience.jackcess.PropertyMap;
28  import com.healthmarketscience.jackcess.RowId;
29  import com.healthmarketscience.jackcess.impl.DatabaseImpl;
30  import com.healthmarketscience.jackcess.impl.PropertyMaps;
31  import com.healthmarketscience.jackcess.impl.QueryNameMapImpl;
32  import com.healthmarketscience.jackcess.impl.RowIdImpl;
33  import com.healthmarketscience.jackcess.impl.RowImpl;
34  import com.healthmarketscience.jackcess.impl.ToStringBuilder;
35  import static com.healthmarketscience.jackcess.impl.query.QueryFormat.*;
36  import com.healthmarketscience.jackcess.query.Query;
37  
38  
39  /**
40   * Base class for classes which encapsulate information about an Access query.
41   * The {@link #toSQLString()} method can be used to convert this object into
42   * the actual SQL string which this query data represents.
43   *
44   * @author James Ahlborn
45   */
46  public abstract class QueryImpl implements Query, PropertyMaps.Owner
47  {
48    protected static final Logger LOG = System.getLogger(QueryImpl.class.getName());
49  
50    private static final Rowackcess/Row.html#Row">Row EMPTY_ROW = new Row();
51  
52    private final String _name;
53    private final List<Row> _rows;
54    private final int _objectId;
55    private final Type _type;
56    private final int _objectFlag;
57    /** the database which holds this query, {@code null} for a query built
58        from rows alone, which is only done in tests */
59    private DatabaseImpl _database;
60    /** properties group for this query (and its columns) */
61    private PropertyMaps _propertyMaps;
62    /** name map for this query, read on first use */
63    private QueryNameMapImpl _nameMap;
64  
65    protected QueryImpl(String name, List<Row> rows, int objectId, int objectFlag,
66                        Type type)
67    {
68      _name = name;
69      _rows = rows;
70      _objectId = objectId;
71      _type = type;
72      _objectFlag = objectFlag;
73  
74      if(type != Type.UNKNOWN) {
75        short foundType = getShortValue(getQueryType(rows),
76                                        _type.getValue());
77        if(foundType != _type.getValue()) {
78          throw new IllegalStateException(withErrorContext(
79                  "Unexpected query type " + foundType));
80        }
81      }
82    }
83  
84    /**
85     * Returns the name of the query.
86     */
87    @Override
88    public String getName() {
89      return _name;
90    }
91  
92    /**
93     * Returns the type of the query.
94     */
95    @Override
96    public Type getType() {
97      return _type;
98    }
99  
100   @Override
101   public boolean isHidden() {
102     return((_objectFlag & DatabaseImpl.HIDDEN_OBJECT_FLAG) != 0);
103   }
104 
105   /**
106    * Returns the unique object id of the query.
107    */
108   @Override
109   public int getObjectId() {
110     return _objectId;
111   }
112 
113   @Override
114   public int getObjectFlag() {
115     return _objectFlag;
116   }
117 
118   @Override
119   public PropertyMap getProperties() throws IOException {
120     return getPropertyMaps().getDefault();
121   }
122 
123   /**
124    * @return all PropertyMaps for this query (and its columns)
125    * @usage _advanced_method_
126    */
127   public PropertyMaps getPropertyMaps() throws IOException {
128     if(_propertyMaps == null) {
129       _propertyMaps = _database.getPropertiesForObject(_objectId, this);
130     }
131     return _propertyMaps;
132   }
133 
134   @Override
135   public QueryNameMapImpl getNameMap() throws IOException {
136     if(_nameMap == null) {
137       byte[] bytes = (byte[])getProperties().getValue(
138           PropertyMap.QUERY_NAME_MAP_PROP);
139       if(bytes != null) {
140         _nameMap = QueryNameMapImpl.read(bytes, _database);
141       }
142     }
143     return _nameMap;
144   }
145 
146   @Override
147   public void propertiesUpdated() throws IOException {
148     // the name map property may have been replaced
149     _nameMap = null;
150   }
151 
152   /**
153    * Returns the rows from the system query table from which the query
154    * information was derived.
155    */
156   public List<Row> getRows() {
157     return _rows;
158   }
159 
160   protected List<Row> getRowsByAttribute(Byte attribute) {
161     return getRowsByAttribute(getRows(), attribute);
162   }
163 
164   protected Row getRowByAttribute(Byte attribute) {
165     return getUniqueRow(getRowsByAttribute(getRows(), attribute));
166   }
167 
168   public Row getTypeRow() {
169     return getRowByAttribute(TYPE_ATTRIBUTE);
170   }
171 
172   /**
173    * @return the sql text of the type row, without the NUL characters which
174    *         sometimes end the stored value.  Ms access does not show them.
175    */
176   protected String getTypeRowText() {
177     String text = getTypeRow().expression;
178     if(text == null) {
179       return null;
180     }
181     int end = text.length();
182     while((end > 0) && (text.charAt(end - 1) == '\0')) {
183       --end;
184     }
185     return text.substring(0, end);
186   }
187 
188   protected List<Row> getParameterRows() {
189     return getRowsByAttribute(PARAMETER_ATTRIBUTE);
190   }
191 
192   protected Row getFlagRow() {
193     return getRowByAttribute(FLAG_ATTRIBUTE);
194   }
195 
196   protected Row getRemoteDatabaseRow() {
197     return getRowByAttribute(REMOTEDB_ATTRIBUTE);
198   }
199 
200   protected List<Row> getTableRows() {
201     return getRowsByAttribute(TABLE_ATTRIBUTE);
202   }
203 
204   protected List<Row> getColumnRows() {
205     return getRowsByAttribute(COLUMN_ATTRIBUTE);
206   }
207 
208   protected List<Row> getJoinRows() {
209     return getRowsByAttribute(JOIN_ATTRIBUTE);
210   }
211 
212   protected Row getWhereRow() {
213     return getRowByAttribute(WHERE_ATTRIBUTE);
214   }
215 
216   protected List<Row> getGroupByRows() {
217     return getRowsByAttribute(GROUPBY_ATTRIBUTE);
218   }
219 
220   protected Row getHavingRow() {
221     return getRowByAttribute(HAVING_ATTRIBUTE);
222   }
223 
224   protected List<Row> getOrderByRows() {
225     return getRowsByAttribute(ORDERBY_ATTRIBUTE);
226   }
227 
228   protected abstract void toSQLString(StringBuilder builder);
229 
230   protected void toSQLParameterString(StringBuilder builder) {
231     // handle any parameters
232     List<String> params = getParameters();
233     if(!params.isEmpty()) {
234       builder.append("PARAMETERS ").append(params)
235         .append(';').append(NEWLINE);
236     }
237   }
238 
239   @Override
240   public List<String> getParameters()
241   {
242     return (new RowFormatter(getParameterRows()) {
243         @Override protected void format(StringBuilder builder, Row row) {
244           String typeName = PARAM_TYPE_MAP.get(row.flag);
245           if(typeName == null) {
246             throw new IllegalStateException(withErrorContext(
247                 "Unknown param type " + row.flag));
248           }
249 
250           toParameterName(builder, row.name1).append(' ').append(typeName);
251           if((TEXT_FLAG.equals(row.flag)) && (getIntValue(row.extra, 0) > 0)) {
252             builder.append('(').append(row.extra).append(')');
253           }
254         }
255       }).format();
256   }
257 
258   protected List<String> getFromTables()
259   {
260     // grab the list of query tables
261     List<TableSource> tableExprs = new ArrayList<>();
262     for(Row table : getTableRows()) {
263       StringBuilder builder = new StringBuilder();
264 
265       if(table.name1 == null) {
266         // a table row with an expression and no name is a derived table,
267         // which is written in parentheses, the form ms access writes.  Ms
268         // access does not read "[subquery]." when the subquery holds
269         // brackets.
270         if(table.expression != null) {
271           builder.append('(').append(table.expression).append(')');
272         }
273       } else {
274         if(table.expression != null) {
275           toQuotedExpr(builder, table.expression).append(IDENTIFIER_SEP_CHAR);
276         }
277         toOptionalQuotedExpr(builder, table.name1, true);
278       }
279       toAlias(builder, table.name2);
280 
281       String key = ((table.name2 != null) ? table.name2 : table.name1);
282       tableExprs.add(new SimpleTable(key, builder.toString()));
283     }
284 
285     // combine the tables with any query joins
286     List<Row> joins = getJoinRows();
287     for(Row joinRow : joins) {
288 
289       String fromTable = joinRow.name1;
290       String toTable = joinRow.name2;
291 
292       TableSource fromTs = null;
293       TableSource toTs = null;
294 
295       // combine existing join expressions containing the target tables
296       for(Iterator<TableSource> joinIter = tableExprs.iterator();
297           (joinIter.hasNext() && ((fromTs == null) || (toTs == null))); ) {
298         TableSource ts = joinIter.next();
299 
300         if((fromTs == null) && ts.containsTable(fromTable)) {
301           fromTs = ts;
302 
303           // special case adding expr to existing join
304           if((toTs == null) && ts.containsTable(toTable)) {
305             toTs = ts;
306             break;
307           }
308 
309           joinIter.remove();
310 
311         } else if((toTs == null) && ts.containsTable(toTable)) {
312 
313           toTs = ts;
314           joinIter.remove();
315         }
316       }
317 
318       if(fromTs == null) {
319         fromTs = new SimpleTable(fromTable);
320       }
321       if(toTs == null) {
322         toTs = new SimpleTable(toTable);
323       }
324 
325       if(fromTs == toTs) {
326 
327         if(fromTs.addCondition(fromTable, toTable, joinRow.flag,
328                                joinRow.expression)) {
329           // easy-peasy, we just added the join expression to existing join,
330           // nothing more to do
331           continue;
332         }
333 
334         throw new IllegalStateException(withErrorContext(
335                 "Inconsistent join types for " + fromTable + " and " + toTable));
336       }
337 
338       // new join expression
339       tableExprs.add(new Join(fromTs, toTs, joinRow.flag, joinRow.expression));
340     }
341 
342     // convert join objects to SQL strings
343     List<String> result = new AppendableList<>();
344     for(TableSource ts : tableExprs) {
345       result.add(ts.toString());
346     }
347 
348     return result;
349   }
350 
351   protected String getFromRemoteDbPath()
352   {
353     return getRemoteDatabaseRow().name1;
354   }
355 
356   protected String getFromRemoteDbType()
357   {
358     return getRemoteDatabaseRow().expression;
359   }
360 
361   protected String getWhereExpression()
362   {
363     return getWhereRow().expression;
364   }
365 
366   protected List<String> getOrderings()
367   {
368     return (new RowFormatter(getOrderByRows()) {
369         @Override protected void format(StringBuilder builder, Row row) {
370           builder.append(row.expression);
371           if(DESCENDING_FLAG.equalsIgnoreCase(row.name1)) {
372             builder.append(" DESC");
373           }
374         }
375       }).format();
376   }
377 
378   /**
379    * @return the select type keywords for the flags of this query, such as
380    *         "DISTINCTROW TOP 10", or {@link QueryFormat#DEFAULT_TYPE} if there
381    *         are none
382    */
383   public String getSelectType()
384   {
385     StringBuilder builder = new StringBuilder();
386     // TOP goes with either distinct type, as in "SELECT DISTINCTROW TOP 10"
387     if(hasFlag(DISTINCT_SELECT_TYPE)) {
388       builder.append("DISTINCT");
389     } else if(hasFlag(DISTINCT_ROW_SELECT_TYPE)) {
390       builder.append("DISTINCTROW");
391     }
392 
393     if(hasFlag(TOP_SELECT_TYPE)) {
394       if(builder.length() > 0) {
395         builder.append(' ');
396       }
397       builder.append("TOP ").append(getFlagRow().name1);
398       if(hasFlag(PERCENT_SELECT_TYPE)) {
399         builder.append(" PERCENT");
400       }
401     }
402 
403     return ((builder.length() > 0) ? builder.toString() : DEFAULT_TYPE);
404   }
405 
406   /**
407    * Appends the select type keywords and a space, if this query has any.
408    */
409   protected void appendSelectType(StringBuilder builder)
410   {
411     String selectType = getSelectType();
412     if(!DEFAULT_TYPE.equals(selectType)) {
413       builder.append(selectType).append(' ');
414     }
415   }
416 
417   @Override
418   public String getOwnerAccessType() {
419     return(hasFlag(OWNER_ACCESS_SELECT_TYPE) ?
420            "WITH OWNERACCESS OPTION" : DEFAULT_TYPE);
421   }
422 
423   protected boolean hasFlag(int flagMask)
424   {
425     return hasFlag(getFlagRow(), flagMask);
426   }
427 
428   protected boolean supportsStandardClauses() {
429     return true;
430   }
431 
432   /**
433    * Returns the actual SQL string which this query data represents.
434    */
435   @Override
436   public String toSQLString()
437   {
438     StringBuilder builder = new StringBuilder();
439     if(supportsStandardClauses()) {
440       toSQLParameterString(builder);
441     }
442 
443     toSQLString(builder);
444 
445     if(supportsStandardClauses()) {
446 
447       String accessType = getOwnerAccessType();
448       if(!DEFAULT_TYPE.equals(accessType)) {
449         builder.append(NEWLINE).append(accessType);
450       }
451 
452       builder.append(';');
453     }
454     return builder.toString();
455   }
456 
457   @Override
458   public String toString() {
459     // the database and the state read from it stay out of the output
460     return ToStringBuilder.builder(this)
461       .append("name", _name)
462       .append("objectFlag", _objectFlag)
463       .append("objectId", _objectId)
464       .append("rows", _rows)
465       .append("type", _type)
466       .toString();
467   }
468 
469   /**
470    * Creates a concrete Query instance from the given query data alone, for
471    * tests of the sql it generates.  The query has no database, so its
472    * properties cannot be read.
473    *
474    * @param objectFlag the flag indicating the type of the query
475    * @param name the name of the query
476    * @param rows the rows from the system query table containing the data
477    *             describing this query
478    * @param objectId the unique object id of this query
479    *
480    * @return a Query instance for the given query data
481    */
482   public static QueryImpl create(int objectFlag, String name, List<Row> rows,
483                                  int objectId)
484   {
485     return create(null, objectFlag, name, rows, objectId);
486   }
487 
488   /**
489    * Creates a concrete Query instance from the given query data.
490    *
491    * @param database the database which holds the query
492    * @param objectFlag the flag indicating the type of the query
493    * @param name the name of the query
494    * @param rows the rows from the system query table containing the data
495    *             describing this query
496    * @param objectId the unique object id of this query
497    *
498    * @return a Query instance for the given query data
499    */
500   public static QueryImpl create(DatabaseImpl database, int objectFlag,
501                                  String name, List<Row> rows, int objectId)
502   {
503     QueryImpl query = createQuery(objectFlag, name, sortByOrder(rows),
504                                   objectId);
505     query._database = database;
506     return query;
507   }
508 
509   private static QueryImpl createQuery(int objectFlag, String name,
510                                        List<Row> rows, int objectId)
511   {
512     // remove other object flags before testing for query type
513     int objTypeFlag = objectFlag & OBJECT_FLAG_MASK;
514 
515     if(objTypeFlag == 0) {
516       // sometimes the query rows tell a different story
517       short rowTypeFlag = getShortValue(getQueryType(rows), objTypeFlag);
518       Type rowType = TYPE_MAP.get(rowTypeFlag);
519       if((rowType != null) && (rowType.getObjectFlag() != objTypeFlag)) {
520         // use row type instead of object flag type
521         objTypeFlag = rowType.getObjectFlag();
522       }
523     }
524 
525     try {
526       switch(objTypeFlag) {
527       case SELECT_QUERY_OBJECT_FLAG:
528         return new SelectQueryImpl(name, rows, objectId, objectFlag);
529       case MAKE_TABLE_QUERY_OBJECT_FLAG:
530         return new MakeTableQueryImpl(name, rows, objectId, objectFlag);
531       case APPEND_QUERY_OBJECT_FLAG:
532         return new AppendQueryImpl(name, rows, objectId, objectFlag);
533       case UPDATE_QUERY_OBJECT_FLAG:
534         return new UpdateQueryImpl(name, rows, objectId, objectFlag);
535       case DELETE_QUERY_OBJECT_FLAG:
536         return new DeleteQueryImpl(name, rows, objectId, objectFlag);
537       case CROSS_TAB_QUERY_OBJECT_FLAG:
538         return new CrossTabQueryImpl(name, rows, objectId, objectFlag);
539       case DATA_DEF_QUERY_OBJECT_FLAG:
540         return new DataDefinitionQueryImpl(name, rows, objectId, objectFlag);
541       case PASSTHROUGH_QUERY_OBJECT_FLAG:
542         return new PassthroughQueryImpl(name, rows, objectId, objectFlag);
543       case UNION_QUERY_OBJECT_FLAG:
544         return new UnionQueryImpl(name, rows, objectId, objectFlag);
545       default:
546         // unknown querytype
547         throw new IllegalStateException(withErrorContext(
548                 "unknown query object flag " + objTypeFlag, name));
549       }
550     } catch(IllegalStateException e) {
551       LOG.log(Logger.Level.WARNING, withErrorContext("Failed parsing query", name), e);
552     }
553 
554     // return unknown query
555     return new UnknownQueryImpl(name, rows, objectId, objectFlag);
556   }
557 
558   /**
559    * Sorts the rows of a query by their order column.  The rows of the system
560    * query table are not always stored in that order, and ms access builds the
561    * sql from the order column, so the columns, sorts and joins come out in
562    * that order.  Rows without an order value are left as they are.
563    *
564    * @return a sorted copy of the given rows, or the given rows if any row
565    *         has no order value
566    */
567   private static List<Row> sortByOrder(List<Row> rows)
568   {
569     for(Row row : rows) {
570       if(row.order == null) {
571         return rows;
572       }
573     }
574     List<Row> sorted = new ArrayList<>(rows);
575     // the sort is stable, so rows with the same order value keep their places
576     sorted.sort(QueryImpl::compareOrder);
577     return sorted;
578   }
579 
580   /**
581    * Compares the rows by attribute, then by order.  The order value starts
582    * again at 1 for each attribute, so the attribute comes first to keep the
583    * rows of each attribute together.
584    */
585   private static int compareOrder(Roww" href="../../../../../com/healthmarketscience/jackcess/Row.html#Row">Row r1, Row r2)
586   {
587     int cmp = Integer.compare(toAttribute(r1), toAttribute(r2));
588     return ((cmp != 0) ? cmp : Arrays.compareUnsigned(r1.order, r2.order));
589   }
590 
591   private static int toAttribute(Row row)
592   {
593     return ((row.attribute != null) ? row.attribute : -1);
594   }
595 
596   private static Short getQueryType(List<Row> rows)
597   {
598     return getFirstRowByAttribute(rows, TYPE_ATTRIBUTE).flag;
599   }
600 
601   private static List<Row> getRowsByAttribute(List<Row> rows, Byte attribute) {
602     List<Row> result = new ArrayList<>();
603     for(Row row : rows) {
604       if(attribute.equals(row.attribute)) {
605         result.add(row);
606       }
607     }
608     return result;
609   }
610 
611   private static Row getFirstRowByAttribute(List<Row> rows, Byte attribute) {
612     for(Row row : rows) {
613       if(attribute.equals(row.attribute)) {
614         return row;
615       }
616     }
617     return EMPTY_ROW;
618   }
619 
620   protected Row getUniqueRow(List<Row> rows) {
621     if(rows.size() == 1) {
622       return rows.get(0);
623     }
624     if(rows.isEmpty()) {
625       return EMPTY_ROW;
626     }
627     throw new IllegalStateException(withErrorContext(
628             "Unexpected number of rows for" + rows));
629   }
630 
631   protected static List<Row> filterRowsByFlag(
632       List<Row> rows, final short flag)
633   {
634     return new RowFilter() {
635         @Override protected boolean keep(Row row) {
636           return hasFlag(row, flag);
637         }
638       }.filter(rows);
639   }
640 
641   protected static List<Row> filterRowsByNotFlag(
642       List<Row> rows, final short flag)
643   {
644     return new RowFilter() {
645         @Override protected boolean keep(Row row) {
646           return !hasFlag(row, flag);
647         }
648       }.filter(rows);
649   }
650 
651   protected static boolean hasFlag(Row row, int flagMask)
652   {
653     return((getShortValue(row.flag, 0) & flagMask) != 0);
654   }
655 
656   protected static short getShortValue(Short s, int def) {
657     return ((s != null) ? (short)s : (short)def);
658   }
659 
660   protected static int getIntValue(Integer i, int def) {
661     return ((i != null) ? (int)i : def);
662   }
663 
664   protected static StringBuilder toOptionalQuotedExpr(StringBuilder builder,
665                                                       String fullExpr,
666                                                       boolean isIdentifier)
667   {
668     String[] exprs = (isIdentifier ?
669                       IDENTIFIER_SEP_PAT.split(fullExpr) :
670                       new String[]{fullExpr});
671     for(int i = 0; i < exprs.length; ++i) {
672       String expr = exprs[i];
673       if(QUOTABLE_CHAR_PAT.matcher(expr).find()) {
674         toQuotedExpr(builder, expr);
675       } else {
676         builder.append(expr);
677       }
678       if(i < (exprs.length - 1)) {
679         builder.append(IDENTIFIER_SEP_CHAR);
680       }
681     }
682     return builder;
683   }
684 
685   protected static StringBuilder toQuotedExpr(StringBuilder builder,
686                                               String expr)
687   {
688     return (!isQuoted(expr) ?
689             builder.append('[').append(expr).append(']') :
690             builder.append(expr));
691   }
692 
693   protected static boolean isQuoted(String expr) {
694     return ((expr.length() >= 2) &&
695             (expr.charAt(0) == '[') && (expr.charAt(expr.length() - 1) == ']'));
696   }
697 
698   protected static StringBuilder toRemoteDb(StringBuilder builder,
699                                             String remoteDbPath,
700                                             String remoteDbType) {
701     if((remoteDbPath != null) || (remoteDbType != null)) {
702       // note, always include path string, even if empty
703       builder.append(" IN '");
704       if(remoteDbPath != null) {
705         builder.append(remoteDbPath);
706       }
707       builder.append('\'');
708       if(remoteDbType != null) {
709         builder.append(" [").append(remoteDbType).append(']');
710       }
711     }
712     return builder;
713   }
714 
715   /**
716    * Appends a parameter name, with each part that needs it in brackets, the
717    * way ms access writes one.  A name such as "Forms!My Form!Field" refers to
718    * a control, so the parts between the "!" and "." separators are quoted
719    * separately: "Forms![My Form]!Field".  A part which is already in brackets
720    * is kept as it is.  Any other name is a prompt, such as "Enter No. of
721    * items", and is quoted as a whole, so that it is the same name as the one
722    * in the query expressions.
723    */
724   protected static StringBuilder toParameterName(StringBuilder builder,
725                                                  String name)
726   {
727     if(name == null) {
728       return builder.append(name);
729     }
730 
731     List<String> parts = new ArrayList<>();
732     List<Character> seps = new ArrayList<>();
733     boolean hasBang = false;
734     int start = 0;
735     boolean inBrackets = false;
736     for(int i = 0; i < name.length(); ++i) {
737       char c = name.charAt(i);
738       if(c == '[') {
739         inBrackets = true;
740       } else if(c == ']') {
741         inBrackets = false;
742       } else if(!inBrackets && ((c == '!') || (c == IDENTIFIER_SEP_CHAR))) {
743         hasBang |= (c == '!');
744         parts.add(name.substring(start, i));
745         seps.add(c);
746         start = i + 1;
747       }
748     }
749     parts.add(name.substring(start));
750 
751     // a control reference always has a "!", and ms access names do not start
752     // or end with a space
753     boolean isReference = hasBang;
754     for(String part : parts) {
755       if(part.isEmpty() || part.startsWith(" ") || part.endsWith(" ")) {
756         isReference = false;
757       }
758     }
759     if(!isReference) {
760       return toOptionalQuotedExpr(builder, name, false);
761     }
762 
763     for(int i = 0; i < parts.size(); ++i) {
764       if(i > 0) {
765         builder.append(seps.get(i - 1));
766       }
767       toOptionalQuotedExpr(builder, parts.get(i), false);
768     }
769     return builder;
770   }
771 
772   protected static StringBuilder toAlias(StringBuilder builder,
773                                          String alias) {
774     if(alias != null) {
775       toOptionalQuotedExpr(builder.append(" AS "), alias, false);
776     }
777     return builder;
778   }
779 
780   private String withErrorContext(String msg) {
781     return withErrorContext(msg, getName());
782   }
783 
784   private static String withErrorContext(String msg, String queryName) {
785     return msg + " (Query: " + queryName + ")";
786   }
787 
788 
789   private static final class UnknownQueryImpl extends QueryImpl
790   {
791     private UnknownQueryImpl(String name, List<Row> rows, int objectId,
792                              int objectFlag)
793     {
794       super(name, rows, objectId, objectFlag, Type.UNKNOWN);
795     }
796 
797     @Override
798     protected void toSQLString(StringBuilder builder) {
799       throw new UnsupportedOperationException();
800     }
801   }
802 
803   /**
804    * Struct containing the information from a single row of the system query
805    * table.
806    */
807   public static final class Row
808   {
809     private final RowId _id;
810     public final Byte attribute;
811     public final String expression;
812     public final Short flag;
813     public final Integer extra;
814     public final String name1;
815     public final String name2;
816     public final Integer objectId;
817     public final byte[] order;
818 
819     private Row() {
820       this._id = null;
821       this.attribute = null;
822       this.expression = null;
823       this.flag = null;
824       this.extra = null;
825       this.name1 = null;
826       this.name2= null;
827       this.objectId = null;
828       this.order = null;
829     }
830 
831     public Row(com.healthmarketscience.jackcess.Row tableRow) {
832       this(tableRow.getId(),
833            tableRow.getByte(COL_ATTRIBUTE),
834            tableRow.getString(COL_EXPRESSION),
835            tableRow.getShort(COL_FLAG),
836            tableRow.getInt(COL_EXTRA),
837            tableRow.getString(COL_NAME1),
838            tableRow.getString(COL_NAME2),
839            tableRow.getInt(COL_OBJECTID),
840            tableRow.getBytes(COL_ORDER));
841     }
842 
843     public Row(RowId id, Byte newAttribute, String newExpression,
844                Short newFlag, Integer newExtra, String newName1,
845                String newName2, Integer newObjectId, byte[] newOrder)
846     {
847       _id = id;
848       attribute = newAttribute;
849       expression = newExpression;
850       flag = newFlag;
851       extra = newExtra;
852       name1 = newName1;
853       name2 = newName2;
854       objectId = newObjectId;
855       order = newOrder;
856     }
857 
858     public com.healthmarketscience.jackcess.Row toTableRow()
859     {
860       com.healthmarketscience.jackcess.Row tableRow = new RowImpl((RowIdImpl)_id);
861 
862       tableRow.put(COL_ATTRIBUTE, attribute);
863       tableRow.put(COL_EXPRESSION, expression);
864       tableRow.put(COL_FLAG, flag);
865       tableRow.put(COL_EXTRA, extra);
866       tableRow.put(COL_NAME1, name1);
867       tableRow.put(COL_NAME2, name2);
868       tableRow.put(COL_OBJECTID, objectId);
869       tableRow.put(COL_ORDER, order);
870 
871       return tableRow;
872     }
873 
874     @Override
875     public String toString() {
876       return ToStringBuilder.builder(this).reflectionToString();
877     }
878   }
879 
880   protected static abstract class RowFormatter
881   {
882     private final List<Row> _list;
883 
884     protected RowFormatter(List<Row> list) {
885       _list = list;
886     }
887 
888     public List<String> format() {
889       return format(new AppendableList<String>());
890     }
891 
892     public List<String> format(List<String> strs) {
893       for(Row row : _list) {
894         StringBuilder builder = new StringBuilder();
895         format(builder, row);
896         strs.add(builder.toString());
897       }
898       return strs;
899     }
900 
901     protected abstract void format(StringBuilder builder, Row row);
902   }
903 
904   protected static abstract class RowFilter
905   {
906     protected RowFilter() {
907     }
908 
909     public List<Row> filter(List<Row> list) {
910       for(Iterator<Row> iter = list.iterator(); iter.hasNext(); ) {
911         if(!keep(iter.next())) {
912           iter.remove();
913         }
914       }
915       return list;
916     }
917 
918     protected abstract boolean keep(Row row);
919   }
920 
921   protected static class AppendableList<E> extends ArrayList<E>
922   {
923     private static final long serialVersionUID = 0L;
924     private static final String DEF_SEP = ", ";
925 
926     private final String _sep;
927 
928     protected AppendableList() {
929       this(DEF_SEP);
930     }
931 
932     protected AppendableList(String sep) {
933       _sep = sep;
934     }
935 
936     protected AppendableList(Collection<? extends E> c) {
937       super(c);
938       _sep = DEF_SEP;
939     }
940 
941     @Override
942     public String toString() {
943       StringBuilder builder = new StringBuilder();
944       for(Iterator<E> iter = iterator(); iter.hasNext(); ) {
945         builder.append(iter.next().toString());
946         if(iter.hasNext()) {
947           builder.append(_sep);
948         }
949       }
950       return builder.toString();
951     }
952   }
953 
954   /**
955    * Base type of something which provides table data in a query
956    */
957   private static abstract class TableSource
958   {
959     @Override
960     public String toString() {
961       StringBuilder sb = new StringBuilder();
962       toString(sb, true);
963       return sb.toString();
964     }
965 
966     protected abstract void toString(StringBuilder sb, boolean isTopLevel);
967 
968     public abstract boolean containsTable(String table);
969 
970     /**
971      * Adds a join condition between two tables which this table source
972      * holds.  Ms access puts the condition on the innermost join which holds
973      * both tables and has the same join type.  When an outer join encloses
974      * that join, the query returns different rows if the condition is on the
975      * outer join.
976      *
977      * @return {@code true} if a join took the condition, {@code false}
978      *         otherwise
979      */
980     public abstract boolean addCondition(String table1, String table2,
981                                          short type, String on);
982   }
983 
984   /**
985    * Table data provided by a single table expression.
986    */
987   private static final class SimpleTable extends TableSource
988   {
989     private final String _tableName;
990     private final String _tableExpr;
991 
992     private SimpleTable(String tableName) {
993       this(tableName, toOptionalQuotedExpr(
994                new StringBuilder(), tableName, true).toString());
995     }
996 
997     private SimpleTable(String tableName, String tableExpr) {
998       _tableName = tableName;
999       _tableExpr = tableExpr;
1000     }
1001 
1002     @Override
1003     protected void toString(StringBuilder sb, boolean isTopLevel) {
1004       sb.append(_tableExpr);
1005     }
1006 
1007     @Override
1008     public boolean containsTable(String table) {
1009       return _tableName.equalsIgnoreCase(table);
1010     }
1011 
1012     @Override
1013     public boolean addCondition(String table1, String table2, short type,
1014                                 String on) {
1015       return false;
1016     }
1017   }
1018 
1019   /**
1020    * Table data provided by a join expression.
1021    */
1022   private final class Join extends TableSource
1023   {
1024     private final TableSource _from;
1025     private final TableSource _to;
1026     private final short _jType;
1027     // combine all the join expressions with "AND"
1028     private final List<String> _on = new AppendableList<>(") AND (");
1029 
1030     private Join(TableSource from, TableSource to, short type, String on) {
1031       _from = from;
1032       _to = to;
1033       _jType = type;
1034       _on.add(on);
1035     }
1036 
1037     @Override
1038     protected void toString(StringBuilder sb, boolean isTopLevel) {
1039       String joinType = JOIN_TYPE_MAP.get(_jType);
1040       if(joinType == null) {
1041         throw new IllegalStateException(withErrorContext(
1042                                             "Unknown join type " + _jType));
1043       }
1044 
1045       if(!isTopLevel) {
1046         sb.append("(");
1047       }
1048 
1049       _from.toString(sb, false);
1050       sb.append(joinType);
1051       _to.toString(sb, false);
1052       sb.append(" ON ");
1053 
1054       boolean multiOnExpr = (_on.size() > 1);
1055       if(multiOnExpr) {
1056         sb.append("(");
1057       }
1058       sb.append(_on);
1059       if(multiOnExpr) {
1060         sb.append(")");
1061       }
1062 
1063       if(!isTopLevel) {
1064         sb.append(")");
1065       }
1066     }
1067 
1068     @Override
1069     public boolean containsTable(String table) {
1070       return _from.containsTable(table) || _to.containsTable(table);
1071     }
1072 
1073     @Override
1074     public boolean addCondition(String table1, String table2, short type,
1075                                 String on) {
1076       if(_from.containsTable(table1) && _from.containsTable(table2) &&
1077          _from.addCondition(table1, table2, type, on)) {
1078         return true;
1079       }
1080       if(_to.containsTable(table1) && _to.containsTable(table2) &&
1081          _to.addCondition(table1, table2, type, on)) {
1082         return true;
1083       }
1084       if(_jType == type) {
1085         // note, AND conditions are added in _reverse_ order
1086         _on.add(0, on);
1087         return true;
1088       }
1089       return false;
1090     }
1091   }
1092 }