提交 edce6934 authored 作者: Thomas Mueller's avatar Thomas Mueller

fulltext search refactoring

上级 a35fdf7d
...@@ -46,16 +46,11 @@ import org.h2.value.DataType; ...@@ -46,16 +46,11 @@ import org.h2.value.DataType;
* This class implements the native full text search. * This class implements the native full text search.
* Most methods can be called using SQL statements as well. * Most methods can be called using SQL statements as well.
*/ */
public class FullText implements Trigger, CloseListener { public class FullText {
private static final String TRIGGER_PREFIX = "FT_"; private static final String TRIGGER_PREFIX = "FT_";
private static final String SCHEMA = "FT"; private static final String SCHEMA = "FT";
/**
* The column name of the result set returned by the search method.
*/
private static final String FIELD_QUERY = "QUERY";
/** /**
* A column name of the result set returned by the searchData method. * A column name of the result set returned by the searchData method.
*/ */
...@@ -76,12 +71,70 @@ public class FullText implements Trigger, CloseListener { ...@@ -76,12 +71,70 @@ public class FullText implements Trigger, CloseListener {
*/ */
private static final String FIELD_KEYS = "KEYS"; private static final String FIELD_KEYS = "KEYS";
private FullTextSettings setting; /**
private IndexInfo index; * The column name of the result set returned by the search method.
private int[] dataTypes; */
private PreparedStatement prepInsertWord, prepInsertRow, prepInsertMap; private static final String FIELD_QUERY = "QUERY";
private PreparedStatement prepDeleteRow, prepDeleteMap;
private PreparedStatement prepSelectRow; /**
* Initializes full text search functionality for this database. This adds
* the following Java functions to the database:
* <ul>
* <li>FT_CREATE_INDEX(schemaNameString, tableNameString,
* columnListString)</li>
* <li>FT_SEARCH(queryString, limitInt, offsetInt): result set</li>
* <li>FT_REINDEX()</li>
* <li>FT_DROP_ALL()</li>
* </ul>
* It also adds a schema FT to the database where bookkeeping information
* is stored. This function may be called from a Java application, or by
* using the SQL statements:
*
* <pre>
* CREATE ALIAS IF NOT EXISTS FT_INIT FOR
* &quot;org.h2.fulltext.FullText.init&quot;;
* CALL FT_INIT();
* </pre>
*
* @param conn the connection
*/
public static void init(Connection conn) throws SQLException {
Statement stat = conn.createStatement();
stat.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA);
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA
+ ".INDEXES(ID INT AUTO_INCREMENT PRIMARY KEY, SCHEMA VARCHAR, TABLE VARCHAR, COLUMNS VARCHAR, UNIQUE(SCHEMA, TABLE))");
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA
+ ".WORDS(ID INT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR, UNIQUE(NAME))");
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA
+ ".ROWS(ID IDENTITY, HASH INT, INDEXID INT, KEY VARCHAR, UNIQUE(HASH, INDEXID, KEY))");
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA
+ ".MAP(ROWID INT, WORDID INT, PRIMARY KEY(WORDID, ROWID))");
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA + ".IGNORELIST(LIST VARCHAR)");
stat.execute("CREATE ALIAS IF NOT EXISTS FT_CREATE_INDEX FOR \"" + FullText.class.getName() + ".createIndex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FT_DROP_INDEX FOR \"" + FullText.class.getName() + ".dropIndex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FT_SEARCH FOR \"" + FullText.class.getName() + ".search\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FT_SEARCH_DATA FOR \"" + FullText.class.getName() + ".searchData\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FT_REINDEX FOR \"" + FullText.class.getName() + ".reindex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FT_DROP_ALL FOR \"" + FullText.class.getName() + ".dropAll\"");
FullTextSettings setting = FullTextSettings.getInstance(conn);
ResultSet rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".IGNORELIST");
while (rs.next()) {
String commaSeparatedList = rs.getString(1);
setIgnoreList(setting, commaSeparatedList);
}
rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".WORDS");
HashMap map = setting.getWordList();
while (rs.next()) {
String word = rs.getString("NAME");
int id = rs.getInt("ID");
word = setting.convertWord(word);
if (word != null) {
map.put(word, ObjectUtils.getInteger(id));
}
}
}
/** /**
* Create a new full text index for a table and column list. Each table may * Create a new full text index for a table and column list. Each table may
...@@ -104,6 +157,29 @@ public class FullText implements Trigger, CloseListener { ...@@ -104,6 +157,29 @@ public class FullText implements Trigger, CloseListener {
indexExistingRows(conn, schema, table); indexExistingRows(conn, schema, table);
} }
/**
* Re-creates the full text index for this database
*
* @param conn the connection
*/
public static void reindex(Connection conn) throws SQLException {
init(conn);
removeAllTriggers(conn, TRIGGER_PREFIX);
FullTextSettings setting = FullTextSettings.getInstance(conn);
setting.getWordList().clear();
Statement stat = conn.createStatement();
stat.execute("TRUNCATE TABLE " + SCHEMA + ".WORDS");
stat.execute("TRUNCATE TABLE " + SCHEMA + ".ROWS");
stat.execute("TRUNCATE TABLE " + SCHEMA + ".MAP");
ResultSet rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".INDEXES");
while (rs.next()) {
String schema = rs.getString("SCHEMA");
String table = rs.getString("TABLE");
createTrigger(conn, schema, table);
indexExistingRows(conn, schema, table);
}
}
/** /**
* Drop an existing full text index for a table. This method returns * Drop an existing full text index for a table. This method returns
* silently if no index for this table exists. * silently if no index for this table exists.
...@@ -147,64 +223,61 @@ public class FullText implements Trigger, CloseListener { ...@@ -147,64 +223,61 @@ public class FullText implements Trigger, CloseListener {
} }
} }
private static void createTrigger(Connection conn, String schema, String table) throws SQLException { /**
createOrDropTrigger(conn, schema, table, true); * Drops all full text indexes from the database.
} *
* @param conn the connection
private static void createOrDropTrigger(Connection conn, String schema, String table, boolean create) throws SQLException { */
public static void dropAll(Connection conn) throws SQLException {
init(conn);
Statement stat = conn.createStatement(); Statement stat = conn.createStatement();
String trigger = StringUtils.quoteIdentifier(schema) + "." stat.execute("DROP SCHEMA IF EXISTS " + SCHEMA);
+ StringUtils.quoteIdentifier(TRIGGER_PREFIX + table); removeAllTriggers(conn, TRIGGER_PREFIX);
stat.execute("DROP TRIGGER IF EXISTS " + trigger); FullTextSettings setting = FullTextSettings.getInstance(conn);
if (create) { setting.removeAllIndexes();
StringBuffer buff = new StringBuffer("CREATE TRIGGER IF NOT EXISTS "); setting.getIgnoreList().clear();
buff.append(trigger); setting.getWordList().clear();
buff.append(" AFTER INSERT, UPDATE, DELETE ON ");
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
buff.append(" FOR EACH ROW CALL \"");
buff.append(FullText.class.getName());
buff.append("\"");
stat.execute(buff.toString());
}
} }
private static void indexExistingRows(Connection conn, String schema, String table) throws SQLException { /**
FullText existing = new FullText(); * Searches from the full text index for this database.
existing.init(conn, schema, null, table, false, INSERT); * The returned result set has the following column:
StringBuffer buff = new StringBuffer("SELECT * FROM "); * <ul><li>QUERY (varchar): The query to use to get the data.
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table)); * The query does not include 'SELECT * FROM '. Example:
ResultSet rs = conn.createStatement().executeQuery(buff.toString()); * PUBLIC.TEST WHERE ID = 1
int columnCount = rs.getMetaData().getColumnCount(); * </li></ul>
while (rs.next()) { *
Object[] row = new Object[columnCount]; * @param conn the connection
for (int i = 0; i < columnCount; i++) { * @param text the search query
row[i] = rs.getObject(i + 1); * @param limit the maximum number of rows or 0 for no limit
} * @param offset the offset or 0 for no offset
existing.fire(conn, null, row); * @return the result set
} */
public static ResultSet search(Connection conn, String text, int limit, int offset) throws SQLException {
return search(conn, text, limit, offset, false);
} }
/** /**
* Re-creates the full text index for this database * Searches from the full text index for this database. The result contains
* the primary key data as an array. The returned result set has the
* following columns:
* <ul>
* <li>SCHEMA (varchar): The schema name. Example: PUBLIC </li>
* <li>TABLE (varchar): The table name. Example: TEST </li>
* <li>COLUMNS (array of varchar): Comma separated list of quoted column
* names. The column names are quoted if necessary. Example: (ID) </li>
* <li>KEYS (array of values): Comma separated list of values. Example: (1)
* </li>
* </ul>
* *
* @param conn the connection * @param conn the connection
* @param text the search query
* @param limit the maximum number of rows or 0 for no limit
* @param offset the offset or 0 for no offset
* @return the result set
*/ */
public static void reindex(Connection conn) throws SQLException { public static ResultSet searchData(Connection conn, String text, int limit, int offset) throws SQLException {
init(conn); return search(conn, text, limit, offset, true);
removeAllTriggers(conn);
FullTextSettings setting = FullTextSettings.getInstance(conn);
setting.getWordList().clear();
Statement stat = conn.createStatement();
stat.execute("TRUNCATE TABLE " + SCHEMA + ".WORDS");
stat.execute("TRUNCATE TABLE " + SCHEMA + ".ROWS");
stat.execute("TRUNCATE TABLE " + SCHEMA + ".MAP");
ResultSet rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".INDEXES");
while (rs.next()) {
String schema = rs.getString("SCHEMA");
String table = rs.getString("TABLE");
createTrigger(conn, schema, table);
indexExistingRows(conn, schema, table);
}
} }
/** /**
...@@ -227,110 +300,396 @@ public class FullText implements Trigger, CloseListener { ...@@ -227,110 +300,396 @@ public class FullText implements Trigger, CloseListener {
prep.execute(); prep.execute();
} }
private static void setIgnoreList(FullTextSettings setting, String commaSeparatedList) { /**
String[] list = StringUtils.arraySplit(commaSeparatedList, ',', true); * INTERNAL.
HashSet set = setting.getIgnoreList(); * Convert the object to a string.
for (int i = 0; i < list.length; i++) { *
String word = list[i]; * @param data the object
word = setting.convertWord(word); * @param type the SQL type
if (word != null) { * @return the string
set.add(list[i]); */
} protected static String asString(Object data, int type) throws SQLException {
if (data == null) {
return "NULL";
} }
switch (type) {
case Types.BIT:
case DataType.TYPE_BOOLEAN:
case Types.INTEGER:
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.NUMERIC:
case Types.REAL:
case Types.SMALLINT:
case Types.TINYINT:
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
case Types.LONGVARCHAR:
case Types.CHAR:
case Types.VARCHAR:
return data.toString();
case Types.CLOB:
try {
if (data instanceof Clob) {
data = ((Clob) data).getCharacterStream();
} }
return IOUtils.readStringAndClose((Reader) data, -1);
private static void removeAllTriggers(Connection conn) throws SQLException { } catch (IOException e) {
Statement stat = conn.createStatement(); throw Message.convert(e);
ResultSet rs = stat.executeQuery("SELECT * FROM INFORMATION_SCHEMA.TRIGGERS");
Statement stat2 = conn.createStatement();
while (rs.next()) {
String schema = rs.getString("TRIGGER_SCHEMA");
String name = rs.getString("TRIGGER_NAME");
if (name.startsWith(TRIGGER_PREFIX)) {
name = StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(name);
stat2.execute("DROP TRIGGER " + name);
} }
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BINARY:
case Types.JAVA_OBJECT:
case Types.OTHER:
case Types.BLOB:
case Types.STRUCT:
case Types.REF:
case Types.NULL:
case Types.ARRAY:
case DataType.TYPE_DATALINK:
case Types.DISTINCT:
throw new SQLException("FULLTEXT", "Unsupported column data type: " + type);
default:
return "";
} }
} }
/** /**
* Drops all full text indexes from the database. * Create an empty search result and initialize the columns.
* *
* @param conn the connection * @param data true if the result set should contain the primary key data as
* an array.
* @return the empty result set
*/ */
public static void dropAll(Connection conn) throws SQLException { protected static SimpleResultSet createResultSet(boolean data) throws SQLException {
init(conn); SimpleResultSet result = new SimpleResultSet();
Statement stat = conn.createStatement(); if (data) {
stat.execute("DROP SCHEMA IF EXISTS " + SCHEMA); result.addColumn(FullText.FIELD_SCHEMA, Types.VARCHAR, 0, 0);
removeAllTriggers(conn); result.addColumn(FullText.FIELD_TABLE, Types.VARCHAR, 0, 0);
FullTextSettings setting = FullTextSettings.getInstance(conn); result.addColumn(FullText.FIELD_COLUMNS, Types.ARRAY, 0, 0);
setting.removeAllIndexes(); result.addColumn(FullText.FIELD_KEYS, Types.ARRAY, 0, 0);
setting.getIgnoreList().clear(); } else {
setting.getWordList().clear(); result.addColumn(FullText.FIELD_QUERY, Types.VARCHAR, 0, 0);
}
return result;
} }
/** /**
* Initializes full text search functionality for this database. This adds * Parse a primary key condition into the primary key columns.
* the following Java functions to the database:
* <ul>
* <li>FT_CREATE_INDEX(schemaNameString, tableNameString, columnListString)
* </li><li>FT_SEARCH(queryString, limitInt, offsetInt): result set
* </li><li>FT_REINDEX()
* </li><li>FT_DROP_ALL()
* </li></ul>
* It also adds a schema FULLTEXT to the database where bookkeeping
* information is stored. This function may be called from a Java
* application, or by using the SQL statements:
* <pre>
* CREATE ALIAS IF NOT EXISTS FULLTEXT_INIT FOR
* &quot;org.h2.fulltext.FullText.init&quot;;
* CALL FULLTEXT_INIT();
* </pre>
* *
* @param conn the connection * @param conn the database connection
* @param key the primary key condition as a string
* @return an array containing the column name list and the data list
*/ */
public static void init(Connection conn) throws SQLException { protected static Object[][] parseKey(Connection conn, String key) throws SQLException {
ArrayList columns = new ArrayList();
ArrayList data = new ArrayList();
JdbcConnection c = (JdbcConnection) conn;
Session session = (Session) c.getSession();
Parser p = new Parser(session);
Expression expr = p.parseExpression(key);
addColumnData(columns, data, expr);
Object[] col = new Object[columns.size()];
columns.toArray(col);
Object[] dat = new Object[columns.size()];
data.toArray(dat);
Object[][] columnData = new Object[][] {
col, dat
};
return columnData;
}
/**
* INTERNAL.
* Convert an object to a String as used in a SQL statement.
*
* @param data the object
* @param type the SQL type
* @return the SQL String
*/
protected static String quoteSQL(Object data, int type) throws SQLException {
if (data == null) {
return "NULL";
}
switch (type) {
case Types.BIT:
case DataType.TYPE_BOOLEAN:
case Types.INTEGER:
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.NUMERIC:
case Types.REAL:
case Types.SMALLINT:
case Types.TINYINT:
return data.toString();
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
case Types.LONGVARCHAR:
case Types.CHAR:
case Types.VARCHAR:
return quoteString(data.toString());
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BINARY:
return "'" + ByteUtils.convertBytesToString((byte[]) data) + "'";
case Types.CLOB:
case Types.JAVA_OBJECT:
case Types.OTHER:
case Types.BLOB:
case Types.STRUCT:
case Types.REF:
case Types.NULL:
case Types.ARRAY:
case DataType.TYPE_DATALINK:
case Types.DISTINCT:
throw new SQLException("FULLTEXT", "Unsupported key data type: " + type);
default:
return "";
}
}
/**
* Remove all triggers that start with the given prefix.
*
* @param conn the database connection
* @param prefix the prefix
*/
protected static void removeAllTriggers(Connection conn, String prefix) throws SQLException {
Statement stat = conn.createStatement(); Statement stat = conn.createStatement();
stat.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA); ResultSet rs = stat.executeQuery("SELECT * FROM INFORMATION_SCHEMA.TRIGGERS");
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA Statement stat2 = conn.createStatement();
+ ".INDEXES(ID INT AUTO_INCREMENT PRIMARY KEY, SCHEMA VARCHAR, TABLE VARCHAR, COLUMNS VARCHAR, UNIQUE(SCHEMA, TABLE))"); while (rs.next()) {
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA String schema = rs.getString("TRIGGER_SCHEMA");
+ ".WORDS(ID INT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR, UNIQUE(NAME))"); String name = rs.getString("TRIGGER_NAME");
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA if (name.startsWith(TRIGGER_PREFIX)) {
+ ".ROWS(ID IDENTITY, HASH INT, INDEXID INT, KEY VARCHAR, UNIQUE(HASH, INDEXID, KEY))"); name = StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(name);
stat2.execute("DROP TRIGGER " + name);
}
}
}
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA /**
+ ".MAP(ROWID INT, WORDID INT, PRIMARY KEY(WORDID, ROWID))"); * Set the column indices of a set of keys.
*
* @param index the column indices (will be modified)
* @param keys the key list
* @param columns the column list
*/
protected static void setColumns(int[] index, ArrayList keys, ArrayList columns) throws SQLException {
for (int i = 0; i < keys.size(); i++) {
String key = (String) keys.get(i);
int found = -1;
for (int j = 0; found == -1 && j < columns.size(); j++) {
String column = (String) columns.get(j);
if (column.equals(key)) {
found = j;
}
}
if (found < 0) {
throw new SQLException("FULLTEXT", "Column not found: " + key);
}
index[i] = found;
}
}
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA + ".IGNORELIST(LIST VARCHAR)"); private static ResultSet search(Connection conn, String text, int limit, int offset, boolean data) throws SQLException {
stat.execute("CREATE ALIAS IF NOT EXISTS FT_CREATE_INDEX FOR \"" + FullText.class.getName() + ".createIndex\""); SimpleResultSet result = createResultSet(data);
stat.execute("CREATE ALIAS IF NOT EXISTS FT_DROP_INDEX FOR \"" + FullText.class.getName() + ".dropIndex\""); if (conn.getMetaData().getURL().startsWith("jdbc:columnlist:")) {
stat.execute("CREATE ALIAS IF NOT EXISTS FT_SEARCH FOR \"" + FullText.class.getName() + ".search\""); // this is just to query the result set columns
stat.execute("CREATE ALIAS IF NOT EXISTS FT_SEARCH_DATA FOR \"" + FullText.class.getName() + ".searchData\""); return result;
stat.execute("CREATE ALIAS IF NOT EXISTS FT_REINDEX FOR \"" + FullText.class.getName() + ".reindex\""); }
stat.execute("CREATE ALIAS IF NOT EXISTS FT_DROP_ALL FOR \"" + FullText.class.getName() + ".dropAll\"");
FullTextSettings setting = FullTextSettings.getInstance(conn); FullTextSettings setting = FullTextSettings.getInstance(conn);
ResultSet rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".IGNORELIST"); HashSet words = new HashSet();
addWords(setting, words, text);
HashSet rIds = null, lastRowIds = null;
HashMap allWords = setting.getWordList();
PreparedStatement prepSelectMapByWordId = setting.getPrepSelectMapByWordId();
for (Iterator it = words.iterator(); it.hasNext();) {
lastRowIds = rIds;
rIds = new HashSet();
String word = (String) it.next();
Integer wId = (Integer) allWords.get(word);
if (wId == null) {
continue;
}
prepSelectMapByWordId.setInt(1, wId.intValue());
ResultSet rs = prepSelectMapByWordId.executeQuery();
while (rs.next()) { while (rs.next()) {
String commaSeparatedList = rs.getString(1); Integer rId = ObjectUtils.getInteger(rs.getInt(1));
setIgnoreList(setting, commaSeparatedList); if (lastRowIds == null || lastRowIds.contains(rId)) {
rIds.add(rId);
} }
rs = stat.executeQuery("SELECT * FROM " + SCHEMA + ".WORDS"); }
HashMap map = setting.getWordList(); }
if (rIds == null || rIds.size() == 0) {
return result;
}
PreparedStatement prepSelectRowById = setting.getPrepSelectRowById();
int rowCount = 0;
for (Iterator it = rIds.iterator(); it.hasNext();) {
int rowId = ((Integer) it.next()).intValue();
prepSelectRowById.setInt(1, rowId);
ResultSet rs = prepSelectRowById.executeQuery();
if (!rs.next()) {
continue;
}
if (offset > 0) {
offset--;
} else {
String key = rs.getString(1);
int indexId = rs.getInt(2);
IndexInfo index = setting.getIndexInfo(indexId);
if (data) {
Object[][] columnData = parseKey(conn, key);
Object[] row = new Object[] {
index.schema,
index.table,
columnData[0],
columnData[1]
};
result.addRow(row);
} else {
StringBuffer buff = new StringBuffer();
buff.append(StringUtils.quoteIdentifier(index.schema));
buff.append('.');
buff.append(StringUtils.quoteIdentifier(index.table));
buff.append(" WHERE ");
buff.append(key);
String query = buff.toString();
result.addRow(new String[] { query });
}
rowCount++;
if (limit > 0 && rowCount >= limit) {
break;
}
}
}
return result;
}
private static void addColumnData(ArrayList columns, ArrayList data, Expression expr) {
if (expr instanceof ConditionAndOr) {
ConditionAndOr and = (ConditionAndOr) expr;
Expression left = and.getExpression(true);
Expression right = and.getExpression(false);
addColumnData(columns, data, left);
addColumnData(columns, data, right);
} else {
Comparison comp = (Comparison) expr;
ExpressionColumn ec = (ExpressionColumn) comp.getExpression(true);
ValueExpression ev = (ValueExpression) comp.getExpression(false);
String columnName = ec.getColumnName();
columns.add(columnName);
if (ev == null) {
data.add(null);
} else {
data.add(ev.getValue(null).getString());
}
}
}
private static void addWords(FullTextSettings setting, HashSet set, String text) {
StringTokenizer tokenizer = new StringTokenizer(text, " \t\n\r\f+\"*%&/()=?'!,.;:-_#@|^~`{}[]");
while (tokenizer.hasMoreTokens()) {
String word = tokenizer.nextToken();
word = setting.convertWord(word);
if (word != null) {
set.add(word);
}
}
}
private static void createTrigger(Connection conn, String schema, String table) throws SQLException {
createOrDropTrigger(conn, schema, table, true);
}
private static void createOrDropTrigger(Connection conn, String schema, String table, boolean create) throws SQLException {
Statement stat = conn.createStatement();
String trigger = StringUtils.quoteIdentifier(schema) + "."
+ StringUtils.quoteIdentifier(TRIGGER_PREFIX + table);
stat.execute("DROP TRIGGER IF EXISTS " + trigger);
if (create) {
StringBuffer buff = new StringBuffer("CREATE TRIGGER IF NOT EXISTS ");
buff.append(trigger);
buff.append(" AFTER INSERT, UPDATE, DELETE ON ");
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
buff.append(" FOR EACH ROW CALL \"");
buff.append(FullText.FullTextTrigger.class.getName());
buff.append("\"");
stat.execute(buff.toString());
}
}
private static void indexExistingRows(Connection conn, String schema, String table) throws SQLException {
FullText.FullTextTrigger existing = new FullText.FullTextTrigger();
existing.init(conn, schema, null, table, false, Trigger.INSERT);
StringBuffer buff = new StringBuffer("SELECT * FROM ");
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
ResultSet rs = conn.createStatement().executeQuery(buff.toString());
int columnCount = rs.getMetaData().getColumnCount();
while (rs.next()) { while (rs.next()) {
String word = rs.getString("NAME"); Object[] row = new Object[columnCount];
int id = rs.getInt("ID"); for (int i = 0; i < columnCount; i++) {
row[i] = rs.getObject(i + 1);
}
existing.fire(conn, null, row);
}
}
private static String quoteString(String data) {
if (data.indexOf('\'') < 0) {
return "'" + data + "'";
}
StringBuffer buff = new StringBuffer(data.length() + 2);
buff.append('\'');
for (int i = 0; i < data.length(); i++) {
char ch = data.charAt(i);
if (ch == '\'') {
buff.append(ch);
}
buff.append(ch);
}
buff.append('\'');
return buff.toString();
}
private static void setIgnoreList(FullTextSettings setting, String commaSeparatedList) {
String[] list = StringUtils.arraySplit(commaSeparatedList, ',', true);
HashSet set = setting.getIgnoreList();
for (int i = 0; i < list.length; i++) {
String word = list[i];
word = setting.convertWord(word); word = setting.convertWord(word);
if (word != null) { if (word != null) {
map.put(word, ObjectUtils.getInteger(id)); set.add(list[i]);
} }
} }
} }
/**
* Trigger updates the index when a inserting, updating, or deleting a row.
*/
public static class FullTextTrigger implements Trigger, CloseListener {
private FullTextSettings setting;
private IndexInfo index;
private int[] columnTypes;
private PreparedStatement prepInsertWord, prepInsertRow, prepInsertMap;
private PreparedStatement prepDeleteRow, prepDeleteMap;
private PreparedStatement prepSelectRow;
/** /**
* INTERNAL * INTERNAL
*/ */
public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException { public void init(Connection conn, String schemaName, String triggerName,
String tableName, boolean before, int type) throws SQLException {
setting = FullTextSettings.getInstance(conn); setting = FullTextSettings.getInstance(conn);
ArrayList keyList = new ArrayList(); ArrayList keyList = new ArrayList();
DatabaseMetaData meta = conn.getMetaData(); DatabaseMetaData meta = conn.getMetaData();
...@@ -339,15 +698,15 @@ public class FullText implements Trigger, CloseListener { ...@@ -339,15 +698,15 @@ public class FullText implements Trigger, CloseListener {
while (rs.next()) { while (rs.next()) {
columnList.add(rs.getString("COLUMN_NAME")); columnList.add(rs.getString("COLUMN_NAME"));
} }
dataTypes = new int[columnList.size()]; columnTypes = new int[columnList.size()];
index = new IndexInfo(); index = new IndexInfo();
index.schemaName = schemaName; index.schema = schemaName;
index.tableName = tableName; index.table = tableName;
index.columnNames = new String[columnList.size()]; index.columns = new String[columnList.size()];
columnList.toArray(index.columnNames); columnList.toArray(index.columns);
rs = meta.getColumns(null, schemaName, tableName, null); rs = meta.getColumns(null, schemaName, tableName, null);
for (int i = 0; rs.next(); i++) { for (int i = 0; rs.next(); i++) {
dataTypes[i] = rs.getInt("DATA_TYPE"); columnTypes[i] = rs.getInt("DATA_TYPE");
} }
if (keyList.size() == 0) { if (keyList.size() == 0) {
rs = meta.getPrimaryKeys(null, schemaName, tableName); rs = meta.getPrimaryKeys(null, schemaName, tableName);
...@@ -400,220 +759,34 @@ public class FullText implements Trigger, CloseListener { ...@@ -400,220 +759,34 @@ public class FullText implements Trigger, CloseListener {
PreparedStatement prepSelectRowById = conn.prepareStatement( PreparedStatement prepSelectRowById = conn.prepareStatement(
"SELECT KEY, INDEXID FROM " + SCHEMA + ".ROWS WHERE ID=?"); "SELECT KEY, INDEXID FROM " + SCHEMA + ".ROWS WHERE ID=?");
setting.setPrepSelectMapByWordId(prepSelectMapByWordId); setting.setPrepSelectMapByWordId(prepSelectMapByWordId);
setting.setPrepSelectRowById(prepSelectRowById); setting.setPrepSelectRowById(prepSelectRowById);
}
private void setColumns(int[] index, ArrayList keys, ArrayList columns) throws SQLException {
for (int i = 0; i < keys.size(); i++) {
String key = (String) keys.get(i);
int found = -1;
for (int j = 0; found == -1 && j < columns.size(); j++) {
String column = (String) columns.get(j);
if (column.equals(key)) {
found = j;
}
}
if (found < 0) {
throw new SQLException("FULLTEXT", "Column not found: " + key);
}
index[i] = found;
}
}
/**
* INTERNAL
*/
public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException {
if (oldRow != null) {
delete(setting, oldRow);
}
if (newRow != null) {
insert(setting, newRow);
}
}
private String getKey(Object[] row) throws SQLException {
StringBuffer buff = new StringBuffer();
for (int i = 0; i < index.keys.length; i++) {
if (i > 0) {
buff.append(" AND ");
}
int columnIndex = index.keys[i];
buff.append(StringUtils.quoteIdentifier(index.columnNames[columnIndex]));
Object o = row[columnIndex];
if (o == null) {
buff.append(" IS NULL");
} else {
buff.append("=");
buff.append(quoteSQL(o, dataTypes[columnIndex]));
}
}
String key = buff.toString();
return key;
}
private String quoteString(String data) {
if (data.indexOf('\'') < 0) {
return "'" + data + "'";
}
StringBuffer buff = new StringBuffer(data.length() + 2);
buff.append('\'');
for (int i = 0; i < data.length(); i++) {
char ch = data.charAt(i);
if (ch == '\'') {
buff.append(ch);
}
buff.append(ch);
}
buff.append('\'');
return buff.toString();
}
private String quoteBinary(byte[] data) {
return "'" + ByteUtils.convertBytesToString(data) + "'";
}
/**
* INTERNAL.
* Convert the object to a string.
*
* @param data the object
* @param type the SQL type
* @return the string
*/
protected static String asString(Object data, int type) throws SQLException {
if (data == null) {
return "NULL";
}
switch (type) {
case Types.BIT:
case DataType.TYPE_BOOLEAN:
case Types.INTEGER:
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.NUMERIC:
case Types.REAL:
case Types.SMALLINT:
case Types.TINYINT:
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
case Types.LONGVARCHAR:
case Types.CHAR:
case Types.VARCHAR:
return data.toString();
case Types.CLOB:
try {
if (data instanceof Clob) {
data = ((Clob) data).getCharacterStream();
}
return IOUtils.readStringAndClose((Reader) data, -1);
} catch (IOException e) {
throw Message.convert(e);
}
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BINARY:
case Types.JAVA_OBJECT:
case Types.OTHER:
case Types.BLOB:
case Types.STRUCT:
case Types.REF:
case Types.NULL:
case Types.ARRAY:
case DataType.TYPE_DATALINK:
case Types.DISTINCT:
throw new SQLException("FULLTEXT", "Unsupported column data type: " + type);
default:
return "";
}
}
private String quoteSQL(Object data, int type) throws SQLException {
if (data == null) {
return "NULL";
}
switch (type) {
case Types.BIT:
case DataType.TYPE_BOOLEAN:
case Types.INTEGER:
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.NUMERIC:
case Types.REAL:
case Types.SMALLINT:
case Types.TINYINT:
return data.toString();
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
case Types.LONGVARCHAR:
case Types.CHAR:
case Types.VARCHAR:
return quoteString(data.toString());
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BINARY:
return quoteBinary((byte[]) data);
case Types.CLOB:
case Types.JAVA_OBJECT:
case Types.OTHER:
case Types.BLOB:
case Types.STRUCT:
case Types.REF:
case Types.NULL:
case Types.ARRAY:
case DataType.TYPE_DATALINK:
case Types.DISTINCT:
throw new SQLException("FULLTEXT", "Unsupported key data type: " + type);
default:
return "";
}
} }
private static void addWords(FullTextSettings setting, HashSet set, String text) { /**
StringTokenizer tokenizer = new StringTokenizer(text, " \t\n\r\f+\"*%&/()=?'!,.;:-_#@|^~`{}[]"); * INTERNAL
while (tokenizer.hasMoreTokens()) { */
String word = tokenizer.nextToken(); public void fire(Connection conn, Object[] oldRow, Object[] newRow)
word = setting.convertWord(word); throws SQLException {
if (word != null) { if (oldRow != null) {
set.add(word); delete(setting, oldRow);
} }
if (newRow != null) {
insert(setting, newRow);
} }
} }
private int[] getWordIds(FullTextSettings setting, Object[] row) throws SQLException { /**
HashSet words = new HashSet(); * INTERNAL
for (int i = 0; i < index.indexColumns.length; i++) { */
int idx = index.indexColumns[i]; public void close() throws SQLException {
String data = asString(row[idx], dataTypes[idx]); setting.removeIndexInfo(index);
addWords(setting, words, data);
}
HashMap allWords = setting.getWordList();
int[] wordIds = new int[words.size()];
Iterator it = words.iterator();
for (int i = 0; it.hasNext(); i++) {
String word = (String) it.next();
Integer wId = (Integer) allWords.get(word);
int wordId;
if (wId == null) {
prepInsertWord.setString(1, word);
prepInsertWord.execute();
ResultSet rs = JdbcUtils.getGeneratedKeys(prepInsertWord);
rs.next();
wordId = rs.getInt(1);
allWords.put(word, ObjectUtils.getInteger(wordId));
} else {
wordId = wId.intValue();
}
wordIds[i] = wordId;
} }
Arrays.sort(wordIds);
return wordIds; /**
* INTERNAL
*/
public void remove() throws SQLException {
setting.removeIndexInfo(index);
} }
private void insert(FullTextSettings setting, Object[] row) throws SQLException { private void insert(FullTextSettings setting, Object[] row) throws SQLException {
...@@ -656,201 +829,56 @@ public class FullText implements Trigger, CloseListener { ...@@ -656,201 +829,56 @@ public class FullText implements Trigger, CloseListener {
} }
} }
/** private int[] getWordIds(FullTextSettings setting, Object[] row) throws SQLException {
* Searches from the full text index for this database. The result contains
* the primary key data as an array. The returned result set has the
* following columns:
* <ul>
* <li>SCHEMA (varchar): The schema name. Example: PUBLIC </li>
* <li>TABLE (varchar): The table name. Example: TEST </li>
* <li>COLUMNS (array of varchar): Comma separated list of quoted column
* names. The column names are quoted if necessary. Example: (ID) </li>
* <li>KEYS (array of values): Comma separated list of values. Example: (1)
* </li>
* </ul>
*
* @param conn the connection
* @param text the search query
* @param limit the maximum number of rows or 0 for no limit
* @param offset the offset or 0 for no offset
* @return the result set
*/
public static ResultSet searchData(Connection conn, String text, int limit, int offset) throws SQLException {
return search(conn, text, limit, offset, true);
}
/**
* Searches from the full text index for this database.
* The returned result set has the following column:
* <ul><li>QUERY (varchar): The query to use to get the data.
* The query does not include 'SELECT * FROM '. Example:
* PUBLIC.TEST WHERE ID = 1
* </li></ul>
*
* @param conn the connection
* @param text the search query
* @param limit the maximum number of rows or 0 for no limit
* @param offset the offset or 0 for no offset
* @return the result set
*/
public static ResultSet search(Connection conn, String text, int limit, int offset) throws SQLException {
return search(conn, text, limit, offset, false);
}
/**
* Create an empty search result and initialize the columns.
*
* @param data true if the result set should contain the primary key data as
* an array.
* @return the empty result set
*/
static SimpleResultSet createResultSet(boolean data) throws SQLException {
SimpleResultSet result = new SimpleResultSet();
if (data) {
result.addColumn(FullText.FIELD_SCHEMA, Types.VARCHAR, 0, 0);
result.addColumn(FullText.FIELD_TABLE, Types.VARCHAR, 0, 0);
result.addColumn(FullText.FIELD_COLUMNS, Types.ARRAY, 0, 0);
result.addColumn(FullText.FIELD_KEYS, Types.ARRAY, 0, 0);
} else {
result.addColumn(FullText.FIELD_QUERY, Types.VARCHAR, 0, 0);
}
return result;
}
private static ResultSet search(Connection conn, String text, int limit, int offset, boolean data) throws SQLException {
SimpleResultSet result = createResultSet(data);
if (conn.getMetaData().getURL().startsWith("jdbc:columnlist:")) {
// this is just to query the result set columns
return result;
}
FullTextSettings setting = FullTextSettings.getInstance(conn);
HashSet words = new HashSet(); HashSet words = new HashSet();
addWords(setting, words, text); for (int i = 0; i < index.indexColumns.length; i++) {
HashSet rIds = null, lastRowIds = null; int idx = index.indexColumns[i];
String data = asString(row[idx], columnTypes[idx]);
addWords(setting, words, data);
}
HashMap allWords = setting.getWordList(); HashMap allWords = setting.getWordList();
int[] wordIds = new int[words.size()];
PreparedStatement prepSelectMapByWordId = setting.getPrepSelectMapByWordId(); Iterator it = words.iterator();
for (Iterator it = words.iterator(); it.hasNext();) { for (int i = 0; it.hasNext(); i++) {
lastRowIds = rIds;
rIds = new HashSet();
String word = (String) it.next(); String word = (String) it.next();
Integer wId = (Integer) allWords.get(word); Integer wId = (Integer) allWords.get(word);
int wordId;
if (wId == null) { if (wId == null) {
continue; prepInsertWord.setString(1, word);
} prepInsertWord.execute();
prepSelectMapByWordId.setInt(1, wId.intValue()); ResultSet rs = JdbcUtils.getGeneratedKeys(prepInsertWord);
ResultSet rs = prepSelectMapByWordId.executeQuery(); rs.next();
while (rs.next()) { wordId = rs.getInt(1);
Integer rId = ObjectUtils.getInteger(rs.getInt(1)); allWords.put(word, ObjectUtils.getInteger(wordId));
if (lastRowIds == null || lastRowIds.contains(rId)) {
rIds.add(rId);
}
}
}
if (rIds == null || rIds.size() == 0) {
return result;
}
PreparedStatement prepSelectRowById = setting.getPrepSelectRowById();
int rowCount = 0;
for (Iterator it = rIds.iterator(); it.hasNext();) {
int rowId = ((Integer) it.next()).intValue();
prepSelectRowById.setInt(1, rowId);
ResultSet rs = prepSelectRowById.executeQuery();
if (!rs.next()) {
continue;
}
if (offset > 0) {
offset--;
} else {
String key = rs.getString(1);
int indexId = rs.getInt(2);
IndexInfo index = setting.getIndexInfo(indexId);
if (data) {
Object[][] columnData = parseKey(conn, key);
Object[] row = new Object[] {
index.schemaName,
index.tableName,
columnData[0],
columnData[1]
};
result.addRow(row);
} else { } else {
StringBuffer buff = new StringBuffer(); wordId = wId.intValue();
buff.append(StringUtils.quoteIdentifier(index.schemaName));
buff.append('.');
buff.append(StringUtils.quoteIdentifier(index.tableName));
buff.append(" WHERE ");
buff.append(key);
String query = buff.toString();
result.addRow(new String[] { query });
}
rowCount++;
if (limit > 0 && rowCount >= limit) {
break;
}
} }
wordIds[i] = wordId;
} }
return result; Arrays.sort(wordIds);
return wordIds;
} }
/** private String getKey(Object[] row) throws SQLException {
* Parse a primary key condition into the primary key columns. StringBuffer buff = new StringBuffer();
* for (int i = 0; i < index.keys.length; i++) {
* @param conn the database connection if (i > 0) {
* @param key the primary key condition as a string buff.append(" AND ");
* @return an array containing the column name list and the data list
*/
static Object[][] parseKey(Connection conn, String key) throws SQLException {
ArrayList columns = new ArrayList();
ArrayList data = new ArrayList();
JdbcConnection c = (JdbcConnection) conn;
Session session = (Session) c.getSession();
Parser p = new Parser(session);
Expression expr = p.parseExpression(key);
addColumnData(columns, data, expr);
Object[] col = new Object[columns.size()];
columns.toArray(col);
Object[] dat = new Object[columns.size()];
data.toArray(dat);
Object[][] columnData = new Object[][] {
col, dat
};
return columnData;
} }
int columnIndex = index.keys[i];
private static void addColumnData(ArrayList columns, ArrayList data, Expression expr) { buff.append(StringUtils.quoteIdentifier(index.columns[columnIndex]));
if (expr instanceof ConditionAndOr) { Object o = row[columnIndex];
ConditionAndOr and = (ConditionAndOr) expr; if (o == null) {
Expression left = and.getExpression(true); buff.append(" IS NULL");
Expression right = and.getExpression(false);
addColumnData(columns, data, left);
addColumnData(columns, data, right);
} else {
Comparison comp = (Comparison) expr;
ExpressionColumn ec = (ExpressionColumn) comp.getExpression(true);
ValueExpression ev = (ValueExpression) comp.getExpression(false);
String columnName = ec.getColumnName();
columns.add(columnName);
if (ev == null) {
data.add(null);
} else { } else {
data.add(ev.getValue(null).getString()); buff.append("=");
} buff.append(quoteSQL(o, columnTypes[columnIndex]));
} }
} }
String key = buff.toString();
/** return key;
* INTERNAL
*/
public void close() throws SQLException {
setting.removeIndexInfo(index);
} }
/**
* INTERNAL
*/
public void remove() throws SQLException {
setting.removeIndexInfo(index);
} }
} }
...@@ -14,7 +14,6 @@ import java.sql.PreparedStatement; ...@@ -14,7 +14,6 @@ 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;
import java.sql.Types;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
...@@ -39,7 +38,6 @@ import org.h2.expression.ExpressionColumn; ...@@ -39,7 +38,6 @@ import org.h2.expression.ExpressionColumn;
import org.h2.jdbc.JdbcConnection; import org.h2.jdbc.JdbcConnection;
import org.h2.store.fs.FileSystem; import org.h2.store.fs.FileSystem;
import org.h2.tools.SimpleResultSet; import org.h2.tools.SimpleResultSet;
import org.h2.util.ByteUtils;
import org.h2.util.StringUtils; import org.h2.util.StringUtils;
//## Java 1.4 end ## //## Java 1.4 end ##
...@@ -47,28 +45,57 @@ import org.h2.util.StringUtils; ...@@ -47,28 +45,57 @@ import org.h2.util.StringUtils;
* This class implements the full text search based on Apache Lucene. * This class implements the full text search based on Apache Lucene.
* Most methods can be called using SQL statements as well. * Most methods can be called using SQL statements as well.
*/ */
public class FullTextLucene extends FullText public class FullTextLucene extends FullText {
//## Java 1.4 begin ##
implements Trigger, CloseListener
//## Java 1.4 end ##
{
//## Java 1.4 begin ## //## Java 1.4 begin ##
private static final String TRIGGER_PREFIX = "FTL_";
private static final String SCHEMA = "FTL";
private static final boolean STORE_DOCUMENT_TEXT_IN_INDEX = Boolean.getBoolean("h2.storeDocumentTextInIndex"); private static final boolean STORE_DOCUMENT_TEXT_IN_INDEX = Boolean.getBoolean("h2.storeDocumentTextInIndex");
private static HashMap indexers = new HashMap(); private static final HashMap INDEX_MODIFIERS = new HashMap();
private static final String FIELD_DATA = "DATA"; private static final String FIELD_DATA = "DATA";
private static final String FIELD_QUERY = "QUERY";
private static final String FIELD_COLUMN_PREFIX = "_"; private static final String FIELD_COLUMN_PREFIX = "_";
private static final String TRIGGER_PREFIX = "FTL_"; private static final String FIELD_QUERY = "QUERY";
private static final String SCHEMA = "FTL"; //## Java 1.4 end ##
private String schemaName;
private String tableName; /**
private int[] keys; * Initializes full text search functionality for this database. This adds
private int[] indexColumns; * the following Java functions to the database:
private String[] columnNames; * <ul>
private int[] dataTypes; * <li>FTL_CREATE_INDEX(schemaNameString, tableNameString,
private String indexPath; * columnListString)</li>
private IndexModifier indexer; * <li>FTL_SEARCH(queryString, limitInt, offsetInt): result set</li>
* <li>FTL_REINDEX()</li>
* <li>FTL_DROP_ALL()</li>
* </ul>
* It also adds a schema FTL to the database where bookkeeping information
* is stored. This function may be called from a Java application, or by
* using the SQL statements:
*
* <pre>
* CREATE ALIAS IF NOT EXISTS FTL_INIT FOR
* &quot;org.h2.fulltext.FullTextLucene.init&quot;;
* CALL FTL_INIT();
* </pre>
*
* @param conn the connection
*/
//## Java 1.4 begin ##
public static void init(Connection conn) throws SQLException {
Statement stat = conn.createStatement();
stat.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA);
stat.execute("CREATE TABLE IF NOT EXISTS " + SCHEMA
+ ".INDEXES(SCHEMA VARCHAR, TABLE VARCHAR, COLUMNS VARCHAR, PRIMARY KEY(SCHEMA, TABLE))");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_CREATE_INDEX FOR \"" + FullTextLucene.class.getName() + ".createIndex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH FOR \"" + FullTextLucene.class.getName() + ".search\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH_DATA FOR \"" + FullTextLucene.class.getName() + ".searchData\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_REINDEX FOR \"" + FullTextLucene.class.getName() + ".reindex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_DROP_ALL FOR \"" + FullTextLucene.class.getName() + ".dropAll\"");
try {
getIndexModifier(conn);
} catch (Exception e) {
throw convertException(e);
}
}
//## Java 1.4 end ## //## Java 1.4 end ##
/** /**
...@@ -76,14 +103,15 @@ implements Trigger, CloseListener ...@@ -76,14 +103,15 @@ implements Trigger, CloseListener
* only have one index at any time. * only have one index at any time.
* *
* @param conn the connection * @param conn the connection
* @param schema the schema name of the table * @param schema the schema name of the table (case sensitive)
* @param table the table name * @param table the table name (case sensitive)
* @param columnList the column list (null for all columns) * @param columnList the column list (null for all columns)
*/ */
//## Java 1.4 begin ## //## Java 1.4 begin ##
public static void createIndex(Connection conn, String schema, String table, String columnList) throws SQLException { public static void createIndex(Connection conn, String schema, String table, String columnList) throws SQLException {
init(conn); init(conn);
PreparedStatement prep = conn.prepareStatement("INSERT INTO "+SCHEMA+".INDEXES(SCHEMA, TABLE, COLUMNS) VALUES(?, ?, ?)"); PreparedStatement prep = conn.prepareStatement("INSERT INTO " + SCHEMA
+ ".INDEXES(SCHEMA, TABLE, COLUMNS) VALUES(?, ?, ?)");
prep.setString(1, schema); prep.setString(1, schema);
prep.setString(2, table); prep.setString(2, table);
prep.setString(3, columnList); prep.setString(3, columnList);
...@@ -94,14 +122,14 @@ implements Trigger, CloseListener ...@@ -94,14 +122,14 @@ implements Trigger, CloseListener
//## Java 1.4 end ## //## Java 1.4 end ##
/** /**
* Re-creates the full text index for this database * Re-creates the full text index for this database.
* *
* @param conn the connection * @param conn the connection
*/ */
//## Java 1.4 begin ## //## Java 1.4 begin ##
public static void reindex(Connection conn) throws SQLException { public static void reindex(Connection conn) throws SQLException {
init(conn); init(conn);
removeAllTriggers(conn); removeAllTriggers(conn, TRIGGER_PREFIX);
removeIndexFiles(conn); removeIndexFiles(conn);
Statement stat = conn.createStatement(); Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("SELECT * FROM "+SCHEMA+".INDEXES"); ResultSet rs = stat.executeQuery("SELECT * FROM "+SCHEMA+".INDEXES");
...@@ -123,118 +151,28 @@ implements Trigger, CloseListener ...@@ -123,118 +151,28 @@ implements Trigger, CloseListener
public static void dropAll(Connection conn) throws SQLException { public static void dropAll(Connection conn) throws SQLException {
Statement stat = conn.createStatement(); Statement stat = conn.createStatement();
stat.execute("DROP SCHEMA IF EXISTS " + SCHEMA); stat.execute("DROP SCHEMA IF EXISTS " + SCHEMA);
removeAllTriggers(conn); removeAllTriggers(conn, TRIGGER_PREFIX);
removeIndexFiles(conn); removeIndexFiles(conn);
} }
//## Java 1.4 end ## //## Java 1.4 end ##
/** /**
* Initializes full text search functionality for this database. This adds * Searches from the full text index for this database.
* the following Java functions to the database: * The returned result set has the following column:
* <ul> * <ul><li>QUERY (varchar): The query to use to get the data.
* <li>FTL_CREATE_INDEX(schemaNameString, tableNameString, * The query does not include 'SELECT * FROM '. Example:
* columnListString) </li> * PUBLIC.TEST WHERE ID = 1
* <li>FTL_SEARCH(queryString, limitInt, offsetInt): result set </li> * </li></ul>
* <li>FTL_REINDEX() </li>
* <li>FTL_DROP_ALL() </li>
* </ul>
* It also adds a schema FTL to the database where bookkeeping information
* is stored. This function may be called from a Java application, or by
* using the SQL statements:
*
* <pre>
* CREATE ALIAS IF NOT EXISTS FTL_INIT FOR
* &quot;org.h2.fulltext.FullTextLucene.init&quot;;
* CALL FTL_INIT();
* </pre>
* *
* @param conn the connection * @param conn the connection
* @param text the search query
* @param limit the maximum number of rows or 0 for no limit
* @param offset the offset or 0 for no offset
* @return the result set
*/ */
//## Java 1.4 begin ## //## Java 1.4 begin ##
public static void init(Connection conn) throws SQLException { public static ResultSet search(Connection conn, String text, int limit, int offset) throws SQLException {
Statement stat = conn.createStatement(); return search(conn, text, limit, offset, false);
stat.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA);
stat.execute("CREATE TABLE IF NOT EXISTS "+SCHEMA+".INDEXES(SCHEMA VARCHAR, TABLE VARCHAR, COLUMNS VARCHAR, PRIMARY KEY(SCHEMA, TABLE))");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_CREATE_INDEX FOR \"" + FullTextLucene.class.getName() + ".createIndex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH FOR \"" + FullTextLucene.class.getName() + ".search\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_SEARCH_DATA FOR \"" + FullTextLucene.class.getName() + ".searchData\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_REINDEX FOR \"" + FullTextLucene.class.getName() + ".reindex\"");
stat.execute("CREATE ALIAS IF NOT EXISTS FTL_DROP_ALL FOR \"" + FullTextLucene.class.getName() + ".dropAll\"");
try {
getIndexModifier(conn);
} catch (Exception e) {
throw convertException(e);
}
}
//## Java 1.4 end ##
/**
* INTERNAL
*/
//## Java 1.4 begin ##
public void init(Connection conn, String schemaName, String triggerName, String tableName, boolean before, int type) throws SQLException {
this.schemaName = schemaName;
this.tableName = tableName;
this.indexPath = getIndexPath(conn);
this.indexer = getIndexModifier(conn);
ArrayList keyList = new ArrayList();
DatabaseMetaData meta = conn.getMetaData();
ResultSet rs = meta.getColumns(null, schemaName, tableName, null);
ArrayList columnList = new ArrayList();
while (rs.next()) {
columnList.add(rs.getString("COLUMN_NAME"));
}
dataTypes = new int[columnList.size()];
columnNames = new String[columnList.size()];
columnList.toArray(columnNames);
rs = meta.getColumns(null, schemaName, tableName, null);
for (int i = 0; rs.next(); i++) {
dataTypes[i] = rs.getInt("DATA_TYPE");
}
if (keyList.size() == 0) {
rs = meta.getPrimaryKeys(null, schemaName, tableName);
while (rs.next()) {
keyList.add(rs.getString("COLUMN_NAME"));
}
}
if (keyList.size() == 0) {
throw new SQLException("No primary key for table " + tableName);
}
ArrayList indexList = new ArrayList();
PreparedStatement prep = conn.prepareStatement("SELECT COLUMNS FROM "+SCHEMA+".INDEXES WHERE SCHEMA=? AND TABLE=?");
prep.setString(1, schemaName);
prep.setString(2, tableName);
rs = prep.executeQuery();
if (rs.next()) {
String columns = rs.getString(1);
if (columns != null) {
String[] list = StringUtils.arraySplit(columns, ',', true);
for (int i = 0; i < list.length; i++) {
indexList.add(list[i]);
}
}
}
if (indexList.size() == 0) {
indexList.addAll(columnList);
}
keys = new int[keyList.size()];
setColumns(keys, keyList, columnList);
indexColumns = new int[indexList.size()];
setColumns(indexColumns, indexList, columnList);
}
//## Java 1.4 end ##
/**
* INTERNAL
*/
//## Java 1.4 begin ##
public void fire(Connection conn, Object[] oldRow, Object[] newRow) throws SQLException {
if (oldRow != null) {
delete(oldRow);
}
if (newRow != null) {
insert(newRow);
}
} }
//## Java 1.4 end ## //## Java 1.4 end ##
...@@ -261,25 +199,87 @@ implements Trigger, CloseListener ...@@ -261,25 +199,87 @@ implements Trigger, CloseListener
public static ResultSet searchData(Connection conn, String text, int limit, int offset) throws SQLException { public static ResultSet searchData(Connection conn, String text, int limit, int offset) throws SQLException {
return search(conn, text, limit, offset, true); return search(conn, text, limit, offset, true);
} }
//## Java 1.4 end ##
/** private static SQLException convertException(Exception e) {
* Searches from the full text index for this database. SQLException e2 = new SQLException("FULLTEXT", "Error while indexing document");
* The returned result set has the following column: e2.initCause(e);
* <ul><li>QUERY (varchar): The query to use to get the data. return e2;
* The query does not include 'SELECT * FROM '. Example: }
* PUBLIC.TEST WHERE ID = 1
* </li></ul> private static void createTrigger(Connection conn, String schema, String table) throws SQLException {
* Statement stat = conn.createStatement();
* @param conn the connection String trigger = StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(TRIGGER_PREFIX + table);
* @param text the search query stat.execute("DROP TRIGGER IF EXISTS " + trigger);
* @param limit the maximum number of rows or 0 for no limit StringBuffer buff = new StringBuffer("CREATE TRIGGER IF NOT EXISTS ");
* @param offset the offset or 0 for no offset buff.append(trigger);
* @return the result set buff.append(" AFTER INSERT, UPDATE, DELETE ON ");
*/ buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
//## Java 1.4 begin ## buff.append(" FOR EACH ROW CALL \"");
public static ResultSet search(Connection conn, String text, int limit, int offset) throws SQLException { buff.append(FullTextLucene.FullTextTrigger.class.getName());
return search(conn, text, limit, offset, false); buff.append("\"");
stat.execute(buff.toString());
}
private static IndexModifier getIndexModifier(Connection conn) throws SQLException {
String path = getIndexPath(conn);
IndexModifier indexer;
synchronized (INDEX_MODIFIERS) {
indexer = (IndexModifier) INDEX_MODIFIERS.get(path);
if (indexer == null) {
try {
boolean recreate = !IndexReader.indexExists(path);
Analyzer analyzer = new StandardAnalyzer();
indexer = new IndexModifier(path, analyzer, recreate);
} catch (IOException e) {
throw convertException(e);
}
INDEX_MODIFIERS.put(path, indexer);
}
}
return indexer;
}
private static String getIndexPath(Connection conn) throws SQLException {
Statement stat = conn.createStatement();
ResultSet rs = stat.executeQuery("CALL DATABASE_PATH()");
rs.next();
String path = rs.getString(1);
if (path == null) {
throw new SQLException("FULLTEXT", "Fulltext search for in-memory databases is not supported.");
}
rs.close();
return path;
}
private static void indexExistingRows(Connection conn, String schema, String table) throws SQLException {
FullTextLucene.FullTextTrigger existing = new FullTextLucene.FullTextTrigger();
existing.init(conn, schema, null, table, false, Trigger.INSERT);
StringBuffer buff = new StringBuffer("SELECT * FROM ");
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
ResultSet rs = conn.createStatement().executeQuery(buff.toString());
int columnCount = rs.getMetaData().getColumnCount();
while (rs.next()) {
Object[] row = new Object[columnCount];
for (int i = 0; i < columnCount; i++) {
row[i] = rs.getObject(i + 1);
}
existing.fire(conn, null, row);
}
}
private static void removeIndexFiles(Connection conn) throws SQLException {
String path = getIndexPath(conn);
IndexModifier index = (IndexModifier) INDEX_MODIFIERS.get(path);
if (index != null) {
INDEX_MODIFIERS.remove(path);
try {
index.flush();
index.close();
} catch (IOException e) {
throw convertException(e);
}
}
FileSystem.getInstance(path).deleteRecursive(path);
} }
private static ResultSet search(Connection conn, String text, int limit, int offset, boolean data) throws SQLException { private static ResultSet search(Connection conn, String text, int limit, int offset, boolean data) throws SQLException {
...@@ -334,126 +334,124 @@ implements Trigger, CloseListener ...@@ -334,126 +334,124 @@ implements Trigger, CloseListener
} }
return result; return result;
} }
//## Java 1.4 end ##
private static void removeAllTriggers(Connection conn) throws SQLException { /**
Statement stat = conn.createStatement(); * Trigger updates the index when a inserting, updating, or deleting a row.
ResultSet rs = stat.executeQuery("SELECT * FROM INFORMATION_SCHEMA.TRIGGERS"); */
Statement stat2 = conn.createStatement(); public static class FullTextTrigger
//## Java 1.4 begin ##
implements Trigger, CloseListener
//## Java 1.4 end ##
{
//## Java 1.4 begin ##
private String schema;
private String table;
private int[] keys;
private int[] indexColumns;
private String[] columns;
private int[] columnTypes;
private String indexPath;
private IndexModifier indexModifier;
//## Java 1.4 end ##
/**
* INTERNAL
*/
//## Java 1.4 begin ##
public void init(Connection conn, String schemaName, String triggerName,
String tableName, boolean before, int type) throws SQLException {
this.schema = schemaName;
this.table = tableName;
this.indexPath = getIndexPath(conn);
this.indexModifier = getIndexModifier(conn);
ArrayList keyList = new ArrayList();
DatabaseMetaData meta = conn.getMetaData();
ResultSet rs = meta.getColumns(null, schemaName, tableName, null);
ArrayList columnList = new ArrayList();
while (rs.next()) { while (rs.next()) {
String schema = rs.getString("TRIGGER_SCHEMA"); columnList.add(rs.getString("COLUMN_NAME"));
String name = rs.getString("TRIGGER_NAME");
if (name.startsWith(TRIGGER_PREFIX)) {
name = StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(name);
stat2.execute("DROP TRIGGER " + name);
}
} }
columnTypes = new int[columnList.size()];
columns = new String[columnList.size()];
columnList.toArray(columns);
rs = meta.getColumns(null, schemaName, tableName, null);
for (int i = 0; rs.next(); i++) {
columnTypes[i] = rs.getInt("DATA_TYPE");
} }
if (keyList.size() == 0) {
private static void removeIndexFiles(Connection conn) throws SQLException { rs = meta.getPrimaryKeys(null, schemaName, tableName);
String path = getIndexPath(conn); while (rs.next()) {
IndexModifier index = (IndexModifier) indexers.get(path); keyList.add(rs.getString("COLUMN_NAME"));
if (index != null) {
indexers.remove(path);
try {
index.flush();
index.close();
} catch (IOException e) {
throw convertException(e);
} }
} }
FileSystem.getInstance(path).deleteRecursive(path); if (keyList.size() == 0) {
throw new SQLException("No primary key for table " + tableName);
} }
ArrayList indexList = new ArrayList();
private String getQuery(Object[] row) throws SQLException { PreparedStatement prep = conn.prepareStatement("SELECT COLUMNS FROM "+SCHEMA+".INDEXES WHERE SCHEMA=? AND TABLE=?");
StringBuffer buff = new StringBuffer(); prep.setString(1, schemaName);
if (schemaName != null) { prep.setString(2, tableName);
buff.append(StringUtils.quoteIdentifier(schemaName)); rs = prep.executeQuery();
buff.append("."); if (rs.next()) {
String columns = rs.getString(1);
if (columns != null) {
String[] list = StringUtils.arraySplit(columns, ',', true);
for (int i = 0; i < list.length; i++) {
indexList.add(list[i]);
} }
buff.append(StringUtils.quoteIdentifier(tableName));
buff.append(" WHERE ");
for (int i = 0; i < keys.length; i++) {
if (i > 0) {
buff.append(" AND ");
} }
int columnIndex = keys[i];
buff.append(StringUtils.quoteIdentifier(columnNames[columnIndex]));
Object o = row[columnIndex];
if (o == null) {
buff.append(" IS NULL");
} else {
buff.append("=");
buff.append(quoteSQL(o, dataTypes[columnIndex]));
} }
if (indexList.size() == 0) {
indexList.addAll(columnList);
} }
String key = buff.toString(); keys = new int[keyList.size()];
return key; setColumns(keys, keyList, columnList);
indexColumns = new int[indexList.size()];
setColumns(indexColumns, indexList, columnList);
} }
//## Java 1.4 end ##
private String quoteString(String data) { /**
if (data.indexOf('\'') < 0) { * INTERNAL
return "'" + data + "'"; */
} //## Java 1.4 begin ##
StringBuffer buff = new StringBuffer(data.length() + 2); public void fire(Connection conn, Object[] oldRow, Object[] newRow)
buff.append('\''); throws SQLException {
for (int i = 0; i < data.length(); i++) { if (oldRow != null) {
char ch = data.charAt(i); delete(oldRow);
if (ch == '\'') {
buff.append(ch);
} }
buff.append(ch); if (newRow != null) {
insert(newRow);
} }
buff.append('\'');
return buff.toString();
} }
//## Java 1.4 end ##
private String quoteBinary(byte[] data) { /**
return "'" + ByteUtils.convertBytesToString(data) + "'"; * INTERNAL
*/
//## Java 1.4 begin ##
public void close() throws SQLException {
if (indexModifier != null) {
try {
indexModifier.flush();
indexModifier.close();
INDEX_MODIFIERS.remove(indexPath);
indexModifier = null;
} catch (Exception e) {
throw convertException(e);
} }
private String quoteSQL(Object data, int type) throws SQLException {
if (data == null) {
return "NULL";
}
switch (type) {
case Types.BIT:
case Types.BOOLEAN:
case Types.INTEGER:
case Types.BIGINT:
case Types.DECIMAL:
case Types.DOUBLE:
case Types.FLOAT:
case Types.NUMERIC:
case Types.REAL:
case Types.SMALLINT:
case Types.TINYINT:
return data.toString();
case Types.DATE:
case Types.TIME:
case Types.TIMESTAMP:
case Types.LONGVARCHAR:
case Types.CHAR:
case Types.VARCHAR:
return quoteString(data.toString());
case Types.VARBINARY:
case Types.LONGVARBINARY:
case Types.BINARY:
return quoteBinary((byte[]) data);
case Types.CLOB:
case Types.JAVA_OBJECT:
case Types.OTHER:
case Types.BLOB:
case Types.STRUCT:
case Types.REF:
case Types.NULL:
case Types.ARRAY:
case Types.DATALINK:
case Types.DISTINCT:
throw new SQLException("FULLTEXT", "Unsupported key data type: " + type);
default:
return "";
} }
} }
//## Java 1.4 end ##
/**
* INTERNAL
*/
public void remove() {
// ignore
}
private void insert(Object[] row) throws SQLException { private void insert(Object[] row) throws SQLException {
String query = getQuery(row); String query = getQuery(row);
...@@ -464,8 +462,8 @@ implements Trigger, CloseListener ...@@ -464,8 +462,8 @@ implements Trigger, CloseListener
StringBuffer allData = new StringBuffer(); StringBuffer allData = new StringBuffer();
for (int i = 0; i < indexColumns.length; i++) { for (int i = 0; i < indexColumns.length; i++) {
int index = indexColumns[i]; int index = indexColumns[i];
String columnName = columnNames[index]; String columnName = columns[index];
String data = asString(row[index], dataTypes[index]); String data = asString(row[index], columnTypes[index]);
doc.add(new Field(FIELD_COLUMN_PREFIX + columnName, data, Field.Store.NO, Field.Index.TOKENIZED)); doc.add(new Field(FIELD_COLUMN_PREFIX + columnName, data, Field.Store.NO, Field.Index.TOKENIZED));
if (i > 0) { if (i > 0) {
allData.append(" "); allData.append(" ");
...@@ -473,9 +471,10 @@ implements Trigger, CloseListener ...@@ -473,9 +471,10 @@ implements Trigger, CloseListener
allData.append(data); allData.append(data);
} }
Field.Store storeText = STORE_DOCUMENT_TEXT_IN_INDEX ? Field.Store.YES : Field.Store.NO; Field.Store storeText = STORE_DOCUMENT_TEXT_IN_INDEX ? Field.Store.YES : Field.Store.NO;
doc.add(new Field(FIELD_DATA, allData.toString(), storeText, Field.Index.TOKENIZED)); doc.add(new Field(FIELD_DATA, allData.toString(), storeText,
Field.Index.TOKENIZED));
try { try {
indexer.addDocument(doc); indexModifier.addDocument(doc);
} catch (IOException e) { } catch (IOException e) {
throw convertException(e); throw convertException(e);
} }
...@@ -485,122 +484,38 @@ implements Trigger, CloseListener ...@@ -485,122 +484,38 @@ implements Trigger, CloseListener
String query = getQuery(row); String query = getQuery(row);
try { try {
Term term = new Term(FIELD_QUERY, query); Term term = new Term(FIELD_QUERY, query);
indexer.deleteDocuments(term); indexModifier.deleteDocuments(term);
} catch (IOException e) {
throw convertException(e);
}
}
private static SQLException convertException(Exception e) {
SQLException e2 = new SQLException("FULLTEXT", "Error while indexing document");
e2.initCause(e);
return e2;
}
private static void createTrigger(Connection conn, String schema, String table) throws SQLException {
Statement stat = conn.createStatement();
String trigger = StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(TRIGGER_PREFIX + table);
stat.execute("DROP TRIGGER IF EXISTS " + trigger);
StringBuffer buff = new StringBuffer("CREATE TRIGGER IF NOT EXISTS ");
buff.append(trigger);
buff.append(" AFTER INSERT, UPDATE, DELETE ON ");
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
buff.append(" FOR EACH ROW CALL \"");
buff.append(FullTextLucene.class.getName());
buff.append("\"");
stat.execute(buff.toString());
}
private static void indexExistingRows(Connection conn, String schema, String table) throws SQLException {
FullTextLucene existing = new FullTextLucene();
existing.init(conn, schema, null, table, false, INSERT);
StringBuffer buff = new StringBuffer("SELECT * FROM ");
buff.append(StringUtils.quoteIdentifier(schema) + "." + StringUtils.quoteIdentifier(table));
ResultSet rs = conn.createStatement().executeQuery(buff.toString());
int columnCount = rs.getMetaData().getColumnCount();
while (rs.next()) {
Object[] row = new Object[columnCount];
for (int i = 0; i < columnCount; i++) {
row[i] = rs.getObject(i + 1);
}
existing.fire(conn, null, row);
}
}
private static IndexModifier getIndexModifier(Connection conn) throws SQLException {
String path = getIndexPath(conn);
IndexModifier indexer;
synchronized (indexers) {
indexer = (IndexModifier) indexers.get(path);
if (indexer == null) {
try {
boolean recreate = !IndexReader.indexExists(path);
Analyzer analyzer = new StandardAnalyzer();
indexer = new IndexModifier(path, analyzer, recreate);
} catch (IOException e) { } catch (IOException e) {
throw convertException(e); throw convertException(e);
} }
indexers.put(path, indexer);
}
}
return indexer;
} }
private static String getIndexPath(Connection conn) throws SQLException { private String getQuery(Object[] row) throws SQLException {
Statement stat = conn.createStatement(); StringBuffer buff = new StringBuffer();
ResultSet rs = stat.executeQuery("CALL DATABASE_PATH()"); if (schema != null) {
rs.next(); buff.append(StringUtils.quoteIdentifier(schema));
String path = rs.getString(1); buff.append(".");
if (path == null) {
throw new SQLException("FULLTEXT", "Fulltext search for in-memory databases is not supported.");
}
rs.close();
return path;
}
private void setColumns(int[] index, ArrayList keys, ArrayList columns) throws SQLException {
for (int i = 0; i < keys.size(); i++) {
String key = (String) keys.get(i);
int found = -1;
for (int j = 0; found == -1 && j < columns.size(); j++) {
String column = (String) columns.get(j);
if (column.equals(key)) {
found = j;
}
}
if (found < 0) {
throw new SQLException("FULLTEXT", "Column not found: " + key);
}
index[i] = found;
} }
buff.append(StringUtils.quoteIdentifier(table));
buff.append(" WHERE ");
for (int i = 0; i < keys.length; i++) {
if (i > 0) {
buff.append(" AND ");
} }
//## Java 1.4 end ## int columnIndex = keys[i];
buff.append(StringUtils.quoteIdentifier(columns[columnIndex]));
/** Object o = row[columnIndex];
* INTERNAL if (o == null) {
*/ buff.append(" IS NULL");
//## Java 1.4 begin ## } else {
public void close() throws SQLException { buff.append("=");
try { buff.append(FullText.quoteSQL(o, columnTypes[columnIndex]));
if (indexer != null) {
indexer.flush();
indexer.close();
indexers.remove(indexPath);
indexer = null;
} }
} catch (Exception e) {
throw convertException(e);
} }
String key = buff.toString();
return key;
} }
//## Java 1.4 end ##
/**
* INTERNAL
*/
//## Java 1.4 begin ##
public void remove() throws SQLException {
// ignore
} }
//## Java 1.4 end ##
} }
...@@ -21,7 +21,7 @@ import org.h2.util.ObjectUtils; ...@@ -21,7 +21,7 @@ import org.h2.util.ObjectUtils;
*/ */
class FullTextSettings { class FullTextSettings {
private static HashMap settings = new HashMap(); private static final HashMap SETTINGS = new HashMap();
private HashSet ignoreList = new HashSet(); private HashSet ignoreList = new HashSet();
private HashMap words = new HashMap(); private HashMap words = new HashMap();
...@@ -84,10 +84,10 @@ class FullTextSettings { ...@@ -84,10 +84,10 @@ class FullTextSettings {
*/ */
static FullTextSettings getInstance(Connection conn) throws SQLException { static FullTextSettings getInstance(Connection conn) throws SQLException {
String path = getIndexPath(conn); String path = getIndexPath(conn);
FullTextSettings setting = (FullTextSettings) settings.get(path); FullTextSettings setting = (FullTextSettings) SETTINGS.get(path);
if (setting == null) { if (setting == null) {
setting = new FullTextSettings(); setting = new FullTextSettings();
settings.put(path, setting); SETTINGS.put(path, setting);
} }
return setting; return setting;
} }
......
...@@ -19,12 +19,12 @@ class IndexInfo { ...@@ -19,12 +19,12 @@ class IndexInfo {
/** /**
* The schema name. * The schema name.
*/ */
String schemaName; String schema;
/** /**
* The table name. * The table name.
*/ */
String tableName; String table;
/** /**
* The column indexes of the key columns. * The column indexes of the key columns.
...@@ -39,5 +39,5 @@ class IndexInfo { ...@@ -39,5 +39,5 @@ class IndexInfo {
/** /**
* The column names. * The column names.
*/ */
String[] columnNames; String[] columns;
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论