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