| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.openjpa.jdbc.sql; |
| |
| import java.sql.Connection; |
| import java.sql.DatabaseMetaData; |
| import java.sql.ResultSet; |
| import java.sql.SQLException; |
| import java.sql.Types; |
| import java.text.MessageFormat; |
| import java.util.Arrays; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.apache.openjpa.jdbc.identifier.DBIdentifier; |
| import org.apache.openjpa.jdbc.kernel.exps.FilterValue; |
| import org.apache.openjpa.jdbc.schema.Column; |
| import org.apache.openjpa.jdbc.schema.ForeignKey; |
| import org.apache.openjpa.jdbc.schema.Index; |
| import org.apache.openjpa.jdbc.schema.Sequence; |
| import org.apache.openjpa.jdbc.schema.Unique; |
| import org.apache.openjpa.lib.identifier.IdentifierUtil; |
| import org.apache.openjpa.lib.util.Localizer; |
| import org.apache.openjpa.util.ExceptionInfo; |
| import org.apache.openjpa.util.UnsupportedException; |
| |
| /** |
| * Dictionary for Firebird. Supports Firebird versions 1.5, 2.0 and 2.1. |
| */ |
| public class FirebirdDictionary |
| extends DBDictionary { |
| |
| public int firebirdVersion = 0; |
| public int indexedVarcharMaxSizeFB15 = 252; |
| public String rangeSyntax = null; |
| protected long maxRowNumberInRange = 16000000000L; |
| |
| protected String alterSequenceSQLFB15 = "SET GENERATOR {0} TO {1}"; |
| protected String alterSequenceSQLFB20 = |
| "ALTER SEQUENCE {0} RESTART WITH {1}"; |
| protected String createSequenceSQLFB15 = "CREATE GENERATOR {0}"; |
| protected String createSequenceSQLFB20 = "CREATE SEQUENCE {0}"; |
| protected String dropSequenceSQLFB15 = "DROP GENERATOR "; |
| protected String alterSequenceSQL = alterSequenceSQLFB20; |
| protected String createSequenceSQL = createSequenceSQLFB20; |
| |
| public static final int FB_VERSION_15 = 15; |
| public static final int FB_VERSION_20 = 20; |
| public static final int FB_VERSION_21 = 21; |
| |
| public static final String RANGE_SYNTAX_FIRST_SKIP = "firstskip"; |
| public static final String RANGE_SYNTAX_ROWS = "rows"; |
| |
| private static final Localizer _loc = |
| Localizer.forPackage(FirebirdDictionary.class); |
| |
| @SuppressWarnings("unchecked") |
| public FirebirdDictionary() { |
| platform = "Firebird"; |
| validationSQL = "SELECT 1 FROM RDB$DATABASE"; |
| supportsDeferredConstraints = false; |
| |
| useGetStringForClobs = true; |
| useSetStringForClobs = true; |
| useGetBytesForBlobs = true; |
| useSetBytesForBlobs = true; |
| |
| maxTableNameLength = 31; |
| maxColumnNameLength = 31; |
| maxConstraintNameLength = 31; |
| maxIndexNameLength = 31; |
| |
| supportsSelectStartIndex = true; |
| supportsSelectEndIndex = true; |
| |
| supportsMultipleNontransactionalResultSets = false; |
| |
| // On Firebird 2 the recommended syntax is "SELECT NEXT VALUE FOR {0} FROM RDB$DATABASE". |
| // However, that syntax allows incrementing the sequence value by 1 only. |
| nextSequenceQuery = "SELECT GEN_ID({0}, {1}) FROM RDB$DATABASE"; |
| sequenceSQL = |
| "SELECT NULL AS SEQUENCE_SCHEMA, RDB$GENERATOR_NAME " |
| + "AS SEQUENCE_NAME FROM RDB$GENERATORS " |
| + "WHERE (RDB$SYSTEM_FLAG IS NULL OR RDB$SYSTEM_FLAG = 0) "; |
| sequenceNameSQL = "AND RDB$GENERATOR_NAME = ?"; |
| |
| // A rough sum of reserved words in Firebird 1.0 - 2.1. |
| reservedWordSet.addAll(Arrays.asList(new String[] { "ACTIVE", "ADMIN", |
| "AFTER", "ASCENDING", "AUTO", "AUTODDL", "BASED", "BASENAME", |
| "BASE_NAME", "BEFORE", "BIGINT", "BLOB", "BLOBEDIT", "BUFFER", |
| "CACHE", "CHECK_POINT_LEN", "CHECK_POINT_LENGTH", "COMPILETIME", |
| "COMPUTED", "CLOSE", "CONDITIONAL", "CONTAINING", "CSTRING", |
| "CURRENT_CONNECTION", "CURRENT_ROLE", "CURRENT_TRANSACTION", |
| "DATABASE", "DB_KEY", "DEBUG", "DESCENDING", "DO", "ECHO", "EDIT", |
| "ENTRY_POINT", "EVENT", "EXIT", "EXTERN", "FILE", "FILTER", |
| "FREE_IT", "FUNCTION", "GDSCODE", "GENERATOR", "GEN_ID", "GLOBAL", |
| "GOTO", "GROUP_COMMIT_WAIT", "GROUP_COMMIT_WAIT_TIME", "HELP", |
| "IF", "INACTIVE", "INDEX", "INIT", "INPUT_TYPE", "ISQL", |
| "LC_MESSAGES", "LC_TYPE", "LEV", "LOGFILE", "LOG_BUFFER_SIZE", |
| "LOG_BUF_SIZE", "LONG", "MANUAL", "MAXIMUM", "MAXIMUM_SEGMENT", |
| "MAX_SEGMENT", "MERGE", "MESSAGE", "MINUTE", "MODULE_NAME", |
| "NOAUTO", "NUM_LOG_BUFS", "NUM_LOG_BUFFERS", "OUTPUT_TYPE", |
| "OVERFLOW", "PAGE", "PAGELENGTH", "PAGES", "PAGE_SIZE", |
| "PARAMETER", "PASSWORD", "PLAN", "POST_EVENT", "PROCEDURE", |
| "PROTECTED", "QUIT", "RAW_PARTITIONS", "RDB$DB_KEY", |
| "RECORD_VERSION", "RECREATE", "RELEASE", "RESERV", "RESERVING", |
| "RETAIN", "RETURN", "RETURNING_VALUES", "RETURNS", "ROLE", |
| "RUNTIME", "SAVEPOINT", "SEGMENT", "SHADOW", "SHARED", "SHELL", |
| "SHOW", "SINGULAR", "SNAPSHOT", "SORT", "SQLWARNING", "STABILITY", |
| "START", "STARTING", "STARTS", "STATEMENT", "STATIC", "STATISTICS", |
| "SUB_TYPE", "SUSPEND", "TERMINATOR", "TRIGGER", "VARIABLE", |
| "VERSION", "WAIT", "WEEKDAY", "WHILE" })); |
| |
| binaryTypeName = "BLOB"; |
| bitTypeName = "SMALLINT"; |
| charTypeName = "CHAR(1)"; |
| clobTypeName = "BLOB SUB_TYPE 1"; |
| doubleTypeName = "DOUBLE PRECISION"; |
| floatTypeName = "DOUBLE PRECISION"; |
| longVarbinaryTypeName = "BLOB"; |
| longVarcharTypeName = "BLOB SUB_TYPE 1"; |
| realTypeName = "FLOAT"; |
| smallintTypeName = "SMALLINT"; |
| tinyintTypeName = "SMALLINT"; |
| varbinaryTypeName = "BLOB"; |
| |
| supportsLockingWithDistinctClause = false; |
| supportsLockingWithMultipleTables = false; |
| supportsLockingWithOuterJoin = false; |
| supportsLockingWithInnerJoin = false; |
| forUpdateClause = "FOR UPDATE WITH LOCK"; |
| supportsQueryTimeout = false; |
| } |
| |
| /** |
| * Determine Firebird version and configure itself accordingly. |
| */ |
| @SuppressWarnings("unchecked") |
| @Override |
| public void connectedConfiguration(Connection conn) throws SQLException { |
| super.connectedConfiguration(conn); |
| firebirdVersion = determineFirebirdVersion(conn); |
| determineRangeSyntax(); |
| |
| if (firebirdVersion == FB_VERSION_21) |
| selectWordSet.add("WITH"); |
| if (!(firebirdVersion == FB_VERSION_21)) { |
| crossJoinClause = "JOIN"; |
| requiresConditionForCrossJoin = true; |
| } |
| if (firebirdVersion == FB_VERSION_15) { |
| stringLengthFunction = "STRLEN({0})"; |
| trimLeadingFunction = "LTRIM({0})"; |
| trimTrailingFunction = "RTRIM({0})"; |
| trimBothFunction = "LTRIM(RTRIM({0}))"; |
| alterSequenceSQL = alterSequenceSQLFB15; |
| createSequenceSQL = createSequenceSQLFB15; |
| } |
| } |
| |
| /** |
| * Use either <code>FIRST <p> SKIP <q></code> or |
| * <code>ROWS <m> TO <n></code> syntax. If <code>ROWS</code> |
| * variant is used and <code>end</code> equals {@link Long#MAX_VALUE}, a |
| * constant is used as <code><n></code> value. |
| */ |
| @Override |
| protected void appendSelectRange(SQLBuffer buf, long start, long end, |
| boolean subselect) { |
| if (RANGE_SYNTAX_FIRST_SKIP.equals(rangeSyntax)) { |
| if (end != Long.MAX_VALUE) |
| buf.append(" FIRST ").appendValue(end - start); |
| if (start != 0) |
| buf.append(" SKIP ").appendValue(start); |
| return; |
| } |
| |
| buf.append(" ROWS "); |
| if (start == 0) { |
| buf.appendValue(end); |
| return; |
| } |
| |
| buf.appendValue(start + 1).append(" TO "); |
| if (end == Long.MAX_VALUE) |
| buf.appendValue(maxRowNumberInRange); |
| else |
| buf.appendValue(end); |
| } |
| |
| /** |
| * Determine Firebird version either by using JDBC 3 methods or, if they |
| * are not available, by parsing the value returned by |
| * {@linkplain DatabaseMetaData#getDatabaseProductVersion()}. User can |
| * override Firebird version. |
| */ |
| protected int determineFirebirdVersion(Connection con) |
| throws SQLException { |
| // Let user override firebirdVersion. |
| if (firebirdVersion != 0) |
| return firebirdVersion; |
| |
| DatabaseMetaData metaData = con.getMetaData(); |
| int maj = 0; |
| int min = 0; |
| if (isJDBC3) { |
| maj = metaData.getDatabaseMajorVersion(); |
| min = metaData.getDatabaseMinorVersion(); |
| } else { |
| try { |
| // The product version looks like |
| // "LI-V2.1.1.17910 Firebird 2.1,LI-V2.1.1.17910 Firebird |
| // 2.1/tcp (hostname)/P10" or |
| // "WI-V1.5.5.4926 Firebird 1.52WI-V1.5.5.4926 Firebird 1.5/tcp |
| // (hostname)/P10" |
| String productVersion = metaData.getDatabaseProductVersion(); |
| Pattern p = Pattern.compile(".*-V(\\d)\\.(\\d)\\..*", |
| Pattern.CASE_INSENSITIVE); |
| Matcher m = p.matcher(productVersion); |
| m.matches(); |
| String majString = m.group(1); |
| String minString = m.group(2); |
| maj = Integer.parseInt(majString); |
| min = Integer.parseInt(minString); |
| } catch (Exception e) { |
| // We don't understand the version format. |
| if (log.isWarnEnabled()) |
| log.warn(e.toString(), e); |
| } |
| } |
| if (maj < 2) |
| return FB_VERSION_15; |
| if (maj == 2 && min == 0) |
| return FB_VERSION_20; |
| return FB_VERSION_21; |
| } |
| |
| /** |
| * Determine range syntax to be used depending on Firebird version. |
| * User can override range syntax. |
| */ |
| protected void determineRangeSyntax() { |
| // Let user override rangeSyntax. |
| if (rangeSyntax == null) |
| rangeSyntax = |
| (firebirdVersion == FB_VERSION_15) ? RANGE_SYNTAX_FIRST_SKIP |
| : RANGE_SYNTAX_ROWS; |
| |
| if (RANGE_SYNTAX_FIRST_SKIP.equals(rangeSyntax)) |
| rangePosition = RANGE_PRE_DISTINCT; |
| else |
| rangePosition = RANGE_POST_SELECT; |
| } |
| |
| /** |
| * Return <code><value> AS <type></code>. |
| */ |
| @Override |
| public String getPlaceholderValueString(Column col) { |
| return super.getPlaceholderValueString(col) + " AS " |
| + getTypeName(col); |
| } |
| |
| /** |
| * Return <code>%</code> if <code>tableName</code> is <code>null</code>, |
| * otherwise delegate to super implementation. |
| */ |
| @Override |
| protected String getTableNameForMetadata(String tableName) { |
| return (tableName == null) ? IdentifierUtil.PERCENT : |
| getTableNameForMetadata(DBIdentifier.newTable(tableName)); |
| } |
| |
| @Override |
| protected String getTableNameForMetadata(DBIdentifier tableName) { |
| if (DBIdentifier.isNull(tableName)) { |
| return IdentifierUtil.PERCENT; |
| } |
| return super.getTableNameForMetadata(tableName); |
| } |
| |
| /** |
| * Return <code>%</code> if <code>columnName</code> is <code>null</code>, |
| * otherwise delegate to super implementation. |
| */ |
| @Override |
| protected String getColumnNameForMetadata(String columnName) { |
| return (columnName == null) ? "%" : super |
| .getColumnNameForMetadata(columnName); |
| } |
| |
| /** |
| * Return |
| * <code>ALTER TABLE <table name> DROP <col name></code>. |
| */ |
| @Override |
| public String[] getDropColumnSQL(Column column) { |
| return new String[] { "ALTER TABLE " |
| + getFullName(column.getTable(), false) + " DROP " + getColumnDBName(column) }; |
| } |
| |
| /** |
| * Return either |
| * <code>CREATE SEQUENCE <sequence name></code> or |
| * <code>CREATE GENERATOR <sequence name></code>. |
| * If initial value of sequence is set, return also |
| * an appropriate <code>ALTER</code> statement. |
| */ |
| @Override |
| public String[] getCreateSequenceSQL(Sequence seq) { |
| String seqName = |
| checkNameLength(getFullName(seq), maxTableNameLength, |
| "long-seq-name"); |
| String createSeq = |
| MessageFormat.format(createSequenceSQL, new Object[] { seqName }); |
| if (seq.getInitialValue() == 0) |
| return new String[] { createSeq }; |
| |
| // Use String.valueOf to get rid of possible number formatting. |
| String alterSeq = |
| MessageFormat.format(alterSequenceSQL, new Object[] { seqName, |
| String.valueOf(seq.getInitialValue()) }); |
| return new String[] { createSeq, alterSeq }; |
| } |
| |
| /** |
| * Return Firebird-specific statement to select the list of sequences. |
| */ |
| @Override |
| protected String getSequencesSQL(String schemaName, String sequenceName) { |
| return getSequencesSQL(DBIdentifier.newSchema(schemaName), DBIdentifier.newSequence(sequenceName)); |
| } |
| |
| @Override |
| protected String getSequencesSQL(DBIdentifier schemaName, DBIdentifier sequenceName) { |
| StringBuilder buf = new StringBuilder(sequenceSQL); |
| if (!DBIdentifier.isNull(sequenceName)) { |
| buf.append(sequenceNameSQL); |
| } |
| return buf.toString(); |
| } |
| |
| /** |
| * Call super implementation and trim sequence name. This is because of |
| * trailing spaces problem: <code>RDB$GENERATORS.RDB$GENERATOR_NAME</code> |
| * is <code>CHAR(31)</code> and using <code>RTRIM</code> UDF function on |
| * Firebird 1.5 surprisingly returns a string right-padded with spaces up |
| * to the length of 255. |
| */ |
| @Override |
| protected Sequence newSequence(ResultSet sequenceMeta) throws SQLException { |
| Sequence seq = super.newSequence(sequenceMeta); |
| seq.setIdentifier(DBIdentifier.trim(seq.getIdentifier())); |
| return seq; |
| } |
| |
| /** |
| * On Firebird 1.5 return |
| * <code>DROP GENERATOR <sequence name></code>. |
| * On Firebird 2.0 and later delegate to the super implementation. |
| */ |
| @Override |
| public String[] getDropSequenceSQL(Sequence seq) { |
| if (firebirdVersion == FB_VERSION_15) |
| return new String[] { dropSequenceSQLFB15 + getFullName(seq) }; |
| return super.getDropSequenceSQL(seq); |
| } |
| |
| /** |
| * On Firebird 2.1 return <code>POSITION(<find>, <str> [, <start>])<code>. |
| * On older versions throw {@link UnsupportedException} - no suitable function exists. |
| */ |
| @Override |
| public void indexOf(SQLBuffer buf, FilterValue str, FilterValue find, |
| FilterValue start) { |
| if (firebirdVersion < FB_VERSION_21) { |
| throw new UnsupportedException(_loc.get("function-not-supported", getClass(), "LOCATE")); |
| } |
| buf.append("POSITION("); |
| find.appendTo(buf); |
| buf.append(", "); |
| str.appendTo(buf); |
| if (start != null) { |
| buf.append(", "); |
| buf.append("CAST("); |
| start.appendTo(buf); |
| buf.append(" AS INTEGER)"); |
| } |
| buf.append(")"); |
| } |
| |
| /** |
| * Use |
| * <code>SUBSTRING(<col name> FROM <m> FOR <n>)</code>. |
| * Parameters are inlined because neither parameter binding nor expressions |
| * are accepted by Firebird here. As a result, an |
| * {@link UnsupportedException} is thrown when something else than a |
| * constant is used in <code>start</code> or <code>length</code>. |
| */ |
| @Override |
| public void substring(SQLBuffer buf, FilterValue str, FilterValue start, |
| FilterValue length) { |
| buf.append(substringFunctionName).append("("); |
| str.appendTo(buf); |
| buf.append(" FROM "); |
| if (start.getValue() instanceof Number) { |
| long startLong = toLong(start); |
| buf.append(Long.toString(startLong)); |
| } else { |
| throw new UnsupportedException(_loc.get("function-not-supported", |
| getClass(), substringFunctionName + " with non-constants")); |
| } |
| if (length != null) { |
| buf.append(" FOR "); |
| if (length.getValue() instanceof Number) { |
| long lengthLong = toLong(length); |
| buf.append(Long.toString(lengthLong)); |
| } else { |
| throw new UnsupportedException(_loc.get( |
| "function-not-supported", getClass(), substringFunctionName |
| + " with non-constants")); |
| } |
| } |
| buf.append(")"); |
| } |
| |
| /** |
| * On Firebird 1.5 reduce the size of indexed <code>VARCHAR</code> column |
| * to 252 or a value specified by user. 252 is the maximum Firebird 1.5 can |
| * handle for one-column indexes. On Firebird 2.0 and later delegate to the |
| * super implementation. |
| */ |
| @Override |
| protected String appendSize(Column col, String typeName) { |
| if (firebirdVersion != FB_VERSION_15) |
| return super.appendSize(col, typeName); |
| |
| if (col.getType() == Types.VARCHAR |
| && col.getSize() > indexedVarcharMaxSizeFB15 |
| && col.getTable() != null) { |
| |
| if (col.isPrimaryKey()) { |
| col.setSize(indexedVarcharMaxSizeFB15); |
| return super.appendSize(col, typeName); |
| } |
| Index[] indexes = col.getTable().getIndexes(); |
| for (Index index : indexes) { |
| if (index.containsColumn(col)) { |
| col.setSize(indexedVarcharMaxSizeFB15); |
| return super.appendSize(col, typeName); |
| } |
| } |
| Unique[] uniques = col.getTable().getUniques(); |
| for (Unique unique : uniques) { |
| if (unique.containsColumn(col)) { |
| col.setSize(indexedVarcharMaxSizeFB15); |
| return super.appendSize(col, typeName); |
| } |
| } |
| ForeignKey[] foreignKeys = col.getTable().getForeignKeys(); |
| for (ForeignKey foreignKey : foreignKeys) { |
| if (foreignKey.containsColumn(col)) { |
| col.setSize(indexedVarcharMaxSizeFB15); |
| return super.appendSize(col, typeName); |
| } |
| } |
| } |
| return super.appendSize(col, typeName); |
| } |
| |
| /** |
| * Use error code as SQL state returned by Firebird is ambiguous. |
| */ |
| @Override |
| protected int matchErrorState(Map<Integer,Set<String>> errorStates, SQLException ex) { |
| String errorState = ""+ex.getErrorCode(); |
| for (Map.Entry<Integer,Set<String>> states : errorStates.entrySet()) { |
| if (states.getValue().contains(errorState)) |
| return states.getKey(); |
| } |
| return ExceptionInfo.GENERAL; |
| } |
| } |