More HSQL support:
- HSQL doesn't have full text search, so we just OR up a bunch of LIKE conditions to catch any and all matches. This is not efficient, but it'll do just fine for things like unit tests and development work. - HSQL doesn't have & and |, it calls them BITAND and BITOR. - HSQL doesn't have MySQL's unix_timestamp() or a way to get an 'epoch' from a date like PostgreSQL, so we count the number of seconds since 1970-01-01. I have not investigated to make sure this is exact, but it should at least be internally consistent. - HSQL has a very neat list of data types that (no surprise) map very well to Java types.
This commit is contained in:
@@ -29,16 +29,25 @@ import java.sql.SQLException;
|
|||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
import java.sql.Time;
|
import java.sql.Time;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import com.samskivert.jdbc.ColumnDefinition;
|
||||||
import com.samskivert.jdbc.JDBCUtil;
|
import com.samskivert.jdbc.JDBCUtil;
|
||||||
|
import com.samskivert.util.ArrayUtil;
|
||||||
|
|
||||||
import com.samskivert.depot.DatabaseException;
|
import com.samskivert.depot.DatabaseException;
|
||||||
import com.samskivert.depot.PersistentRecord;
|
import com.samskivert.depot.PersistentRecord;
|
||||||
import com.samskivert.depot.annotation.FullTextIndex;
|
import com.samskivert.depot.annotation.FullTextIndex;
|
||||||
|
import com.samskivert.depot.annotation.GeneratedValue;
|
||||||
import com.samskivert.depot.clause.DeleteClause;
|
import com.samskivert.depot.clause.DeleteClause;
|
||||||
import com.samskivert.depot.expression.ColumnExp;
|
import com.samskivert.depot.expression.ColumnExp;
|
||||||
import com.samskivert.depot.expression.EpochSeconds;
|
import com.samskivert.depot.expression.EpochSeconds;
|
||||||
|
import com.samskivert.depot.expression.FunctionExp;
|
||||||
|
import com.samskivert.depot.expression.SQLExpression;
|
||||||
import com.samskivert.depot.impl.FieldMarshaller.BooleanMarshaller;
|
import com.samskivert.depot.impl.FieldMarshaller.BooleanMarshaller;
|
||||||
import com.samskivert.depot.impl.FieldMarshaller.ByteArrayMarshaller;
|
import com.samskivert.depot.impl.FieldMarshaller.ByteArrayMarshaller;
|
||||||
import com.samskivert.depot.impl.FieldMarshaller.ByteEnumMarshaller;
|
import com.samskivert.depot.impl.FieldMarshaller.ByteEnumMarshaller;
|
||||||
@@ -50,7 +59,12 @@ import com.samskivert.depot.impl.FieldMarshaller.IntMarshaller;
|
|||||||
import com.samskivert.depot.impl.FieldMarshaller.LongMarshaller;
|
import com.samskivert.depot.impl.FieldMarshaller.LongMarshaller;
|
||||||
import com.samskivert.depot.impl.FieldMarshaller.ObjectMarshaller;
|
import com.samskivert.depot.impl.FieldMarshaller.ObjectMarshaller;
|
||||||
import com.samskivert.depot.impl.FieldMarshaller.ShortMarshaller;
|
import com.samskivert.depot.impl.FieldMarshaller.ShortMarshaller;
|
||||||
|
import com.samskivert.depot.operator.Arithmetic.BitAnd;
|
||||||
|
import com.samskivert.depot.operator.Arithmetic.BitOr;
|
||||||
import com.samskivert.depot.operator.Conditionals.FullTextMatch;
|
import com.samskivert.depot.operator.Conditionals.FullTextMatch;
|
||||||
|
import com.samskivert.depot.operator.Conditionals.Like;
|
||||||
|
import com.samskivert.depot.operator.Logic.Or;
|
||||||
|
import com.samskivert.depot.operator.SQLOperator.BinaryOperator;
|
||||||
|
|
||||||
import static com.samskivert.Log.log;
|
import static com.samskivert.Log.log;
|
||||||
|
|
||||||
@@ -61,40 +75,57 @@ public class HSQLBuilder
|
|||||||
{
|
{
|
||||||
@Override public void visit (FullTextMatch match)
|
@Override public void visit (FullTextMatch match)
|
||||||
{
|
{
|
||||||
_builder.append("match(");
|
// HSQL doesn't have real full text search, so we fake it by creating a condition like
|
||||||
|
// (lower(COL1) like '%foo%') OR (lower(COL1) like '%bar%') OR ...
|
||||||
|
// (lower(COL2) like '%foo%') OR (lower(COL2) like '%bar%') OR ...
|
||||||
|
// ... and so on. Not efficient, but basically functional.
|
||||||
Class<? extends PersistentRecord> pClass = match.getPersistentRecord();
|
Class<? extends PersistentRecord> pClass = match.getPersistentRecord();
|
||||||
String[] fields =
|
|
||||||
_types.getMarshaller(pClass).getFullTextIndex(match.getName()).fields();
|
// find the fields involved
|
||||||
|
String[] fields = _types.getMarshaller(pClass).
|
||||||
|
getFullTextIndex(match.getName()).fields();
|
||||||
|
|
||||||
|
// explode the query into words
|
||||||
|
String[] ftsWords = match.getQuery().toLowerCase().split("\\W+");
|
||||||
|
if (ftsWords.length > 0 && ftsWords[0].length() == 0) {
|
||||||
|
// if the query led with whitespace, the first 'word' will be empty; strip it
|
||||||
|
ftsWords = ArrayUtil.splice(ftsWords, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now iterate over the cartesian product of the query words & the fields
|
||||||
|
List<SQLExpression> bits = Lists.newArrayList();
|
||||||
for (int ii = 0; ii < fields.length; ii ++) {
|
for (int ii = 0; ii < fields.length; ii ++) {
|
||||||
if (ii > 0) {
|
FunctionExp colexp = new FunctionExp("lower", new ColumnExp(pClass, fields[ii]));
|
||||||
_builder.append(", ");
|
for (int jj = 0; jj < ftsWords.length; jj ++) {
|
||||||
|
// build comparisons between each word and column
|
||||||
|
bits.add(new Like(colexp, "%" + ftsWords[jj] + "%"));
|
||||||
}
|
}
|
||||||
new ColumnExp(pClass, fields[ii]).accept(this);
|
|
||||||
}
|
}
|
||||||
_builder.append(") against (? in boolean mode)");
|
// then just OR them all together and we have our query
|
||||||
|
_ftsCondition = new Or(bits);
|
||||||
|
_ftsCondition.accept(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public void visit (DeleteClause<? extends PersistentRecord> deleteClause)
|
@Override
|
||||||
|
public void visit (BinaryOperator binaryOperator)
|
||||||
{
|
{
|
||||||
_builder.append("delete from ");
|
// HSQL doesn't handle & and | operators
|
||||||
appendTableName(deleteClause.getPersistentClass());
|
if (binaryOperator instanceof BitAnd) {
|
||||||
_builder.append(" ");
|
_builder.append("BITAND(?, ?)");
|
||||||
|
|
||||||
// MySQL can't do DELETE FROM SomeTable AS T1, so we turn off abbreviations briefly.
|
} else if (binaryOperator instanceof BitOr) {
|
||||||
boolean savedFlag = _types.getUseTableAbbreviations();
|
_builder.append("BITOR(?, ?)");
|
||||||
_types.setUseTableAbbreviations(false);
|
|
||||||
try {
|
} else {
|
||||||
deleteClause.getWhereClause().accept(this);
|
super.visit(binaryOperator);
|
||||||
} finally {
|
|
||||||
_types.setUseTableAbbreviations(savedFlag);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void visit (EpochSeconds epochSeconds)
|
public void visit (EpochSeconds epochSeconds)
|
||||||
{
|
{
|
||||||
_builder.append("unix_timestamp(");
|
_builder.append("datediff('ss', ");
|
||||||
epochSeconds.getArgument().accept(this);
|
epochSeconds.getArgument().accept(this);
|
||||||
_builder.append(")");
|
_builder.append(", '1970-01-01')");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected HBuildVisitor (DepotTypes types)
|
protected HBuildVisitor (DepotTypes types)
|
||||||
@@ -102,19 +133,8 @@ public class HSQLBuilder
|
|||||||
super(types);
|
super(types);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void appendTableName (Class<? extends PersistentRecord> type)
|
@Override protected void appendIdentifier (String field) {
|
||||||
{
|
_builder.append("\"").append(field).append("\"");
|
||||||
_builder.append(_types.getTableName(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override protected void appendTableAbbreviation (Class<? extends PersistentRecord> type)
|
|
||||||
{
|
|
||||||
_builder.append(_types.getTableAbbreviation(type));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override protected void appendIdentifier (String field)
|
|
||||||
{
|
|
||||||
_builder.append(field);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,12 +145,8 @@ public class HSQLBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override public void visit (FullTextMatch match) {
|
@Override public void visit (FullTextMatch match) {
|
||||||
try {
|
_ftsCondition.accept(this);
|
||||||
_stmt.setString(_argIdx++, match.getQuery());
|
_ftsCondition = null;
|
||||||
} catch (SQLException sqe) {
|
|
||||||
throw new DatabaseException("Failed to configure full-text match column " +
|
|
||||||
"[idx=" + (_argIdx-1) + ", match=" + match + "]", sqe);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,11 +159,7 @@ public class HSQLBuilder
|
|||||||
public void getFtsIndexes (
|
public void getFtsIndexes (
|
||||||
Iterable<String> columns, Iterable<String> indexes, Set<String> target)
|
Iterable<String> columns, Iterable<String> indexes, Set<String> target)
|
||||||
{
|
{
|
||||||
for (String index : indexes) {
|
// do nothing
|
||||||
if (index.startsWith("ftsIx_")) {
|
|
||||||
target.add(index.substring("ftsIx_".length()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -155,26 +167,8 @@ public class HSQLBuilder
|
|||||||
Connection conn, DepotMarshaller<T> marshaller, FullTextIndex fts)
|
Connection conn, DepotMarshaller<T> marshaller, FullTextIndex fts)
|
||||||
throws SQLException
|
throws SQLException
|
||||||
{
|
{
|
||||||
Class<T> pClass = marshaller.getPersistentClass();
|
// nothing to do for HSQL
|
||||||
StringBuilder update = new StringBuilder("ALTER TABLE ").
|
|
||||||
append(marshaller.getTableName()).append(" ADD FULLTEXT INDEX ftsIx_").
|
|
||||||
append(fts.name()).append(" (");
|
|
||||||
String[] fields = fts.fields();
|
|
||||||
for (int ii = 0; ii < fields.length; ii ++) {
|
|
||||||
if (ii > 0) {
|
|
||||||
update.append(", ");
|
|
||||||
}
|
|
||||||
update.append(_types.getColumnName(pClass, fields[ii]));
|
|
||||||
}
|
|
||||||
update.append(")");
|
|
||||||
|
|
||||||
Statement stmt = conn.createStatement();
|
|
||||||
try {
|
|
||||||
log.info("Adding full-text search index: ftsIx_" + fts.name());
|
|
||||||
stmt.executeUpdate(update.toString());
|
|
||||||
} finally {
|
|
||||||
JDBCUtil.close(stmt);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +182,7 @@ public class HSQLBuilder
|
|||||||
@Override
|
@Override
|
||||||
protected String getBooleanDefault ()
|
protected String getBooleanDefault ()
|
||||||
{
|
{
|
||||||
return "0";
|
return "false";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -203,25 +197,43 @@ public class HSQLBuilder
|
|||||||
return new HBindVisitor(_types, conn, stmt);
|
return new HBindVisitor(_types, conn, stmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void maybeMutateForGeneratedValue (GeneratedValue genValue, ColumnDefinition column)
|
||||||
|
{
|
||||||
|
// HSQL's IDENTITY() implementation does not take the form of a type, as MySQL's
|
||||||
|
// and PostgreSQL's conveniently shared SERIAL alias, nor as MySQL's original
|
||||||
|
// AUTO_INCREMENT modifier -- but as a default value, which admittedly makes sense
|
||||||
|
switch (genValue.strategy()) {
|
||||||
|
case AUTO:
|
||||||
|
case IDENTITY:
|
||||||
|
column.defaultValue = "IDENTITY";
|
||||||
|
column.unique = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
super.maybeMutateForGeneratedValue(genValue, column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected <T> String getColumnType (FieldMarshaller<?> fm, int length)
|
protected <T> String getColumnType (FieldMarshaller<?> fm, int length)
|
||||||
{
|
{
|
||||||
if (fm instanceof ByteMarshaller) {
|
if (fm instanceof FieldMarshaller.ByteMarshaller) {
|
||||||
return "TINYINT";
|
return "TINYINT";
|
||||||
} else if (fm instanceof ShortMarshaller) {
|
} else if (fm instanceof FieldMarshaller.ShortMarshaller) {
|
||||||
return "SMALLINT";
|
return "SMALLINT";
|
||||||
} else if (fm instanceof IntMarshaller) {
|
} else if (fm instanceof FieldMarshaller.IntMarshaller) {
|
||||||
return "INTEGER";
|
return "INTEGER";
|
||||||
} else if (fm instanceof LongMarshaller) {
|
} else if (fm instanceof FieldMarshaller.LongMarshaller) {
|
||||||
return "BIGINT";
|
return "BIGINT";
|
||||||
} else if (fm instanceof FloatMarshaller) {
|
} else if (fm instanceof FieldMarshaller.FloatMarshaller) {
|
||||||
return "FLOAT";
|
return "REAL";
|
||||||
} else if (fm instanceof DoubleMarshaller) {
|
} else if (fm instanceof FieldMarshaller.DoubleMarshaller) {
|
||||||
return "DOUBLE";
|
return "DOUBLE PRECISION";
|
||||||
} else if (fm instanceof ObjectMarshaller) {
|
} else if (fm instanceof FieldMarshaller.ObjectMarshaller) {
|
||||||
Class<?> ftype = fm.getField().getType();
|
Class<?> ftype = fm.getField().getType();
|
||||||
if (ftype.equals(Byte.class)) {
|
if (ftype.equals(Byte.class)) {
|
||||||
return "SMALLINT";
|
return "TINYINT";
|
||||||
} else if (ftype.equals(Short.class)) {
|
} else if (ftype.equals(Short.class)) {
|
||||||
return "SMALLINT";
|
return "SMALLINT";
|
||||||
} else if (ftype.equals(Integer.class)) {
|
} else if (ftype.equals(Integer.class)) {
|
||||||
@@ -231,49 +243,37 @@ public class HSQLBuilder
|
|||||||
} else if (ftype.equals(Float.class)) {
|
} else if (ftype.equals(Float.class)) {
|
||||||
return "FLOAT";
|
return "FLOAT";
|
||||||
} else if (ftype.equals(Double.class)) {
|
} else if (ftype.equals(Double.class)) {
|
||||||
return "DOUBLE";
|
return "DOUBLE PRECISION";
|
||||||
} else if (ftype.equals(String.class)) {
|
} else if (ftype.equals(String.class)) {
|
||||||
if (length < (1 << 15)) {
|
|
||||||
return "VARCHAR(" + length + ")";
|
return "VARCHAR(" + length + ")";
|
||||||
}
|
|
||||||
return "TEXT";
|
|
||||||
} else if (ftype.equals(Date.class)) {
|
} else if (ftype.equals(Date.class)) {
|
||||||
return "DATE";
|
return "DATE";
|
||||||
} else if (ftype.equals(Time.class)) {
|
} else if (ftype.equals(Time.class)) {
|
||||||
return "DATETIME";
|
return "TIME";
|
||||||
} else if (ftype.equals(Timestamp.class)) {
|
} else if (ftype.equals(Timestamp.class)) {
|
||||||
return "DATETIME";
|
return "TIMESTAMP";
|
||||||
} else if (ftype.equals(Blob.class)) {
|
} else if (ftype.equals(Blob.class)) {
|
||||||
return "BYTEA";
|
return "VARBINARY";
|
||||||
} else if (ftype.equals(Clob.class)) {
|
} else if (ftype.equals(Clob.class)) {
|
||||||
return "TEXT";
|
return "VARCHAR";
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Don't know how to create SQL for " + ftype + ".");
|
"Don't know how to create SQL for " + ftype + ".");
|
||||||
}
|
}
|
||||||
} else if (fm instanceof ByteArrayMarshaller ||
|
} else if (fm instanceof FieldMarshaller.ByteArrayMarshaller) {
|
||||||
fm instanceof IntArrayMarshaller) {
|
return "VARBINARY";
|
||||||
if (fm instanceof IntArrayMarshaller) {
|
} else if (fm instanceof FieldMarshaller.IntArrayMarshaller) {
|
||||||
length *= 4;
|
return "VARBINARY";
|
||||||
}
|
} else if (fm instanceof FieldMarshaller.ByteEnumMarshaller) {
|
||||||
// semi-arbitrarily use VARBINARY() up to 32767
|
|
||||||
if (length < (1 << 15)) {
|
|
||||||
return "VARBINARY(" + length + ")";
|
|
||||||
}
|
|
||||||
// use BLOB to 65535
|
|
||||||
if (length < (1 << 16)) {
|
|
||||||
return "BLOB";
|
|
||||||
}
|
|
||||||
if (length < (1 << 24)) {
|
|
||||||
return "MEDIUMBLOB";
|
|
||||||
}
|
|
||||||
return "LONGBLOB";
|
|
||||||
} else if (fm instanceof ByteEnumMarshaller) {
|
|
||||||
return "TINYINT";
|
|
||||||
} else if (fm instanceof BooleanMarshaller) {
|
|
||||||
return "TINYINT";
|
return "TINYINT";
|
||||||
|
} else if (fm instanceof FieldMarshaller.BooleanMarshaller) {
|
||||||
|
return "BOOLEAN";
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown field marshaller type: " + fm.getClass());
|
throw new IllegalArgumentException("Unknown field marshaller type: " + fm.getClass());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Holds the Full Text Seach condition between build and bind phases. */
|
||||||
|
protected SQLExpression _ftsCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user