298 lines
11 KiB
Java
298 lines
11 KiB
Java
//
|
|
// $Id$
|
|
//
|
|
// Depot library - a Java relational persistence library
|
|
// Copyright (C) 2006-2008 Michael Bayne and 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.depot;
|
|
|
|
import java.sql.Blob;
|
|
import java.sql.Clob;
|
|
import java.sql.Connection;
|
|
import java.sql.Date;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.SQLException;
|
|
import java.sql.Statement;
|
|
import java.sql.Time;
|
|
import java.sql.Timestamp;
|
|
import java.util.Set;
|
|
|
|
import com.samskivert.jdbc.DatabaseLiaison;
|
|
import com.samskivert.jdbc.JDBCUtil;
|
|
import com.samskivert.jdbc.LiaisonRegistry;
|
|
import com.samskivert.depot.FieldMarshaller.BooleanMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.ByteArrayMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.ByteEnumMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.ByteMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.DoubleMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.FloatMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.IntArrayMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.IntMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.LongMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.ObjectMarshaller;
|
|
import com.samskivert.depot.FieldMarshaller.ShortMarshaller;
|
|
import com.samskivert.depot.annotation.FullTextIndex;
|
|
import com.samskivert.depot.expression.EpochSeconds;
|
|
import com.samskivert.depot.operator.Conditionals.FullTextMatch;
|
|
import com.samskivert.util.ArrayUtil;
|
|
import com.samskivert.util.StringUtil;
|
|
|
|
import static com.samskivert.Log.log;
|
|
|
|
public class PostgreSQLBuilder
|
|
extends SQLBuilder
|
|
{
|
|
public class PGBuildVisitor extends BuildVisitor
|
|
{
|
|
@Override public void visit (FullTextMatch match) {
|
|
appendIdentifier("ftsCol_" + match.getName());
|
|
_builder.append(" @@ TO_TSQUERY('default', ?)");
|
|
}
|
|
|
|
@Override public void visit (EpochSeconds epochSeconds) {
|
|
_builder.append("date_part('epoch', ");
|
|
epochSeconds.getArgument().accept(this);
|
|
_builder.append(")");
|
|
}
|
|
|
|
// TODO: enable when we can require 1.6 support
|
|
// @Override public void visit (In in) {
|
|
// in.getColumn().accept(this);
|
|
// _builder.append(" = any (?)");
|
|
// }
|
|
|
|
protected PGBuildVisitor (DepotTypes types) {
|
|
super(types);
|
|
}
|
|
|
|
@Override protected void appendIdentifier (String field) {
|
|
_builder.append("\"").append(field).append("\"");
|
|
}
|
|
}
|
|
|
|
public class PGBindVisitor extends BindVisitor
|
|
{
|
|
@Override public void visit (FullTextMatch match) {
|
|
// The tsearch2 engine takes queries on the form
|
|
// (foo&bar)|goop
|
|
// so in this first simple implementation, we just take the user query, chop it into
|
|
// words by space/punctuation and 'or' those together like so:
|
|
// 'ho! who goes there?' -> 'ho|who|goes|there'
|
|
String[] searchTerms = match.getQuery().toLowerCase().split("\\W+");
|
|
if (searchTerms.length > 0 && searchTerms[0].length() == 0) {
|
|
searchTerms = ArrayUtil.splice(searchTerms, 0, 1);
|
|
}
|
|
String query = StringUtil.join(searchTerms, "|");
|
|
try {
|
|
_stmt.setString(_argIdx++, query);
|
|
} catch (SQLException sqe) {
|
|
throw new DatabaseException("Failed to configure full-text match column " +
|
|
"[idx=" + (_argIdx-1) + ", query=" + query + "]", sqe);
|
|
}
|
|
}
|
|
|
|
// TODO: enable when we can require 1.6 support
|
|
// @Override public void visit (In in) {
|
|
// Comparable<?>[] values = in.getValues();
|
|
// try {
|
|
// _stmt.setObject(
|
|
// _argIdx++, _conn.createArrayOf(getElementType(values), (Object[])values));
|
|
// } catch (SQLException sqe) {
|
|
// throw new DatabaseException(
|
|
// "Failed to write value to statement [idx=" + (_argIdx-1) +
|
|
// ", values=" + StringUtil.safeToString(values) + "]", sqe);
|
|
// }
|
|
// }
|
|
|
|
// protected String getElementType (Comparable<?>[] values) {
|
|
// if (values instanceof Integer[]) {
|
|
// return "integer";
|
|
// } else if (values instanceof String[]) {
|
|
// return "character varying";
|
|
// } else {
|
|
// throw new DatabaseException(
|
|
// "Don't know how to make Postgres array for " + values.getClass());
|
|
// }
|
|
// }
|
|
|
|
protected PGBindVisitor (DepotTypes types, Connection conn, PreparedStatement stmt) {
|
|
super(types, conn, stmt);
|
|
}
|
|
}
|
|
|
|
public PostgreSQLBuilder (DepotTypes types)
|
|
{
|
|
super(types);
|
|
}
|
|
|
|
@Override
|
|
public void getFtsIndexes (
|
|
Iterable<String> columns, Iterable<String> indexes, Set<String> target)
|
|
{
|
|
for (String column : columns) {
|
|
if (column.startsWith("ftsCol_")) {
|
|
target.add(column.substring("ftsCol_".length()));
|
|
}
|
|
}
|
|
}
|
|
|
|
@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.fields();
|
|
|
|
String table = marshaller.getTableName();
|
|
String column = "ftsCol_" + fts.name();
|
|
String index = table + "_ftsIx_" + fts.name();
|
|
String trigger = table + "_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
|
|
public boolean isPrivateColumn (String column)
|
|
{
|
|
// filter out any column that we created as part of FTS support
|
|
return column.startsWith("ftsCol_");
|
|
}
|
|
|
|
@Override
|
|
protected BuildVisitor getBuildVisitor ()
|
|
{
|
|
return new PGBuildVisitor(_types);
|
|
}
|
|
|
|
@Override
|
|
protected BindVisitor getBindVisitor (Connection conn, PreparedStatement stmt)
|
|
{
|
|
return new PGBindVisitor(_types, conn, stmt);
|
|
}
|
|
|
|
@Override
|
|
protected <T> String getColumnType (FieldMarshaller<?> fm, int length)
|
|
{
|
|
if (fm instanceof ByteMarshaller) {
|
|
return "SMALLINT";
|
|
} else if (fm instanceof ShortMarshaller) {
|
|
return "SMALLINT";
|
|
} else if (fm instanceof IntMarshaller) {
|
|
return "INTEGER";
|
|
} else if (fm instanceof LongMarshaller) {
|
|
return "BIGINT";
|
|
} else if (fm instanceof FloatMarshaller) {
|
|
return "REAL";
|
|
} else if (fm instanceof DoubleMarshaller) {
|
|
return "DOUBLE PRECISION";
|
|
} else if (fm instanceof ObjectMarshaller) {
|
|
Class<?> ftype = fm.getField().getType();
|
|
if (ftype.equals(Byte.class)) {
|
|
return "SMALLINT";
|
|
} else if (ftype.equals(Short.class)) {
|
|
return "SMALLINT";
|
|
} else if (ftype.equals(Integer.class)) {
|
|
return "INTEGER";
|
|
} else if (ftype.equals(Long.class)) {
|
|
return "BIGINT";
|
|
} else if (ftype.equals(Float.class)) {
|
|
return "FLOAT";
|
|
} else if (ftype.equals(Double.class)) {
|
|
return "DOUBLE";
|
|
} else if (ftype.equals(String.class)) {
|
|
if (length < (1 << 15)) {
|
|
return "VARCHAR(" + length + ")";
|
|
}
|
|
return "TEXT";
|
|
} else if (ftype.equals(Date.class)) {
|
|
return "DATE";
|
|
} else if (ftype.equals(Time.class)) {
|
|
return "TIME";
|
|
} else if (ftype.equals(Timestamp.class)) {
|
|
return "TIMESTAMP";
|
|
} else if (ftype.equals(Blob.class)) {
|
|
return "BYTEA";
|
|
} else if (ftype.equals(Clob.class)) {
|
|
return "TEXT";
|
|
} else {
|
|
throw new IllegalArgumentException(
|
|
"Don't know how to create SQL for " + ftype + ".");
|
|
}
|
|
} else if (fm instanceof ByteArrayMarshaller) {
|
|
return "BYTEA";
|
|
} else if (fm instanceof IntArrayMarshaller) {
|
|
return "BYTEA";
|
|
} else if (fm instanceof ByteEnumMarshaller) {
|
|
return "SMALLINT";
|
|
} else if (fm instanceof BooleanMarshaller) {
|
|
return "BOOLEAN";
|
|
} else {
|
|
throw new IllegalArgumentException("Unknown field marshaller type: " + fm.getClass());
|
|
}
|
|
}
|
|
}
|