MariaDbLenientDialect.java
/*
** Module : MariaDbLenientDialect.java
** Abstract : Concrete implementation of Dialect for a lenient MariaDb backend.
**
** Copyright (c) 2022-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20221018 Created initial version with implementation extracted from MariaDb. This latter class
** becomes a subclass and will implement the methods to adhere to MariaDb strict(er) mode.
** OM 20221025 Improve sequence compatibility with 4GL.
** OM 20221026 Excluded table name from index name by default. It will be added as needed by dialects.
** RAA 20230109 Overrided method getSequenceSetValString().
** ECF 20230122 Changed range parameter inlining default to false.
** BS 20230126 Added support for SAVE CACHE.
** ECF 20230127 Data type adjustment for SAVE CACHE.
** OM 20230131 Code cleanup.
** 002 RAA 20230208 Added parameter for getCreateTemporaryTablePostfix.
** 003 BS 20230331 Added support for sequences.
** 004 DDF 20230306 Added a parameter to getSqlMappedType() (refs: #7108).
** 005 DDF 20230406 Removed isAutoRtrimAndCi. Added isAutoRtrim and isAutoCi methods.
** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 007 OM 20230614 Added 'lead' as reserved keyword (abbreviation of leading?). Fixed scale value for non-
** character column data types.
** 008 OM 20230615 Converting the MAX-WIDTH from size of UTF-16 encoding to number of characters.
** 009 OM 20230908 Added checkIndexConstraints() method.
** 010 IAS 20230910 Fixed flags for word tables' generation.
** 011 DDF 20231127 Added isUniqueConstraintViolation() method.
** 012 OM 20231128 Enlarged BLOB capacity to 4GiB by using LONGBLOB instead.
** 013 OM 20231218 Added implementation for DBCODEPAGE and DBCOLLATION 4GL functions.
** 014 IAS 20230208 Added error handler support.
** 015 OM 20240131 Fixed flaw in DBCODEPAGE / DBCOLLATION implementation.
** 016 TJD 20240220 Java 17 javadoc fix.
** 017 OM 20240718 Fixed the list of columns in foreign key when creating FK constraints.
** 018 OM 20250117 Added support for multiple dialects for multi-tenant landlord database.
** 019 OM 20250331 Added c3p0 connection parameters.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist.dialect;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.deploy.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.sequence.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager.*;
import com.mchange.v2.c3p0.*;
import java.io.*;
import java.sql.*;
import java.sql.Date;
import java.text.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.*;
import com.goldencode.p2j.util.logging.*;
import org.apache.commons.lang.*;
/**
* Extends abstract {@link Dialect} for an MariaDb/MySql backend. This is a lenient implementation and not all
* 4GL features are supported. The customer which uses this is responsible for any unexpected results of
* processing his data in lenient (non-strict) mode.
*/
public class MariaDbLenientDialect
extends Dialect
{
/** The id of this dialect variant as a string. */
public static final String DIALECT_ID = "mariadblenient";
/** The maximum size of a character / VARCHAR field in MariaDb. */
protected static final int MAX_VARCHAR_SIZE = 100;
/** The limit at which VARCHAR columns are converted to TEXT columns in MariaDb. */
protected static final int MIN_TEXT_SIZE = 512;
/** The maximum key size in a MariaDb index. */
private static final int INDEX_MAX_KEY_SIZE = 3072;
/**
* The number of bytes per character in the used text encoding (assuming utf8mb4 / utf32) used when
* computing the space requirements.
*/
private static final int BPC = 4;
// #1074 - Column length too big for column 'X' (max = 16383); use BLOB or TEXT instead
// #1118 - Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.
// Otherwise: you have to change some columns to TEXT or BLOBs
// #1071 - Specified key was too long; max key length is 3072 bytes
/** Regex Pattern for parsing message cased by error raised by UDF. */
protected static final Pattern UDF_ERROR_MESSAGE = Pattern.compile(
"^(\\(conn=[0-9]+\\) )(\\*\\* )?(.*)(\\.)? *\\((-?[0-9]*)\\).*$");
/** Lowest date supported by MariaDb (?) */
protected static final Date lowDate;
/** Highest date supported by MariaDb (?) */
protected static final Date highDate;
/** Mapping between the FWD possible field/column types and their SQL counterparts. */
protected static final Map<String, String> fwd2sql = new HashMap<>();
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(MariaDbLenientDialect.class);
/** Flag indicating whether range parameters should be inlined */
private static boolean inlineRangeParms = false; // just to allow dynamic value changes in debug
/*
* Initialization of static data.
*
* @see https://mariadb.com/kb/en/data-types/
*/
static
{
fwd2sql.put("blob", "longblob"); // 4GiB, alternatives: MEDIUMBLOB (16MiB) and BLOB (64 KiB)
fwd2sql.put("character", "varchar(" + SCALE_TOK + ")");
fwd2sql.put("clob", "mediumtext"); // 16MiB, alternatives: TEXT (64 KiB) and LONGTEXT (4GiB)
fwd2sql.put("comhandle", "text");
fwd2sql.put("date", "date");
fwd2sql.put("datetime", "datetime(3)");
fwd2sql.put("datetimetz", "timestamp(3)"); // lacks "with time zone". All values adjusted automatically to session's time zone
fwd2sql.put("decimal", "decimal(50," + SCALE_TOK + ")");
fwd2sql.put("handle", "bigint");
fwd2sql.put("integer", "integer");
fwd2sql.put("Integer", "integer");
fwd2sql.put("int64", "bigint");
fwd2sql.put("logical", "boolean");
fwd2sql.put("Long", "bigint");
fwd2sql.put("object", "bigint");
fwd2sql.put("String", "varchar(" + SCALE_TOK + ")");
fwd2sql.put("recid", "bigint"); // not integer ?
fwd2sql.put("raw", "varbinary(" + SCALE_TOK + ")");
fwd2sql.put("rowid", "bigint"); // UUID desired
// prepare supported date values. We use the GMT timezone to define our date range, since
// this is how com.goldencode.p2j.util.date handles its dates.
TimeZone tz = TimeZone.getTimeZone("GMT");
GregorianCalendar cal = new GregorianCalendar(tz);
cal.set(Calendar.ERA, GregorianCalendar.AD);
cal.set(Calendar.YEAR, 1000);
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DATE, 1);
lowDate = new Date(cal.getTime().getTime()); // 1000-01-01 AD
cal.clear();
cal.set(Calendar.ERA, GregorianCalendar.AD);
cal.set(Calendar.YEAR, 9999);
cal.set(Calendar.MONTH, Calendar.DECEMBER);
cal.set(Calendar.DATE, 31);
highDate = new Date(cal.getTime().getTime()); // 9999-12-31 AD
}
/**
* Get a collection of reserved keywords specific to this dialect.
* <p>
* The {@link com.goldencode.p2j.convert.NameConverter} uses thes list to create an exclusion
* list of names so there will not be schema collisions with identifiers (tables, fields) from
* the generated schema.
*
* @return A list with MariaDb reserved keywords.
*
* @see <a href="https://msdn.microsoft.com/en-us/library/ms189822%28v=sql.110%29.aspx">
* https://msdn.microsoft.com/en-us/library/ms189822%28v=sql.110%29.aspx</a>
*/
public static List<String> getReservedKeywords()
{
// SQL Server keywords, some may overlap ANSI
String[] keywords = {
"accessible",
"add",
"all",
"alter",
"analyze",
"and",
"as",
"asc",
"asensitive",
"before",
"between",
"bigint",
"binary",
"blob",
"both",
"by",
"call",
"cascade",
"case",
"change",
"char",
"character",
"check",
"collate",
"column",
"condition",
"constraint",
"continue",
"convert",
"create",
"cross",
"current_date",
"current_role",
"current_time",
"current_timestamp",
"current_user",
"cursor",
"database",
"databases",
"day_hour",
"day_microsecond",
"day_minute",
"day_second",
"dec",
"decimal",
"declare",
"default",
"delayed",
"delete",
"delete_domain_id",
"desc",
"describe",
"deterministic",
"distinct",
"distinctrow",
"div",
"do_domain_ids",
"double",
"drop",
"dual",
"each",
"else",
"elseif",
"enclosed",
"escaped",
"except",
"exists",
"exit",
"explain",
"false",
"fetch",
"float",
"float4",
"float8",
"for",
"force",
"foreign",
"from",
"fulltext",
"general",
"grant",
"group",
"having",
"high_priority",
"hour_microsecond",
"hour_minute",
"hour_second",
"if",
"ignore",
"ignore_domain_ids",
"ignore_server_ids",
"in",
"index",
"infile",
"inner",
"inout",
"insensitive",
"insert",
"int",
"int1",
"int2",
"int3",
"int4",
"int8",
"integer",
"intersect",
"interval",
"into",
"is",
"iterate",
"join",
"key",
"keys",
"kill",
"leading",
"lead",
"leave",
"left",
"like",
"limit",
"linear",
"lines",
"load",
"localtime",
"localtimestamp",
"lock",
"long",
"longblob",
"longtext",
"loop",
"low_priority",
"master_heartbeat_period",
"master_ssl_verify_server_cert",
"match",
"maxvalue",
"mediumblob",
"mediumint",
"mediumtext",
"middleint",
"minute_microsecond",
"minute_second",
"mod",
"modifies",
"natural",
"not",
"no_write_to_binlog",
"null",
"numeric",
"offset",
"on",
"optimize",
"option",
"optionally",
"or",
"order",
"out",
"outer",
"outfile",
"over",
"page_checksum",
"parse_vcol_expr",
"partition",
"position",
"precision",
"primary",
"procedure",
"purge",
"range",
"read",
"reads",
"read_write",
"real",
"recursive",
"ref_system_id",
"references",
"regexp",
"release",
"rename",
"repeat",
"replace",
"require",
"resignal",
"restrict",
"return",
"returning",
"revoke",
"right",
"rlike",
"rows",
"schema",
"schemas",
"second_microsecond",
"select",
"sensitive",
"separator",
"set",
"show",
"signal",
"slow",
"smallint",
"spatial",
"specific",
"sql",
"sqlexception",
"sqlstate",
"sqlwarning",
"sql_big_result",
"sql_calc_found_rows",
"sql_small_result",
"ssl",
"starting",
"stats_auto_recalc",
"stats_persistent",
"stats_sample_pages",
"straight_join",
"table",
"terminated",
"then",
"tinyblob",
"tinyint",
"tinytext",
"to",
"trailing",
"trigger",
"true",
"undo",
"union",
"unique",
"unlock",
"unsigned",
"update",
"usage",
"use",
"using",
"utc_date",
"utc_time",
"utc_timestamp",
"values",
"varbinary",
"varchar",
"varcharacter",
"varying",
"when",
"where",
"while",
"window",
"with",
"write",
"xor",
"year_month",
"zerofill",
};
return Arrays.asList(keywords);
}
/**
* Get dialect id.
*
* @return the id of the dialect.
*/
@Override
public String id()
{
return MariaDbLenientDialect.DIALECT_ID;
}
/**
* Get the name of the JDBC database type (as in JBC URL).
*
* @return The name of the JDBC database type.
*/
@Override
public String getJdbcDatabaseType()
{
return "mariadb";
}
/**
* Get the name of the JDBC driver class associated with this dialect.
*
* @return The fully qualified driver class name.
*
* @see <a href="https://mariadb.org/connector-java/all-releases/">Download MariaDB Connector/J</a>
*/
@Override
public String getDriverClassName()
{
return "org.mariadb.jdbc.Driver";
}
/**
* Get c3p0 ConnectionCustomizer class for the dialect.
* @param cfg
* Database configuration.
* @return c3p0 ConnectionCustomizer class for the dialect.
*/
@Override
public Class<? extends AbstractConnectionCustomizer> connectionCustomizer(DatabaseConfig cfg)
{
return FWDMariaDBConnectionCustomizer.class;
}
/**
* Activate error handler at the database side.
* @param conn
* Database connection
* @throws SQLException
* on error.
*/
@Override
public void activateErrorHandler(Connection conn)
throws SQLException
{
conn.prepareCall("{ call activateErrorHandler()}").executeUpdate();
}
/**
* Reset error handler at the database side.
* @param conn
* Database connection
* @throws SQLException
* on error.
*/
@Override
public void resetErrorHandler(Connection conn)
throws SQLException
{
conn.prepareCall("{ call resetErrorHandler()}").executeUpdate();
}
/**
* Get a new instance of a splitter for DDL scripts. Reuses the splitter for PG dialect.
*
* @return A new instance of splitter for DDL scripts.
*/
@Override
public ScriptSplitter scriptSplitter()
{
return new P2JPostgreSQLDialect.PgScriptSplitter()
{
@Override
public State state(String line)
{
if (line != null)
{
if (line.startsWith("DELIMITER ") || line.startsWith("$$") || line.startsWith(";"))
{
return State.IGNORE;
}
if (line.startsWith("##"))
{
if (currentState == State.DDL)
{
return currentState;
}
return State.IGNORE;
}
}
return super.state(line);
}
};
}
/**
* Get a ScriptRunner instance for the dialect.
*
* @return a ScriptRunner instance for the dialect.
*
*/
public ScriptRunner getScriptRunner()
{
return new MariaDbScriptRunner(this);
}
/**
* Create a {@link Blob} instance from the specified bytes.
*
* @param conn
* The JDBC connection.
* @param bytes
* The blob data.
*
* @return The blob instance.
*/
@Override
public Blob blobCreator(Connection conn, byte[] bytes)
{
try
{
Blob blob = conn.createBlob();
blob.setBytes(1, bytes);
return blob;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
/**
* Create a {@link Clob} instance from the specified value.
*
* @param conn
* The JDBC connection.
* @param value
* The clob data.
*
* @return The clob instance.
*/
@Override
public Clob clobCreator(Connection conn, String value)
{
try
{
Clob clob = conn.createClob();
clob.setString(1, value);
return clob;
}
catch (SQLException e)
{
throw new RuntimeException(e);
}
}
private static boolean udf4contains = true; // just to allow dynamic value changes in debug
/**
* Check if UDF should be used for converted CONTAINS operator.
*
* @return {@code true} if UDF should be used for converted CONTAINS operator.
*/
@Override
public boolean useUdf4Contains()
{
UnimplementedFeature.missing("MariaDbDialect.useUdf4Contains()");
return udf4contains;
}
/**
* Check if word tables should be used for converted CONTAINS operator.
*
* @return {@code true} if tables should be used for converted CONTAINS operator.
*/
public boolean useWordTables()
{
return false;
}
/**
* Check if native UDFs are supported for the dialect.
*
* @return {@code true} since we want tho TEST this NEW feature.
*/
@Override
public boolean isNativeUDFsSupported()
{
return true;
}
/**
* Get name of the checkError UDF.
* @return the name of the checkError UDF.
*/
@Override
public String checkErrorFn()
{
return "checkError_BB";
}
/**
* Get name of the initError UDF.
* @return the name of the initError UDF.
*/
@Override
public String initErrorFn()
{
return "initError_B";
}
/**
* Check if the exception was caused by an error in UDF.
*
* @param e
* The exception in question.
*
* @return {@code true} if the exception was caused by error in UDF.
*/
public boolean isUdfOriginatedError(SQLException e)
{
return "FWD00".equals(e.getSQLState());
}
/**
* Get regex Pattern for parsing message cased by error raised by UDF.
*
* @return regex Pattern for parsing message cased by error raised by UDF.
*/
public Pattern getUdfErrorMessagePattern()
{
return UDF_ERROR_MESSAGE;
}
/**
* Check if the exception was caused by a warning in UDF.
*
* @param e
* The exception in question.
*
* @return {@code true} if the exception was caused by error in UDF.
*/
public boolean isUdfOriginatedWarning(SQLException e)
{
return "01FWD".equals(e.getSQLState());
}
/**
* Create the error descriptor from the {@code SQLException} message
*
* @param message
* {@code SQLException} message
*
* @return Error descriptor.
*/
@Override
public ErrorEntry errorEntry(String message)
{
Matcher matcher = UDF_ERROR_MESSAGE.matcher(message);
if (matcher.matches() && matcher.groupCount() == 5)
{
try
{
return new ErrorEntry(Integer.parseInt(matcher.group(5)),
matcher.group(3), !StringUtils.isBlank(matcher.group(2)));
}
catch (NumberFormatException e)
{
// no-op
}
}
return new ErrorEntry(-1, message, false);
}
/**
* Worker method for {@code generateTriggerDDLs()}. It generates the DDLs needed to create all
* triggers in a database with a specific schema and using a certain dialect.
*
* @param dbName
* The target schema.
* @param out
* The {@code PrintStream} used for output.
* @param eoln
* OS-specific end of line terminator.
* @param wordTables
* The collection of word tables, ordered by name.
*/
@Override
public void generateWordTablesDDLImpl(String dbName,
PrintStream out,
String eoln,
Collection<WordTable> wordTables)
{
UnimplementedFeature.missing("MariaDbDialect.generateWordTablesDDLImpl()");
out.print("-- MISSING: MariaDbDialect.generateWordTablesDDLImpl()");
out.print(eoln);
}
/**
* Format a date object as a string which is appropriate for use in an SQL statement. The preferred format
* is {@code YYYY-MM-DD}.
*
* @param d
* Date to be formatted as a string.
*
* @return Formatted representation of the given date.
*
* @see <a href="https://mariadb.com/kb/en/date/">DATE Syntax</a>
*/
@Override
public String formatDate(date d)
{
NumberFormat nf = NumberFormat.getInstance();
nf.setMinimumIntegerDigits(4);
nf.setGroupingUsed(false);
StringBuilder buf = new StringBuilder();
int year = d.getYear();
buf.append(nf.format(Math.abs(year))).append('-');
nf.setMinimumIntegerDigits(2);
buf.append(nf.format(d.getMonth())).append('-').append(nf.format(d.getDay()));
if (year < 0)
{
buf.append(" BC");
}
return buf.toString();
}
/**
* Given a column expression as queried from database metadata, extract the root name of a column.
* <p>
* Since the {@code VARCHAR(n)} datatype does not require the column to be processed in any way before
* being added to an index, this method returns the parameter unchanged.
*
* @param expr
* Expression containing column name as natively represented by the database dialect.
*
* @return Basic column name.
*/
@Override
public String extractColumnName(String expr)
{
return expr;
}
/**
* Adjust a character column name appropriate to this dialect, such that it can be inserted into an index.
* This may involve wrapping it within one or more functions, prepending a special prefix to designate a
* computed column, or any other transformation appropriate to the database syntax.
*
* TODO: the case-sensitive fields might present the "binary" prefix.
*
* @param name
* Base column name.
* @param ignoreCase
* {@code true} to cause case to be ignored, else {@code false}.
*
* @return Processed column name.
*/
@Override
public String getProcessedCharacterColumnName(String name, boolean ignoreCase)
{
// If we observe the indexes on VARCHAR columns will not work as expected we will inject computed columns
// for the fields needed to be indexed. For the moment we hope the automatic TRIM-ing will work and get
// this feature for free.
return name;
// StringBuilder buf = new StringBuilder();
// if (ignoreCase)
// {
// buf.append("upper(");
// }
// buf.append("rtrim(").append(name).append(")");
// if (ignoreCase)
// {
// buf.append(")");
// }
//
// return buf.toString();
}
/**
* Report whether the given column name represents an alias or an expression which indicates the column is
* case-insensitive. Assumes the column represented by <code>name</code> is already determined to be the
* appropriate data type for textual data.
*
* @param name
* Column name, alias, or expression.
*
* @return {@code true} if {@code name} represents case-insensitive data.
*/
@Override
public boolean isCaseInsensitiveColumn(String name)
{
return true;
// return !name.startsWith("binary ");
}
/**
* Indicate whether this dialect requires computed columns to participate
* within index definitions, instead of embedded expressions.
*
* @return {@code true} to indicate computed columns are required;
* {@code false} if the backing database can embed an expression within an index definition.
*/
@Override
public boolean needsComputedColumns()
{
return true;
}
/**
* Returns {@code true} if the dialect handles automatically rtrimming for
* character columns.
*
* @return as described above.
*/
public boolean isAutoRtrim()
{
return true;
}
/**
* Returns {@code true} if the dialect handles case-insensitive compare
* operations for case-insensitive character columns
*
* @return as described above.
*/
public boolean isAutoCi()
{
return true;
}
/**
* Get the prefix, if any, which should be prepended to computed column and/or property names. In the case
* of this dialect the SQL server handles automatically the case-insensitive strings the same way as 4GL.
* The computed column is a NO-OP.
*
* @param caseSensitive
* {@code true} if this is a character field was declared with {@code CASE-SENSITIVE} attribute.
* In each case the column name will be name-decorated differently.
*
* @return Computed column/property prefix, or {@code null} if the backing database does not use computed
* columns.
*
* @see #needsComputedColumns()
*/
@Override
public String getComputedColumnPrefix(boolean caseSensitive)
{
return "";
}
/**
* Returns the formula for character computed columns. In the case of this dialect the SQL server handles
* automatically the case-insensitive strings the same way as 4GL. The computed column is a NO-OP.
* The case-sensitive cases must be handled, later, on a fully qualified column, by adding {@code binary}
* token.
*
* @param colName
* The base column name.
* @param ignoreCase
* {@code true} if this is a char field which has the "ignore case" flag set.
*
* @return The column name.
*/
@Override
public String getComputedColumnFormula(String colName, boolean ignoreCase)
{
return colName;
}
/**
* Report whether the given column name represents an alias or an expression which indicates the column is
* computed. Assume the column represented by {@code name} is already determined to be the appropriate data
* type for textual data.
*
* @param name
* Column name, alias, or expression.
*
* @return always {@code false}.
*/
@Override
public boolean isComputedColumn(String name)
{
// MariaDb cannot properly distinct whether a column is computed or not
return false;
}
/**
* Indicate whether this dialect natively supports the overloading of user defined functions by parameter
* number and data type. If not, the overloaded UDFs must be decorated in some way.
*
* @return {@code false} since the backing database does not support overloading natively.
*/
@Override
public boolean supportsFunctionOverloading()
{
return false;
}
/**
* Indicates whether this dialect supports boolean datatype. Support includes:
* <ul>
* <li>usage of {@code true} and {@code false} literals;
* <li>operations with boolean values;
* <li>allowing functions to have booleans as parameters and return datatype.
* </ul>
*
* @return {@code true} if the above-mentioned items are supported or {@code false} if they are handled by
* a replacement datatype (integer).
*
* @see <a href="https://mariadb.com/kb/en/boolean/">BOOLEAN Syntax</a>
*/
@Override
public boolean supportsBooleanDatatype()
{
return true;
}
/**
* Generate a DDL string which will create a global sequence with the given starting value.
*
* @param name
* Sequence name.
*
* @return Dialect-specific string to create the sequence, or {@code null} if not supported.
*
* @see <a href="https://mariadb.com/kb/en/create-sequence/">CREATE SEQUENCE Syntax</a>
*/
@Override
public String getCreateSequenceString(String name)
{
return "create sequence " + name;
}
/**
* Generate a DDL string which will create a global sequence with the given starting value.
*
* @param name
* Sequence name.
* @param start
* Sequence starting value.
*
* @return Dialect-specific string to create the sequence, or {@code null} if not supported.
*
* @see <a href="https://mariadb.com/kb/en/create-sequence/">CREATE SEQUENCE Syntax</a>
*/
@Override
public String getCreateSequenceString(String name, long start)
{
return "create sequence " + name + " start with " + start;
}
/**
* Generate a DDL string which will create a sequence with given properties.
*
* @param name
* Sequence name. Mandatory not null.
* @param init
* Sequence starting value. Mandatory(not null).
* @param increment
* Sequence incrementing value. Mandatory(not null).
* @param minVal
* Sequence minimum value. Optional (may be null).
* @param maxVal
* Sequence maximum value. Optional (may be null).
* @param cycle
* Sequence cycling property. Optional, if missing assumed not cycling.
*
* @return Dialect-specific string to create the sequence, or {@code null} if not supported.
*
* @see <a href="https://mariadb.com/kb/en/create-sequence/">CREATE SEQUENCE Syntax</a>
*/
@Override
public String getCreateSequenceString(String name,
Long init,
Long increment,
Long minVal,
Long maxVal,
Boolean cycle)
{
// compute the default values
if (increment == null || increment == 0)
{
increment = 1L;
}
long calcMinVal = (minVal != null) ? minVal : (increment > 0 ? 1L : Long.MIN_VALUE + 1);
long calcMaxVal = (maxVal != null) ? maxVal : (increment > 0 ? Long.MAX_VALUE - 1 : -1L);
if (init == null)
{
init = (increment > 0) ? calcMinVal : calcMaxVal;
}
// validation
boolean minAltered = false;
boolean maxAltered = false;
String minStr = String.valueOf(calcMinVal);
String maxStr = String.valueOf(calcMaxVal);
if (calcMaxVal < init)
{
calcMaxVal = init;
maxStr = calcMaxVal + " /* adjusted from " + maxStr + " */";
maxAltered = true;
}
else if (calcMaxVal == Long.MAX_VALUE)
{
calcMaxVal = Long.MAX_VALUE - 1;
maxStr = calcMaxVal + " /* adjusted from " + Long.MAX_VALUE + " */";
maxAltered = true;
}
if (calcMinVal > init)
{
calcMinVal = init;
minStr = calcMinVal + " /* adjusted from " + minStr + " */";
minAltered = true;
}
else if (calcMinVal == Long.MIN_VALUE)
{
calcMinVal = Long.MIN_VALUE + 1;
minStr = calcMinVal + " /* adjusted from " + Long.MIN_VALUE + " */";
minAltered = true;
}
assert (calcMaxVal >= calcMinVal);
StringBuilder sb = new StringBuilder("create sequence ");
sb.append(name);
sb.append(" increment by ").append(increment);
if (minVal == null && !minAltered)
{
sb.append(" nominvalue");
}
else
{
sb.append(" minvalue ").append(minStr);
}
if (maxVal == null && !maxAltered)
{
sb.append(" nomaxvalue");
}
else
{
sb.append(" maxvalue ").append(maxStr);
}
sb.append(" start with ").append(init);
sb.append(cycle != null && cycle ? " cycle" : " nocycle");
return sb.toString();
}
/**
* Generate a DDL string which will drop a sequence.
*
* @param name
* The name of the sequence to be dropped.
*
* @return Dialect-specific string to drop the sequence, or {@code null} if not supported.
*
* @see <a href="https://mariadb.com/kb/en/create-sequence/">DROP SEQUENCE Syntax</a>
*/
@Override
public String getDropSequenceString(String name)
{
return "drop sequence if exists " + name;
}
/**
* Generate the appropriate select statement to reset the value of a sequence.
*
* @param sequenceName
* The name of the sequence.
* @param newVal
* The new value to be set.
*
* @return Dialect-specific string to set the current value of the sequence, or {@code null}
* if not supported.
*
* @see <a href="https://mariadb.com/kb/en/setval/">SETVAL() Function</a>
*/
@Override
public String getSequenceSetValString(String sequenceName, long newVal)
{
return "select setval(" + sequenceName + ", " + newVal + ", true)";
}
/**
* Generate the appropriate select statement to reset the value of a sequence.
*
* @param sequenceName
* The name of the sequence.
*
* @return Dialect-specific string to set the current value of the sequence, or {@code null}
* if not supported.
*
* @see <a href="https://mariadb.com/kb/en/setval/">SETVAL() Function</a>
*/
@Override
public String getSequenceSetValString(String sequenceName)
{
return "select setval(" + sequenceName + ", ?)";
}
/**
* Generate the appropriate select statement to reset the value of a sequence.
*
* @return Dialect-specific string to set the current value of the sequence, or {@code null}
* if not supported.
*
* @see <a href="https://mariadb.com/kb/en/setval/">SETVAL() Function</a>
*/
@Override
public String getSequenceSetValString()
{
return "select setval(?, ?)";
}
/**
* Generate the appropriate select statement to retrieve the next value of a sequence.
*
* @param sequenceName
* The name of the sequence to be queried.
*
* @return Dialect-specific string to query the sequence, or {@code null} if not supported.
*
* @see <a href="https://mariadb.com/kb/en/next-value-for-sequence_name/">NEXT VALUE for sequence_name</a>
*/
@Override
public String getSequenceNextValString(String sequenceName)
{
return "select next value for " + sequenceName;
}
/**
* Generate the appropriate select statement to retrieve the current value of a sequence.
*
* @param sequenceName
* The name of the sequence to be queried.
*
* @return Dialect-specific string to query the sequence, or {@code null} if not supported.
*
* @see <a href="https://mariadb.com/kb/en/previous-value-for-sequence_name/">
* PREVIOUS VALUE for sequence_name</a>
*/
@Override
public String getSequenceCurrValString(String sequenceName)
{
return "select previous value for " + sequenceName;
}
/**
* Returns {@code true} if this dialect fully support the sequences as 4GL language (most important:
* bounded values, cycle). If not fully supported, additional code is needed in P2J sequence class
* implementation to fix this.
*
* @return {@code true} if this dialect fully support 4GL sequences
*/
@Override
public boolean hasFullSequenceSupport()
{
return true;
}
/**
* Generate the appropriate select statement to pre-fetch multiple, ascending values from a sequence.
* The values need not be contiguous.
*
* @param sequenceName
* The name of the sequence from which the values are to be retrieved.
*
* @return At the moment MariaDb does not support prefetch for sequences. Returning {@code null}.
*
* @see <a href="https://mariadb.com/kb/en/sequence-functions/">SEQUENCE Functions</a>
*/
@Override
public String getSequencePrefetchString(String sequenceName)
{
return null;
}
/**
* Given an exception, this method will check if it indicates a database connectivity error.
*
* @param exc
* The exception to be checked.
*
* @return {@code true} if the exception indicates a database connectivity error.
*
* @see <a href="https://mariadb.com/kb/en/mariadb-error-codes/">MariaDB Error Codes</a>
*/
@Override
public boolean isConnectionError(Exception exc)
{
Throwable next = exc;
while (next != null)
{
if (next instanceof SQLException)
{
SQLException sqlEx = (SQLException) next;
String state = sqlEx.getSQLState();
int errorCode = sqlEx.getErrorCode();
return "08S01".equals(state) ||
"08004".equals(state) ||
"HY000".equals(state) &&
(errorCode == 1077 || // ER_NORMAL_SHUTDOWN
errorCode == 1078 || // ER_GOT_SIGNAL
errorCode == 1079); // ER_SHUTDOWN_COMPLETE
}
next = next.getCause();
}
return false;
}
/**
* Indicate whether a query parameter which is part of a range check should
* be inlined into the where clause, or whether it should be substituted
* into a prepared statement. This will result in an "inlined"
* substitution parameter within a where clause. For example, given a
* where clause of:
* <pre>
* where value ≥ ? and id = ?
* </pre>
* along with respective substitution parameters of 45 and 100, the
* inlined version would read:
* <pre>
* where value ≥ 45 and id = ?
* </pre>
* Only the <code>id</code> substitution parameter of 100 would be
* substituted into the prepared statement.
* <p>
* Inlining in such a way can give some database dialects the opportunity
* to choose a better query plan, than if the parameter being tested with
* a range check were to be substituted at statement execution time.
* <p>
* Note: this serves as a hint which may be overridden by runtime logic or
* other configuration settings.
*
* @return <code>true</code> to inline range check parameters;
* <code>false</code> to use normal parameter substitution.
*/
@Override
public boolean isQueryRangeParameterInlined()
{
return inlineRangeParms;
}
/**
* Enable/disable limit clause inlining. Inlining the limit clause will cause the limit/max value to be
* hard-coded into the SQL statement, rather than being bound as a substitution parameter.
* <p>
* Dialects are free to treat this method as a hint.
*
* @param on
* {@code true} to enable inlining; {@code false} to disable it.
*/
@Override
public void inlineLimit(boolean on)
{
// ignoring this directive, for the moment
}
/**
* Determines whether substitution parameters should be explicitly cast to their datatypes inside
* {@code "then ? else ?"} statement.
*
* @return {@code true}: this dialect requires casting in this case.
*/
@Override
public boolean requiresExplicitCastInsideTernary()
{
return true;
}
/**
* Build the DDL to drop an index based upon the specified definition. The SQL is generated by using the
* database-specific dialect.
*
* @param index
* Definition of the index to be dropped.
*
* @return The DDL to drop the given index.
*
* @see <a href="https://mariadb.com/kb/en/drop-index/">DROP INDEX Syntax</a>
*/
@Override
public String getDropIndexString(P2JIndex index)
{
// unlike the other dialects, in MariaDb, the index names are NOT unique between tables, so the table
// name is mandatory in this statement
return "drop index if exists " + quote(getSqlIndexName(index)) + " on " + index.getTable();
}
/**
* Prepare the given metadata query parameter.
*
* @param param
* The metadata query parameter.
*
* @return the prepared parameter.
*/
@Override
public String prepareMetadataParameter(String param)
{
return param == null ? null : param.toLowerCase();
}
/**
* Process the given metadata query result.
*
* @param value
* The metadata query result.
*
* @return the processed result.
*/
@Override
public String processMetadataResult(String value)
{
return value == null ? null : value.toLowerCase();
}
/**
* Check if the given value is an ascending clause for a column index.
*
* @param value
* The value to be checked.
*
* @return {@code true} if the value is the "ascending" clause.
*/
@Override
public boolean isMetadataSortDesc(String value)
{
return "D".equalsIgnoreCase(value);
}
/**
* Check if the given value is a descending clause for a column index.
*
* @param value
* The value to be checked.
*
* @return {@code true} if the value is the "descending" clause.
*/
@Override
public boolean isMetadataSortAsc(String value)
{
return "A".equalsIgnoreCase(value);
}
/**
* Check if the dialect needs to set an explicit collation during schema DDL generation.
*
* @return {@code true} if the collation needs to be explicitly set.
*/
@Override
public boolean explicitSetCollation()
{
return false;
}
/**
* Get the statement delimiter for this dialect.
*
* @return the statement delimiter.
*/
@Override
public String getDelimiter()
{
return ";";
}
/**
* Build the URL to be used for a remote connection, based on the given URL and database.
*
* @param url
* The URL as received from the remote server.
* @param database
* The database to which this URL belongs.
*
* @return The built URL as to be used for a remote connection.
*
* @throws PersistenceException
* if the specified connection URL is invalid.
*/
@Override
public String buildRemoteURL(String url, Database database)
throws PersistenceException
{
return DialectHelper.buildRemoteURL(url, database);
}
/**
* Creates and returns a {@code SequenceHandler} for a database.
*
* @param ldbName
* The logical name of the database to create the {@code SequenceHandler} for.
*
* @return A {@code SequenceHandler} for respective database.
*/
@Override
public SequenceHandler createSequenceHandler(String ldbName)
{
return new MariaDbSequenceHandler(ldbName);
}
/**
* This MariaDb dialect variant does NOT honour the "nulls last" feature of 4GL.
*
* @return {@code false}.
*/
@Override
public boolean injectComputedColumns()
{
return false;
}
/**
* Checks if the SQLState of the SQLException matches the dialect specific UNIQUE_VIOLATION
* code for MariaDB (23000).
*
* @param sqle
* The SQLException that is causing the unique constraint violation.
*
* @return <code>true</code> if the SQLState matches the dialect specific code for
* UNIQUE_VIOLATION, <code>false</code> otherwise.
*/
@Override
public boolean isUniqueConstraintViolation(SQLException sqle)
{
String sqleState = sqle.getSQLState();
if ("23000".equals(sqleState))
{
return true;
}
return false;
}
/**
* Returns the size of the synthetic part of the index. At this moment only the {@code id} of the record
* may be additionally added to an index to add custom properties (eg uniqueness) but other fields could be
* added. This part of the index is not visible using index reflection routines from {@code IndexHelper}.
* The synthetic components may differ between unique and non-unique indexes.
*
* @param unique
* {@code true} if the index is unique
*
* @return The size of synthetic part of the index, in bytes.
*/
@Override
public int getSyntheticIndexSize(boolean unique)
{
return 8;
}
/**
* Composes a {@code rtrim} equivalent call for a specified expression. This is dialect specific. Usually
* the returned value looks like:
* <pre>
* rtrim(expr)
* </pre>
* The {@code rtrim}-med expression is appended to the string builder. The return value (the number of
* occurrences of the initial {@code expr} added to {@code sb}) should be use by caller to count the number
* of possible parameters from a query.
*
* @param expr
* The expression to be {@code rtrim}-med. It's up to the caller if it is a qualified field or not
* or any other kind of character-type expression.
* @param sb
* The {@link StringBuilder} to append the trimmed field to.
*/
@Override
public void addRtrimmedExpression(String expr, StringBuilder sb)
{
sb.append("rtrim(").append(expr).append(")");
}
/**
* Return the beginning of the DDL statement that creates a temporary table.
*
* @param ifNotExist
* If {@code true} a dialect-specific, optional {@code if not exist} clause is added.
*
* @return A string representing the beginning of the DDL statement that creates a temporary table.
*
* @see <a href="https://mariadb.com/kb/en/create-table/">CREATE TABLE Syntax</a>
*/
@Override
public String getCreateTemporaryTableString(boolean ifNotExist)
{
return "create temporary table " + (ifNotExist ? "if not exists " : "");
}
/**
* Return the ending of the DDL statement that creates a temporary table.
*
* @param noUndo
* This parameter is not used at the moment. The intention was to generate a keyword for
* no-undo tables. Only {@link P2JH2Dialect} benefits from this parameter.
*
* @return A string representing the ending of the DDL statement that creates a temporary table, if any,
* or empty string otherwise.
*
* @see <a href="https://mariadb.com/kb/en/create-table/#transactional">CREATE TABLE TRANSACTIONAL</a>
*/
@Override
public String getCreateTemporaryTablePostfix(boolean noUndo)
{
return " transactional";
}
/**
* Creates and returns a string which represent the {@code alter table} statement for adding a new
* foreign key constraint.
*
* @param table
* The table to be altered.
* @param fkName
* The name of the constraint.
* @param fkFields
* Comma separated names of the fields which make the reference to parent table.
* @param refTable
* The parent / referred table.
* @param refFields
* Comma separated names of the fields which uniquely identify the record in parent table.
* @param indent
* The indentation for pretty print the result.
* @param eoln
* OS-specific end of line terminator (if the result is a multiline).
*
* @return A string used to create the foreign key constraint as described above.
*/
@Override
public String getAddForeignKeyConstraintString(String table,
String fkName,
String fkFields,
String refTable,
String refFields,
String indent,
String eoln)
{
if (indent == null)
{
indent = "";
}
if (eoln == null || eoln.isEmpty())
{
eoln = " ";
}
return "alter table " + table + eoln +
indent + "add constraint " + fkName + eoln +
indent + "foreign key (" + fkFields + ")" + eoln +
indent + "references " + refTable + "(" + refFields + ")" + eoln +
indent + "on delete cascade";
}
/**
* Provides catalog matching pattern to load data correctly from JDBC.
*
* @param database
* database object
* @return catalog pattern
*/
@Override
public String getCatalogPattern(Database database)
{
return database.getName();
}
/**
* Provides schema matching pattern to load data correctly from JDBC.
*
* @param database
* database object
* @return schema pattern
*/
@Override
public String getSchemaPattern(Database database)
{
return database.getName();
}
/**
* Provides SQL to load sequences.
*
* @param database
* database object
* @return sequence SQL
*/
@Override
public String getSequenceSQL(Database database)
{
return "SELECT `table_name` AS sequence_name FROM information_schema.tables WHERE `table_schema` = '" + database.getName() + "' AND `table_type` = 'SEQUENCE'";
}
/**
* Obtain a DDL statement which drops a SQL index.
*
* @param ifExists
* Flag that requires that the DDL include an optional {@code IF EXIST} clause.
* @param idxName
* The name of the index to be dropped by this DDL.
* @param tableName
* The name of the table to which the index belongs to. (Optional, is needed by the dialect).
*
* @return a DDL statement which drops a SQL index.
*
* @see <a href="https://mariadb.com/kb/en/drop-index/">DROP INDEX Syntax</a>
*/
@Override
public String getDropIndexString(boolean ifExists, String idxName, String tableName)
{
// unlike the other dialects, in MariaDb, the index names are NOT unique between tables, so the table
// name is mandatory in this statement
return "drop index " + (ifExists ? "if exists " : "") + idxName + " on " + tableName;
}
/**
* Obtain a DDL statement which drops a SQL table.
*
* @param ifExists
* Flag that requires that the DDL include an optional {@code IF EXIST} clause.
* @param tableName
* The name of the table to be dropped by this DDL.
*
* @return a DDL statement which drops a SQL index.
*
* @see <a href="https://mariadb.com/kb/en/drop-table/">DROP TABLE Syntax</a>
*/
@Override
public String getDropTableString(boolean ifExists, String tableName)
{
return "drop table " + (ifExists ? "if exists " : "") + tableName + " cascade";
}
/**
* Construct and return a string which represents the {@code analyze} or similar statements for this
* dialect. If the {@code table} is not {@code null} the statement will analyze only the respective table.
* Otherwise, all the tables are analyzed.
*
* @param table
* The name of the table to be analyzed. If {@code null}, all tables from the database will be
* analyzed, if possible.
*
* @return The ANALYZE statement as requested. If the dialect does not support the {@code ANALYZE}
* statement in the form requested {@code null} is returned.
*
* @see <a href="https://mariadb.com/kb/en/analyze-table/">ANALYZE TABLE Syntax</a>
*/
@Override
public String getAnalyzeString(String table)
{
return table == null ? "analyze" : "analyze table " + table;
}
/**
* Converts and return the specified {@code FqlType} to a string representation which can be used in a
* SQL query ({@code cast} or other construct).
*
* @param type
* The type to be converted to string.
*
* @return A dialect specific string representation of the respective fql data type.
*/
@Override
public String getSpecificTypeAsString(FqlType type)
{
switch (type.getSqlType())
{
case Types.BIT: return "boolean";
case Types.INTEGER: return "integer";
case Types.BIGINT: return "bigint";
case Types.NUMERIC: return "decimal";
case Types.DATE: return "date";
case Types.VARBINARY: return "VARBINARY(" + MAX_VARCHAR_SIZE + ")";
case Types.TIMESTAMP: return "timestamp";
case Types.TIMESTAMP_WITH_TIMEZONE: return "timestamp"; // no time zone
case Types.ARRAY: return "array";
case Types.BLOB: return "blob";
case Types.VARCHAR: // fall through
case Types.CLOB: return "text";
default: return type.toString(); // maybe log this?
}
}
/**
* Translation from database column type to {@code ParmType}.
*
* @param type
* Column type.
* @param comment
* comment with optional specification of type
*
* @return ParmType
*
* @throws IllegalArgumentException
* for unsupported types.
*/
@Override
public ParmType translateToParmType(String type, String comment)
{
ParmType parmTypeFromComment = DialectHelper.getParmTypeFromComment(comment);
if (parmTypeFromComment != null)
{
return parmTypeFromComment;
}
switch (type.toUpperCase())
{
case "BIT":
case "BOOLEAN":
return ParmType.LOG;
case "INT":
return ParmType.INT;
case "BIGINT":
return ParmType.INT64;
case "DECIMAL":
return ParmType.DEC;
case "VARCHAR":
case "TEXT":
return ParmType.CHAR;
case "DATE":
return ParmType.DATE;
case "DATETIME":
case "DATETIME(3)":
return ParmType.DT;
case "TIMESTAMP":
case "TIMESTAMP(3)":
return ParmType.DTTZ;
case "BLOB":
return ParmType.BLOB;
case "MEDIUMTEXT":
return ParmType.CLOB;
case "VARBINARY":
return ParmType.RAW;
default:
throw new IllegalArgumentException("DB column type " + type + " is not supported");
}
}
/**
* Get the SQL mapping for a specific FWD data type.
*
* @param fwdType
* The FWD type.
* @param scale
* Optional scale parameter (for {@code decimal} data type).
*
* @return The string representation of the SQL datatype mapped by {@code fwdType}.
*/
@Override
public String getSqlMappedType(String fwdType, int scale, Boolean caseSensitive)
{
String s = fwd2sql.get(fwdType);
if (s == null)
{
CentralLogger log = getLog();
if (log.isLoggable(Level.WARNING))
{
log.log(Level.WARNING, "Unknown '" + fwdType + "' type for MariaDb dialect.");
}
return fwdType; // returning the [fwdType]!
}
int k = s.indexOf(SCALE_TOK);
if (k > 0)
{
if ("character".equalsIgnoreCase(fwdType) || "String".equalsIgnoreCase(fwdType))
{
if (scale >= MIN_TEXT_SIZE)
{
return "text";
}
if (scale == 0)
{
scale = DEFAULT_MAX_WIDTH;
}
}
return s.substring(0, k) + scale + s.substring(k + SCALE_TOK.length());
}
return s;
}
/**
* This dialect has a constraints of 3KiB on the index key size. This method does a heuristic calculation
* and report a possible error/warning when the database schema is set.
*
* @param p2JIndex
* The index to be processed.
* @param fields
* All table fields, mapped by their legacy name.
*
* @see <a href="https://mariadb.com/kb/en/data-type-storage-requirements/">Data Type Requirements<a>
*/
public void checkIndexConstraints(P2JIndex p2JIndex, Map<String, P2JField> fields)
{
int indexRowLen = 0;
for (P2JIndexComponent comp : p2JIndex.components())
{
P2JField f = fields.get(comp.getLegacyName());
if (f != null)
{
switch (f.getType())
{
case CHAR:
case LC:
case CLOB:
case TEXT:
case MEM:
case RAW:
case BIN:
if (f.getMaxWidth() > MIN_TEXT_SIZE)
{
LOG.log(Level.WARNING,
"MariaDbLenient constraint: Column '" + comp.getColumnName() +
"' has TEXT type and cannot be indexed without a specified length!");
}
indexRowLen += BPC + BPC * f.getMaxWidth(); break;
case BLOB:
indexRowLen += f.getMaxWidth(); break;
case INT:
indexRowLen += 4; break;
case NUM:
case NUM_VARARGS:
case DEC:
// Highly heuristic first implementation: "Each nine-digit multiple requires 4 bytes,
// followed by a number of bytes for whatever remains... " (see online resource)
indexRowLen += 6 * 4; break;
case ROWID:
case RECID:
case INT64:
indexRowLen += 8; break;
case DATE:
indexRowLen += 3; break;
case DT:
case DTTZ:
indexRowLen += 8; break;
case LOG:
indexRowLen += 1; break;
case COMHANDLE:
case OBJECT:
case JOBJECT:
case WID:
case HANDLE:
indexRowLen += 8; break;
case TEXT_VARARGS:
case RECORD:
case ROWID_ARRAY:
case BDT:
case BDT_ARRAY:
case BDT_VARARGS:
break; // unknown (?)
}
}
}
if (indexRowLen >= INDEX_MAX_KEY_SIZE)
{
LOG.log(Level.WARNING,
"MariaDbLenient constraint: Index '" + p2JIndex.getLegacyName() + "' exceeded 3KiB limit of MariaDb! "+
"Computed key size is: " + indexRowLen + ".");
}
}
/**
* Prepare the specified database by creating all necessary function aliases and setting global parameters.
*
* @param db
* Database to prepare.
*
* @throws PersistenceException
* if there is any error executing DDL.
*/
@Override
public void preparePermanentDatabase(Database db)
throws PersistenceException
{
MariaDbHelper.registerOverloadedFunctions(db);
}
/**
* Obtains an object responsible with providing dialect specific SQL statements for tenant management.
*/
@Override
public Dialect.TenantSQL getTenantSql()
{
return new Dialect.TenantSQL()
{
/** SQL for creating the master table, if it doesn't already exist. */
@Override
public String getCreateMasterTable()
{
return "CREATE TABLE IF NOT EXISTS master (\n"+
" tenant_id UUID NOT NULL,\n" +
" tenant_name VARCHAR(255) NOT NULL,\n" +
" tenant_description VARCHAR(1023),\n" +
" tenant_info VARCHAR(1023),\n" +
" status BOOLEAN,\n" +
" PRIMARY KEY(tenant_id))";
}
/**
* Obtain the SQL statement for creating the {@code tenant_databases} table, if it doesn't already
* exist. The statement must declare at least the following fields: {@code tenant_name (string)},
* {@code pdb_name (string)}, {@code ldb_name (string)}, {@code url (string)},
* {@code username (string)}, {@code password (string)}, {@code C3P0_MAX_STMTS (integer)},
* {@code C3P0_MIN_POOL (integer)}, {@code C3P0_MAX_POOL (integer)}, {@code C3P0_ACQ_INC (integer)},
* {@code C3P0_MAX_IDLE (integer)}.
*
* @return the SQL statement for creating the {@code tenant_databases} table, if it doesn't already
* exist.
*/
@Override
public String getCreateDatabasesTable()
{
return "CREATE TABLE IF NOT EXISTS tenant_databases (\n" +
" tenant_name VARCHAR(255) NOT NULL,\n" +
" pdb_name VARCHAR(255) NOT NULL,\n" +
" ldb_name VARCHAR(255) NOT NULL,\n" +
" url VARCHAR(1023),\n" +
" username VARCHAR(255),\n" +
" password VARCHAR(255),\n" +
" " + C3P0_MAX_STMTS + " INTEGER,\n" +
" " + C3P0_MIN_POOL + " INTEGER,\n" +
" " + C3P0_MAX_POOL + " INTEGER,\n" +
" " + C3P0_ACQ_INC + " INTEGER,\n" +
" " + C3P0_MAX_IDLE + " INTEGER,\n" +
" PRIMARY KEY(tenant_name, pdb_name))";
}
/** SQL for creating an index for the master table on the {@code tenant_name} column. */
@Override
public String getCreateMasterIndex()
{
return "CREATE INDEX IF NOT EXISTS master_idx ON master (tenant_name)";
}
/**
* Obtain the SQL statement for creating an index for the {@code tenant_databases} table.
*
* @return the SQL statement for creating an index for the {@code tenant_databases} table.
*/
@Override
public String getCreateTenantDbIndex()
{
return "CREATE INDEX IF NOT EXISTS tenant_db_idx ON tenant_databases (tenant_name, pdb_name)";
}
};
}
/**
* Use the provided database connection to extract the codepage/CharSet used by the database and report it
* back to the caller.
*
* @param conn
* The JDBC database connection to be used.
*
* @return The name of the codepage/CharSet used by the database to store character data.
*/
public String getCodepage(Connection conn)
{
try
{
PreparedStatement ps = conn.prepareStatement(
"SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=DATABASE()");
ResultSet rs = ps.executeQuery();
if (rs.next())
{
return mapCP(rs.getString(1)); // DEFAULT_COLLATION_NAME
}
}
catch (SQLException e)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.warning("Failed to retrieve CODEPAGE from database.", e);
}
}
// fallback for all errors
return null;
}
/**
* Use the provided database connection to extract the collation used by the database and report it
* back to the caller.
*
* @param conn
* The JDBC database connection to be used.
*
* @return The name of the collation used by the database to store character data.
*/
@Override
public String getCollation(Connection conn)
{
try
{
PreparedStatement ps = conn.prepareStatement(
"SELECT DEFAULT_COLLATION_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME=DATABASE()");
ResultSet rs = ps.executeQuery();
if (rs.next())
{
return mapCollation(rs.getString(1)); // DEFAULT_COLLATION_NAME
}
}
catch (SQLException e)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.warning("Failed to retrieve COLLATION from database.", e);
}
}
// fallback for all errors
return null;
}
/**
* Get the logger specific to this MariaDb dialect variant.
*
* @return the logger specific to this MariaDb dialect variant.
*/
protected CentralLogger getLog()
{
return LOG;
}
/**
* Maps/extract from the MariaDb specific charset names to 4GL codepage names.
*
* @param collation
* A MariaDb specific collation name. (Ex: "latin1_swedish_ci", "cp1250_polish_ci")
*
* @return The 4GL codepage name of {@code h2Charset}.
*/
private static String mapCP(String collation)
{
if (collation == null)
{
return null;
}
int k = collation.indexOf('_');
if (k == -1)
{
return collation;
}
collation = collation.substring(0, k);
switch (collation.toUpperCase())
{
case "ASCII": return "1252"; // not perfect match
case "CP850": return "IBM850";
case "CP852": return "IBM852";
case "LATIN1": return "ISO8859-1";
case "LATIN2": return "ISO8859-2";
case "LATIN5": return "ISO8859-9";
case "LATIN7": return "ISO8859-13";
case "LATIN9": return "ISO8859-15";
case "UTF8MB4": return "UTF-8";
default: return collation;
}
}
/**
* Maps the MariaDb specific collation names to 4GL collation names.
*
* @param collation
* A MariaDb specific collation name. (Ex: "latin1_swedish_ci", "cp1250_polish_ci")
*
* @return The 4GL codepage name of {@code h2Charset}.
*/
private static String mapCollation(String collation)
{
if (collation == null)
{
return null;
}
int k = collation.indexOf('_');
if (k == -1)
{
return collation;
}
collation = collation.substring(k + 1);
k = collation.indexOf('_');
if (k == -1)
{
return collation;
}
collation = collation.substring(0, k);
switch (collation.toUpperCase())
{
case "UNICODE": return "BASIC";
default: return collation;
}
}
}