Coverage Report - org.melati.poem.JdbcTable
 
Classes in this File Line Coverage Branch Coverage Complexity
JdbcTable
89%
771/864
80%
283/352
2.515
JdbcTable$1
100%
5/5
N/A
2.515
JdbcTable$10
100%
4/4
100%
4/4
2.515
JdbcTable$2
100%
4/4
100%
2/2
2.515
JdbcTable$3
100%
2/2
100%
2/2
2.515
JdbcTable$4
100%
4/4
50%
1/2
2.515
JdbcTable$5
100%
3/3
N/A
2.515
JdbcTable$6
100%
3/3
N/A
2.515
JdbcTable$7
100%
2/2
N/A
2.515
JdbcTable$8
100%
2/2
N/A
2.515
JdbcTable$9
100%
2/2
N/A
2.515
JdbcTable$TransactionStuff
100%
5/5
N/A
2.515
 
 1  
 /*
 2  
  * $Source$
 3  
  * $Revision$
 4  
  *
 5  
  * Copyright (C) 2008 Tim Pizey
 6  
  * 
 7  
  * Part of Melati (http://melati.org), a framework for the rapid
 8  
  * development of clean, maintainable web applications.
 9  
  *
 10  
  * Melati is free software; Permission is granted to copy, distribute
 11  
  * and/or modify this software under the terms either:
 12  
  *
 13  
  * a) the GNU General Public License as published by the Free Software
 14  
  *    Foundation; either version 2 of the License, or (at your option)
 15  
  *    any later version,
 16  
  *
 17  
  *    or
 18  
  *
 19  
  * b) any version of the Melati Software License, as published
 20  
  *    at http://melati.org
 21  
  *
 22  
  * You should have received a copy of the GNU General Public License and
 23  
  * the Melati Software License along with this program;
 24  
  * if not, write to the Free Software Foundation, Inc.,
 25  
  * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
 26  
  * GNU General Public License and visit http://melati.org to obtain the
 27  
  * Melati Software License.
 28  
  *
 29  
  * Feel free to contact the Developers of Melati (http://melati.org),
 30  
  * if you would like to work out a different arrangement than the options
 31  
  * outlined here.  It is our intention to allow Melati to be used by as
 32  
  * wide an audience as possible.
 33  
  *
 34  
  * This program is distributed in the hope that it will be useful,
 35  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 36  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 37  
  * GNU General Public License for more details.
 38  
  *
 39  
  * Contact details for copyright holder:
 40  
  *
 41  
  *     Tim Pizey <timp At paneris.org>
 42  
  *     http://paneris.org/~timp
 43  
  */
 44  
 
 45  
 package org.melati.poem;
 46  
 
 47  
 import java.io.PrintStream;
 48  
 import java.sql.Connection;
 49  
 import java.sql.PreparedStatement;
 50  
 import java.sql.ResultSet;
 51  
 import java.sql.SQLException;
 52  
 import java.sql.Statement;
 53  
 import java.util.Arrays;
 54  
 import java.util.Enumeration;
 55  
 import java.util.Hashtable;
 56  
 import java.util.List;
 57  
 import java.util.Vector;
 58  
 
 59  
 import org.melati.poem.dbms.Dbms;
 60  
 import org.melati.poem.transaction.Transactioned;
 61  
 import org.melati.poem.transaction.TransactionedSerial;
 62  
 import org.melati.poem.util.ArrayEnumeration;
 63  
 import org.melati.poem.util.ArrayUtils;
 64  
 import org.melati.poem.util.Cache;
 65  
 import org.melati.poem.util.CachedIndexFactory;
 66  
 import org.melati.poem.util.EnumUtils;
 67  
 import org.melati.poem.util.MappedEnumeration;
 68  
 import org.melati.poem.util.Procedure;
 69  
 import org.melati.poem.util.FilteredEnumeration;
 70  
 import org.melati.poem.util.FlattenedEnumeration;
 71  
 import org.melati.poem.util.Order;
 72  
 import org.melati.poem.util.SortUtils;
 73  
 import org.melati.poem.util.StringUtils;
 74  
 
 75  
 /**
 76  
  * A Table.
 77  
  * @since 14 April 2008 
 78  
  */
 79  6150
 public class JdbcTable <P extends Persistent>  implements Selectable<P>, Table<P> {
 80  
 
 81  
   /** Default limit for row cache. */
 82  
   private static final int CACHE_LIMIT_DEFAULT = 100;
 83  
   private static final int DISPLAY_ORDER_DEFAULT = 100;
 84  
 
 85  932
   private JdbcTable<P> _this = this;
 86  
 
 87  
   Database database;
 88  
   private String name;
 89  
   private String quotedName;
 90  
   private DefinitionSource definitionSource;
 91  
 
 92  932
   private TableInfo info = null;
 93  
 
 94  932
   private TableListener[] listeners = {};
 95  
 
 96  932
   private Column<?>[] columns = {};
 97  932
   private Hashtable<String, Column<?>> columnsByName = new Hashtable<String, Column<?>>();
 98  
 
 99  932
   private Column<Integer> troidColumn = null;
 100  932
   private Column<Boolean> deletedColumn = null;
 101  932
   private Column<Capability> canReadColumn = null;
 102  932
   private Column<Capability> canSelectColumn = null;
 103  932
   private Column<Capability> canWriteColumn = null;
 104  932
   private Column<Capability> canDeleteColumn = null;
 105  932
   private Column<?> displayColumn = null;
 106  932
   private Column<?> searchColumn = null;
 107  
 
 108  932
   private String defaultOrderByClause = null;
 109  
 
 110  932
   private Column<?>[][] displayColumns = new Column[DisplayLevel.count()][];
 111  932
   private Column<?>[] searchColumns = null;
 112  
 
 113  
   private TransactionedSerial serial;
 114  
 
 115  932
   private CachedSelection<P> allTroids = null;
 116  932
   private Hashtable<String, CachedSelection<P>> cachedSelections = new Hashtable<String, CachedSelection<P>>();
 117  932
   private Hashtable<String, CachedCount> cachedCounts = new Hashtable<String, CachedCount>();
 118  932
   private Hashtable<String, CachedExists> cachedExists = new Hashtable<String, CachedExists>();
 119  
 
 120  932
   private int mostRecentTroid = -1;
 121  932
   private int extrasIndex = 0;
 122  
 
 123  
   
 124  
   /**
 125  
    * Constructor.
 126  
    */
 127  
   public JdbcTable(Database database, String name,
 128  932
                DefinitionSource definitionSource) {
 129  932
     this.database = database;
 130  932
     this.name = name;
 131  
     // database.log("Creating table with name " + name + " from " + definitionSource);
 132  
     // Don't do this here as the database does not know about the dbms yet
 133  
     // this.quotedName = database.quotedName(name);
 134  
     // this is actually set the first time it is accessed in quotedName()
 135  932
     this.definitionSource = definitionSource;
 136  932
     serial = new TransactionedSerial(database);
 137  932
   }
 138  
 
 139  
   /**
 140  
    * Override this to perform pre-unification initialisation.
 141  
    */
 142  
   public void init() {
 143  705
   }
 144  
 
 145  
   /**
 146  
    * Do stuff immediately after table initialisation.
 147  
    * <p>
 148  
    * This base method clears the column info caches and adds a listener
 149  
    * to the column info table to maintain the caches.
 150  
    * <p>
 151  
    * It may be overridden to perform other actions. For example to
 152  
    * ensure required rows exist in tables that define numeric ID's for
 153  
    * codes.
 154  
    *
 155  
    * @see #notifyColumnInfo(ColumnInfo)
 156  
    * @see #clearColumnInfoCaches()
 157  
    */
 158  
   public void postInitialise() {
 159  708
     clearColumnInfoCaches();
 160  708
     database.getColumnInfoTable().addListener(
 161  708
         new TableListener() {
 162  
           public void notifyTouched(PoemTransaction transaction, Table<?> table,
 163  
                                     Persistent persistent) {
 164  2600
             _this.notifyColumnInfo((ColumnInfo)persistent);
 165  2600
           }
 166  
 
 167  
           public void notifyUncached(Table<?> table) {
 168  1492
             _this.clearColumnInfoCaches();
 169  1492
           }
 170  
         });
 171  708
   }
 172  
 
 173  
   // 
 174  
   // ===========
 175  
   //  Accessors
 176  
   // ===========
 177  
   // 
 178  
 
 179  
   /**
 180  
    * The database to which the table is attached.
 181  
    * @return the db
 182  
    */
 183  
   public final Database getDatabase() {
 184  60900
     return database;
 185  
   }
 186  
 
 187  
   /** 
 188  
    * The table's programmatic name.  Identical with its name in the DSD (if the
 189  
    * table was defined there) and in its <TT>tableinfo</TT> entry.
 190  
    * This will normally be the same as the name in the RDBMS itself, however that name 
 191  
    * may be translated to avoid DBMS specific name clashes. 
 192  
    *
 193  
    * @return the table name, case as defined in the DSD
 194  
    * @see org.melati.poem.dbms.Dbms#melatiName(String)
 195  
    */
 196  
   public final String getName() {
 197  6119
     return name;
 198  
   }
 199  
 
 200  
  /**
 201  
   * @return table name quoted using the DBMS' specific quoting rules.
 202  
   */
 203  
   public final String quotedName() {
 204  19289
     if (quotedName == null) quotedName = database.quotedName(name);
 205  19289
     return quotedName;
 206  
   }
 207  
 
 208  
  /**
 209  
   * The human-readable name of the table.  POEM itself doesn't use this, but
 210  
   * it's available to applications and Melati's generic admin system as a
 211  
   * default label for the table and caption for its records.
 212  
    * @return The human-readable name of the table
 213  
    */
 214  
   public final String getDisplayName() {
 215  2
     return info.getDisplayname();
 216  
   }
 217  
 
 218  
  /**
 219  
   * A brief description of the table's function.  POEM itself doesn't use
 220  
   * this, but it's available to applications and Melati's generic admin system
 221  
   * as a default label for the table and caption for its records.
 222  
   * @return the brief description
 223  
   */
 224  
   public final String getDescription() {
 225  2
     return info.getDescription();
 226  
   }
 227  
 
 228  
   /**
 229  
    * The category of this table.  POEM itself doesn't use
 230  
    * this, but it's available to applications and Melati's generic admin system
 231  
    * as a default label for the table and caption for its records.
 232  
    * 
 233  
    * @return the category
 234  
    */
 235  
   public final TableCategory getCategory() {
 236  1
      return info.getCategory();
 237  
   }
 238  
 
 239  
  /**
 240  
   * @return the {@link TableInfo} for this table
 241  
   */
 242  
   public final TableInfo getInfo() {
 243  69
      return info;
 244  
   }
 245  
 
 246  
  /**
 247  
   * The troid (<TT>id</TT>) of the table's entry in the <TT>tableinfo</TT>
 248  
   * table.  It will always have one (except during initialisation, which the
 249  
   * application programmer will never see).
 250  
   * 
 251  
   * @return id in TableInfo metadata table
 252  
   */
 253  
   public final Integer tableInfoID() {
 254  8984
     return info == null ? null : info.troid();
 255  
   }
 256  
 
 257  
   /**
 258  
    * The table's column with a given name.  If the table is defined in the DSD
 259  
    * under the name <TT><I>foo</I></TT>, there will be an
 260  
    * application-specialised <TT>Table</TT> subclass, called
 261  
    * <TT><I>Foo</I>Table</TT> (and available as <TT>get<I>Foo</I>Table</TT>
 262  
    * from the application-specialised <TT>Database</TT> subclass) which has
 263  
    * extra named methods for accessing the table's predefined <TT>Column</TT>s.
 264  
    *
 265  
    * @param nameP name of column to get
 266  
    * @return column of that name
 267  
    * @throws NoSuchColumnPoemException if there is no column with that name
 268  
    */
 269  
   public final Column<?> getColumn(String nameP) throws NoSuchColumnPoemException {
 270  891
     Column<?> column = _getColumn(nameP); 
 271  891
     if (column == null)
 272  1
       throw new NoSuchColumnPoemException(this, nameP);
 273  
     else
 274  890
       return column;
 275  
   }
 276  
   protected final Column<?> _getColumn(String nameP) {
 277  10024
     Column<?> column = columnsByName.get(nameP.toLowerCase());    
 278  10024
     return column;
 279  
   }
 280  
   
 281  
   /**
 282  
    * All the table's columns.
 283  
    *
 284  
    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
 285  
    * @see Column
 286  
    */
 287  
   public final Enumeration<Column<?>> columns() {
 288  15524
     return new ArrayEnumeration<Column<?>>(columns);
 289  
   }
 290  
 
 291  
   public final List<Column<?>> getColumns() { 
 292  0
     return Arrays.asList(columns);
 293  
   }
 294  
   
 295  
  /**
 296  
   * @return the number of columns in this table.
 297  
   */
 298  
   public final int getColumnsCount() {
 299  1
     return columns.length;
 300  
   }
 301  
 
 302  
   /**
 303  
    * @param columnInfoID
 304  
    * @return the Column with a TROID equal to columnInfoID
 305  
    */
 306  
   public Column<?> columnWithColumnInfoID(int columnInfoID) {
 307  152
     for (Enumeration<Column<?>> c = columns(); c.hasMoreElements();) {
 308  1015
       Column<?> column = c.nextElement();
 309  1015
       Integer id = column.columnInfoID();
 310  1015
       if (id != null && id.intValue() == columnInfoID)
 311  134
         return column;
 312  881
     }
 313  18
     return null; // Happens when columns exist but are not defined in DSD
 314  
   }
 315  
 
 316  
   /**
 317  
    * The table's troid column.  Every table in a POEM database must have a
 318  
    * troid (table row ID, or table-unique non-nullable integer primary key),
 319  
    * often but not necessarily called <TT>id</TT>, so that it can be
 320  
    * conveniently `named'.
 321  
    *
 322  
    * @return the id column
 323  
    * @see #getObject(java.lang.Integer)
 324  
    */
 325  
   public final Column<Integer> troidColumn() {
 326  1185
     return troidColumn;
 327  
   }
 328  
 
 329  
   /**
 330  
    * @return The table's deleted-flag column, if any.
 331  
    */
 332  
   public final Column<Boolean> deletedColumn() {
 333  1
     return deletedColumn;
 334  
   }
 335  
 
 336  
   /**
 337  
    * The table's primary display column, the Troid column if not set.  
 338  
    * This is the column used to represent records from the table 
 339  
    * concisely in reports or whatever.  It is determined 
 340  
    * at initialisation time by examining the <TT>Column</TT>s
 341  
    * <TT>getPrimaryDisplay()</TT> flags.
 342  
    *
 343  
    * @return the table's display column, or <TT>null</TT> if it hasn't got one
 344  
    *
 345  
    * see Column#setColumnInfo
 346  
    * @see ReferencePoemType#_stringOfCooked
 347  
    * @see DisplayLevel#primary
 348  
    */
 349  
   public final Column<?> displayColumn() {
 350  667
     return displayColumn == null ? troidColumn : displayColumn;
 351  
   }
 352  
 
 353  
   /**
 354  
    * @param column the display column to set
 355  
    */
 356  
   @SuppressWarnings("rawtypes")
 357  
   public final void setDisplayColumn(Column column) {
 358  440
     displayColumn = column;
 359  440
   }
 360  
 
 361  
  /**
 362  
   * In a similar manner to the primary display column, each table can have 
 363  
   * one primary criterion column.
 364  
   * <p>
 365  
   * The Primary Criterion is the main grouping field of the table, 
 366  
   * ie the most important non-unique type field.
 367  
   * <p>
 368  
   * For example the Primary Criterion for a User table might be Nationality.
 369  
   *
 370  
   * @return the search column, if any
 371  
   * @see Searchability
 372  
   */
 373  
   public final Column<?> primaryCriterionColumn() {
 374  1
     return searchColumn;
 375  
   }
 376  
 
 377  
   /**
 378  
    * @param column the search column to set
 379  
    */
 380  
   public void setSearchColumn(Column<?> column) {
 381  432
     searchColumn = column;
 382  432
   }
 383  
 
 384  
   /**
 385  
    * If the troidColumn has yet to be set then returns an empty string.
 386  
    *  
 387  
    * @return comma separated list of the columns to order by
 388  
    */
 389  
   @SuppressWarnings({ "unchecked", "rawtypes" })
 390  
   public String defaultOrderByClause() {
 391  687
     String clause = defaultOrderByClause;
 392  
 
 393  687
     if (clause == null) {
 394  822
       clause = EnumUtils.concatenated(
 395  
           ", ",
 396  411
           new MappedEnumeration(new ArrayEnumeration(SortUtils.sorted(
 397  411
               new Order() {
 398  
                 public boolean lessOrEqual(Object a, Object b) {
 399  33
                   return
 400  33
                       ((Column)a).getDisplayOrderPriority().intValue() <=
 401  33
                       ((Column)b).getDisplayOrderPriority().intValue();
 402  
                 }
 403  
               },
 404  3554
               new FilteredEnumeration<Column<?>>(columns()) {
 405  
                 public boolean isIncluded(Column<?> column) {
 406  3143
                   return ((Column)column).getDisplayOrderPriority() != null;
 407  
                 }
 408  411
               }))) {
 409  
             public Object mapped(Object column) {
 410  140
               String sort = ((Column)column).fullQuotedName();
 411  140
               if (((Column)column).getSortDescending()) sort += " desc";
 412  140
               return sort;
 413  
             }
 414  
           });
 415  
 
 416  411
       if (clause.equals("") && displayColumn() != null)
 417  299
         clause = displayColumn().fullQuotedName();
 418  
 
 419  411
       defaultOrderByClause = clause;
 420  
     }
 421  
 
 422  687
     return clause;
 423  
   }
 424  
 
 425  
   /**
 426  
    * Clear caches.
 427  
    */
 428  
   public void clearColumnInfoCaches() {
 429  2437
     defaultOrderByClause = null;
 430  14622
     for (int i = 0; i < displayColumns.length; ++i)
 431  12185
       displayColumns[i] = null;
 432  2437
   }
 433  
 
 434  
   /**
 435  
    * Clears columnInfo caches, normally a no-op.
 436  
    * 
 437  
    * @param infoP the possibly null ColumnInfo meta-data persistent
 438  
    */
 439  
   public void notifyColumnInfo(ColumnInfo infoP) {
 440  
     // FIXME info == null means deleted: effect is too broad really
 441  2763
     if (infoP == null || infoP.getTableinfo_unsafe().equals(tableInfoID()))
 442  234
       clearColumnInfoCaches();
 443  2763
   }
 444  
 
 445  
   /**
 446  
    * Get an Array of columns meeting the criteria of whereClause.
 447  
    * 
 448  
    * It is the programmer's responsibility to ensure that the where clause 
 449  
    * is suitable for the target DBMS.
 450  
    * 
 451  
    * @param whereClause an SQL snippet
 452  
    * @return an array of Columns
 453  
    */
 454  
   private Column<?>[] columnsWhere(String whereClause) {
 455  
     // get the col IDs from the committed session
 456  26
     Enumeration<Integer> colIDs =
 457  52
         getDatabase().getColumnInfoTable().troidSelection(
 458  26
             database.quotedName("tableinfo") + " = " + tableInfoID() + 
 459  
               " AND (" + whereClause + ")",
 460  26
             null, false, PoemThread.inSession() ? PoemThread.transaction() : null);
 461  
 
 462  26
     Vector<Column<?>> them = new Vector<Column<?>>();
 463  155
     while (colIDs.hasMoreElements()) {
 464  129
       Column<?> column =
 465  129
           columnWithColumnInfoID(((Integer)colIDs.nextElement()).intValue());
 466  
       // null shouldn't happen but let's not gratuitously fail if it does
 467  129
       if (column != null)
 468  129
         them.addElement(column);
 469  129
     }
 470  
 
 471  26
     Column<?>[] columnsLocal = new Column<?>[them.size()];
 472  26
     them.copyInto(columnsLocal);
 473  26
     return columnsLocal;
 474  
   }
 475  
 
 476  
   /**
 477  
    * Return columns at a display level in display order.
 478  
    *
 479  
    * @param level the {@link DisplayLevel} to select
 480  
    * @return an Enumeration of columns at the given level
 481  
    */ 
 482  
   public final Enumeration<Column<?>> displayColumns(DisplayLevel level) {
 483  104
     Column<?>[] columnsLocal = displayColumns[level.getIndex().intValue()];
 484  
 
 485  104
     if (columnsLocal == null) {
 486  24
       columnsLocal =
 487  48
         columnsWhere(database.quotedName("displaylevel") + " <= " + 
 488  24
                                                          level.getIndex());
 489  24
       displayColumns[level.getIndex().intValue()] = columnsLocal;
 490  
     }
 491  104
     return new ArrayEnumeration<Column<?>>(columnsLocal);
 492  
   }
 493  
 
 494  
   /**
 495  
    * @param level the {@link DisplayLevel} to select
 496  
    * @return the number of columns at a display level.
 497  
    */ 
 498  
   public final int displayColumnsCount(DisplayLevel level) {
 499  22
     int l = level.getIndex().intValue();
 500  22
     if (displayColumns[l] == null)
 501  
       // FIXME Race 
 502  10
       displayColumns(level);
 503  
 
 504  22
     return displayColumns[l].length;
 505  
   }
 506  
 
 507  
   /**
 508  
    * The table's columns for detailed display in display order.
 509  
    *
 510  
    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
 511  
    * @see Column
 512  
    * @see #displayColumns(DisplayLevel)
 513  
    * @see DisplayLevel#detail
 514  
    */
 515  
   public final Enumeration<Column<?>> getDetailDisplayColumns() {
 516  72
     return displayColumns(DisplayLevel.detail);
 517  
   }
 518  
 
 519  
   /**
 520  
    * @return the number of columns at display level <tt>Detail</tt>
 521  
    */ 
 522  
   public final int getDetailDisplayColumnsCount() {
 523  19
     return displayColumnsCount(DisplayLevel.detail);
 524  
   }
 525  
 
 526  
   /**
 527  
    * The table's columns designated for display in a record, in display order.
 528  
    *
 529  
    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
 530  
    * @see Column
 531  
    * @see #displayColumns(DisplayLevel)
 532  
    * @see DisplayLevel#record
 533  
    */
 534  
   public final Enumeration<Column<?>> getRecordDisplayColumns() {
 535  17
     return displayColumns(DisplayLevel.record);
 536  
   }
 537  
 
 538  
   /**
 539  
    * @return the number of columns at display level <tt>Record</tt>
 540  
    */ 
 541  
   public final int getRecordDisplayColumnsCount() {
 542  1
     return displayColumnsCount(DisplayLevel.record);
 543  
   }
 544  
 
 545  
   /**
 546  
    * The table's columns designated for display in a record summary, in display
 547  
    * order.
 548  
    *
 549  
    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
 550  
    * @see Column
 551  
    * @see #displayColumns(DisplayLevel)
 552  
    * @see DisplayLevel#summary
 553  
    */
 554  
   public final Enumeration<Column<?>> getSummaryDisplayColumns() {
 555  5
     return displayColumns(DisplayLevel.summary);
 556  
   }
 557  
   
 558  
   /**
 559  
    * @return the number of columns at display level <tt>Summary</tt>
 560  
    */ 
 561  
   public final int getSummaryDisplayColumnsCount() {
 562  1
     return displayColumnsCount(DisplayLevel.summary);
 563  
   }
 564  
 
 565  
   /**
 566  
    * The table's columns designated for use as search criteria, in display
 567  
    * order.
 568  
    *
 569  
    * @return an <TT>Enumeration</TT> of <TT>Column</TT>s
 570  
    * @see Column
 571  
    */
 572  
   public final Enumeration<Column<?>> getSearchCriterionColumns() {
 573  3
     Column<?>[] columnsLocal = searchColumns;
 574  
 
 575  3
     if (columnsLocal == null) {
 576  2
       columnsLocal = 
 577  4
          columnsWhere(database.quotedName("searchability") + " <= " +
 578  2
                                           Searchability.yes.getIndex());
 579  2
       searchColumns = columnsLocal;
 580  
     }
 581  3
     return new ArrayEnumeration<Column<?>>(searchColumns);
 582  
   }
 583  
 
 584  
   /**
 585  
    * @return the number of columns which are searchable
 586  
    */ 
 587  
   public final int getSearchCriterionColumnsCount() {
 588  1
     if (searchColumns == null)
 589  
       // FIXME Race 
 590  1
       getSearchCriterionColumns();
 591  
       
 592  1
     return searchColumns.length;
 593  
   }
 594  
 
 595  
   private Dbms dbms() {
 596  11126
     return getDatabase().getDbms();
 597  
   }
 598  
 
 599  
   // 
 600  
   // =========================
 601  
   //  Low-level DB operations
 602  
   // =========================
 603  
   // 
 604  
 
 605  
   // 
 606  
   // -----------
 607  
   //  Structure
 608  
   // -----------
 609  
   // 
 610  
 
 611  
   /**
 612  
    * @deprecated Use {@link org.melati.poem.Database#modifyStructure(String)} instead
 613  
    */
 614  
   public void dbModifyStructure(String sql)
 615  
       throws StructuralModificationFailedPoemException {
 616  976
         database.modifyStructure(sql);
 617  976
   }
 618  
 
 619  
   private void dbCreateTable() {
 620  293
     String createTableSql = dbms().createTableSql(this);
 621  293
     database.modifyStructure(createTableSql);
 622  293
     String tableSetup = database.getDbms().tableInitialisationSql(this); 
 623  293
     if (tableSetup != null) { 
 624  0
       database.modifyStructure(tableSetup);
 625  
     }
 626  293
   }
 627  
   
 628  
     
 629  
 
 630  
   /**
 631  
    * @return A type string eg "TEXT"
 632  
    * @see org.melati.poem.dbms.Hsqldb
 633  
    */
 634  
   public String getDbmsTableType() {
 635  294
     return null;
 636  
   }
 637  
 
 638  
   /**
 639  
    * Constraints are not used in POEM, but you might want to use them if 
 640  
    * exporting the db or using schema visualisation tools.
 641  
    */
 642  
   public void dbAddConstraints() {
 643  153
     StringBuffer sqb = new StringBuffer();
 644  1326
     for (int c = 0; c < columns.length; ++c) {
 645  1173
       if (columns[c].getSQLType() instanceof TroidPoemType){
 646  153
         sqb.append("ALTER TABLE " + quotedName());
 647  306
         sqb.append(dbms().getPrimaryKeyDefinition(
 648  153
             columns[c].getName()));
 649  
         try {
 650  153
           dbModifyStructure(sqb.toString());
 651  0
         } catch (StructuralModificationFailedPoemException e) {
 652  
           // It is more expensive to only add constaints 
 653  
           // if they are missing than to ignore exceptions.  
 654  0
           e = null;
 655  153
         }
 656  
       }
 657  
     }
 658  1326
     for (int c = 0; c < columns.length; ++c) {
 659  1173
       if (columns[c].getSQLType() instanceof ReferencePoemType){
 660  170
         IntegrityFix fix = columns[c].getIntegrityFix();
 661  170
         sqb = new StringBuffer();
 662  170
         sqb.append("ALTER TABLE " + quotedName());
 663  340
         sqb.append(dbms().getForeignKeyDefinition(
 664  170
                       getName(),
 665  170
                       columns[c].getName(),
 666  170
                       ((PersistentReferencePoemType)columns[c].getSQLType()).
 667  170
                           targetTable().getName(),
 668  170
                       ((PersistentReferencePoemType)columns[c].getSQLType()).
 669  170
                           targetTable().troidColumn().getName(),
 670  170
                        fix.getName()));
 671  
         try {
 672  170
           dbModifyStructure(sqb.toString());
 673  0
         } catch (StructuralModificationFailedPoemException e) {
 674  
           // It is more expensive to only add constaints 
 675  
           // if they are missing than to ignore exceptions.  
 676  0
           e = null;          
 677  170
         }
 678  
       }
 679  
     }
 680  
 
 681  
 
 682  153
   }
 683  
 
 684  
   private void dbAddColumn(Column<?> column) {
 685  84
     if (column.getType().getNullable()) {
 686  130
       dbModifyStructure(
 687  65
           "ALTER TABLE " + quotedName() +
 688  65
           " ADD " + column.quotedName() +
 689  65
           " " + column.getSQLType().sqlDefinition(dbms()));
 690  
     } else {
 691  19
       if (column.getUnique()) {
 692  2
         throw new UnificationPoemException("Cannot add new unique, non-nullable column " 
 693  2
              + column.getName() + " to table " + getName());
 694  
       } else {
 695  34
         dbModifyStructure(
 696  17
             "ALTER TABLE " + quotedName() +
 697  17
             " ADD " + column.quotedName() +
 698  17
             " " + column.getSQLType().sqlTypeDefinition(dbms()));
 699  34
         dbModifyStructure(
 700  17
             "UPDATE " + quotedName() +
 701  17
             " SET " + column.quotedName() +
 702  34
             " = " + dbms().getQuotedValue(column.getSQLType(), 
 703  17
                     column.getSQLType().sqlDefaultValue(dbms())));
 704  
       }
 705  34
       dbModifyStructure(
 706  17
           dbms().alterColumnNotNullableSQL(name, column));
 707  
       
 708  
     }
 709  82
   }
 710  
 
 711  
   
 712  
   private void dbCreateIndex(Column<?> column) {
 713  4198
     if (column.getIndexed()) {
 714  535
       if (!dbms().canBeIndexed(column)) {
 715  0
         database.log(new UnindexableLogEvent(column));
 716  
       } else {
 717  1070
         dbModifyStructure(
 718  535
             "CREATE " + (column.getUnique() ? "UNIQUE " : "") + "INDEX " +
 719  535
             indexName(column) +
 720  535
             " ON " + quotedName() + " " +
 721  535
             "(" + column.quotedName() + 
 722  535
              dbms().getIndexLength(column) + ")");
 723  
       }
 724  
     }
 725  4198
   }
 726  
 
 727  
   private String indexName(Column<?> column) { 
 728  2276
     return database.quotedName(
 729  1138
             dbms().unreservedName(name) + "_" + 
 730  1138
             dbms().unreservedName(column.getName()) + "_index");
 731  
   }
 732  
   // 
 733  
   // -------------------------------
 734  
   //  Standard `PreparedStatement's
 735  
   // -------------------------------
 736  
   // 
 737  
 
 738  
   /**
 739  
    * 
 740  
    * @param connection the connection the PreparedStatement is tied to
 741  
    * @return a PreparedStatment to perform a simple INSERT
 742  
    */
 743  
   private PreparedStatement simpleInsert(Connection connection) {
 744  343
     StringBuffer sql = new StringBuffer();
 745  343
     sql.append("INSERT INTO " + quotedName() + " (");
 746  2644
     for (int c = 0; c < columns.length; ++c) {
 747  2301
       if (c > 0) sql.append(", ");
 748  2301
       sql.append(columns[c].quotedName());
 749  
     }
 750  343
     sql.append(") VALUES (");
 751  2644
     for (int c = 0; c < columns.length; ++c) {
 752  2301
       if (c > 0) sql.append(", ");
 753  2301
       sql.append("?");
 754  
     }
 755  
 
 756  343
     sql.append(")");
 757  
 
 758  
     try {
 759  343
       return connection.prepareStatement(sql.toString());
 760  
     }
 761  0
     catch (SQLException e) {
 762  0
       throw new SimplePrepareFailedPoemException(sql.toString(), e);
 763  
     }
 764  
   }
 765  
 
 766  
   private PreparedStatement simpleGet(Connection connection) {
 767  343
     StringBuffer sql = new StringBuffer();
 768  343
     sql.append("SELECT ");
 769  2644
     for (int c = 0; c < columns.length; ++c) {
 770  2301
       if (c > 0) sql.append(", ");
 771  2301
       sql.append(columns[c].quotedName());
 772  
     }
 773  686
     sql.append(" FROM " + quotedName() +
 774  343
                " WHERE " + troidColumn.quotedName() + " = ?");
 775  
 
 776  
     try {
 777  343
       return connection.prepareStatement(sql.toString());
 778  
     }
 779  0
     catch (SQLException e) {
 780  0
       throw new SimplePrepareFailedPoemException(sql.toString(), e);
 781  
     }
 782  
   }
 783  
 
 784  
   private PreparedStatement simpleModify(Connection connection) {
 785  
     // FIXME synchronize this too
 786  343
     StringBuffer sql = new StringBuffer();
 787  343
     sql.append("UPDATE " + quotedName() + " SET ");
 788  2644
     for (int c = 0; c < columns.length; ++c) {
 789  2301
       if (c > 0) sql.append(", ");
 790  2301
       sql.append(columns[c].quotedName());
 791  2301
       sql.append(" = ?");
 792  
     }
 793  343
     sql.append(" WHERE " + troidColumn.quotedName() + " = ?");
 794  
 
 795  
     try {
 796  343
       return connection.prepareStatement(sql.toString());
 797  
     }
 798  0
     catch (SQLException e) {
 799  0
       throw new SimplePrepareFailedPoemException(sql.toString(), e);
 800  
     }
 801  
   }
 802  
 
 803  
   // 
 804  
   // -----------------------------
 805  
   //  Transaction-specific things
 806  
   // -----------------------------
 807  
   // 
 808  
 
 809  
   private class TransactionStuff {
 810  
     PreparedStatement insert, modify, get;
 811  343
     TransactionStuff(Connection connection) {
 812  343
       insert = _this.simpleInsert(connection);
 813  343
       modify = _this.simpleModify(connection);
 814  343
       get = _this.simpleGet(connection);
 815  343
     }
 816  
   }
 817  
 
 818  932
   private CachedIndexFactory transactionStuffs = new CachedIndexFactory() {
 819  
     public Object reallyGet(int index) {
 820  
       // "Table.this" is attempt to work around Dietmar's problem with JDK1.3.1
 821  340
       return new TransactionStuff(
 822  340
           JdbcTable.this.database.poemTransaction(index).getConnection());
 823  
     }
 824  
   };
 825  
 
 826  932
   private TransactionStuff committedTransactionStuff = null;
 827  
 
 828  
   /**
 829  
    * When deleting a table and used in tests.
 830  
    */
 831  
   public void invalidateTransactionStuffs() { 
 832  1
     transactionStuffs.invalidate();
 833  1
   }
 834  
   /**
 835  
    * Called when working outside a Transaction.
 836  
    * @return the TransactionStuff for the committed transaction
 837  
    * @see org.melati.poem.PoemDatabase#inCommittedTransaction(AccessToken, PoemTask)
 838  
    */
 839  
   private synchronized TransactionStuff getCommittedTransactionStuff() {
 840  3
     if (committedTransactionStuff == null)
 841  3
       committedTransactionStuff =
 842  3
           new TransactionStuff(database.getCommittedConnection());
 843  3
     return committedTransactionStuff;
 844  
   }
 845  
 
 846  
   // 
 847  
   // --------------------
 848  
   //  Loading and saving
 849  
   // --------------------
 850  
   // 
 851  
 
 852  
   private void load(PreparedStatement select, Persistent p) {
 853  2395
     JdbcPersistent persistent = (JdbcPersistent)p;
 854  
     try {
 855  2395
       synchronized (select) {
 856  2395
         select.setInt(1, persistent.troid().intValue());
 857  2395
         ResultSet rs = select.executeQuery();
 858  2395
         if (database.logSQL())
 859  460
           database.log(new SQLLogEvent(select.toString()));
 860  2395
         database.incrementQueryCount(select.toString());
 861  
         try {
 862  2395
           if (!rs.next())
 863  1313
             persistent.setStatusNonexistent();
 864  
           else {
 865  1082
             persistent.setStatusExistent();
 866  17220
             for (int c = 0; c < columns.length; ++c)
 867  16138
               columns[c].load_unsafe(rs, c + 1, persistent);
 868  
           }
 869  2395
           persistent.setDirty(false);
 870  2395
           persistent.markValid();
 871  2395
           if (rs.next())
 872  0
             throw new DuplicateTroidPoemException(this, persistent.troid());
 873  
         }
 874  
         finally {
 875  2395
           try { rs.close(); } catch (Exception e) {
 876  0
             database.log("Cannot close resultset after exception.");  
 877  2395
           }
 878  0
         }
 879  2395
       }
 880  
     }
 881  0
     catch (SQLException e) {
 882  0
       throw new SimpleRetrievalFailedPoemException(e, select.toString());
 883  
     }
 884  0
     catch (ValidationPoemException e) {
 885  0
       throw new UnexpectedValidationPoemException(e);
 886  2395
     }
 887  2395
   }
 888  
 
 889  
   /**
 890  
    * @param transaction possibly null if working with the committed transaction
 891  
    * @param persistent the Persistent to load
 892  
    */
 893  
   @SuppressWarnings("unchecked")
 894  
   public void load(PoemTransaction transaction, Persistent persistent) {
 895  4790
     load(transaction == null ?
 896  3
             getCommittedTransactionStuff().get :
 897  2392
             ((TransactionStuff)transactionStuffs.get(transaction.index)).get,
 898  
          persistent);
 899  2395
   }
 900  
 
 901  
   private void modify(PoemTransaction transaction, Persistent persistent) {
 902  
     @SuppressWarnings("unchecked")
 903  514
     PreparedStatement modify =
 904  514
         ((TransactionStuff)transactionStuffs.get(transaction.index)).modify;
 905  514
     synchronized (modify) {
 906  6244
       for (int c = 0; c < columns.length; ++c)
 907  5730
         columns[c].save_unsafe(persistent, modify, c + 1);
 908  
 
 909  
       try {
 910  514
         modify.setInt(columns.length + 1, persistent.troid().intValue());
 911  
       }
 912  0
       catch (SQLException e) {
 913  0
         throw new SQLSeriousPoemException(e);
 914  514
       }
 915  
 
 916  
       try {
 917  514
         modify.executeUpdate();
 918  
       }
 919  0
       catch (SQLException e) {
 920  0
         throw dbms().exceptionForUpdate(this, modify, false, e);
 921  514
       }
 922  514
       database.incrementQueryCount(modify.toString());
 923  
 
 924  514
       if (database.logSQL())
 925  301
         database.log(new SQLLogEvent(modify.toString()));
 926  514
     }
 927  514
     persistent.postModify();
 928  514
   }
 929  
 
 930  
   @SuppressWarnings("unchecked")
 931  
   private void insert(PoemTransaction transaction, Persistent persistent) {
 932  
     
 933  6009
     PreparedStatement insert =
 934  6008
         ((TransactionStuff)transactionStuffs.get(transaction.index)).insert;
 935  6008
     synchronized (insert) {
 936  136320
       for (int c = 0; c < columns.length; ++c)
 937  130312
         columns[c].save_unsafe(persistent, insert, c + 1);
 938  
       try {
 939  6008
         insert.executeUpdate();
 940  
       }
 941  0
       catch (SQLException e) {
 942  0
         throw dbms().exceptionForUpdate(this, insert, true, e);
 943  6008
       }
 944  6008
       database.incrementQueryCount(insert.toString());
 945  6008
       if (database.logSQL())
 946  4124
         database.log(new SQLLogEvent(insert.toString()));
 947  6008
     }
 948  6008
     persistent.postInsert();
 949  6008
   }
 950  
 
 951  
   /**
 952  
    * The Transaction cannot be null, as this is trapped in 
 953  
    * #deleteLock(SessionToken).
 954  
    * @param troid id of row to delete
 955  
    * @param transaction a non-null transaction
 956  
    */
 957  
   public void delete(Integer troid, PoemTransaction transaction) {
 958  118
     String sql =
 959  118
         "DELETE FROM " + quotedName() +
 960  118
         " WHERE " + troidColumn.quotedName() + " = " +
 961  118
         troid.toString();
 962  
     try {
 963  118
       transaction.writeDown();
 964  118
       Connection connection = transaction.getConnection();
 965  
 
 966  118
       Statement deleteStatement = connection.createStatement();
 967  118
       int deleted = deleteStatement.executeUpdate(sql);
 968  118
       if (deleted != 1) { 
 969  0
         throw new RowDisappearedPoemException(this,troid);
 970  
       }
 971  118
       deleteStatement.close();
 972  118
       database.incrementQueryCount(sql);
 973  118
       if (database.logSQL())
 974  78
         database.log(new SQLLogEvent(sql));
 975  
 
 976  118
       cache.delete(troid);
 977  
     }
 978  0
     catch (SQLException e) {
 979  0
       throw new ExecutingSQLPoemException(sql, e);
 980  118
     }
 981  118
   }
 982  
 
 983  
   /**
 984  
    * @param transaction our PoemTransaction 
 985  
    * @param p the Persistent to write
 986  
    */
 987  
   public void writeDown(PoemTransaction transaction, Persistent p) {
 988  8353
     JdbcPersistent persistent = (JdbcPersistent)p;
 989  
     // NOTE No race, provided that the one-thread-per-transaction parity is
 990  
     // maintained
 991  
 
 992  8353
     if (persistent.isDirty()) {
 993  6523
       troidColumn.setRaw_unsafe(persistent, persistent.troid());
 994  
 
 995  6523
       if (persistent.statusExistent()) {
 996  514
         modify(transaction, persistent);
 997  6009
       } else if (persistent.statusNonexistent()) {
 998  6009
         insert(transaction, persistent);
 999  6008
         persistent.setStatusExistent();
 1000  
       }
 1001  
 
 1002  6522
       persistent.setDirty(false);
 1003  6522
       persistent.postWrite();
 1004  
     }
 1005  8352
   }
 1006  
 
 1007  
   // 
 1008  
   // ============
 1009  
   //  Operations
 1010  
   // ============
 1011  
   // 
 1012  
 
 1013  
   // 
 1014  
   // ----------
 1015  
   //  Cacheing
 1016  
   // ----------
 1017  
   // 
 1018  
 
 1019  932
   private Cache cache = new Cache(CACHE_LIMIT_DEFAULT);
 1020  
 
 1021  1
   private static final Procedure invalidator =
 1022  1
       new Procedure() {
 1023  
         public void apply(Object arg) {
 1024  14356
           ((Transactioned)arg).invalidate();
 1025  14356
         }
 1026  
       };
 1027  
 
 1028  
   /**
 1029  
    * Invalidate table cache.
 1030  
    * 
 1031  
    * NOTE Invalidated cache elements are reloaded when next read
 1032  
    */
 1033  
   public void uncache() {
 1034  1670
     cache.iterate(invalidator);
 1035  1670
     serial.invalidate();
 1036  1670
     TableListener[] listenersLocal = this.listeners;
 1037  3162
     for (int l = 0; l < listenersLocal.length; ++l)
 1038  1492
       listenersLocal[l].notifyUncached(this);
 1039  1670
   }
 1040  
 
 1041  
   /**
 1042  
    * @param maxSize new maximum size
 1043  
    */
 1044  
   public void trimCache(int maxSize) {
 1045  9
     cache.trim(maxSize);
 1046  9
   }
 1047  
 
 1048  
   /**
 1049  
    * Enable reporting of the status of the cache.
 1050  
    * 
 1051  
    * @return the Cache Info object
 1052  
    */ 
 1053  
   public Cache.Info getCacheInfo() {
 1054  3
     return cache.getInfo();
 1055  
   }
 1056  
 
 1057  
   /**
 1058  
    * Add a {@link TableListener} to this Table.
 1059  
    */ 
 1060  
   public void addListener(TableListener listener) {
 1061  708
     listeners = (TableListener[])ArrayUtils.added(listeners, listener);
 1062  708
   }
 1063  
 
 1064  
   /**
 1065  
    * Notify the table that one if its records is about to be changed in a
 1066  
    * transaction.  You can (with care) use this to support cacheing of
 1067  
    * frequently-used facts about the table's records.  
 1068  
    *
 1069  
    * @param transaction the transaction in which the change will be made
 1070  
    * @param persistent  the record to be changed
 1071  
    */
 1072  
   public void notifyTouched(PoemTransaction transaction, Persistent persistent) {
 1073  7462
     serial.increment(transaction);
 1074  
 
 1075  7462
     TableListener[] listenersLocal = this.listeners;
 1076  10062
     for (int l = 0; l < listenersLocal.length; ++l)
 1077  2600
       listenersLocal[l].notifyTouched(transaction, this, persistent);
 1078  7462
   }
 1079  
 
 1080  
   /**
 1081  
    * @return the Transaction serial 
 1082  
    */ 
 1083  
   public long serial(PoemTransaction transaction) {
 1084  198
     return serial.current(transaction);
 1085  
   }
 1086  
 
 1087  
   /**
 1088  
    * Lock this record.
 1089  
    */ 
 1090  
   public void readLock() {
 1091  3
     serial(PoemThread.transaction());
 1092  3
   }
 1093  
 
 1094  
   // 
 1095  
   // ----------
 1096  
   //  Fetching
 1097  
   // ----------
 1098  
   // 
 1099  
 
 1100  
   /**
 1101  
    * The object from the table with a given troid.
 1102  
    *
 1103  
    * @param troid       Every record (object) in a POEM database must have a
 1104  
    *                    troid (table row ID, or table-unique non-nullable
 1105  
    *                    integer primary key), often but not necessarily called
 1106  
    *                    <TT>id</TT>, so that it can be conveniently `named' for
 1107  
    *                    retrieval by this method.
 1108  
    *
 1109  
    * @return A <TT>Persistent</TT> of the record with the given troid;
 1110  
    *         or, if the table was defined in the DSD under the name
 1111  
    *         <TT><I>foo</I></TT>, an application-specialised subclass
 1112  
    *         <TT><I>Foo</I></TT> of <TT>Persistent</TT>.  In that case, there
 1113  
    *         will also be an application-specialised <TT>Table</TT> subclass,
 1114  
    *         called <TT><I>Foo</I>Table</TT> (and available as
 1115  
    *         <TT>get<I>Foo</I>Table</TT> from the application-specialised
 1116  
    *         <TT>Database</TT> subclass), which has a matching method
 1117  
    *         <TT>get<I>Foo</I>Object</TT> for obtaining the specialised object
 1118  
    *         under its own type.  Note that no access checks are done at this
 1119  
    *         stage: you may not be able to do anything with the object handle
 1120  
    *         returned from this method without provoking a
 1121  
    *         <TT>PoemAccessException</TT>.
 1122  
    *
 1123  
    * @exception NoSuchRowPoemException
 1124  
    *                if there is no row in the table with the given troid
 1125  
    *
 1126  
    * @see Persistent#getTroid()
 1127  
    */
 1128  
   @SuppressWarnings("unchecked")
 1129  
   public P getObject(Integer troid) throws NoSuchRowPoemException {
 1130  7921
     JdbcPersistent persistent = (JdbcPersistent)cache.get(troid);
 1131  
 
 1132  7921
     if (persistent == null) {
 1133  1568
       persistent = (JdbcPersistent)newPersistent();
 1134  1568
       claim(persistent, troid);
 1135  1568
       load(PoemThread.transaction(), persistent);
 1136  1568
       if (persistent.statusExistent())
 1137  257
         synchronized (cache) {
 1138  257
           JdbcPersistent tryAgain = (JdbcPersistent)cache.get(troid);
 1139  257
           if (tryAgain == null) {
 1140  
             try { 
 1141  257
               cache.put(troid, persistent);
 1142  0
             } catch (Cache.InconsistencyException e) { 
 1143  0
               throw new PoemBugPoemException(
 1144  
                   "Problem putting persistent " + persistent + " into cache:", e);
 1145  257
             }
 1146  
           } else
 1147  0
             persistent = tryAgain;
 1148  257
         }
 1149  
     }
 1150  
 
 1151  7921
     if (!persistent.statusExistent())
 1152  1311
       throw new NoSuchRowPoemException(this, troid);
 1153  
 
 1154  6610
     persistent.existenceLock(PoemThread.sessionToken());
 1155  
 
 1156  6610
     return (P)persistent;
 1157  
   }
 1158  
 
 1159  
   /**
 1160  
    * The object from the table with a given troid.  See previous.
 1161  
    *
 1162  
    * @param troid the table row id
 1163  
    * @return the Persistent
 1164  
    * @throws NoSuchRowPoemException if not found
 1165  
    * @see #getObject(java.lang.Integer)
 1166  
    */
 1167  
   public Persistent getObject(int troid) throws NoSuchRowPoemException {
 1168  2370
     return getObject(new Integer(troid));
 1169  
   }
 1170  
 
 1171  
   // 
 1172  
   // -----------
 1173  
   //  Searching
 1174  
   // -----------
 1175  
   // 
 1176  
 
 1177  
   /**
 1178  
    * The from clause has been added as an argument because it is
 1179  
    * inextricably linked to the where clause, but the default is 
 1180  
    * {@link #quotedName()}.
 1181  
    *
 1182  
    * It is the programmer's responsibility to ensure that the where clause 
 1183  
    * is suitable for the target DBMS.
 1184  
    * 
 1185  
    * @param fromClause Comma separated list of table names or null for default.
 1186  
    * @param whereClause SQL fragment
 1187  
    * @param orderByClause Comma separated list
 1188  
    * @param includeDeleted Flag as to whether to include soft deleted records
 1189  
    * @param excludeUnselectable Whether to append unselectable exclusion SQL 
 1190  
    * TODO Should work within some kind of limit
 1191  
    * @return an SQL SELECT statement put together from the arguments and
 1192  
    * default order by clause.
 1193  
    */
 1194  
   public String selectionSQL(String fromClause, String whereClause, 
 1195  
                              String orderByClause, boolean includeDeleted, 
 1196  
                              boolean excludeUnselectable) {
 1197  691
     return selectOrCountSQL(troidColumn().fullQuotedName(),
 1198  
                             fromClause, whereClause, orderByClause,
 1199  
                             includeDeleted, excludeUnselectable);
 1200  
   }
 1201  
 
 1202  
   /**
 1203  
    * It is the programmer's responsibility to ensure that the where clause 
 1204  
    * is suitable for the target DBMS.
 1205  
    * 
 1206  
    * @param fromClause SQL fragment
 1207  
    * @param whereClause SQL fragment
 1208  
    * @param orderByClause comma separated list
 1209  
    * @param includeDeleted flag as to whether to include soft deleted records
 1210  
    * @param excludeUnselectable whether to append unselectable exclusion SQL 
 1211  
    * @param transaction null now defaults to 
 1212  
    *                    {@link PoemThread#transaction()} but
 1213  
    *                    we do not rely on this much yet.
 1214  
    * @return a ResultSet                     
 1215  
    * @throws SQLPoemException if necessary
 1216  
    */
 1217  
   private ResultSet selectionResultSet(String fromClause, String whereClause,
 1218  
                                        String orderByClause, 
 1219  
                                        boolean includeDeleted, 
 1220  
                                        boolean excludeUnselectable,
 1221  
                                        PoemTransaction transaction)
 1222  
       throws SQLPoemException {
 1223  
 
 1224  103
     String sql = selectionSQL(fromClause, whereClause, orderByClause,
 1225  
                               includeDeleted, excludeUnselectable);
 1226  
 
 1227  
 
 1228  
     try {
 1229  
       Connection connection;
 1230  103
       if (transaction == null) {
 1231  17
         connection = getDatabase().getCommittedConnection();
 1232  
       } else {
 1233  86
         transaction.writeDown();
 1234  86
         connection = transaction.getConnection();
 1235  
       }
 1236  
 
 1237  103
       Statement selectionStatement = connection.createStatement();
 1238  103
       ResultSet rs = selectionStatement.executeQuery(sql);
 1239  103
       database.incrementQueryCount(sql);
 1240  
 
 1241  103
       SessionToken token = PoemThread._sessionToken();
 1242  103
       if (token != null) {
 1243  103
         token.toTidy().add(rs);
 1244  103
         token.toTidy().add(selectionStatement);
 1245  
       }
 1246  103
       if (database.logSQL())
 1247  50
         database.log(new SQLLogEvent(sql));
 1248  103
       return rs;
 1249  
     }
 1250  0
     catch (SQLException e) {
 1251  0
       throw new ExecutingSQLPoemException(sql, e);
 1252  
     }
 1253  
   }
 1254  
 
 1255  
   /**
 1256  
    * It is the programmer's responsibility to ensure that the where clause 
 1257  
    * is suitable for the target DBMS.
 1258  
    * 
 1259  
    * @return an {@link Enumeration} of Troids satisfying the criteria.
 1260  
    */ 
 1261  
   public Enumeration<Integer> troidSelection(String whereClause, String orderByClause,
 1262  
                                     boolean includeDeleted, 
 1263  
                                     PoemTransaction transaction) {
 1264  86
     return troidsFrom(selectionResultSet(null, whereClause, orderByClause,
 1265  
                                          includeDeleted, true,
 1266  
                                          transaction));
 1267  
   }
 1268  
 
 1269  
   /**
 1270  
    *
 1271  
    * @see #troidSelection(String, String, boolean, PoemTransaction)
 1272  
    * @param criteria Represents selection criteria possibly on joined tables
 1273  
    * @param transaction A transaction or null for 
 1274  
    *                    {@link PoemThread#transaction()}
 1275  
    * @return a selection of troids given arguments specifying a query
 1276  
    */
 1277  
   public Enumeration<Integer> troidSelection(Persistent criteria, String orderByClause,
 1278  
                                     boolean includeDeleted, 
 1279  
                                     boolean excludeUnselectable,
 1280  
                                     PoemTransaction transaction) {
 1281  34
     return troidsFrom(selectionResultSet(((JdbcPersistent)criteria).fromClause(), 
 1282  17
                                          whereClause(criteria),
 1283  
                                          orderByClause,
 1284  
                                          includeDeleted, excludeUnselectable,
 1285  
                                          transaction));
 1286  
   }
 1287  
 
 1288  
   /**
 1289  
    * Return an enumeration of troids given 
 1290  
    * a result set where the first column is an int. 
 1291  
    */
 1292  
   private Enumeration<Integer> troidsFrom(ResultSet them) {
 1293  310
     return new ResultSetEnumeration<Integer>(them) {
 1294  
         public Integer mapped(ResultSet rs) throws SQLException {
 1295  207
           return new Integer(rs.getInt(1));
 1296  
         }
 1297  
       };
 1298  
   }
 1299  
 
 1300  
   /**
 1301  
    * @param flag whether to remember or forget
 1302  
    */
 1303  
   public void rememberAllTroids(boolean flag) {
 1304  725
     if (flag) {
 1305  287
       if (allTroids == null &&
 1306  
               // troid column can be null during unification
 1307  287
               troidColumn() != null)
 1308  287
         allTroids = new CachedSelection<P>(this, null, null);
 1309  
     }
 1310  
     else
 1311  438
       allTroids = null;
 1312  725
   }
 1313  
 
 1314  
   /**
 1315  
    * @param limit the limit to set
 1316  
    */
 1317  
   public void setCacheLimit(Integer limit) {
 1318  725
     cache.setSize(limit == null ? CACHE_LIMIT_DEFAULT : limit.intValue());
 1319  725
   }
 1320  
 
 1321  
   /**
 1322  
    * A <TT>SELECT</TT>ion of troids of objects from the table meeting given
 1323  
    * criteria.
 1324  
    *
 1325  
    * It is the programmer's responsibility to ensure that the where clause 
 1326  
    * is suitable for the target DBMS.
 1327  
    * 
 1328  
    * If the orderByClause is null, then the default order by clause is applied.
 1329  
    * If the orderByClause is an empty string, ie "", then no ordering is 
 1330  
    * applied.
 1331  
    *
 1332  
    * @param whereClause an SQL snippet
 1333  
    * @param orderByClause an SQL snippet
 1334  
    * @param includeDeleted whether to include deleted records, if any
 1335  
    * 
 1336  
    * @return an <TT>Enumeration</TT> of <TT>Integer</TT>s, which can be mapped
 1337  
    *         onto <TT>Persistent</TT> objects using <TT>getObject</TT>;
 1338  
    *         or you can just use <TT>selection</TT>
 1339  
    *
 1340  
    * @see #getObject(java.lang.Integer)
 1341  
    * @see #selection(java.lang.String, java.lang.String, boolean)
 1342  
    */
 1343  
   public Enumeration<Integer> troidSelection(String whereClause, String orderByClause,
 1344  
                                     boolean includeDeleted)
 1345  
       throws SQLPoemException {
 1346  83
     if (allTroids != null &&
 1347  1
         (whereClause == null || whereClause.equals("")) &&
 1348  0
         (orderByClause == null || orderByClause.equals("") ||
 1349  0
         orderByClause == /* sic, for speed */ defaultOrderByClause()) &&
 1350  
         !includeDeleted) 
 1351  25
       return allTroids.troids();
 1352  
     else
 1353  116
       return troidSelection(whereClause, orderByClause, includeDeleted,
 1354  58
                             PoemThread.inSession() ? PoemThread.transaction() : null);
 1355  
     }
 1356  
 
 1357  
   /**
 1358  
    * All the objects in the table.
 1359  
    *
 1360  
    * @return An <TT>Enumeration</TT> of <TT>Persistent</TT>s, or, if the table
 1361  
    *         was defined in the DSD under the name <TT><I>foo</I></TT>, of
 1362  
    *         application-specialised subclasses <TT><I>Foo</I></TT>.  Note
 1363  
    *         that no access checks are done at this stage: you may not be able
 1364  
    *         to do anything with some of the object handles in the enumeration
 1365  
    *         without provoking a <TT>PoemAccessException</TT>.  If the table
 1366  
    *         has a <TT>deleted</TT> column, the objects flagged as deleted will
 1367  
    *         be passed over.
 1368  
    * {@inheritDoc}
 1369  
    * @see org.melati.poem.Selectable#selection()
 1370  
    */
 1371  
   public Enumeration<P> selection() throws SQLPoemException {
 1372  65
     return selection((String)null, (String)null, false);
 1373  
   }
 1374  
 
 1375  
   /**
 1376  
    * A <TT>SELECT</TT>ion of objects from the table meeting given criteria.
 1377  
    * This is one way to run a search against the database and return the
 1378  
    * results as a series of typed POEM objects.
 1379  
    * 
 1380  
    * It is the programmer's responsibility to ensure that the where clause 
 1381  
    * is suitable for the target DBMS.
 1382  
    *
 1383  
    * @param whereClause         SQL <TT>SELECT</TT>ion criteria for the search:
 1384  
    *                            the part that should appear after the
 1385  
    *                            <TT>WHERE</TT> keyword
 1386  
    *
 1387  
    * @return An <TT>Enumeration</TT> of <TT>Persistent</TT>s, or, if the table
 1388  
    *         was defined in the DSD under the name <TT><I>foo</I></TT>, of
 1389  
    *         application-specialised subclasses <TT><I>Foo</I></TT>.  Note
 1390  
    *         that no access checks are done at this stage: you may not be able
 1391  
    *         to do anything with some of the object handles in the enumeration
 1392  
    *         without provoking a <TT>PoemAccessException</TT>.  If the table
 1393  
    *         has a <TT>deleted</TT> column, the objects flagged as deleted will
 1394  
    *         be passed over.
 1395  
    *
 1396  
    * @see Column#selectionWhereEq(java.lang.Object)
 1397  
    */
 1398  
   public final Enumeration<P> selection(String whereClause)
 1399  
       throws SQLPoemException {
 1400  8
     return selection(whereClause, null, false);
 1401  
   }
 1402  
 
 1403  
 
 1404  
  /**
 1405  
   * Get an object satisfying the where clause.
 1406  
   * It is the programmer's responsibility to use this in a 
 1407  
   * context where only one result will be found, if more than one 
 1408  
   * actually exist only the first will be returned. 
 1409  
   * 
 1410  
   * It is the programmer's responsibility to ensure that the where clause 
 1411  
   * is suitable for the target DBMS.
 1412  
   *
 1413  
   * @param whereClause         SQL <TT>SELECT</TT>ion criteria for the search:
 1414  
   *                            the part that should appear after the
 1415  
   *                            <TT>WHERE</TT> keyword
 1416  
   * @return the first item satisfying criteria
 1417  
   */
 1418  
   public P firstSelection(String whereClause) {
 1419  8
     Enumeration<P> them = selection(whereClause);
 1420  8
     return them.hasMoreElements() ? them.nextElement() : null;
 1421  
   }
 1422  
 
 1423  
   /**
 1424  
    * A <TT>SELECT</TT>ion of objects from the table meeting given criteria,
 1425  
    * possibly including those flagged as deleted.
 1426  
    *
 1427  
    * If the orderByClause is null, then the default order by clause is applied.
 1428  
    * If the orderByClause is an empty string, ie "", then no ordering is 
 1429  
    * applied.
 1430  
    *
 1431  
    * It is the programmer's responsibility to ensure that the where clause 
 1432  
    * is suitable for the target DBMS.
 1433  
    * 
 1434  
    * @param includeDeleted      whether to return objects flagged as deleted
 1435  
    *                            (ignored if the table doesn't have a
 1436  
    *                            <TT>deleted</TT> column)
 1437  
    * @return a ResultSet as an Enumeration 
 1438  
    * @see #selection(java.lang.String)
 1439  
    */   
 1440  
   public Enumeration<P> selection(String whereClause, String orderByClause,
 1441  
                                 boolean includeDeleted)
 1442  
       throws SQLPoemException {
 1443  73
      return objectsFromTroids(troidSelection(whereClause, orderByClause,
 1444  
                                              includeDeleted));
 1445  
   }
 1446  
 
 1447  
   /**
 1448  
    * Return a selection of rows given an exemplar.
 1449  
    *
 1450  
    * @param criteria Represents selection criteria possibly on joined tables
 1451  
    * @return an enumeration of like objects
 1452  
    * @see #selection(String, String, boolean)
 1453  
    */
 1454  
   public Enumeration<P> selection(Persistent criteria)
 1455  
       throws SQLPoemException {
 1456  32
     return selection(criteria, 
 1457  16
                        criteria.getTable().defaultOrderByClause(), false, true);
 1458  
   }
 1459  
     
 1460  
   /**
 1461  
    * Return a selection of rows given arguments specifying a query.
 1462  
    *
 1463  
    * @see #selection(String, String, boolean)
 1464  
    * @param criteria Represents selection criteria possibly on joined tables
 1465  
    * @param orderByClause Comma separated list
 1466  
    * @return an enumeration of like objects with the specified ordering
 1467  
    */
 1468  
   public Enumeration<P> selection(Persistent criteria, String orderByClause)
 1469  
       throws SQLPoemException {
 1470  1
     return selection(criteria, orderByClause, false, true);
 1471  
   }
 1472  
    
 1473  
   /**
 1474  
    * Return a selection of rows given arguments specifying a query.
 1475  
    *
 1476  
    * @see #selection(String, String, boolean)
 1477  
    * @param criteria Represents selection criteria possibly on joined tables
 1478  
    * @param orderByClause Comma separated list
 1479  
    * @param excludeUnselectable Whether to append unselectable exclusion SQL
 1480  
    * @return an enumeration of like Persistents 
 1481  
    */
 1482  
   public Enumeration<P> selection(Persistent criteria, String orderByClause,
 1483  
                                 boolean includeDeleted, boolean excludeUnselectable)
 1484  
       throws SQLPoemException {
 1485  17
     return objectsFromTroids(troidSelection(criteria, orderByClause,
 1486  
                                             includeDeleted, excludeUnselectable, 
 1487  
                                             null));
 1488  
   }
 1489  
 
 1490  
   /**
 1491  
    * @return an enumeration of objects given an enumeration of troids.
 1492  
    */
 1493  
   private Enumeration<P> objectsFromTroids(Enumeration<Integer> troids) {
 1494  154
     return new MappedEnumeration<P, Integer>(troids) {
 1495  
         public P mapped(Integer troid) {
 1496  64
           return getObject(troid);
 1497  
         }
 1498  
       };
 1499  
   }
 1500  
 
 1501  
 
 1502  
   /**
 1503  
    * @param whereClause
 1504  
    * @return the SQL string for the current SQL dialect
 1505  
    */
 1506  
   public String countSQL(String whereClause) {
 1507  11897
     return countSQL(null, whereClause, false, true);
 1508  
   }
 1509  
 
 1510  
   /**
 1511  
    * Return an SQL statement to count rows put together from the arguments.
 1512  
    * 
 1513  
    * It is the programmer's responsibility to ensure that the where clause 
 1514  
    * is suitable for the target DBMS.
 1515  
    *
 1516  
    * @param fromClause Comma separated list of table names
 1517  
    * @return the SQL query 
 1518  
    */
 1519  
   public String countSQL(String fromClause, String whereClause,
 1520  
                          boolean includeDeleted, boolean excludeUnselectable) {
 1521  11898
     return selectOrCountSQL("count(*)", fromClause, whereClause, "",
 1522  
                             includeDeleted, excludeUnselectable);
 1523  
   }
 1524  
 
 1525  
   /**
 1526  
    * Return an SQL SELECT statement for selecting or counting rows.
 1527  
    *
 1528  
    * It is the programmer's responsibility to ensure that the where clause 
 1529  
    * is suitable for the target DBMS.
 1530  
    * 
 1531  
    * @param selectClause the columns to return
 1532  
    * @param fromClause Comma separated list of table names or null for default.
 1533  
    * @param whereClause SQL fragment
 1534  
    * @param orderByClause Comma separated list
 1535  
    * @param includeDeleted Flag as to whether to include soft deleted records
 1536  
    * @param excludeUnselectable Whether to append unselectable exclusion SQL
 1537  
    * @return the SQL query 
 1538  
    */
 1539  
   private String selectOrCountSQL(String selectClause, String fromClause,
 1540  
                                   String whereClause, String orderByClause,
 1541  
                                   boolean includeDeleted, 
 1542  
                                   boolean excludeUnselectable) {
 1543  
 
 1544  12589
     if (fromClause == null) {
 1545  11985
       fromClause = quotedName();
 1546  
     }
 1547  
 
 1548  12589
     String result = "SELECT " + selectClause + " FROM " + fromClause;
 1549  
 
 1550  12589
     whereClause = appendWhereClauseFilters(whereClause, includeDeleted, 
 1551  
                                            excludeUnselectable);
 1552  
 
 1553  12589
     if (whereClause.length() > 0) {
 1554  429
       result += " WHERE " + whereClause;
 1555  
     }
 1556  
 
 1557  12589
     if (orderByClause == null) {
 1558  671
       orderByClause = defaultOrderByClause();
 1559  
     }
 1560  
 
 1561  12589
     if (orderByClause.trim().length() > 0) {
 1562  691
       result += " ORDER BY " + orderByClause;
 1563  
     }
 1564  12589
     return result;
 1565  
   }
 1566  
 
 1567  
   /**
 1568  
    * Optionally add where clause expressions to filter out deleted/
 1569  
    * unselectable rows and ensure an "empty" where clause is
 1570  
    * indeed an empty string.
 1571  
    * <p>
 1572  
    * This is an attempt to treat "delete" and "can select" columns
 1573  
    * consistently. But I believe that there is an important difference
 1574  
    * in that unselectable rows must be considered when ensuring integrity.
 1575  
    * So <code>excludeUnselectable</code> should default to <code>true</code>
 1576  
    * and is only specified when selecting rows.
 1577  
    * <p>
 1578  
    * Despite the name this does not use a <code>StringBuffer</code>.
 1579  
    * in the belief that the costs outweigh the benefits here.
 1580  
    *
 1581  
    * It is the programmer's responsibility to ensure that the where clause 
 1582  
    * is suitable for the target DBMS.
 1583  
    * 
 1584  
    * @param whereClause SQL fragment
 1585  
    * @param includeDeleted Flag as to whether to include soft deleted records
 1586  
    * @param excludeUnselectable Whether to append unselectable exclusion SQL 
 1587  
    */
 1588  
   private String appendWhereClauseFilters(String whereClause,
 1589  
                                           boolean includeDeleted,
 1590  
                                           boolean excludeUnselectable) {
 1591  12698
     if (whereClause == null || whereClause.trim().length() == 0) {
 1592  12166
       whereClause = "";
 1593  
     } else {
 1594  
       // We could skip this if both the flags are true, or in
 1595  
       // more complicated circumstances, but what for?
 1596  532
       whereClause = "(" + whereClause + ")";
 1597  
     }
 1598  
 
 1599  12698
     if (deletedColumn != null && !includeDeleted) {
 1600  1
       if(whereClause.length() > 0) {
 1601  1
         whereClause += " AND";
 1602  
       }
 1603  1
       whereClause += " NOT " + dbms().booleanTrueExpression(deletedColumn);
 1604  
     }
 1605  
 
 1606  12698
     if (excludeUnselectable){
 1607  12692
       String s = canSelectClause();
 1608  12692
       if (s != null) {
 1609  2
         if (whereClause.length() >  0) {
 1610  2
           whereClause += " AND ";
 1611  
         }
 1612  2
         whereClause += s;
 1613  
       }
 1614  
     }
 1615  12698
     return whereClause;
 1616  
   }
 1617  
 
 1618  
   /**
 1619  
    * Return a where clause fragment that filters out rows that cannot
 1620  
    * be selected, or null.
 1621  
    * <p>
 1622  
    * By default the result is null unless there is a canselect column.
 1623  
    * But in that case an SQL EXISTS() expression is used, which will
 1624  
    * not yet work for all dbmses - sorry.
 1625  
    *
 1626  
    * @return null or a non-empty boolean SQL expression that can be
 1627  
    * appended with AND to a parenthesised prefix.
 1628  
    */
 1629  
   private String canSelectClause() {
 1630  12692
     Column<Capability> canSelect = canSelectColumn();
 1631  12692
     AccessToken accessToken = PoemThread.inSession() ? 
 1632  12689
             PoemThread.sessionToken().accessToken : null;
 1633  12692
     if (canSelect == null ||
 1634  
         accessToken instanceof RootAccessToken) {
 1635  12690
       return null;
 1636  2
     } else if (accessToken instanceof User) {
 1637  2
       String query =  "(" +
 1638  2
         canSelect.fullQuotedName() + " IS NULL OR EXISTS( SELECT 1 FROM " +
 1639  2
         quotedName() +
 1640  
         ", " +
 1641  2
         database.getGroupCapabilityTable().quotedName() +
 1642  
         ", " +
 1643  2
         database.getGroupMembershipTable().quotedName() +
 1644  
         " WHERE " +
 1645  2
         database.getGroupMembershipTable().getUserColumn().fullQuotedName() +
 1646  
         " = " +
 1647  2
         ((User)accessToken).getId() +
 1648  
         " AND " +
 1649  2
         database.getGroupMembershipTable().getGroupColumn().fullQuotedName() +
 1650  
         " = " +
 1651  2
         database.getGroupCapabilityTable().getGroupColumn().fullQuotedName() +
 1652  
         " AND " +
 1653  2
         database.getGroupCapabilityTable().getCapabilityColumn().
 1654  2
                                                             fullQuotedName() +
 1655  
         " = " +
 1656  2
         canSelect.fullQuotedName() +
 1657  
         "))";
 1658  2
       return query;
 1659  
     } else {  // a read only guest for example
 1660  0
       return canSelect.fullQuotedName() + " IS NULL";
 1661  
     }
 1662  
   }
 1663  
 
 1664  
   /**
 1665  
    * It is the programmer's responsibility to ensure that the where clause 
 1666  
    * is suitable for the target DBMS.
 1667  
    * 
 1668  
    * @return the number records satisfying criteria.
 1669  
    */ 
 1670  
   public int count(String whereClause,
 1671  
                    boolean includeDeleted, boolean excludeUnselectable)
 1672  
       throws SQLPoemException {
 1673  1
     return count(appendWhereClauseFilters(whereClause,
 1674  
                                           includeDeleted, excludeUnselectable));
 1675  
   }
 1676  
 
 1677  
   /**
 1678  
    * It is the programmer's responsibility to ensure that the where clause 
 1679  
    * is suitable for the target DBMS.
 1680  
    * 
 1681  
    * @return the number records satisfying criteria.
 1682  
    */ 
 1683  
   public int count(String whereClause, boolean includeDeleted)
 1684  
       throws SQLPoemException {
 1685  1
     return count(whereClause, includeDeleted, true);
 1686  
   }
 1687  
 
 1688  
   /**
 1689  
    * It is the programmer's responsibility to ensure that the where clause 
 1690  
    * is suitable for the target DBMS.
 1691  
    * 
 1692  
    * @return the number of records satisfying criteria.
 1693  
    */ 
 1694  
   public int count(String whereClause)
 1695  
       throws SQLPoemException {
 1696  
 
 1697  11886
     String sql = countSQL(whereClause);
 1698  
 
 1699  
     try {
 1700  
       Connection connection;
 1701  11886
       if (PoemThread.inSession()) {
 1702  11884
         PoemTransaction transaction = PoemThread.transaction();
 1703  11884
         transaction.writeDown();
 1704  11884
         connection = transaction.getConnection();
 1705  11884
       } else 
 1706  2
         connection = getDatabase().getCommittedConnection();
 1707  
 
 1708  
 
 1709  11886
       Statement s = connection.createStatement();
 1710  11886
       ResultSet rs = s.executeQuery(sql);
 1711  11886
       database.incrementQueryCount(sql);
 1712  11886
       if (database.logSQL())
 1713  1370
         database.log(new SQLLogEvent(sql));
 1714  11886
       rs.next();
 1715  11886
       int count = rs.getInt(1);
 1716  11886
       rs.close();
 1717  11886
       s.close();
 1718  11886
       return count;
 1719  
     }
 1720  0
     catch (SQLException e) {
 1721  0
       throw new ExecutingSQLPoemException(sql, e);
 1722  
     }
 1723  
   }
 1724  
 
 1725  
   /**
 1726  
    * @return the number records in this table.
 1727  
    */ 
 1728  
   public int count()
 1729  
       throws SQLPoemException {
 1730  11803
     return count(null);
 1731  
   }
 1732  
 
 1733  
 
 1734  
   /**
 1735  
    * It is the programmer's responsibility to ensure that the where clause 
 1736  
    * is suitable for the target DBMS.
 1737  
    * 
 1738  
    * @param whereClause the SQL criteria
 1739  
    * @return whether any  records satisfy criteria.
 1740  
    */ 
 1741  
   public boolean exists(String whereClause) throws SQLPoemException {
 1742  82
     return count(whereClause) > 0;
 1743  
   }
 1744  
 
 1745  
   /**
 1746  
    * @param persistent a {@link Persistent} with some fields filled in 
 1747  
    * @return whether any  records exist with the same fields filled
 1748  
    */ 
 1749  
   public boolean exists(Persistent persistent) {
 1750  82
     return exists(whereClause(persistent));
 1751  
   }
 1752  
 
 1753  
   /**
 1754  
    * Append an SQL logical expression to the given buffer to match rows
 1755  
    * according to criteria represented by the given object.
 1756  
    * <p>
 1757  
    * This default selects rows for which the non-null fields in the
 1758  
    * given object match, but subtypes may add other criteria.
 1759  
    * <p>
 1760  
    * The column names are now qualified with the table name so that
 1761  
    * subtypes can append elements of a join but there is no filtering
 1762  
    * by canselect columns.
 1763  
    * 
 1764  
    * TODO Add mechanism for searching for Nulls (that would be query
 1765  
    * constructs as per SQL parse tree, but efferent not afferent)
 1766  
    *
 1767  
    * @see #notifyColumnInfo(ColumnInfo)
 1768  
    * @see #clearColumnInfoCaches()
 1769  
    */
 1770  
   public void appendWhereClause(StringBuffer clause, Persistent persistent) {
 1771  107
     Column<?>[] columnsLocal = this.columns;
 1772  107
     boolean hadOne = false;
 1773  513
     for (int c = 0; c < columnsLocal.length; ++c) {
 1774  406
       Column<?> column = columnsLocal[c];
 1775  406
       Object raw = column.getRaw_unsafe(persistent);
 1776  406
       if (raw != null) { //FIXME you can't search for NULLs ...
 1777  226
         if (hadOne)
 1778  123
           clause.append(" AND ");
 1779  
         else
 1780  103
           hadOne = true;
 1781  
 
 1782  226
         String columnSQL = column.fullQuotedName();
 1783  226
         if (column.getType() instanceof StringPoemType) {
 1784  42
           clause.append(
 1785  42
             dbms().caseInsensitiveRegExpSQL(
 1786  
                   columnSQL,
 1787  21
                   column.getSQLType().quotedRaw(raw)));
 1788  205
         } else if (column.getType() instanceof BooleanPoemType) {
 1789  10
           clause.append(columnSQL);
 1790  10
           clause.append(" = ");
 1791  10
           clause.append(dbms().sqlBooleanValueOfRaw(raw));
 1792  
         } else {
 1793  195
           clause.append(columnSQL);
 1794  195
           clause.append(" = ");
 1795  195
           clause.append(column.getSQLType().quotedRaw(raw));
 1796  
         }
 1797  
       }
 1798  
     }
 1799  107
   }
 1800  
 
 1801  
   /**
 1802  
    * Return an SQL WHERE clause to select rows that match the non-null
 1803  
    * fields of the given object.
 1804  
    * <p>
 1805  
    * This does not filter out any rows with a capability the user
 1806  
    * does not have in a canselect column, nor did it ever filter
 1807  
    * out rows deleted according to a "deleted" column.
 1808  
    * But the caller usually gets a second chance to do both.
 1809  
    * @return an SQL fragment
 1810  
    */
 1811  
   public String whereClause(Persistent criteria) {
 1812  100
     return whereClause(criteria, true, true);
 1813  
   }
 1814  
 
 1815  
   /**
 1816  
    * Return an SQL WHERE clause to select rows using the given object
 1817  
    * as a selection criteria and optionally deleted rows or those
 1818  
    * included rows the user is not capable of selecting.
 1819  
    * <p>
 1820  
    * This is currently implemented in terms of
 1821  
    * {@link JdbcTable#appendWhereClause(StringBuffer, Persistent)}.
 1822  
    * @return an SQL fragment
 1823  
    */
 1824  
   public String whereClause(Persistent criteria,
 1825  
                             boolean includeDeleted, boolean excludeUnselectable) {
 1826  104
     StringBuffer clause = new StringBuffer();
 1827  104
     appendWhereClause(clause, criteria);
 1828  104
     return appendWhereClauseFilters(clause.toString(),
 1829  
                                     includeDeleted, excludeUnselectable);
 1830  
   }
 1831  
 
 1832  
   /**
 1833  
    * @return an SQL fragment
 1834  
    * @see #cnfWhereClause(Enumeration, boolean, boolean)
 1835  
    * @see #whereClause(Persistent)
 1836  
    */
 1837  
   public String cnfWhereClause(Enumeration<P> persistents) {
 1838  3
     return cnfWhereClause(persistents, false, true);
 1839  
   }
 1840  
 
 1841  
   /**
 1842  
    * Return a Conjunctive Normal Form (CNF) where clause.
 1843  
    * See http://en.wikipedia.org/wiki/Conjunctive_normal_form.
 1844  
    *  
 1845  
    * @return an SQL fragment
 1846  
    */
 1847  
   public String cnfWhereClause(Enumeration<P> persistents,
 1848  
                                boolean includeDeleted, boolean excludeUnselectable) {
 1849  3
     StringBuffer clause = new StringBuffer();
 1850  
 
 1851  3
     boolean hadOne = false;
 1852  6
     while (persistents.hasMoreElements()) {
 1853  3
       StringBuffer pClause = new StringBuffer();
 1854  3
       appendWhereClause(pClause, (Persistent)persistents.nextElement());
 1855  3
       if (pClause.length() > 0) {
 1856  2
         if (hadOne)
 1857  1
           clause.append(" OR ");
 1858  
         else
 1859  1
           hadOne = true;
 1860  2
         clause.append("(");
 1861  2
         clause.append(pClause);
 1862  2
         clause.append(")");
 1863  
       }
 1864  3
     }
 1865  
 
 1866  3
     return appendWhereClauseFilters(clause.toString(),
 1867  
                                     includeDeleted, excludeUnselectable);
 1868  
   }
 1869  
 
 1870  
 
 1871  
   /**
 1872  
    * All the objects in the table which refer to a given object.  If none of
 1873  
    * the table's columns are reference columns, the <TT>Enumeration</TT>
 1874  
    * returned will obviously be empty.  
 1875  
    * <p>
 1876  
    * It is not guaranteed to be quick to execute!
 1877  
    *
 1878  
    * @return an <TT>Enumeration</TT> of <TT>Persistent</TT>s
 1879  
    */
 1880  
 
 1881  
   @SuppressWarnings({ "unchecked", "rawtypes" })
 1882  
   public Enumeration<P> referencesTo(final Persistent object) {
 1883  18
     return new FlattenedEnumeration<P>(
 1884  156
         new MappedEnumeration(columns()) {
 1885  
           public Enumeration mapped(Object column) {
 1886  138
             return ((Column)column).referencesTo(object);
 1887  
           }
 1888  
         });
 1889  
   }
 1890  
 
 1891  
 
 1892  
   /**
 1893  
    * All the columns in the table which refer to the given table.
 1894  
    * 
 1895  
    * @param table
 1896  
    * @return an Enumeration of Columns referring to the specified Table
 1897  
    */
 1898  
   public Enumeration<Column<?>> referencesTo(final Table<?> table) {
 1899  2222
     return
 1900  16972
       new FilteredEnumeration<Column<?>>(columns()) {
 1901  
         public boolean isIncluded(Column<?> column) {
 1902  14750
           PoemType<?> type = ((Column<?>)column).getType();
 1903  14750
           return type instanceof PersistentReferencePoemType &&
 1904  1671
                  ((PersistentReferencePoemType)type).targetTable() == table;
 1905  
         }
 1906  
       };
 1907  
   }
 1908  
 
 1909  
   // 
 1910  
   // ----------
 1911  
   //  Creation
 1912  
   // ----------
 1913  
   // 
 1914  
 
 1915  
   private void validate(Persistent persistent)
 1916  
       throws FieldContentsPoemException {
 1917  136313
     for (int c = 0; c < columns.length; ++c) {
 1918  130306
       Column<?> column = columns[c];
 1919  
       try {
 1920  130306
         column.getType().assertValidRaw(column.getRaw_unsafe(persistent));
 1921  
       }
 1922  0
       catch (Exception e) {
 1923  0
         throw new FieldContentsPoemException(column, e);
 1924  130306
       }
 1925  
     }
 1926  6007
   }
 1927  
 
 1928  
   /**
 1929  
    * @return the current highest troid
 1930  
    */
 1931  
   public int getMostRecentTroid() {
 1932  0
     if (mostRecentTroid == -1)
 1933  0
       throw new PoemBugPoemException("Troid still unitialised in " + name);
 1934  0
     return mostRecentTroid;
 1935  
   }
 1936  
 
 1937  
   /**
 1938  
    * @param persistent unused parameter, but might be needed in another troid schema
 1939  
    * @return the next Troid
 1940  
    */
 1941  
   public synchronized Integer troidFor(Persistent persistent) {
 1942  6015
     Persistent foolEclipse = persistent;
 1943  6015
     persistent = foolEclipse;
 1944  6015
     if (mostRecentTroid == -1)
 1945  0
       throw new PoemBugPoemException("Troid still unitialised in " + name);
 1946  6015
     return new Integer(mostRecentTroid++);
 1947  
   }
 1948  
 
 1949  
  /**
 1950  
    * Write a new row containing the given object.
 1951  
    * <p>
 1952  
    * The given object will be assigned the next troid and its internal
 1953  
    * state will also be modified.
 1954  
    *
 1955  
    * @exception InitialisationPoemException The object failed validation
 1956  
    */
 1957  
   public void create(Persistent p)
 1958  
       throws AccessPoemException, ValidationPoemException,
 1959  
          InitialisationPoemException {
 1960  6015
     JdbcPersistent persistent = (JdbcPersistent)p;
 1961  
 
 1962  6015
     SessionToken sessionToken = PoemThread.sessionToken();
 1963  
 
 1964  6015
     if (persistent.getTable() == null)
 1965  713
       persistent.setTable(this, null);
 1966  6015
     persistent.assertCanCreate(sessionToken.accessToken);
 1967  
 
 1968  6015
     claim(persistent, troidFor(persistent));
 1969  6007
     persistent.setStatusNonexistent();
 1970  
 
 1971  
     // Are the values they have put in legal; is the result something they
 1972  
     // could have created by writing into a record?
 1973  
 
 1974  
     try {
 1975  6007
       validate(persistent);
 1976  
     }
 1977  0
     catch (Exception e) {
 1978  0
       throw new InitialisationPoemException(this, e);
 1979  6007
     }
 1980  
 
 1981  
     // Lock the cache while we try an initial write-down to see if the DB picks
 1982  
     // up any inconsistencies like duplicated unique fields
 1983  
 
 1984  6007
     synchronized (cache) {
 1985  6007
       persistent.setDirty(true);
 1986  6007
       writeDown(sessionToken.transaction, persistent);
 1987  
 
 1988  
       // OK, it worked.  Plug the object into the cache.
 1989  
 
 1990  6006
       persistent.readLock(sessionToken.transaction);
 1991  6006
       cache.put(persistent.troid(), persistent);
 1992  6006
     }
 1993  
 
 1994  6006
     notifyTouched(sessionToken.transaction, persistent);
 1995  6006
   }
 1996  
 
 1997  
   /**
 1998  
    * Create a new object (record) in the table.
 1999  
    *
 2000  
    * @param initialiser         A piece of code for setting the new object's
 2001  
    *                            initial values.  You'll probably want to define
 2002  
    *                            it as an anonymous class.
 2003  
    *
 2004  
    * @return A <TT>Persistent</TT> representing the new object, or, if the
 2005  
    *         table was defined in the DSD under the name <TT><I>foo</I></TT>,
 2006  
    *         an application-specialised subclass <TT><I>Foo</I></TT> of
 2007  
    *         <TT>Persistent</TT>.
 2008  
    *
 2009  
    * @exception AccessPoemException
 2010  
    *                if <TT>initialiser</TT> provokes one during its work (which
 2011  
    *                is unlikely, since POEM's standard checks are disabled
 2012  
    *                while it runs)
 2013  
    * @exception ValidationPoemException
 2014  
    *                if <TT>initialiser</TT> provokes one during its work
 2015  
    * @exception InitialisationPoemException
 2016  
    *                if the object is left by <TT>initialiser</TT> in a state in
 2017  
    *                which not all of its fields have legal values, or in which
 2018  
    *                the calling thread would not be allowed write access to the
 2019  
    *                object under its <TT>AccessToken</TT>---<I>i.e.</I> you
 2020  
    *                can't create objects you wouldn't be allowed to write to.
 2021  
    *
 2022  
    * @see Initialiser#init(org.melati.poem.Persistent)
 2023  
    * @see PoemThread#accessToken()
 2024  
    * @see #getCanCreate()
 2025  
    */
 2026  
 
 2027  
   public Persistent create(Initialiser initialiser)
 2028  
       throws AccessPoemException, ValidationPoemException,
 2029  
              InitialisationPoemException {
 2030  4723
     Persistent persistent = newPersistent();
 2031  4723
     initialiser.init(persistent);
 2032  4723
     create(persistent);
 2033  4723
     return persistent;
 2034  
   }
 2035  
 
 2036  
   private void claim(Persistent p, Integer troid) {
 2037  7583
     JdbcPersistent persistent = (JdbcPersistent)p;
 2038  
     // We don't want to end up with two of this object in the cache
 2039  
  
 2040  7583
     if (cache.get(troid) != null)
 2041  0
       throw new DuplicateTroidPoemException(this, troid);
 2042  
 
 2043  7583
     if (persistent.troid() != null)
 2044  8
       throw new DoubleCreatePoemException(persistent);
 2045  
 
 2046  7575
     persistent.setTable(this, troid);
 2047  
 
 2048  7575
     troidColumn.setRaw_unsafe(persistent, troid);
 2049  7575
     if (deletedColumn != null)
 2050  0
       deletedColumn.setRaw_unsafe(persistent, Boolean.FALSE);
 2051  7575
   }
 2052  
 
 2053  
   /**
 2054  
    * @return A freshly minted floating <TT>Persistent</TT> object for this table, 
 2055  
    * ie one without a troid set
 2056  
    */
 2057  
   public Persistent newPersistent() {
 2058  7608
     JdbcPersistent it = _newPersistent();
 2059  7608
     it.setTable(this, null);
 2060  7608
     return it;
 2061  
   }
 2062  
 
 2063  
   /**
 2064  
    * A freshly minted, and uninitialised, <TT>Persistent</TT> object for the
 2065  
    * table.  You don't ever have to call this and there is no point in doing so
 2066  
    * This method is overridden in application-specialised <TT>Table</TT>
 2067  
    * subclasses derived from the Data Structure Definition.
 2068  
    */
 2069  
   protected JdbcPersistent _newPersistent() {
 2070  12
     return new JdbcPersistent();
 2071  
   }
 2072  
 
 2073  
   /**
 2074  
    * It is the programmer's responsibility to ensure that the where clause 
 2075  
    * is suitable for the target DBMS.
 2076  
    * 
 2077  
    * @param whereClause the criteria
 2078  
    */
 2079  
   public void delete_unsafe(String whereClause) {
 2080  1
     serial.increment(PoemThread.transaction());
 2081  1
     getDatabase().sqlUpdate("DELETE FROM " + quotedName + 
 2082  
             " WHERE " + whereClause);
 2083  1
     uncache();
 2084  1
   }
 2085  
 
 2086  
   /**
 2087  
    * The number of `extra' (non-DSD-defined) columns in the table.
 2088  
    */
 2089  
   public int extrasCount() {
 2090  420
     return extrasIndex;
 2091  
   }
 2092  
 
 2093  
   // 
 2094  
   // ----------------
 2095  
   //  Access control
 2096  
   // ----------------
 2097  
   // 
 2098  
 
 2099  
   /**
 2100  
    * The capability required for reading records from the table, unless
 2101  
    * overridden in the record itself.  This simply comes from the table's
 2102  
    * record in the <TT>tableinfo</TT> table.
 2103  
    *
 2104  
    * @return the capability needed to read this table
 2105  
    */
 2106  
   public final Capability getDefaultCanRead() {
 2107  350
     return info == null ? null : info.getDefaultcanread();
 2108  
   }
 2109  
 
 2110  
   /**
 2111  
    * The capability required for updating records in the table, unless
 2112  
    * overridden in the record itself.  This simply comes from the table's
 2113  
    * record in the <TT>tableinfo</TT> table.
 2114  
    *
 2115  
    * @return the default  {@link Capability} required to write  a 
 2116  
    *         {@link Persistent}, if any
 2117  
    */
 2118  
   public final Capability getDefaultCanWrite() {
 2119  921
     return info == null ? null : info.getDefaultcanwrite();
 2120  
   }
 2121  
 
 2122  
   /**
 2123  
    * The capability required for deleting records in the table, unless
 2124  
    * overridden in the record itself.  This simply comes from the table's
 2125  
    * record in the <TT>tableinfo</TT> table.
 2126  
    * @return the default  {@link Capability} required to delete a 
 2127  
    *         {@link Persistent}, if any
 2128  
    */ 
 2129  
   public final Capability getDefaultCanDelete() {
 2130  122
     return info == null ? null : info.getDefaultcandelete();
 2131  
   }
 2132  
 
 2133  
   /**
 2134  
    * The capability required for creating records in the table.  This simply
 2135  
    * comes from the table's record in the <TT>tableinfo</TT> table.
 2136  
    *
 2137  
    * @return the Capability required to write to this table 
 2138  
    * @see #create(org.melati.poem.Initialiser)
 2139  
    */
 2140  
   public final Capability getCanCreate() {
 2141  6021
     return info == null ? null : info.getCancreate();
 2142  
   }
 2143  
 
 2144  
   /**
 2145  
    * @return the canReadColumn or the canSelectColumn or null
 2146  
    */
 2147  
   public final Column<Capability> canReadColumn() {
 2148  24
     return canReadColumn == null ? canSelectColumn() : canReadColumn;
 2149  
   }
 2150  
 
 2151  
   /**
 2152  
    * @return the canSelectColumn or null
 2153  
    */
 2154  
   
 2155  
   public final Column<Capability> canSelectColumn() {
 2156  12713
     return canSelectColumn;
 2157  
   }
 2158  
 
 2159  
   /**
 2160  
    * @return the canWriteColumn or null
 2161  
    */
 2162  
   public final Column<Capability> canWriteColumn() {
 2163  0
     return canWriteColumn;
 2164  
   }
 2165  
 
 2166  
   /**
 2167  
    * @return the canDeleteColumn or null
 2168  
    */
 2169  
   public final Column<Capability> canDeleteColumn() {
 2170  0
     return canDeleteColumn;
 2171  
   }
 2172  
 
 2173  
   // 
 2174  
   // -----------
 2175  
   //  Structure
 2176  
   // -----------
 2177  
   // 
 2178  
 
 2179  
   /**
 2180  
    * Add a {@link Column} to the database and the {@link TableInfo} table.
 2181  
    *
 2182  
    * @param infoP the meta data about the {@link Column} 
 2183  
    * @return the newly added column
 2184  
    */
 2185  
   public Column<?> addColumnAndCommit(ColumnInfo infoP) throws PoemException {
 2186  
 
 2187  
     // Set the new column up
 2188  
 
 2189  174
     database.log("Adding extra column from runtime " + 
 2190  87
         dbms().melatiName(infoP.getName_unsafe()) + 
 2191  
         " to " + name);
 2192  87
     Column<?> column = ExtraColumn.from(this, infoP, getNextExtrasIndex(),
 2193  
                                      DefinitionSource.runtime);
 2194  87
     column.setColumnInfo(infoP);
 2195  
 
 2196  
     // Do a dry run to make sure no problems (ALTER TABLE ADD COLUMN is
 2197  
     // well-nigh irrevocable in Postgres)
 2198  
 
 2199  87
     defineColumn(column, false);
 2200  
 
 2201  
     // ALTER TABLE ADD COLUMN
 2202  
 
 2203  84
     database.beginStructuralModification();
 2204  
     try {
 2205  84
       dbAddColumn(column);
 2206  82
       synchronized (cache) {    // belt and braces
 2207  82
         uncache();
 2208  82
         transactionStuffs.invalidate();
 2209  82
         defineColumn(column, true);
 2210  82
       }
 2211  82
       PoemThread.commit();
 2212  
     }
 2213  
     finally {
 2214  84
       database.endStructuralModification();
 2215  82
     }
 2216  
 
 2217  82
     return column;
 2218  
   }
 2219  
 
 2220  
   /**
 2221  
    * @param columnInfo metadata about the column to delete, which is itself deleted
 2222  
    */
 2223  
   public void deleteColumnAndCommit(ColumnInfo columnInfo) throws PoemException { 
 2224  1
     database.beginStructuralModification();
 2225  
     try {
 2226  1
       Column<?> column = columnInfo.column();
 2227  1
       columnInfo.delete(); // Ensure we have no references in metadata
 2228  1
       if (database.getDbms().canDropColumns())
 2229  2
         dbModifyStructure(
 2230  1
             "ALTER TABLE " + quotedName() +
 2231  1
             " DROP " + column.quotedName());
 2232  
       // else silently leave it
 2233  
       
 2234  1
       columns = (Column[])ArrayUtils.removed(columns, column);
 2235  1
       columnsByName.remove(column.getName().toLowerCase());
 2236  
 
 2237  1
       synchronized (cache) {    // belt and braces
 2238  1
         uncache();
 2239  1
         transactionStuffs.invalidate();
 2240  1
       }
 2241  1
       PoemThread.commit();
 2242  
     }
 2243  
     finally {
 2244  1
       database.endStructuralModification();
 2245  1
     }
 2246  
     
 2247  1
   }
 2248  
   // 
 2249  
   // ===========
 2250  
   //  Utilities
 2251  
   // ===========
 2252  
   // 
 2253  
 
 2254  
   /**
 2255  
    * A concise string to stand in for the table.  The table's name and a
 2256  
    * description of where it was defined (the DSD, the metadata tables or the
 2257  
    * JDBC metadata).
 2258  
    * {@inheritDoc}
 2259  
    * @see java.lang.Object#toString()
 2260  
    */
 2261  
   public String toString() {
 2262  36
     return getName() + " (from " + definitionSource + ")";
 2263  
   }
 2264  
 
 2265  
   /**
 2266  
    * Print some diagnostic information about the contents and consistency of
 2267  
    * POEM's cache for this table to stderr.
 2268  
    */
 2269  
   public void dumpCacheAnalysis() {
 2270  9
     database.log("\n-------- Analysis of " + name + "'s cache\n");
 2271  9
     cache.dumpAnalysis();
 2272  9
   }
 2273  
 
 2274  
   /**
 2275  
    * Print information about the structure of the table to stdout.
 2276  
    */
 2277  
   public void dump() {
 2278  9
     dump(System.out);
 2279  9
   }
 2280  
 
 2281  
   /**
 2282  
    * Print information to PrintStream. 
 2283  
    * 
 2284  
    * @param ps PrintStream to dump to
 2285  
    */
 2286  
   public void dump(PrintStream ps) {
 2287  20
     ps.println("=== table " + name +
 2288  10
         " (tableinfo id " + tableInfoID() + ")");
 2289  83
     for (int c = 0; c < columns.length; ++c)
 2290  73
       columns[c].dump(ps);
 2291  10
   }
 2292  
   
 2293  
   /**
 2294  
    * A mechanism for caching a selection of records.
 2295  
    * 
 2296  
    * It is the programmer's responsibility to ensure that the where clause 
 2297  
    * is suitable for the target DBMS.
 2298  
    * 
 2299  
    * @param whereClause raw SQL selection clause appropriate for this DBMS
 2300  
    * @param orderByClause which field to order by or null
 2301  
    * @return the results
 2302  
    */
 2303  
   public CachedSelection<P> cachedSelection(String whereClause,
 2304  
                                            String orderByClause) {
 2305  4
     String key = whereClause + "/" + orderByClause;
 2306  4
     CachedSelection<P> them = cachedSelections.get(key);
 2307  4
     if (them == null) {
 2308  2
       CachedSelection<P> newThem =
 2309  
           new CachedSelection<P>(this, whereClause, orderByClause);
 2310  2
       cachedSelections.put(key, newThem);
 2311  2
       them = newThem;
 2312  
     }
 2313  4
     return them;
 2314  
   }
 2315  
 
 2316  
   /**
 2317  
    * A mechanism for caching a record count.
 2318  
    * 
 2319  
    * It is the programmer's responsibility to ensure that the where clause 
 2320  
    * is suitable for the target DBMS.
 2321  
    * 
 2322  
    * @param whereClause raw SQL selection clause appropriate for this DBMS
 2323  
    * @param includeDeleted whether to include soft deleted records
 2324  
    * @return a cached count
 2325  
    */
 2326  
   public CachedCount cachedCount(String whereClause, boolean includeDeleted) {
 2327  0
     return cachedCount(whereClause, includeDeleted, true);
 2328  
   }
 2329  
 
 2330  
   /**
 2331  
    * A mechanism for caching a record count.
 2332  
    * 
 2333  
    * It is the programmer's responsibility to ensure that the where clause 
 2334  
    * is suitable for the target DBMS.
 2335  
    * 
 2336  
    * @param whereClause raw SQL selection clause appropriate for this DBMS
 2337  
    * @param includeDeleted whether to include soft deleted records
 2338  
    * @param excludeUnselectable whether to exclude columns which cannot be selected
 2339  
    * @return a cached count
 2340  
    */
 2341  
   public CachedCount cachedCount(String whereClause, boolean includeDeleted, 
 2342  
                                  boolean excludeUnselectable) {
 2343  1
     return cachedCount(appendWhereClauseFilters(whereClause,
 2344  
                                                 includeDeleted, excludeUnselectable));
 2345  
   }
 2346  
 
 2347  
   /**
 2348  
    * A mechanism for caching a record count.
 2349  
    * 
 2350  
    * @param criteria a {@link Persistent} with selection fields filled
 2351  
    * @param includeDeleted whether to include soft deleted records
 2352  
    * @param excludeUnselectable whether to exclude columns which cannot be selected
 2353  
    * @return a cached count
 2354  
    */
 2355  
   public CachedCount cachedCount(Persistent criteria, boolean includeDeleted, 
 2356  
                                  boolean excludeUnselectable) {
 2357  2
     return cachedCount(whereClause(criteria, includeDeleted, excludeUnselectable));
 2358  
   }
 2359  
 
 2360  
   /**
 2361  
    * @param criteria a Persistent to extract where clause from 
 2362  
    * @return a CachedCount of records matching Criteria
 2363  
    */
 2364  
   public CachedCount cachedCount(Persistent criteria) {
 2365  2
     return cachedCount(whereClause(criteria, true, false));
 2366  
   }
 2367  
 
 2368  
   /**
 2369  
    * A mechanism for caching a record count.
 2370  
    * 
 2371  
    * It is the programmer's responsibility to ensure that the where clause 
 2372  
    * is suitable for the target DBMS.
 2373  
    * 
 2374  
    * @param whereClause raw SQL selection clause appropriate for this DBMS
 2375  
    * @return a cached count
 2376  
    */
 2377  
   public CachedCount cachedCount(String whereClause) {
 2378  8
     String key = "" + whereClause;
 2379  8
     CachedCount it = cachedCounts.get(key);
 2380  8
     if (it == null) {
 2381  4
       it = new CachedCount(this, whereClause);
 2382  4
       cachedCounts.put(key, it);
 2383  
     }
 2384  8
     return it;
 2385  
   }
 2386  
 
 2387  
   /**
 2388  
    * @return a cached count of all records in the table, 
 2389  
    * obeying includedDeleted and other exclusions
 2390  
    */
 2391  
   public CachedCount cachedCount() {
 2392  0
     return cachedCount((String)null);
 2393  
   }
 2394  
 
 2395  
   /**
 2396  
    * A mechanism for caching an existance.
 2397  
    * 
 2398  
    * It is the programmer's responsibility to ensure that the where clause 
 2399  
    * is suitable for the target DBMS.
 2400  
    * 
 2401  
    * NOTE It is possible for the count to be written simultaneously, 
 2402  
    * but the cache will end up with the same result.
 2403  
    * 
 2404  
    * @param whereClause raw SQL selection clause appropriate for this DBMS
 2405  
    * @return a cached exists
 2406  
    */
 2407  
   public CachedExists cachedExists(String whereClause) {
 2408  1
     String key = "" + whereClause;
 2409  1
     CachedExists it = null;
 2410  1
       it = cachedExists.get(key);
 2411  1
     if (it == null) {
 2412  1
       it = new CachedExists(this, whereClause);
 2413  1
       cachedExists.put(key, it);
 2414  
     }
 2415  1
     return it;
 2416  
   }
 2417  
 
 2418  
   /**
 2419  
    * A mechanism for caching a record count.
 2420  
    * 
 2421  
    * It is the programmer's responsibility to ensure that the where clause 
 2422  
    * is suitable for the target DBMS.
 2423  
    * 
 2424  
    * @param whereClause raw SQL selection clause appropriate for this DBMS
 2425  
    * @param orderByClause raw SQL order clause appropriate for this DBMS
 2426  
    * @param nullable whether the ReferencePoemType is nullable
 2427  
    * @return a {@link RestrictedReferencePoemType}
 2428  
    */
 2429  
   @SuppressWarnings({ "unchecked", "rawtypes" })
 2430  
   public RestrictedReferencePoemType<?> cachedSelectionType(String whereClause, 
 2431  
                                    String orderByClause, boolean nullable) {
 2432  4
     return new RestrictedReferencePoemType(
 2433  4
                cachedSelection(whereClause, orderByClause), nullable);
 2434  
   }
 2435  
 
 2436  
   /**
 2437  
    * Make up a <TT>Field</TT> object whose possible values are a selected
 2438  
    * subset of the records in the table.  You can make a "dropdown" offering a
 2439  
    * choice of your green customers by putting this in your handler
 2440  
    *
 2441  
    * <BLOCKQUOTE><PRE>
 2442  
    * context.put("greens",
 2443  
    *             melati.getDatabase().getCustomerTable().cachedSelectionField(
 2444  
    *                 "colour = 'green'", null, true, null, "greens"));
 2445  
    * </PRE></BLOCKQUOTE>
 2446  
    *
 2447  
    * and this in your template
 2448  
    *
 2449  
    * <BLOCKQUOTE><PRE>
 2450  
    *   Select a customer: $ml.input($greens)
 2451  
    * </PRE></BLOCKQUOTE>
 2452  
    *
 2453  
    * The list of member records is implicitly cached---permanently, and however
 2454  
    * big it turns out to be.  So don't go mad with this.  It is recomputed on
 2455  
    * demand if the contents of the table are changed.  The <TT>whereClause</TT>
 2456  
    * and <TT>orderByClause</TT> you pass in are checked to see if you have
 2457  
    * asked for the same list before, so however many times you call this
 2458  
    * method, you should only trigger actual <TT>SELECT</TT>s when the table
 2459  
    * contents have changed.  The list is also transaction-safe, in that it will
 2460  
    * always reflect the state of affairs within your transaction even if you
 2461  
    * haven't done a commit.
 2462  
    *
 2463  
    * It is the programmer's responsibility to ensure that the WHERE clause 
 2464  
    * is suitable for the target DBMS.
 2465  
    * 
 2466  
    * @param whereClause         an SQL expression (the bit after the
 2467  
    *                            <TT>SELECT</TT> ... <TT>WHERE</TT>) for picking
 2468  
    *                            out the records you want
 2469  
    *
 2470  
    * @param orderByClause       a comma-separated list of column names which
 2471  
    *                            determine the order in which the records are
 2472  
    *                            presented; if this is <TT>null</TT>, the
 2473  
    *                            <TT>displayorderpriority</TT> attributes of the
 2474  
    *                            table's columns determine the order
 2475  
    *
 2476  
    * @param nullable            whether to allow a blank <TT>NULL</TT> option
 2477  
    *                            as the first possibility
 2478  
    *
 2479  
    * @param selectedTroid       the troid of the record to which the
 2480  
    *                            <TT>SELECT</TT> field should initially be set
 2481  
    *
 2482  
    * @param nameP               the HTML name attribute of the field,
 2483  
    *                            <I>i.e.</I>
 2484  
    *                            <TT>&lt;SELECT NAME=<I>name</I>&gt;</TT>
 2485  
    * @return a Field object
 2486  
    */
 2487  
   @SuppressWarnings({ "rawtypes", "unchecked" })
 2488  
   public Field<?> cachedSelectionField(
 2489  
       String whereClause, String orderByClause, boolean nullable,
 2490  
       Integer selectedTroid, String nameP) {
 2491  4
     return new Field(
 2492  
         selectedTroid,
 2493  
         new BaseFieldAttributes(nameP,
 2494  4
                                 cachedSelectionType(whereClause,
 2495  
                                                     orderByClause, nullable)));
 2496  
   }
 2497  
 
 2498  
   // 
 2499  
   // ================
 2500  
   //  Initialization
 2501  
   // ================
 2502  
   // 
 2503  
 
 2504  
   @SuppressWarnings({ "rawtypes", "unchecked" })
 2505  
   private synchronized void defineColumn(Column<?> column, boolean reallyDoIt)
 2506  
       throws DuplicateColumnNamePoemException,
 2507  
              DuplicateTroidColumnPoemException,
 2508  
              DuplicateDeletedColumnPoemException {
 2509  4968
     if (column.getTable() != this)
 2510  0
       throw new ColumnInUsePoemException(this, column);
 2511  
 
 2512  4968
     if (_getColumn(column.getName()) != null)
 2513  1
       throw new DuplicateColumnNamePoemException(this, column);
 2514  
 
 2515  4967
     if (column.isTroidColumn()) {
 2516  726
       if (troidColumn != null)
 2517  1
         throw new DuplicateTroidColumnPoemException(this, column);
 2518  725
       if (reallyDoIt)
 2519  725
         troidColumn = (Column<Integer>) column;
 2520  
     }
 2521  4241
     else if (column.isDeletedColumn()) {
 2522  48
       if (deletedColumn != null)
 2523  1
         throw new DuplicateDeletedColumnPoemException(this, column);
 2524  47
       if (reallyDoIt)
 2525  46
         deletedColumn = (Column<Boolean>)column;
 2526  
     }
 2527  
     else {
 2528  4193
       if (reallyDoIt) {
 2529  4110
         PoemType type = column.getType();
 2530  4110
         if (type instanceof ReferencePoemType &&
 2531  540
             ((PersistentReferencePoemType)type).targetTable() ==
 2532  540
                  database.getCapabilityTable()) {
 2533  289
           if (column.getName().equals("canRead"))
 2534  21
             canReadColumn = (Column<Capability>) column;
 2535  268
           else if (column.getName().equals("canWrite"))
 2536  21
             canWriteColumn = (Column<Capability>) column;
 2537  247
           else if (column.getName().equals("canDelete"))
 2538  21
             canDeleteColumn = (Column<Capability>) column;
 2539  226
           else if (column.getName().equals("canSelect"))
 2540  21
             canSelectColumn = (Column<Capability>) column;
 2541  
         }
 2542  
       }
 2543  
     }
 2544  
 
 2545  4965
     if (reallyDoIt) {
 2546  4881
       column.setTable(this);
 2547  4881
       columns = (Column[])ArrayUtils.added(columns, column);
 2548  4881
       columnsByName.put(column.getName().toLowerCase(), column);
 2549  
     }
 2550  4965
   }
 2551  
 
 2552  
   /**
 2553  
    * Don't call this in your application code.  
 2554  
    * Columns should be defined either in the DSD (in which
 2555  
    * case the boilerplate code generated by the preprocessor will call this
 2556  
    * method) or directly in the RDBMS (in which case the initialisation code
 2557  
    * will).
 2558  
    */
 2559  
   public final void defineColumn(Column<?> column)
 2560  
       throws DuplicateColumnNamePoemException,
 2561  
              DuplicateTroidColumnPoemException,
 2562  
              DuplicateDeletedColumnPoemException {
 2563  4799
     defineColumn(column, true);
 2564  4799
   }
 2565  
 
 2566  
   private void _defineColumn(Column<?> column) {
 2567  
     try {
 2568  21
       defineColumn(column);
 2569  
     }
 2570  0
     catch (DuplicateColumnNamePoemException e) {
 2571  0
       throw new UnexpectedExceptionPoemException(e);
 2572  
     }
 2573  0
     catch (DuplicateTroidColumnPoemException e) {
 2574  0
       throw new UnexpectedExceptionPoemException(e);
 2575  21
     }
 2576  21
   }
 2577  
 
 2578  
   /**
 2579  
    * @return incremented extra columns index 
 2580  
    */
 2581  
   public int getNextExtrasIndex() { 
 2582  125
     return extrasIndex++;
 2583  
   }
 2584  
   
 2585  
   /**
 2586  
    * @param tableInfo the TableInfo to set
 2587  
    */
 2588  
   public void setTableInfo(TableInfo tableInfo) {
 2589  725
     info = tableInfo;
 2590  725
     rememberAllTroids(tableInfo.getSeqcached().booleanValue());
 2591  725
     setCacheLimit(tableInfo.getCachelimit());
 2592  725
   }
 2593  
   
 2594  
   /**
 2595  
    * @return the {@link TableInfo} for this table.
 2596  
    */
 2597  
   public TableInfo getTableInfo() {
 2598  17701
     return info;
 2599  
   }
 2600  
 
 2601  
   /**
 2602  
    * The `factory-default' display name for the table.  By default this is the
 2603  
    * table's programmatic name, capitalised.  Application-specialised tables
 2604  
    * override this to return any <TT>(displayname = </TT>...<TT>)</TT> provided
 2605  
    * in the DSD.  This is only ever used at startup time when creating
 2606  
    * <TT>columninfo</TT> records for tables that don't have them.
 2607  
    */
 2608  
   public String defaultDisplayName() {
 2609  225
     return StringUtils.capitalised(getName());
 2610  
   }
 2611  
   
 2612  
 
 2613  
   public int defaultDisplayOrder() {
 2614  2
     return DISPLAY_ORDER_DEFAULT;
 2615  
   }
 2616  
 
 2617  
   /**
 2618  
    * The `factory-default' description for the table, or <TT>null</TT> if it
 2619  
    * doesn't have one.  Application-specialised tables override this to return
 2620  
    * any <TT>(description = </TT>...<TT>)</TT> provided in the DSD.  This is
 2621  
    * only ever used at startup time when creating <TT>columninfo</TT> records
 2622  
    * for tables that don't have them.
 2623  
    */
 2624  
   public String defaultDescription() {
 2625  2
     return null;
 2626  
   }
 2627  
 
 2628  
   public Integer defaultCacheLimit() {
 2629  265
     return new Integer(CACHE_LIMIT_DEFAULT);
 2630  
   }
 2631  
 
 2632  
   public boolean defaultRememberAllTroids() {
 2633  418
     return false;
 2634  
   }
 2635  
 
 2636  
   public String defaultCategory() {
 2637  2
     return TableCategoryTable.normalTableCategoryName;
 2638  
   }
 2639  
 
 2640  
   /**
 2641  
    * Create the (possibly overridden) TableInfo if it has not yet been created.
 2642  
    * 
 2643  
    * @throws PoemException
 2644  
    */
 2645  
   public void createTableInfo() throws PoemException {
 2646  708
     if (info == null) {
 2647  698
       info = getDatabase().getTableInfoTable().defaultTableInfoFor(this);
 2648  
       try { 
 2649  698
         getDatabase().getTableInfoTable().create(info);
 2650  0
       } catch (PoemException e) { 
 2651  0
         throw new UnificationPoemException(
 2652  0
                 "Problem creating new tableInfo for table " + getName() + ":", e);
 2653  698
       }        
 2654  698
       setTableInfo(info);
 2655  
     }
 2656  708
   }
 2657  
 
 2658  
   /**
 2659  
    * Match columnInfo with this Table's columns.
 2660  
    * Conversely, create a ColumnInfo for any columns which don't have one. 
 2661  
    */
 2662  
   public synchronized void unifyWithColumnInfo() throws PoemException {
 2663  
 
 2664  723
     if (info == null)
 2665  0
       throw new PoemBugPoemException("Get the initialisation order right ...");
 2666  
     
 2667  723
     for (Enumeration<?> ci =
 2668  723
              database.getColumnInfoTable().getTableinfoColumn().
 2669  723
                  selectionWhereEq(info.troid());
 2670  799
          ci.hasMoreElements();) {
 2671  76
       ColumnInfo columnInfo = (ColumnInfo)ci.nextElement();
 2672  76
       Column<?> column = _getColumn(columnInfo.getName());
 2673  76
       if (column == null) {
 2674  14
         database.log("Adding extra column " 
 2675  7
           + dbms().melatiName(columnInfo.getName_unsafe()) 
 2676  
           + " to " + name + " from definition in columninfo table.");
 2677  7
         column = ExtraColumn.from(this, columnInfo, getNextExtrasIndex(),
 2678  
                                   DefinitionSource.infoTables);
 2679  7
         _defineColumn(column);
 2680  
       }
 2681  76
       column.setColumnInfo(columnInfo);
 2682  76
     }
 2683  
 
 2684  723
     for (Enumeration<Column<?>> c = columns(); c.hasMoreElements();)
 2685  4785
       c.nextElement().createColumnInfo();
 2686  723
   }
 2687  
 
 2688  
   @Override
 2689  
   public void unifyWithMetadata(ResultSet tableDescriptions) throws SQLException {
 2690  0
     if (info == null)
 2691  0
       return;
 2692  0
     String remarks = tableDescriptions.getString("REMARKS");
 2693  0
     if (getDescription() == null) {
 2694  0
       if (remarks != null && !remarks.trim().equals("")) {
 2695  0
         info.setDescription(remarks);
 2696  0
         getDatabase().log("Adding comment to table " + name + 
 2697  
             " from SQL metadata:" + remarks);
 2698  
       }
 2699  
     } else {
 2700  0
       if (!this.getDescription().equals(remarks)) {
 2701  0
         String sql = this.dbms().alterTableAddCommentSQL(this, null); 
 2702  0
         if (sql != null)
 2703  0
           this.getDatabase().modifyStructure(sql);          
 2704  
       }
 2705  
     }    
 2706  0
   }
 2707  
 
 2708  
   /**
 2709  
    * Unify the JDBC description of this tables columns with the
 2710  
    * meta data held in the {@link org.melati.poem.TableInfo}
 2711  
    *
 2712  
    * @param colDescs a JDBC {@link java.sql.ResultSet} describing the columns with cursor at current row
 2713  
    * @param troidColumnName name of primary key column
 2714  
    */
 2715  
   @Override
 2716  
   @SuppressWarnings({ "unchecked", "rawtypes" })
 2717  
   public synchronized void unifyWithDB(ResultSet colDescs, String troidColumnName)
 2718  
       throws PoemException {
 2719  848
     boolean debug = false;
 2720  
     
 2721  848
     Hashtable<Column<?>, Boolean> dbColumns = new Hashtable<Column<?>, Boolean>();
 2722  
 
 2723  848
     int colCount = 0;
 2724  848
     if (colDescs != null){
 2725  
 
 2726  
       try {
 2727  8853
         for (; colDescs.next(); ++colCount) {
 2728  4089
           String colName = colDescs.getString("COLUMN_NAME");
 2729  4089
           Column<?> column = _getColumn(dbms().melatiName(colName));
 2730  
 
 2731  4089
           if (column == null) {
 2732  14
             SQLPoemType<?> colType =
 2733  14
                 database.defaultPoemTypeOfColumnMetaData(colDescs);
 2734  
 
 2735  
 
 2736  14
             if (troidColumn == null && colName.equalsIgnoreCase(troidColumnName) &&
 2737  2
                 dbms().canRepresent(colType, TroidPoemType.it) != null)
 2738  2
               colType = TroidPoemType.it;
 2739  
 
 2740  
             // magically make eligible columns "deleted"
 2741  
             // into soft-deleted-flag columns
 2742  14
             if (deletedColumn == null && colName.equalsIgnoreCase(dbms().unreservedName("deleted")) &&
 2743  2
                 dbms().canRepresent(colType, DeletedPoemType.it) != null)
 2744  2
               colType = DeletedPoemType.it;
 2745  
             
 2746  28
             database.log("Adding extra column from sql meta data " 
 2747  14
                          + name + "." + dbms().melatiName(colName));
 2748  14
             column = new ExtraColumn(this, 
 2749  14
                                      dbms().melatiName(
 2750  
                                              colName),
 2751  
                                      colType, DefinitionSource.sqlMetaData,
 2752  14
                                      getNextExtrasIndex());
 2753  
 
 2754  14
             _defineColumn(column);
 2755  
 
 2756  
             // HACK info == null happens when *InfoTable are unified with
 2757  
             // the database---obviously they haven't been initialised yet but it
 2758  
             // gets fixed in the next round when all tables (including them,
 2759  
             // again) are unified
 2760  
 
 2761  14
             if (info != null)
 2762  14
               column.createColumnInfo();
 2763  14
           }
 2764  
           else {
 2765  4075
             column.assertMatches(colDescs);
 2766  
           }
 2767  4089
           column.unifyWithMetadata(colDescs);
 2768  4089
           dbColumns.put(column, Boolean.TRUE);
 2769  
         }
 2770  0
       } catch (SQLException e) {
 2771  0
         throw new SQLSeriousPoemException(e);
 2772  675
       }
 2773  173
     } else if (debug) database.log(
 2774  
                         "Table.unifyWithDB called with null ResultsSet");
 2775  
 
 2776  848
     if (colCount == 0) {
 2777  
       // No columns found in jdbc metadata, so table does not exist
 2778  293
       dbCreateTable();
 2779  
     } else {
 2780  
       // Create any columns which do not exist in the dbms but are defined in java or metadata 
 2781  4644
       for (int c = 0; c < columns.length; ++c) {
 2782  4089
         if (dbColumns.get(columns[c]) == null) {
 2783  0
           database.log("Adding column to underlying database : " + columns[c]);
 2784  0
           dbAddColumn(columns[c]);
 2785  
         }
 2786  
       }
 2787  
     }
 2788  
 
 2789  848
     if (troidColumn == null)
 2790  0
       throw new NoTroidColumnException(this);
 2791  
 
 2792  
     // HACK info == null happens when *InfoTable are unified with
 2793  
     // the database --- obviously they haven't been initialised yet but it
 2794  
     // gets fixed in the next round when all tables (including them,
 2795  
     // again) are unified
 2796  
 
 2797  848
     if (info != null) {
 2798  
 
 2799  
       // Ensure that column has at least one index of the correct type 
 2800  725
       Hashtable<Column<?>,Boolean> dbHasIndexForColumn = new Hashtable<Column<?>,Boolean>();
 2801  1450
       String unreservedName = dbms().getJdbcMetadataName(
 2802  725
                                   dbms().unreservedName(getName()));
 2803  725
       if (debug) database.log("Getting indexes for " + unreservedName + "(was " + getName() + ")");
 2804  
       ResultSet index;
 2805  
       try {
 2806  725
         index = getDatabase().getCommittedConnection().getMetaData().
 2807  
         // null, "" means ignore catalog, 
 2808  
         // only retrieve those without a schema
 2809  
         // null, null means ignore both
 2810  725
             getIndexInfo(null, dbms().getSchema(), 
 2811  
                          unreservedName, 
 2812  
                          false, true);
 2813  1328
         while (index.next()) {
 2814  
           try {
 2815  603
             String mdIndexName = index.getString("INDEX_NAME");
 2816  603
             String mdColName = index.getString("COLUMN_NAME");
 2817  603
             if (mdColName != null) { // which MSSQL and Oracle seem to return sometimes
 2818  603
               String columnName = dbms().melatiName(mdColName);
 2819  603
               Column<?> column = getColumn(columnName);
 2820  
               
 2821  
               // Deal with non-melati indices
 2822  603
               String expectedIndex = indexName(column).toUpperCase(); 
 2823  
               // Old Postgresql version truncated name at 31 chars
 2824  603
               if (expectedIndex.indexOf(mdIndexName.toUpperCase()) == 0) {
 2825  0
                 column.unifyWithIndex(mdIndexName, index);
 2826  0
                 dbHasIndexForColumn.put(column, Boolean.TRUE);                  
 2827  0
                 if(debug)database.log("Found Expected Index:" + 
 2828  0
                         expectedIndex + " IndexName:" + mdIndexName.toUpperCase());
 2829  
               } else {
 2830  
                 try { 
 2831  603
                   column.unifyWithIndex(mdIndexName, index);
 2832  603
                   dbHasIndexForColumn.put(column, Boolean.TRUE);                  
 2833  603
                   if(debug) database.log("Not creating index because one exists with different name:" + 
 2834  0
                           mdIndexName.toUpperCase() + " != " + expectedIndex);
 2835  0
                 } catch (IndexUniquenessPoemException e) { 
 2836  
                   // Do not add this column, so the correct index will be added below               
 2837  0
                   if(debug) database.log("Creating index because existing one has different properties:" + 
 2838  0
                           mdIndexName.toUpperCase() + " != " + expectedIndex);
 2839  603
                 }
 2840  
               }
 2841  
             } 
 2842  
             // else it is a compound index ??
 2843  
           
 2844  
           }
 2845  0
           catch (NoSuchColumnPoemException e) {
 2846  
             // will never happen
 2847  0
             throw new UnexpectedExceptionPoemException(e);
 2848  603
           }
 2849  
         }
 2850  0
       } catch (SQLException e) {
 2851  0
         throw new SQLSeriousPoemException(e);
 2852  725
       }
 2853  
 
 2854  
       // Create any missing indices
 2855  5524
       for (int c = 0; c < columns.length; ++c) {
 2856  4799
         if (dbHasIndexForColumn.get(columns[c]) != Boolean.TRUE)
 2857  4198
             dbCreateIndex(columns[c]);
 2858  
       }
 2859  
     }
 2860  
 
 2861  
     // Where should we start numbering new records?
 2862  
 
 2863  848
     if (PoemThread.inSession())
 2864  725
       PoemThread.writeDown();
 2865  
 
 2866  848
     String sql = 
 2867  848
         "SELECT " + troidColumn.fullQuotedName() +
 2868  848
         " FROM " + quotedName() +
 2869  848
         " ORDER BY " + troidColumn.fullQuotedName() + " DESC";
 2870  
     try {
 2871  848
       Statement selectionStatement = getDatabase().getCommittedConnection().createStatement();
 2872  848
       ResultSet maxTroid =
 2873  
           selectionStatement.
 2874  848
               executeQuery(sql);
 2875  848
       database.incrementQueryCount(sql);
 2876  848
       if (database.logSQL())
 2877  592
         database.log(new SQLLogEvent(sql));
 2878  848
       if (maxTroid.next())
 2879  234
         mostRecentTroid = maxTroid.getInt(1) + 1;
 2880  
       else
 2881  614
         mostRecentTroid = 0;
 2882  848
       maxTroid.close();
 2883  848
       selectionStatement.close();
 2884  
     }
 2885  0
     catch (SQLException e) {
 2886  0
       throw new SQLSeriousPoemException(e);
 2887  848
     }
 2888  848
   }
 2889  
 
 2890  
   /**
 2891  
    * Ensure tables can be used as hashtable keys.
 2892  
    * {@inheritDoc}
 2893  
    * @see java.lang.Object#hashCode()
 2894  
    */
 2895  
   public final int hashCode() {
 2896  4
     return name.hashCode();
 2897  
   }
 2898  
 
 2899  
   /**
 2900  
    * Make sure that two equal table objects have the same name.
 2901  
    * 
 2902  
    * {@inheritDoc}
 2903  
    * @see java.lang.Object#equals(java.lang.Object)
 2904  
    */
 2905  
   public boolean equals(Object t) {
 2906  1020
     return (t instanceof JdbcTable &&
 2907  1018
             ((Table<?>)t).getName().equals(name));
 2908  
     
 2909  
   }
 2910  
 
 2911  
 
 2912  
 }