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}