Full text index support from Zell, also a fix for deletion.

git-svn-id: https://samskivert.googlecode.com/svn/trunk@2156 6335cc39-0255-0410-8fd6-9bcaacd3b74c
This commit is contained in:
mdb
2007-07-31 21:36:02 +00:00
parent a60eab7504
commit 1f2450d8ba
15 changed files with 437 additions and 131 deletions
+33 -18
View File
@@ -21,7 +21,6 @@
package com.samskivert.jdbc; package com.samskivert.jdbc;
import java.sql.Connection; import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Statement; import java.sql.Statement;
@@ -103,10 +102,9 @@ public abstract class BaseLiaison implements DatabaseLiaison
} }
update.append(")"); update.append(")");
PreparedStatement stmt = null; Statement stmt = null;
try { try {
stmt = conn.prepareStatement(update.toString()); stmt.executeUpdate(update.toString());
stmt.executeUpdate();
} finally { } finally {
JDBCUtil.close(stmt); JDBCUtil.close(stmt);
} }
@@ -115,16 +113,35 @@ public abstract class BaseLiaison implements DatabaseLiaison
return true; return true;
} }
// from DatabaseLiaison
public boolean addColumn (
Connection conn, String table, String column, String definition, boolean check)
throws SQLException
{
if (check && tableContainsColumn(conn, table, column)) {
return false;
}
Statement stmt = null;
try {
stmt.executeUpdate("ALTER TABLE " + tableSQL(table) + " ADD COLUMN " +
columnSQL(column) + " " + definition);
} finally {
JDBCUtil.close(stmt);
}
Log.info("Database column '" + column + "' added to table '" + table + "'.");
return true;
}
// from DatabaseLiaison // from DatabaseLiaison
public boolean changeColumn (Connection conn, String table, String column, String definition) public boolean changeColumn (Connection conn, String table, String column, String definition)
throws SQLException throws SQLException
{ {
PreparedStatement stmt = null; Statement stmt = null;
try { try {
stmt = conn.prepareStatement( stmt.executeUpdate("ALTER TABLE " + tableSQL(table) + " CHANGE " +
"ALTER TABLE " + tableSQL(table) + " CHANGE " + columnSQL(column) + columnSQL(column) + " " + column + " " + definition);
" " + column + " " + definition);
stmt.executeUpdate();
} finally { } finally {
JDBCUtil.close(stmt); JDBCUtil.close(stmt);
} }
@@ -138,12 +155,11 @@ public abstract class BaseLiaison implements DatabaseLiaison
public boolean renameColumn (Connection conn, String table, String from, String to) public boolean renameColumn (Connection conn, String table, String from, String to)
throws SQLException throws SQLException
{ {
PreparedStatement stmt = null; Statement stmt = null;
try { try {
stmt = conn.prepareStatement( String query = "ALTER TABLE " + tableSQL(table) + " RENAME COLUMN " +
"ALTER TABLE " + tableSQL(table) + " RENAME COLUMN " + columnSQL(from) + columnSQL(from) + " TO " + columnSQL(to);
" TO " + columnSQL(to)); if (stmt.executeUpdate(query) == 1) {
if (stmt.executeUpdate() == 1) {
Log.info("Renamed column '" + from + "' on table '" + table + "' to '" + to + "'"); Log.info("Renamed column '" + from + "' on table '" + table + "' to '" + to + "'");
} }
} finally { } finally {
@@ -159,11 +175,10 @@ public abstract class BaseLiaison implements DatabaseLiaison
return false; return false;
} }
PreparedStatement stmt = null; Statement stmt = null;
try { try {
stmt = conn.prepareStatement( String query = "ALTER TABLE " + tableSQL(table) + " DROP COLUMN " + columnSQL(column);
"ALTER TABLE " + tableSQL(table) + " DROP COLUMN " + columnSQL(column)); if (stmt.executeUpdate(query) == 1) {
if (stmt.executeUpdate() == 1) {
Log.info("Database index '" + column + "' removed from table '" + table + "'."); Log.info("Database index '" + column + "' removed from table '" + table + "'.");
} }
} finally { } finally {
@@ -88,10 +88,11 @@ public interface DatabaseLiaison
throws SQLException; throws SQLException;
/** /**
* Alter the name, but not the definition, of a given column on a given table. Returns true or * Adds a column to a table with the given definition. Tests for the previous existence of
* false if the database did or did not report a schema modification. * the column iff 'check' is true.
*/ */
public boolean renameColumn (Connection conn, String table, String oldColumn, String newColumn) public boolean addColumn (
Connection conn, String table, String column, String definition, boolean check)
throws SQLException; throws SQLException;
/** /**
@@ -104,6 +105,13 @@ public interface DatabaseLiaison
Connection conn, String table, String column, String definition) Connection conn, String table, String column, String definition)
throws SQLException; throws SQLException;
/**
* Alter the name, but not the definition, of a given column on a given table. Returns true or
* false if the database did or did not report a schema modification.
*/
public boolean renameColumn (Connection conn, String table, String oldColumn, String newColumn)
throws SQLException;
/** /**
* Created a new table of the given name with the given column names and column definitions; * Created a new table of the given name with the given column names and column definitions;
* the given set of unique constraints (or null) and the given primary key columns (or null). * the given set of unique constraints (or null) and the given primary key columns (or null).
@@ -44,7 +44,7 @@ import com.samskivert.jdbc.depot.expression.SQLExpression;
import com.samskivert.jdbc.depot.expression.ValueExp; import com.samskivert.jdbc.depot.expression.ValueExp;
import com.samskivert.jdbc.depot.operator.Conditionals.In; import com.samskivert.jdbc.depot.operator.Conditionals.In;
import com.samskivert.jdbc.depot.operator.Conditionals.IsNull; import com.samskivert.jdbc.depot.operator.Conditionals.IsNull;
import com.samskivert.jdbc.depot.operator.Conditionals.Match; import com.samskivert.jdbc.depot.operator.Conditionals.FullTextMatch;
import com.samskivert.jdbc.depot.operator.Logic.Not; import com.samskivert.jdbc.depot.operator.Logic.Not;
import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator; import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator;
import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator; import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator;
@@ -128,7 +128,7 @@ public class BindVisitor implements ExpressionVisitor
} }
} }
public void visit (Match match) throws Exception public void visit (FullTextMatch match) throws Exception
{ {
// we never get here // we never get here
} }
@@ -46,7 +46,7 @@ import com.samskivert.jdbc.depot.expression.SQLExpression;
import com.samskivert.jdbc.depot.expression.ValueExp; import com.samskivert.jdbc.depot.expression.ValueExp;
import com.samskivert.jdbc.depot.operator.Conditionals.In; import com.samskivert.jdbc.depot.operator.Conditionals.In;
import com.samskivert.jdbc.depot.operator.Conditionals.IsNull; import com.samskivert.jdbc.depot.operator.Conditionals.IsNull;
import com.samskivert.jdbc.depot.operator.Conditionals.Match; import com.samskivert.jdbc.depot.operator.Conditionals.FullTextMatch;
import com.samskivert.jdbc.depot.operator.Logic.Not; import com.samskivert.jdbc.depot.operator.Logic.Not;
import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator; import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator;
import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator; import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator;
@@ -197,11 +197,8 @@ public abstract class BuildVisitor implements ExpressionVisitor
_builder.append(")"); _builder.append(")");
} }
public void visit (Match match) public abstract void visit (FullTextMatch match)
throws Exception throws Exception;
{
throw new IllegalArgumentException("Match() is MySQL-specific.");
}
public void visit (ColumnExp columnExp) public void visit (ColumnExp columnExp)
throws Exception throws Exception
@@ -456,6 +453,8 @@ public abstract class BuildVisitor implements ExpressionVisitor
{ {
_builder.append("delete from "); _builder.append("delete from ");
appendTableName(deleteClause.getPersistentClass()); appendTableName(deleteClause.getPersistentClass());
_builder.append(" as ");
appendTableAbbreviation(deleteClause.getPersistentClass());
_builder.append(" "); _builder.append(" ");
deleteClause.getWhereClause().accept(this); deleteClause.getWhereClause().accept(this);
} }
@@ -40,6 +40,7 @@ import java.util.logging.Level;
import com.samskivert.io.PersistenceException; import com.samskivert.io.PersistenceException;
import com.samskivert.jdbc.depot.annotation.Computed; import com.samskivert.jdbc.depot.annotation.Computed;
import com.samskivert.jdbc.depot.annotation.Entity; import com.samskivert.jdbc.depot.annotation.Entity;
import com.samskivert.jdbc.depot.annotation.FullTextIndex;
import com.samskivert.jdbc.depot.annotation.GeneratedValue; import com.samskivert.jdbc.depot.annotation.GeneratedValue;
import com.samskivert.jdbc.depot.annotation.Id; import com.samskivert.jdbc.depot.annotation.Id;
import com.samskivert.jdbc.depot.annotation.Index; import com.samskivert.jdbc.depot.annotation.Index;
@@ -95,6 +96,14 @@ public class DepotMarshaller<T extends PersistentRecord>
context.tableGenerators.put(generator.name(), generator); context.tableGenerators.put(generator.name(), generator);
} }
// if there are FTS indexes in the Table, map those out here for future use
Table table = pclass.getAnnotation(Table.class);
if (table != null) {
for (FullTextIndex fts : table.fullTextIndexes()) {
_fullTextIndexes.put(fts.name(), fts);
}
}
// introspect on the class and create marshallers for persistent fields // introspect on the class and create marshallers for persistent fields
ArrayList<String> fields = new ArrayList<String>(); ArrayList<String> fields = new ArrayList<String>();
for (Field field : _pclass.getFields()) { for (Field field : _pclass.getFields()) {
@@ -195,6 +204,14 @@ public class DepotMarshaller<T extends PersistentRecord>
_allFields = fields.toArray(new String[fields.size()]); _allFields = fields.toArray(new String[fields.size()]);
} }
/**
* Returns the persistent class this is object is a marshaller for.
*/
public Class<T> getPersistentClass ()
{
return _pclass;
}
/** /**
* Returns the name of the table in which persistent instances of our class are stored. By * Returns the name of the table in which persistent instances of our class are stored. By
* default this is the classname of the persistent object without the package. * default this is the classname of the persistent object without the package.
@@ -220,6 +237,14 @@ public class DepotMarshaller<T extends PersistentRecord>
return _columnFields; return _columnFields;
} }
/**
* Return the {@link FullTextIndex} registered under the given name, or null if none.
*/
public FullTextIndex getFullTextIndex (String name)
{
return _fullTextIndexes.get(name);
}
/** /**
* Returns the {@link FieldMarshaller} for a named field on our persistent class. * Returns the {@link FieldMarshaller} for a named field on our persistent class.
*/ */
@@ -448,17 +473,17 @@ public class DepotMarshaller<T extends PersistentRecord>
* be created. If the schema version specified by the persistent object is newer than the * be created. If the schema version specified by the persistent object is newer than the
* database schema, it will be migrated. * database schema, it will be migrated.
*/ */
protected void init (PersistenceContext ctx) protected void init (final PersistenceContext ctx)
throws PersistenceException throws PersistenceException
{ {
SQLBuilder builder = ctx.getSQLBuilder(DepotTypes.TRIVIAL);
if (_initialized) { // sanity check if (_initialized) { // sanity check
throw new IllegalStateException( throw new IllegalStateException(
"Cannot re-initialize marshaller [type=" + _pclass + "]."); "Cannot re-initialize marshaller [type=" + _pclass + "].");
} }
_initialized = true; _initialized = true;
final SQLBuilder builder = ctx.getSQLBuilder(new DepotTypes(ctx, _pclass));
// perform the context-sensitive initialization of the field marshallers // perform the context-sensitive initialization of the field marshallers
for (FieldMarshaller fm : _fields.values()) { for (FieldMarshaller fm : _fields.values()) {
fm.init(builder); fm.init(builder);
@@ -494,7 +519,7 @@ public class DepotMarshaller<T extends PersistentRecord>
UniqueConstraint[] uCons = table.uniqueConstraints(); UniqueConstraint[] uCons = table.uniqueConstraints();
uniqueConstraintColumns = new String[uCons.length][]; uniqueConstraintColumns = new String[uCons.length][];
for (int kk = 0; kk < uCons.length; kk ++) { for (int kk = 0; kk < uCons.length; kk ++) {
String[] columns = uCons[kk].columnNames(); String[] columns = uCons[kk].fieldNames();
for (int ii = 0; ii < columns.length; ii ++) { for (int ii = 0; ii < columns.length; ii ++) {
FieldMarshaller fm = getFieldMarshaller(columns[ii]); FieldMarshaller fm = getFieldMarshaller(columns[ii]);
if (fm == null) { if (fm == null) {
@@ -542,8 +567,13 @@ public class DepotMarshaller<T extends PersistentRecord>
primaryKeyColumns[ii] = _pkColumns.get(ii).getColumnName(); primaryKeyColumns[ii] = _pkColumns.get(ii).getColumnName();
} }
} }
liaison.createTableIfMissing(conn, getTableName(), columns, fDeclarations, if (liaison.createTableIfMissing(conn, getTableName(), columns, fDeclarations,
fUniqueConstraintColumns, primaryKeyColumns); fUniqueConstraintColumns, primaryKeyColumns)) {
for (FullTextIndex fti : _fullTextIndexes.values()) {
builder.addFullTextSearch(conn, DepotMarshaller.this, fti);
}
}
updateVersion(conn, liaison, _schemaVersion); updateVersion(conn, liaison, _schemaVersion);
return 0; return 0;
} }
@@ -863,6 +893,9 @@ public class DepotMarshaller<T extends PersistentRecord>
/** The fields of our object with directly corresponding table columns. */ /** The fields of our object with directly corresponding table columns. */
protected String[] _columnFields; protected String[] _columnFields;
/** A mapping of all of the full text index annotations for our persistent record. */
protected Map<String, FullTextIndex> _fullTextIndexes = new HashMap<String, FullTextIndex>();
/** The version of our persistent object schema as specified in the class definition. */ /** The version of our persistent object schema as specified in the class definition. */
protected int _schemaVersion = -1; protected int _schemaVersion = -1;
@@ -24,7 +24,6 @@ import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -22,11 +22,10 @@ package com.samskivert.jdbc.depot;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.Arrays;
import java.util.Collections; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -69,41 +68,11 @@ public class DepotTypes
return new DepotTypes(ctx, classSet); return new DepotTypes(ctx, classSet);
} }
public String getTableName (Class<? extends PersistentRecord> cl) /**
{ * Create a new DepotTypes with the given {@link PersistenceContext} and a collection of
return _classMap.get(cl).getTableName(); * persistent record classes.
} */
public DepotTypes (PersistenceContext ctx, Collection<Class<? extends PersistentRecord>> others)
public String getTableAbbreviation (Class<? extends PersistentRecord> cl)
{
int ix = _classList.indexOf(cl);
if (ix < 0) {
throw new IllegalArgumentException("Unknown persistence class: " + cl);
}
return "T" + (ix+1);
}
public String getColumnName (Class<? extends PersistentRecord> cl, String field)
{
return getMarshaller(cl).getFieldMarshaller(field).getColumnName();
}
public DepotMarshaller getMarshaller (Class cl)
{
return _classMap.get(cl);
}
public void addClass (PersistenceContext ctx, Class <? extends PersistentRecord> type)
throws PersistenceException
{
if (_classMap.containsKey(type)) {
return;
}
_classList.add(type);
_classMap.put(type, ctx.getMarshaller(type));
}
protected DepotTypes (PersistenceContext ctx, Set<Class<? extends PersistentRecord>> others)
throws PersistenceException throws PersistenceException
{ {
for (Class<? extends PersistentRecord> c : others) { for (Class<? extends PersistentRecord> c : others) {
@@ -111,14 +80,114 @@ public class DepotTypes
} }
} }
/**
* Create a new DepotTypes with the given {@link PersistenceContext} and the given
* persistent record.
*/
public DepotTypes (PersistenceContext ctx, Class<? extends PersistentRecord> pClass)
throws PersistenceException
{
addClass(ctx, pClass);
}
/**
* Return the full table name of the given persistent class, which must have been previously
* registered with this object.
*/
public String getTableName (Class<? extends PersistentRecord> cl)
{
return _classMap.get(cl).getTableName();
}
/**
* Return the current abbreviation by which we refer to the table associated with the given
* persistent record -- which must have been previously registered with this object. If the
* useTableAbbreviations flag is false, we return the full table name instead.
*/
public String getTableAbbreviation (Class<? extends PersistentRecord> cl)
{
if (_useTableAbbreviations) {
Integer ix = _classIx.get(cl);
if (ix == null) {
throw new IllegalArgumentException("Unknown persistence class: " + cl);
}
return "T" + (ix+1);
}
return getTableName(cl);
}
/**
* Return the associated database column of the given field of the given persistent class,
* or null if there is no associated marshaller, or if the field is unknown on the class, or
* if the field does not directly associate with a column.
*/
public String getColumnName (Class<? extends PersistentRecord> cl, String field)
{
DepotMarshaller dm = getMarshaller(cl);
if (dm != null) {
FieldMarshaller<?> fm = dm.getFieldMarshaller(field);
if (fm != null) {
return fm.getColumnName();
}
}
return null;
}
/**
* Return the {@link DepotMarshaller} associated with the given persistent class, or null
* if the class has not been previously registered with this object.
*/
public DepotMarshaller getMarshaller (Class<? extends PersistentRecord> cl)
{
return _classMap.get(cl);
}
/**
* Register a new persistent class with this object.
*/
public void addClass (PersistenceContext ctx, Class <? extends PersistentRecord> type)
throws PersistenceException
{
if (_classMap.containsKey(type)) {
return;
}
_classMap.put(type, ctx.getMarshaller(type));
_classIx.put(type, _classIx.size());
}
/**
* Return the value of the useTableAbbreviations flag, which governs the behaviour when
* referencing columns during SQL construction. Normally, this flag is on, and tables are
* referenced as e.g. T1.itemId, but there are cases of weak/broken SQL where abbreviations
* may not be brought into play. In these cases we prepend the full table name.
*/
public boolean getUseTableAbbreviations ()
{
return _useTableAbbreviations;
}
/**
* Sets the value of the useTableAbbreviations flag, which governs the behaviour when
* referencing columns during SQL construction. Normally, this flag is on, and tables are
* referenced as e.g. T1.itemId, but there are cases of weak/broken SQL where abbreviations
* may not be brought into play. In these cases we prepend the full table name.
*/
public void setUseTableAbbreviations (boolean doUse)
{
_useTableAbbreviations = doUse;
}
// constructor used to create TRIVIAL
protected DepotTypes () protected DepotTypes ()
{ {
} }
/** A list of referenced classes, used to generate table abbreviations. */ /** Classes mapped to integers, used for table abbreviation indexing. */
protected List<Class<? extends PersistentRecord>> _classList = protected Map<Class, Integer> _classIx = new HashMap<Class, Integer>();
new ArrayList<Class<? extends PersistentRecord>>();
/** Classes mapped to marshallers, used for table names and field lists. */ /** Classes mapped to marshallers, used for table names and field lists. */
protected Map<Class, DepotMarshaller> _classMap = new HashMap<Class, DepotMarshaller>(); protected Map<Class, DepotMarshaller> _classMap = new HashMap<Class, DepotMarshaller>();
/** When false, override the normal table abbreviations and return full table names instead. */
protected boolean _useTableAbbreviations = true;
} }
@@ -22,11 +22,14 @@ package com.samskivert.jdbc.depot;
import java.sql.Blob; import java.sql.Blob;
import java.sql.Clob; import java.sql.Clob;
import java.sql.Connection;
import java.sql.Date; import java.sql.Date;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Time; import java.sql.Time;
import java.sql.Timestamp; import java.sql.Timestamp;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.depot.FieldMarshaller.BooleanMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.BooleanMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ByteArrayMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ByteArrayMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ByteEnumMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ByteEnumMarshaller;
@@ -38,44 +41,54 @@ import com.samskivert.jdbc.depot.FieldMarshaller.IntMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.LongMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.LongMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ObjectMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ObjectMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ShortMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ShortMarshaller;
import com.samskivert.jdbc.depot.annotation.FullTextIndex;
import com.samskivert.jdbc.depot.clause.DeleteClause;
import com.samskivert.jdbc.depot.expression.ColumnExp; import com.samskivert.jdbc.depot.expression.ColumnExp;
import com.samskivert.jdbc.depot.operator.Conditionals.Match; import com.samskivert.jdbc.depot.operator.Conditionals.FullTextMatch;
public class MySQLBuilder public class MySQLBuilder
extends SQLBuilder extends SQLBuilder
{ {
public class MSBuildVisitor extends BuildVisitor public class MSBuildVisitor extends BuildVisitor
{ {
protected MSBuildVisitor (DepotTypes types)
{
super(types);
}
@Override @Override
public void visit (Match match) public void visit (FullTextMatch match)
throws Exception throws Exception
{ {
_builder.append("match("); _builder.append("match(");
ColumnExp[] columns = match.getColumns(); Class<? extends PersistentRecord> pClass = match.getPersistentRecord();
for (int ii = 0; ii < columns.length; ii ++) { FullTextIndex fts = _types.getMarshaller(pClass).getFullTextIndex(match.getName());
String[] fields = fts.fieldNames();
for (int ii = 0; ii < fields.length; ii ++) {
if (ii > 0) { if (ii > 0) {
_builder.append(", "); _builder.append(", ");
} }
columns[ii].accept(this); new ColumnExp(pClass, fields[ii]).accept(this);
} }
_builder.append(") against (?"); _builder.append(") against (? in boolean mode)");
switch (match.getMode()) { }
case BOOLEAN:
_builder.append(" in boolean mode"); @Override
break; public void visit (DeleteClause<? extends PersistentRecord> deleteClause)
case NATURAL_LANGUAGE: throws Exception
_builder.append(" in natural language mode"); {
break; _builder.append("delete from ");
appendTableName(deleteClause.getPersistentClass());
_builder.append(" ");
// MySQL can't do DELETE FROM SomeTable AS T1, so we turn off abbreviations briefly.
boolean savedFlag = _types.getUseTableAbbreviations();
_types.setUseTableAbbreviations(false);
try {
deleteClause.getWhereClause().accept(this);
} finally {
_types.setUseTableAbbreviations(savedFlag);
} }
if (match.isQueryExpansion()) { }
_builder.append(" with query expansion");
} protected MSBuildVisitor (DepotTypes types)
_builder.append(")"); {
super(types);
} }
protected void appendTableName (Class<? extends PersistentRecord> type) protected void appendTableName (Class<? extends PersistentRecord> type)
@@ -107,7 +120,7 @@ public class MySQLBuilder
} }
@Override @Override
public void visit (Match match) public void visit (FullTextMatch match)
throws Exception throws Exception
{ {
_stmt.setString(_argIdx ++, match.getQuery()); _stmt.setString(_argIdx ++, match.getQuery());
@@ -119,6 +132,34 @@ public class MySQLBuilder
super(types); super(types);
} }
@Override
public <T extends PersistentRecord> boolean addFullTextSearch (
Connection conn, DepotMarshaller<T> marshaller, FullTextIndex fts)
throws SQLException
{
Class<T> pClass = marshaller.getPersistentClass();
StringBuilder update = new StringBuilder("ALTER TABLE ").
append(marshaller.getTableName()).append(" ADD FULLTEXT INDEX ftsIx_").
append(fts.name()).append(" (");
String[] fields = fts.fieldNames();
for (int ii = 0; ii < fields.length; ii ++) {
if (ii > 0) {
update.append(", ");
}
update.append(_types.getColumnName(pClass, fields[ii]));
}
update.append(")");
PreparedStatement stmt = null;
try {
stmt = conn.prepareStatement(update.toString());
stmt.executeUpdate();
} finally {
JDBCUtil.close(stmt);
}
return true;
}
@Override @Override
protected BuildVisitor getBuildVisitor () protected BuildVisitor getBuildVisitor ()
{ {
@@ -22,11 +22,17 @@ package com.samskivert.jdbc.depot;
import java.sql.Blob; import java.sql.Blob;
import java.sql.Clob; import java.sql.Clob;
import java.sql.Connection;
import java.sql.Date; import java.sql.Date;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Time; import java.sql.Time;
import java.sql.Timestamp; import java.sql.Timestamp;
import com.samskivert.jdbc.DatabaseLiaison;
import com.samskivert.jdbc.JDBCUtil;
import com.samskivert.jdbc.LiaisonRegistry;
import com.samskivert.jdbc.depot.FieldMarshaller.BooleanMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.BooleanMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ByteArrayMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ByteArrayMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ByteEnumMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ByteEnumMarshaller;
@@ -38,12 +44,24 @@ import com.samskivert.jdbc.depot.FieldMarshaller.IntMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.LongMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.LongMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ObjectMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ObjectMarshaller;
import com.samskivert.jdbc.depot.FieldMarshaller.ShortMarshaller; import com.samskivert.jdbc.depot.FieldMarshaller.ShortMarshaller;
import com.samskivert.jdbc.depot.annotation.FullTextIndex;
import com.samskivert.jdbc.depot.operator.Conditionals.FullTextMatch;
import com.samskivert.Log;
public class PostgreSQLBuilder public class PostgreSQLBuilder
extends SQLBuilder extends SQLBuilder
{ {
public class PGBuildVisitor extends BuildVisitor public class PGBuildVisitor extends BuildVisitor
{ {
@Override
public void visit (FullTextMatch match)
throws Exception
{
appendField("ftsCol_" + match.getName());
_builder.append(" @@ TO_TSQUERY('default', ?)");
}
protected PGBuildVisitor (DepotTypes types) protected PGBuildVisitor (DepotTypes types)
{ {
super(types); super(types);
@@ -72,6 +90,13 @@ public class PostgreSQLBuilder
public class PGBindVisitor extends BindVisitor public class PGBindVisitor extends BindVisitor
{ {
@Override
public void visit (FullTextMatch match)
throws Exception
{
_stmt.setString(_argIdx ++, match.getQuery());
}
protected PGBindVisitor (DepotTypes types, PreparedStatement stmt) protected PGBindVisitor (DepotTypes types, PreparedStatement stmt)
{ {
super(types, stmt); super(types, stmt);
@@ -83,6 +108,71 @@ public class PostgreSQLBuilder
super(types); super(types);
} }
@Override
public <T extends PersistentRecord> boolean addFullTextSearch (
Connection conn, DepotMarshaller<T> marshaller, FullTextIndex fts)
throws SQLException
{
Class<T> pClass = marshaller.getPersistentClass();
DatabaseLiaison liaison = LiaisonRegistry.getLiaison(conn);
String[] fields = fts.fieldNames();
String table = marshaller.getTableName();
String column = "ftsCol_" + fts.name();
String index = "ftsIx_" + fts.name();
String trigger = "ftsTrig_" + fts.name();
// build the UPDATE
StringBuilder initColumn = new StringBuilder("UPDATE ").
append(liaison.tableSQL(table)).append(" SET ").append(liaison.columnSQL(column)).
append(" = TO_TSVECTOR('default', ");
for (int ii = 0; ii < fields.length; ii ++) {
if (ii > 0) {
initColumn.append(" || ' ' || ");
}
initColumn.append("COALESCE(").
append(liaison.columnSQL(_types.getColumnName(pClass, fields[ii]))).
append(", '')");
}
initColumn.append(")");
// build the CREATE TRIGGER
StringBuilder createTrigger = new StringBuilder("CREATE TRIGGER ").
append(liaison.columnSQL(trigger)).append(" BEFORE UPDATE OR INSERT ON ").
append(liaison.tableSQL(table)).append(" FOR EACH ROW EXECUTE PROCEDURE tsearch2(").
append(liaison.columnSQL(column)).append(", ");
for (int ii = 0; ii < fields.length; ii ++) {
if (ii > 0) {
createTrigger.append(", ");
}
createTrigger.append(liaison.columnSQL(_types.getColumnName(pClass, fields[ii])));
}
createTrigger.append(")");
// build the CREATE INDEX
StringBuilder createIndex = new StringBuilder("CREATE INDEX ").
append(liaison.columnSQL(index)).append(" ON " ).append(liaison.tableSQL(table)).
append(" USING GIST(").append(liaison.columnSQL(column)).append(")");
Statement stmt = conn.createStatement();
try {
Log.info(
"Adding full-text search column, index and trigger: " + column + ", " +
index + ", " + trigger);
liaison.addColumn(conn, table, column, "TSVECTOR", true);
stmt.executeUpdate(initColumn.toString());
stmt.executeUpdate(createIndex.toString());
stmt.executeUpdate(createTrigger.toString());
} finally {
JDBCUtil.close(stmt);
}
return true;
}
@Override @Override
protected BuildVisitor getBuildVisitor () protected BuildVisitor getBuildVisitor ()
{ {
@@ -26,8 +26,10 @@ import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import com.samskivert.jdbc.depot.annotation.Column; import com.samskivert.jdbc.depot.annotation.Column;
import com.samskivert.jdbc.depot.annotation.FullTextIndex;
import com.samskivert.jdbc.depot.annotation.GeneratedValue; import com.samskivert.jdbc.depot.annotation.GeneratedValue;
import com.samskivert.jdbc.depot.clause.QueryClause; import com.samskivert.jdbc.depot.clause.QueryClause;
import com.samskivert.jdbc.depot.operator.Conditionals;
/** /**
* At the heart of Depot's SQL generation, this object constructs two {@link ExpressionVisitor} * At the heart of Depot's SQL generation, this object constructs two {@link ExpressionVisitor}
@@ -171,6 +173,17 @@ public abstract class SQLBuilder
return builder.toString(); return builder.toString();
} }
/**
* Add full-text search capabilities, as defined by the provided {@link FullTextIndex}, on
* the table associated with the given {@link DepotMarshaller}. This is a highly database
* specific operation and must thus be implemented by each dialect subclass.
*
* {@see Conditionals.FullTextIndex}
*/
public abstract <T extends PersistentRecord> boolean addFullTextSearch (
Connection conn, DepotMarshaller<T> marshaller, FullTextIndex fts)
throws SQLException;
/** /**
* Overridden by subclasses to create a dialect-specific {@link BuildVisitor}. * Overridden by subclasses to create a dialect-specific {@link BuildVisitor}.
*/ */
@@ -0,0 +1,44 @@
//
// $Id$
//
// samskivert library - useful routines for java programs
// Copyright (C) 2007 Michael Bayne, Pär Winzell
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.samskivert.jdbc.depot.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation is used to specify that a full text index is to be included in the generated DDL
* for a table.
*/
@Target(value={})
@Retention(value=RetentionPolicy.RUNTIME)
public @interface FullTextIndex
{
/**
* An identifier for this index, unique with the scope of the record.
*/
public String name ();
/**
* An array of the field names that should be indexed.
*/
public String[] fieldNames ();
}
@@ -40,4 +40,9 @@ public @interface Table
* Defaults to no additional constraints. * Defaults to no additional constraints.
*/ */
public UniqueConstraint[] uniqueConstraints () default {}; public UniqueConstraint[] uniqueConstraints () default {};
/**
* Full-text search indexes defined on this entity, if any. Defaults to none.
*/
public FullTextIndex[] fullTextIndexes () default {};
} }
@@ -33,7 +33,7 @@ import java.lang.annotation.Target;
public @interface UniqueConstraint public @interface UniqueConstraint
{ {
/** /**
* An array of the column names that make up the constraint * An array of the field names that make up the constraint
*/ */
public String[] columnNames () default {}; public String[] fieldNames () default {};
} }
@@ -40,7 +40,7 @@ import com.samskivert.jdbc.depot.clause.Where;
import com.samskivert.jdbc.depot.operator.Conditionals.In; import com.samskivert.jdbc.depot.operator.Conditionals.In;
import com.samskivert.jdbc.depot.operator.Conditionals.IsNull; import com.samskivert.jdbc.depot.operator.Conditionals.IsNull;
import com.samskivert.jdbc.depot.operator.Conditionals.Match; import com.samskivert.jdbc.depot.operator.Conditionals.FullTextMatch;
import com.samskivert.jdbc.depot.operator.Logic.Not; import com.samskivert.jdbc.depot.operator.Logic.Not;
import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator; import com.samskivert.jdbc.depot.operator.SQLOperator.BinaryOperator;
import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator; import com.samskivert.jdbc.depot.operator.SQLOperator.MultiOperator;
@@ -70,7 +70,7 @@ public interface ExpressionVisitor
throws Exception; throws Exception;
public void visit (In in) public void visit (In in)
throws Exception; throws Exception;
public void visit (Match match) public void visit (FullTextMatch match)
throws Exception; throws Exception;
public void visit (ColumnExp columnExp) public void visit (ColumnExp columnExp)
throws Exception; throws Exception;
@@ -247,39 +247,33 @@ public abstract class Conditionals
} }
} }
/** The MySQL 'match (...) against (...)' operator. */ /**
@Deprecated * An attempt at a dialect-agnostic full-text search condition, such as MySQL's MATCH() and
public static class Match * PostgreSQL's @@ TO_TSQUERY(...) abilities.
*/
public static class FullTextMatch
implements SQLOperator implements SQLOperator
{ {
public enum Mode { DEFAULT, BOOLEAN, NATURAL_LANGUAGE }; public FullTextMatch (Class<? extends PersistentRecord> pClass, String name, String query)
public Match (String query, Mode mode, boolean queryExpansion, ColumnExp... columns)
{ {
_pClass = pClass;
_name = name;
_query = query; _query = query;
_queryExpansion = queryExpansion;
_mode = mode;
_columns = columns;
} }
public Class<? extends PersistentRecord> getPersistentRecord ()
{
return _pClass;
}
public String getQuery () public String getQuery ()
{ {
return _query; return _query;
} }
public Mode getMode () public String getName ()
{ {
return _mode; return _name;
}
public boolean isQueryExpansion ()
{
return _queryExpansion;
}
public ColumnExp[] getColumns ()
{
return _columns;
} }
// from SQLExpression // from SQLExpression
@@ -291,14 +285,10 @@ public abstract class Conditionals
// from SQLExpression // from SQLExpression
public void addClasses (Collection<Class<? extends PersistentRecord>> classSet) public void addClasses (Collection<Class<? extends PersistentRecord>> classSet)
{ {
for (ColumnExp column : _columns) {
column.addClasses(classSet);
}
} }
protected Class<? extends PersistentRecord> _pClass;
protected String _name;
protected String _query; protected String _query;
protected Mode _mode;
protected boolean _queryExpansion;
protected ColumnExp[] _columns;
} }
} }