001package io.ebean.config.dbplatform;
002
003import io.ebean.BackgroundExecutor;
004import io.ebean.Query;
005import io.ebean.annotation.PartitionMode;
006import io.ebean.annotation.PersistBatch;
007import io.ebean.annotation.Platform;
008import io.ebean.config.CustomDbTypeMapping;
009import io.ebean.config.PlatformConfig;
010import io.ebean.util.JdbcClose;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013
014import javax.persistence.PersistenceException;
015import javax.sql.DataSource;
016import java.sql.Connection;
017import java.sql.DatabaseMetaData;
018import java.sql.PreparedStatement;
019import java.sql.ResultSet;
020import java.sql.SQLException;
021import java.sql.Statement;
022import java.sql.Types;
023
024/**
025 * Database platform specific settings.
026 */
027public class DatabasePlatform {
028
029  private static final Logger log = LoggerFactory.getLogger("io.ebean");
030
031  /**
032   * Behavior used when ending a query only transaction (at read committed isolation level).
033   */
034  public enum OnQueryOnly {
035
036    /**
037     * Rollback the transaction.
038     */
039    ROLLBACK,
040
041    /**
042     * Commit the transaction
043     */
044    COMMIT
045  }
046
047
048  /**
049   * Set to true for MySql, no other jdbc drivers need this workaround.
050   */
051  protected boolean useExtraTransactionOnIterateSecondaryQueries;
052
053  protected boolean supportsDeleteTableAlias;
054
055  protected boolean supportsSavepointId = true;
056
057  protected boolean useMigrationStoredProcedures = false;
058
059  /**
060   * Can we use native java time API objects in
061   * {@link ResultSet#getObject(int, Class)} and
062   * {@link PreparedStatement#setObject(int, Object)}.
063   *
064   * Not all drivers (DB2 e.g.) will support this.
065   */
066  protected boolean supportsNativeJavaTime = true;
067
068  /**
069   * The behaviour used when ending a read only transaction at read committed isolation level.
070   */
071  protected OnQueryOnly onQueryOnly = OnQueryOnly.COMMIT;
072
073  /**
074   * The open quote used by quoted identifiers.
075   */
076  protected String openQuote = "\"";
077
078  /**
079   * The close quote used by quoted identifiers.
080   */
081  protected String closeQuote = "\"";
082
083  /**
084   * When set to true all db column names and table names use quoted identifiers.
085   */
086  protected boolean allQuotedIdentifiers;
087
088  protected boolean caseSensitiveCollation = true;
089
090  /**
091   * Set true if the Database support LIMIT clause on sql update.
092   */
093  protected boolean inlineSqlUpdateLimit;
094
095  /**
096   * For limit/offset, row_number etc limiting of SQL queries.
097   */
098  protected SqlLimiter sqlLimiter = new LimitOffsetSqlLimiter();
099
100  /**
101   * Limit/offset support for SqlQuery only.
102   */
103  protected BasicSqlLimiter basicSqlLimiter = new BasicSqlLimitOffset();
104
105  /**
106   * Mapping of JDBC to Database types.
107   */
108  protected DbPlatformTypeMapping dbTypeMap = new DbPlatformTypeMapping();
109
110  /**
111   * Default values for DB columns.
112   */
113  protected DbDefaultValue dbDefaultValue = new DbDefaultValue();
114
115  /**
116   * Set to true if the DB has native UUID type support.
117   */
118  protected boolean nativeUuidType;
119
120  /**
121   * Defines DB identity/sequence features.
122   */
123  protected DbIdentity dbIdentity = new DbIdentity();
124
125  protected boolean sequenceBatchMode = true;
126
127  protected int sequenceBatchSize = 20;
128
129  /**
130   * The history support for this database platform.
131   */
132  protected DbHistorySupport historySupport;
133
134  /**
135   * The JDBC type to map booleans to (by default).
136   */
137  protected int booleanDbType = Types.BOOLEAN;
138
139  /**
140   * The JDBC type to map Blob to.
141   */
142  protected int blobDbType = Types.BLOB;
143
144  /**
145   * The JDBC type to map Clob to.
146   */
147  protected int clobDbType = Types.CLOB;
148
149  /**
150   * For Oracle treat empty strings as null.
151   */
152  protected boolean treatEmptyStringsAsNull;
153
154  /**
155   * The database platform name.
156   */
157  protected Platform platform = Platform.GENERIC;
158
159  protected String truncateTable = "truncate table %s";
160
161  protected String columnAliasPrefix;
162
163  /**
164   * Use a BackTick ` at the beginning and end of table or column names that you
165   * want to use quoted identifiers for. The backticks get converted to the
166   * appropriate characters in convertQuotedIdentifiers
167   */
168  private static final char[] QUOTED_IDENTIFIERS = new char[] { '"', '\'', '[', ']', '`' };
169
170  /**
171   * The non-escaped like clause (to stop slash being escaped on some platforms).
172   * Used for the 'raw like' expression but not for startsWith, endsWith and contains expressions.
173   */
174  protected String likeClauseRaw = "like ? escape''";
175
176  /**
177   * Escaped like clause for startsWith, endsWith and contains.
178   */
179  protected String likeClauseEscaped = "like ? escape'|'";
180
181  /**
182   * Escape character used for startsWith, endsWith and contains.
183   */
184  protected char likeEscapeChar = '|';
185
186  /**
187   * Characters escaped for startsWith, endsWith and contains.
188   */
189  protected char[] likeSpecialCharacters = {'%', '_', '|'};
190
191  protected DbEncrypt dbEncrypt;
192
193  protected boolean idInExpandedForm;
194
195  protected boolean selectCountWithAlias;
196  protected boolean selectCountWithColumnAlias;
197
198  /**
199   * If set then use the FORWARD ONLY hint when creating ResultSets for
200   * findIterate() and findVisit().
201   */
202  protected boolean forwardOnlyHintOnFindIterate;
203
204  /**
205   * If set then use the CONCUR_UPDATABLE hint when creating ResultSets.
206   * <p>
207   * This is {@code false} for HANA
208   */
209  protected boolean supportsResultSetConcurrencyModeUpdatable = true;
210
211
212  /**
213   * By default we use JDBC batch when cascading (except for SQL Server and HANA).
214   */
215  protected PersistBatch persistBatchOnCascade = PersistBatch.ALL;
216
217  protected int maxInBinding;
218
219  /**
220   * The maximum length of table names - used specifically when derived
221   * default table names for intersection tables.
222   */
223  protected int maxTableNameLength = 60;
224
225  /**
226   * A value of 60 is a reasonable default for all databases except
227   * Oracle (limited to 30) and DB2 (limited to 18).
228   */
229  protected int maxConstraintNameLength = 60;
230
231  protected boolean supportsNativeIlike;
232
233  protected SqlExceptionTranslator exceptionTranslator = new SqlCodeTranslator();
234
235  /**
236   * Instantiates a new database platform.
237   */
238  public DatabasePlatform() {
239  }
240
241  /**
242   * Translate the SQLException into a specific persistence exception if possible.
243   */
244  public PersistenceException translate(String message, SQLException e) {
245    return exceptionTranslator.translate(message, e);
246  }
247
248  /**
249   * Configure the platform given the server configuration.
250   */
251  public void configure(PlatformConfig config) {
252    this.sequenceBatchSize = config.getDatabaseSequenceBatchSize();
253    this.caseSensitiveCollation = config.isCaseSensitiveCollation();
254    this.useMigrationStoredProcedures = config.isUseMigrationStoredProcedures();
255    configureIdType(config.getIdType());
256    configure(config, config.isAllQuotedIdentifiers());
257  }
258
259  /**
260   * Configure UUID Storage etc based on DatabaseConfig settings.
261   */
262  protected void configure(PlatformConfig config, boolean allQuotedIdentifiers) {
263    this.allQuotedIdentifiers = allQuotedIdentifiers;
264    addGeoTypes(config.getGeometrySRID());
265    configureIdType(config.getIdType());
266    dbTypeMap.config(nativeUuidType, config.getDbUuid());
267    for (CustomDbTypeMapping mapping : config.getCustomTypeMappings()) {
268      if (platformMatch(mapping.getPlatform())) {
269        dbTypeMap.put(mapping.getType(), parse(mapping.getColumnDefinition()));
270      }
271    }
272  }
273
274  protected void configureIdType(IdType idType) {
275    if (idType != null) {
276      this.dbIdentity.setIdType(idType);
277    }
278  }
279
280  protected void addGeoTypes(int srid) {
281    // default has no geo type support
282  }
283
284  private DbPlatformType parse(String columnDefinition) {
285    return DbPlatformType.parse(columnDefinition);
286  }
287
288  private boolean platformMatch(Platform platform) {
289    return platform == null || isPlatform(platform);
290  }
291
292  /**
293   * Return true if this matches the given platform.
294   */
295  public boolean isPlatform(Platform platform) {
296    return this.platform.base() == platform;
297  }
298
299  /**
300   * Return the platform key.
301   */
302  public Platform getPlatform() {
303    return platform;
304  }
305
306  /**
307   * Return the name of the underlying Platform in lowercase.
308   * <p>
309   * "generic" is returned when no specific database platform has been set or found.
310   * </p>
311   */
312  public String getName() {
313    return platform.name().toLowerCase();
314  }
315
316  /**
317   * Return true if we are using Sequence batch mode rather than STEP.
318   */
319  public boolean isSequenceBatchMode() {
320    return sequenceBatchMode;
321  }
322
323  /**
324   * Set to false to not use sequence batch mode but instead STEP mode.
325   */
326  public void setSequenceBatchMode(boolean sequenceBatchMode) {
327    this.sequenceBatchMode = sequenceBatchMode;
328  }
329
330  /**
331   * Return true if this database platform supports native ILIKE expression.
332   */
333  public boolean isSupportsNativeIlike() {
334    return supportsNativeIlike;
335  }
336
337  /**
338   * Return true if the platform supports delete statements with table alias.
339   */
340  public boolean isSupportsDeleteTableAlias() {
341    return supportsDeleteTableAlias;
342  }
343
344  /**
345   * Return true if the collation is case sensitive.
346   * <p>
347   * This is expected to be used for testing only.
348   * </p>
349   */
350  public boolean isCaseSensitiveCollation() {
351    return caseSensitiveCollation;
352  }
353
354  /**
355   * Return true if the platform supports SavepointId values.
356   */
357  public boolean isSupportsSavepointId() {
358    return supportsSavepointId;
359  }
360
361  /**
362   * Return true if migrations should use stored procedures.
363   */
364  public boolean isUseMigrationStoredProcedures() {
365    return useMigrationStoredProcedures;
366  }
367
368  /**
369   * Return true if the platform supports LIMIT with sql update.
370   */
371  public boolean isInlineSqlUpdateLimit() {
372    return inlineSqlUpdateLimit;
373  }
374
375  /**
376   * Return the maximum number of bind values this database platform allows or zero for no limit.
377   */
378  public int getMaxInBinding() {
379    return maxInBinding;
380  }
381
382  /**
383   * Return the maximum table name length.
384   * <p>
385   * This is used when deriving names of intersection tables.
386   * </p>
387   */
388  public int getMaxTableNameLength() {
389    return maxTableNameLength;
390  }
391
392  /**
393   * Return the maximum constraint name allowed for the platform.
394   */
395  public int getMaxConstraintNameLength() {
396    return maxConstraintNameLength;
397  }
398
399  /**
400   * Return true if the JDBC driver does not allow additional queries to execute
401   * when a resultSet is being 'streamed' as is the case with findEach() etc.
402   * <p>
403   * Honestly, this is a workaround for a stupid MySql JDBC driver limitation.
404   * </p>
405   */
406  public boolean useExtraTransactionOnIterateSecondaryQueries() {
407    return useExtraTransactionOnIterateSecondaryQueries;
408  }
409
410  /**
411   * Return a DB Sequence based IdGenerator.
412   *
413   * @param be       the BackgroundExecutor that can be used to load the sequence if
414   *                 desired
415   * @param ds       the DataSource
416   * @param stepSize the sequence allocation size as defined by mapping (defaults to 50)
417   * @param seqName  the name of the sequence
418   */
419  public PlatformIdGenerator createSequenceIdGenerator(BackgroundExecutor be, DataSource ds, int stepSize, String seqName) {
420    return null;
421  }
422
423  /**
424   * Return the behaviour to use when ending a read only transaction.
425   */
426  public OnQueryOnly getOnQueryOnly() {
427    return onQueryOnly;
428  }
429
430  /**
431   * Set the behaviour to use when ending a read only transaction.
432   */
433  public void setOnQueryOnly(OnQueryOnly onQueryOnly) {
434    this.onQueryOnly = onQueryOnly;
435  }
436
437  /**
438   * Return the DbEncrypt handler for this DB platform.
439   */
440  public DbEncrypt getDbEncrypt() {
441    return dbEncrypt;
442  }
443
444  /**
445   * Set the DbEncrypt handler for this DB platform.
446   */
447  public void setDbEncrypt(DbEncrypt dbEncrypt) {
448    this.dbEncrypt = dbEncrypt;
449  }
450
451  /**
452   * Return the history support for this database platform.
453   */
454  public DbHistorySupport getHistorySupport() {
455    return historySupport;
456  }
457
458  /**
459   * Set the history support for this database platform.
460   */
461  public void setHistorySupport(DbHistorySupport historySupport) {
462    this.historySupport = historySupport;
463  }
464
465  /**
466   * So no except for Postgres and CockroachDB.
467   */
468  public boolean isNativeArrayType() {
469    return false;
470  }
471
472  /**
473   * Return true if the DB supports native UUID.
474   */
475  public boolean isNativeUuidType() {
476    return nativeUuidType;
477  }
478
479  /**
480   * Return the mapping of JDBC to DB types.
481   *
482   * @return the db type map
483   */
484  public DbPlatformTypeMapping getDbTypeMap() {
485    return dbTypeMap;
486  }
487
488  /**
489   * Return the mapping for DB column default values.
490   */
491  public DbDefaultValue getDbDefaultValue() {
492    return dbDefaultValue;
493  }
494
495  /**
496   * Return the column alias prefix.
497   */
498  public String getColumnAliasPrefix() {
499    return columnAliasPrefix;
500  }
501
502  /**
503   * Set the column alias prefix.
504   */
505  public void setColumnAliasPrefix(String columnAliasPrefix) {
506    this.columnAliasPrefix = columnAliasPrefix;
507  }
508
509  /**
510   * Return the close quote for quoted identifiers.
511   */
512  public String getCloseQuote() {
513    return closeQuote;
514  }
515
516  /**
517   * Return the open quote for quoted identifiers.
518   */
519  public String getOpenQuote() {
520    return openQuote;
521  }
522
523  /**
524   * Return the JDBC type used to store booleans.
525   */
526  public int getBooleanDbType() {
527    return booleanDbType;
528  }
529
530  /**
531   * Return the data type that should be used for Blob.
532   * <p>
533   * This is typically Types.BLOB but for Postgres is Types.LONGVARBINARY for
534   * example.
535   * </p>
536   */
537  public int getBlobDbType() {
538    return blobDbType;
539  }
540
541  /**
542   * Return the data type that should be used for Clob.
543   * <p>
544   * This is typically Types.CLOB but for Postgres is Types.VARCHAR.
545   * </p>
546   */
547  public int getClobDbType() {
548    return clobDbType;
549  }
550
551  /**
552   * Return true if empty strings should be treated as null.
553   *
554   * @return true, if checks if is treat empty strings as null
555   */
556  public boolean isTreatEmptyStringsAsNull() {
557    return treatEmptyStringsAsNull;
558  }
559
560  /**
561   * Return true if a compound ID in (...) type expression needs to be in
562   * expanded form of (a=? and b=?) or (a=? and b=?) or ... rather than (a,b) in
563   * ((?,?),(?,?),...);
564   */
565  public boolean isIdInExpandedForm() {
566    return idInExpandedForm;
567  }
568
569  /**
570   * Return true if the ResultSet TYPE_FORWARD_ONLY Hint should be used on
571   * findIterate() and findVisit() PreparedStatements.
572   * <p>
573   * This specifically is required for MySql when processing large results.
574   * </p>
575   */
576  public boolean isForwardOnlyHintOnFindIterate() {
577    return forwardOnlyHintOnFindIterate;
578  }
579
580  /**
581   * Set to true if the ResultSet TYPE_FORWARD_ONLY Hint should be used by default on findIterate PreparedStatements.
582   */
583  public void setForwardOnlyHintOnFindIterate(boolean forwardOnlyHintOnFindIterate) {
584    this.forwardOnlyHintOnFindIterate = forwardOnlyHintOnFindIterate;
585  }
586
587  /**
588   * Return true if the ResultSet CONCUR_UPDATABLE Hint should be used on
589   * createNativeSqlTree() PreparedStatements.
590   * <p>
591   * This specifically is required for Hana which doesn't support CONCUR_UPDATABLE
592   * </p>
593   */
594  public boolean isSupportsResultSetConcurrencyModeUpdatable() {
595    return supportsResultSetConcurrencyModeUpdatable;
596  }
597
598  /**
599   * Set to true if the ResultSet CONCUR_UPDATABLE Hint should be used by default on createNativeSqlTree() PreparedStatements.
600   */
601  public void setSupportsResultSetConcurrencyModeUpdatable(boolean supportsResultSetConcurrencyModeUpdatable) {
602    this.supportsResultSetConcurrencyModeUpdatable = supportsResultSetConcurrencyModeUpdatable;
603  }
604
605  public void setUseMigrationStoredProcedures(final boolean useMigrationStoredProcedures) {
606    this.useMigrationStoredProcedures = useMigrationStoredProcedures;
607  }
608
609  /**
610   * Return the DB identity/sequence features for this platform.
611   *
612   * @return the db identity
613   */
614  public DbIdentity getDbIdentity() {
615    return dbIdentity;
616  }
617
618  /**
619   * Return the SqlLimiter used to apply additional sql around a query to limit
620   * its results.
621   * <p>
622   * Basically add the clauses for limit/offset, rownum, row_number().
623   * </p>
624   *
625   * @return the sql limiter
626   */
627  public SqlLimiter getSqlLimiter() {
628    return sqlLimiter;
629  }
630
631  /**
632   * Return the BasicSqlLimiter for limit/offset of SqlQuery queries.
633   */
634  public BasicSqlLimiter getBasicSqlLimiter() {
635    return basicSqlLimiter;
636  }
637
638  /**
639   * Set the DB TRUE literal (from the registered boolean ScalarType)
640   */
641  public void setDbTrueLiteral(String dbTrueLiteral) {
642    this.dbDefaultValue.setTrue(dbTrueLiteral);
643  }
644
645  /**
646   * Set the DB FALSE literal (from the registered boolean ScalarType)
647   */
648  public void setDbFalseLiteral(String dbFalseLiteral) {
649    this.dbDefaultValue.setFalse(dbFalseLiteral);
650  }
651
652  /**
653   * Convert backticks to the platform specific open quote and close quote
654   * <p>
655   * Specific plugins may implement this method to cater for platform specific
656   * naming rules.
657   * </p>
658   *
659   * @param dbName the db table or column name
660   * @return the db table or column name with potentially platform specific quoted identifiers
661   */
662  public String convertQuotedIdentifiers(String dbName) {
663    // Ignore null values e.g. schema name or catalog
664    if (dbName != null && !dbName.isEmpty()) {
665      if (isQuote(dbName.charAt(0))) {
666        if (isQuote(dbName.charAt(dbName.length() - 1))) {
667          return openQuote + dbName.substring(1, dbName.length() - 1) + closeQuote;
668        } else {
669          log.error("Missing backquote on [" + dbName + "]");
670        }
671      } else if (allQuotedIdentifiers) {
672        return openQuote + dbName + closeQuote;
673      }
674    }
675    return dbName;
676  }
677
678  private boolean isQuote(char ch) {
679    for (char identifer : QUOTED_IDENTIFIERS) {
680      if (identifer == ch) {
681        return true;
682      }
683    }
684    return false;
685  }
686
687  /**
688   * Remove quoted identifier quotes from the table or column name if present.
689   */
690  public String unQuote(String dbName) {
691    if (dbName != null && !dbName.isEmpty()) {
692      if (dbName.startsWith(openQuote)) {
693        // trim off the open and close quotes
694        return dbName.substring(1, dbName.length() - 1);
695      }
696    }
697    return dbName;
698  }
699
700  /**
701   * Set to true if select count against anonymous view requires an alias.
702   */
703  public boolean isSelectCountWithAlias() {
704    return selectCountWithAlias;
705  }
706
707  /**
708   * Return true if select count with subquery needs column alias (SQL Server).
709   */
710  public boolean isSelectCountWithColumnAlias() {
711    return selectCountWithColumnAlias;
712  }
713
714
715  public String completeSql(String sql, Query<?> query) {
716    if (query.isForUpdate()) {
717      sql = withForUpdate(sql, query.getForUpdateLockWait(), query.getForUpdateLockType());
718    }
719    return sql;
720  }
721
722  /**
723   * For update hint on the FROM clause (SQL server only).
724   */
725  public String fromForUpdate(Query.LockWait lockWait) {
726    // return null except for sql server
727    return null;
728  }
729
730  protected String withForUpdate(String sql, Query.LockWait lockWait, Query.LockType lockType) {
731    // silently assume the database does not support the "for update" clause.
732    log.info("it seems your database does not support the 'for update' clause");
733    return sql;
734  }
735
736  /**
737   * Returns the like clause used by this database platform.
738   * <p>
739   * This may include an escape clause to disable a default escape character.
740   */
741  public String getLikeClause(boolean rawLikeExpression) {
742    return rawLikeExpression ? likeClauseRaw : likeClauseEscaped;
743  }
744
745  /**
746   * Return the platform default JDBC batch mode for persist cascade.
747   */
748  public PersistBatch getPersistBatchOnCascade() {
749    return persistBatchOnCascade;
750  }
751
752  /**
753   * Return a statement to truncate a table.
754   */
755  public String truncateStatement(String table) {
756    return String.format(truncateTable, table);
757  }
758
759  /**
760   * Create the DB schema if it does not exist.
761   */
762  public void createSchemaIfNotExists(String dbSchema, Connection connection) throws SQLException {
763    if (!schemaExists(dbSchema, connection)) {
764      Statement query = connection.createStatement();
765      try {
766        log.debug("create schema:{}", dbSchema);
767        query.executeUpdate("create schema " + dbSchema);
768      } finally {
769        JdbcClose.close(query);
770      }
771    }
772  }
773
774  /**
775   * Return true if the schema exists.
776   */
777  public boolean schemaExists(String dbSchema, Connection connection) throws SQLException {
778    ResultSet schemas = connection.getMetaData().getSchemas();
779    try {
780      while (schemas.next()) {
781        String schema = schemas.getString(1);
782        if (schema.equalsIgnoreCase(dbSchema)) {
783          return true;
784        }
785      }
786    } finally {
787      JdbcClose.close(schemas);
788    }
789    return false;
790  }
791
792  /**
793   * Return true if the table exists.
794   */
795  public boolean tableExists(Connection connection, String catalog, String schema, String table) throws SQLException {
796    DatabaseMetaData metaData = connection.getMetaData();
797    ResultSet tables = metaData.getTables(catalog, schema, table, null);
798    try {
799      return tables.next();
800    } finally {
801      JdbcClose.close(tables);
802    }
803  }
804
805  /**
806   * Return true if partitions exist for the given table.
807   */
808  public boolean tablePartitionsExist(Connection connection, String table) throws SQLException {
809    return true;
810  }
811
812  /**
813   * Return the SQL to create an initial partition for the given table.
814   */
815  public String tablePartitionInit(String tableName, PartitionMode mode, String property, String singlePrimaryKey) {
816    return null;
817  }
818
819  /**
820   * Escapes the like string for this DB-Platform
821   */
822  public String escapeLikeString(String value) {
823    StringBuilder sb = null;
824    for (int i = 0; i < value.length(); i++) {
825      char ch = value.charAt(i);
826      boolean escaped = false;
827      for (char escapeChar : likeSpecialCharacters) {
828        if (ch == escapeChar) {
829          if (sb == null) {
830            sb = new StringBuilder(value.substring(0, i));
831          }
832          escapeLikeCharacter(escapeChar, sb);
833          escaped = true;
834          break;
835        }
836      }
837      if (!escaped && sb != null) {
838        sb.append(ch);
839      }
840    }
841    if (sb == null) {
842      return value;
843    } else {
844      return sb.toString();
845    }
846  }
847
848  protected void escapeLikeCharacter(char ch, StringBuilder sb) {
849    sb.append(likeEscapeChar).append(ch);
850  }
851
852  public boolean supportsNativeJavaTime() {
853    return supportsNativeJavaTime;
854  }
855}