P2JPostgreSQLDialect.java
/*
** Module : P2JPostgreSQLDialect.java
** Abstract : Concrete implementation of Dialect for a PostgreSQL backend
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060929 @30034 Created initial version. Adds P2JDialect
** method implementations to extend Hibernate's
** PostgreSQLDialect.
** 002 ECF 20070514 @33483 Added extractColumnName(). Extracts a root
** column name from its native representation in
** a particular dialect.
** 003 ECF 20070923 @35221 Override superclass' VARCHAR mapping to point
** to PG's 'text' data type. This is desirable
** because we always want to use 'text' instead
** of 'varchar(n)' for string data on the
** backend.
** 004 ECF 20071002 @35334 Implemented new P2JDialect methods. Added
** createContext() and postTransaction() methods
** to enable temp table periodic vacuums.
** 005 ECF 20080311 @37518 Implemented methods for computed columns and
** user defined function overloading, which are
** not needed by this dialect but required by
** the P2JDialect interface.
** 006 ECF 20080430 @38179 Fix to permit schema generation. Defer lookup
** of vacuumInterval in directory until first
** use. Performing the directory bind during
** class initialization fails when generating
** schema via Ant task.
** 007 ECF 20080611 @38705 Added isCaseInsensitiveColumn(). Provides a
** rough test for case-insensitivity, based only
** on an expression which contains a column
** name, such as might be extracted from index
** metadata.
** 008 ECF 20080805 @39316 Added getCreateSequenceString(). Generates
** DDL to create a sequence starting at a
** particular value.
** 009 CA 20080815 @39444 Added isConnectionError method - it checks if
** a given exception represents a connection
** error.
** 010 ECF 20080930 @40001 Added isQueryRangeParameterInlined().
** Indicates whether query substitution
** parameters within range checks should be
** inlined.
** 011 ECF 20081015 @40096 Added formatDate(). Formats a date into a
** string suitable for use in an SQL statement.
** 012 ECF 20090310 @41456 Fixed performance problem with variable limit
** clause in a SELECT statement. In cases of
** simple and very small (2 or less) record
** limits (with no offset) in a SELECT, we now
** inline the number directly in the SQL. In the
** default implementation, Hibernate would use a
** bound parameter, which could result in a poor
** query plan (sequential table scan instead of
** an index scan).
** 013 GES 20090424 @41944 Import change.
** 014 ECF 20090705 @43225 Appended 'analyze' to 'vacuum' command.
** 015 SVL 20090810 @43575 Added requiresExplicitCastInsideTernary().
** Determines whether substitution parameters should
** be explicitly casted to their datatypes inside
** "then ? else ?" statement.
** 016 GES 20100701 Added a type name override to allow honoring of
** precision and scale in numeric types.
** 017 CA 20101026 Added methods to generate the DROP/CREATE INDEX
** statements in a dialect specific form. Also,
** added method to generate the ALTER TABLE statement
** which sets the formula for the computed columns.
** Added methods to prepare/process string metadata
** query parameters and results, to get the database
** prepare DDL statements and collation statement.
** 018 CA 20101202 Added method to build the URL for a remote
** connection.
** 019 SVL 20110622 Added usesSingleBackslash.
** 020 OM 20121120 Implemented DDL/SQL generation for sequences.
** 021 SVL 20130331 Upgraded to Hibernate 4.
** 022 ECF 20130510 Modified sequence-related methods to not use quoted names. Replace
** deprecated superclass.
** 023 OM 20130605 Added if exists clause to drop index statement.
** 024 OM 20130712 Overrided default (invalid) min/max values for sequences.
** Implemented createSequenceHandler().
** 025 VMN 20131005 Added injectComputedColumns. Replaced $p to 50 in
* registerColumnType for Types.NUMERIC.
** 026 OM 20130925 Replaced apache.commons logging with p2j.util.LogHelper.
** 027 OM 20130820 Added support for mixed asc/desc indexes.
** 028 OM 20140410 Fixed support case-sensitive fields in indexes.
** Added Override annotations. Simplified string concatenations.
** 029 VMN 20140502 Added enhance schema name conversion support for hint "escape"
** attribute.
** 030 OM 20140508 Added support for computing the index-key size.
** 031 OM 20140623 Added rtrim() dialect specific implementation. Added query max
** parameters count support.
** 032 OM 20140924 Removed usesSingleBackslash.
** 033 OM 20150522 Added getReservedKeywords() static method.
** 034 OM 20151130 rtrim() dialect specific implementation returns the number of
** occurrences of the expression processed. Implemented
** supportsBooleanDatatype().
** 035 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 036 OM 20160603 Fixed rtrimming to space character only.
** 037 ECF 20160912 Added getSequencePrefetchString.
** 038 ECF 20190327 Replaced ContextLocal for inlineLimit variable with more efficient
** ThreadLocal. It is always accessed on the same thread.
** 039 OM 20200610 Removed Hibernate dependecies.
** 040 IAS 20201204 Added id() method and support for word tables
** IAS 20210219 Word tables support for custom extents
** IAS 20210303 Added useCTE4Contains() function
** CA 20210305 The object and handle fields have a Long representation at the SQL table field.
** IAS 20210310 Added implicit ordering
** IAS 20210315 Refactored for working with word tables for _temp database
** OM 20210412 Replace Hibernate-specific casts with dialect-specific casts
** IAS 20210803 Generate 'words' UDFs which can switch between using Java and SQL
** low-level UDFs
** IAS 20211223 Special processing of errors raised by UDFs.
** IAS 20220112 Resolve dialect by JDBC database type.
** TJD 20220331 Removed reference to IOUtils.toInputStream
** IAS 20220601 Do not emit 'words' UDFs definitions
** IAS 20220602 Added isNativeUDFsSupported() method.
** IAS 20220712 Added getJdbcType() method.
** OM 20220721 Added partial conversion-time MariaDb dialect support.
** IAS 20220816 Added getScriptRunner() method.
** IAS 20220913 Re-work processing of UDFs errors/warnings.
** IAS 20220921 Dialect-specific UDF schema.
** OM 20221026 Excluded table name from index name by default. It will be added as needed by
** dialects.
** RAA 20230109 Overrided method getSequenceSetValString().
** 041 RAA 20230208 Added parameter for getCreateTemporaryTablePostfix.
** 042 BS 20230331 Added support for sequences.
** 043 DDF 20230306 Added a parameter to getSqlMappedType() (refs: #7108).
** 044 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 045 AB 20230828 Overrided getMaxEntriesInSelect().
** 046 DDF 20231127 Added isUniqueConstraintViolation() method.
** 047 OM 20231218 Added implementation for DBCODEPAGE and DBCOLLATION 4GL functions.
** 048 IAS 20230208 Dialect-specific error handler support.
** 049 OM 20240131 Fixed flaw in DBCODEPAGE / DBCOLLATION implementation.
** 050 DDF 20240320 Added preferDisjunctiveForm() method.
** 051 OM 20240909 Concurrent multitenancy functionality implementation.
** 052 AL2 20241105 INCLUDE the recid into the secondary indexes.
** 053 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 054 OM 20250117 Added support for multiple dialects for multi-tenant landlord database.
** 055 RNC 20250224 Overrode isRecoverableError().
** 056 DDF 20240209 Added an additional parameter to the getComputedColumnString method.
** 057 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 java.io.*;
import java.nio.charset.Charset;
import java.sql.*;
import java.sql.Date;
import java.text.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import java.util.regex.*;
import java.util.stream.*;
import org.apache.commons.io.input.*;
import org.apache.commons.lang.StringUtils;
import com.goldencode.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.util.ErrorManager.*;
import com.mchange.v2.c3p0.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.directory.DirectoryService;
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.*;
/**
* Extends Hibernate's <code>PostgreSQLDialect</code> class to add method implementations
* required by the abstract {@link Dialect} base class.
* <p>
* Note: this implementation expects a directory to be available at the time
* the class is loaded.
*/
// TODO: implement base dialect features we need
public class P2JPostgreSQLDialect
extends Dialect
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(P2JPostgreSQLDialect.class.getName());
/** Allow runtime switch between word tables and UDF */
private static final boolean useUdf4Contains = Boolean.parseBoolean(
System.getProperty("P2JPostgreSQLDialect.useUdf4Contains", "false"));
/** Allow runtime switch between using CTE for word tables and IN (SELECT ...) */
private static final boolean useCte4Contains = Boolean.parseBoolean(
System.getProperty("P2JPostgreSQLDialect.useCte4Contains", "true"));
/** Allow runtime switch between 'pure' CTE mode and a 'mixed' one */
private static final boolean useMixedMode4Contains = Boolean.parseBoolean(
System.getProperty("P2JPostgreSQLDialect.useMixedMode4Contains", "true"));
/** Regex Pattern for parsing message cased by error raised by UDF. */
private static final Pattern UDF_ERROR_MESSAGE = Pattern.compile(
"^(ERROR|WARNING): (\\*\\* )?(.*)(\\.)? *\\((-?[0-9]*)\\).*$",
Pattern.DOTALL);
/** Weights' aggregator */
private static final WeightAggregator WEIGHT_AGGREGATOR;
static
{
String aggregatorName = System.getProperty("P2JPostgreSQLDialect.weightAggregator", "SUM");
WeightAggregator weightAggregator;
try
{
weightAggregator = StringUtils.isEmpty(aggregatorName) ? null :
Aggregator.valueOf(aggregatorName.toUpperCase());
}
catch(IllegalArgumentException e)
{
LOG.warning("Unknown aggregator name: [" + aggregatorName + "]");
weightAggregator = null;
}
WEIGHT_AGGREGATOR = weightAggregator;
}
/** Name of the primary key column. */
private static final String PK = Configuration.getSchemaConfig().getPrimaryKeyName();
/** trigger function for words tables' support template */
private static final List<String> TRIGGER_FUNC = Collections.unmodifiableList(
Arrays.asList(
"create or replace function %s()",
"returns trigger",
"language plpgsql",
"as",
"$$",
"begin",
" delete from %s where %s.parent__id = new.%s;",
" insert into %s select * from words(new.%s, new.%s, %s);",
" return new;",
"end;",
"$$;"
)
);
/** trigger function for words tables' support template (extent field) */
private static final List<String> TRIGGER_FUNC_EXT = Collections.unmodifiableList(
Arrays.asList(
"create or replace function %s()",
"returns trigger",
"language plpgsql",
"as",
"$$",
"begin",
" delete from %s where %s.parent__id = new.parent__id and %s.list__index = new.list__index;",
" insert into %s select * from words(new.parent__id, new.list__index, new.%s, %s);",
" return new;",
"end;",
"$$;"
)
);
/** trigger function for words tables' support template (custom extent field) */
private static final List<String> TRIGGER_FUNC_CEXT = Collections.unmodifiableList(
Arrays.asList(
"create or replace function %s()",
"returns trigger",
"language plpgsql",
"as",
"$$",
"begin",
" delete from %s where %s.parent__id = new.recid and %s.list__index = %d;",
" insert into %s select * from words(new.recid, %d, new.%s, %s);",
" return new;",
"end;",
"$$;"
)
);
/** ON INSERT trigger function for words tables' support template (custom extent field) */
private static final List<String> INS_TRIGGER_FUNC_CEXT = Collections.unmodifiableList(
Arrays.asList(
"create or replace function %s()",
"returns trigger",
"language plpgsql",
"as",
"$$",
"begin",
" delete from %s where %s.parent__id = new.recid;",
"%s",
" return new;",
"end;",
"$$;"
)
);
/** Repeatable line template for the ON INSERT trigger function for words tables'
* support template (custom extent field)
*/
private static final String INS_TRIGGER_LINE =
" insert into %s select * from words(new.recid, %d, new.%s, %s);";
/** AFTER INSERT trigger for words tables' support template */
private static final List<String> AFTER_INSERT_TRIGGER = Collections.unmodifiableList(
Arrays.asList(
"create trigger %s after",
"insert",
" on",
" %s for each row execute procedure %s();"
)
);
/** AFTER UPDATE trigger for words tables' support template */
private static final List<String> AFTER_UPDATE_TRIGGER = Collections.unmodifiableList(
Arrays.asList(
"create trigger %s after",
"update of %s",
" on",
" %s for each row execute procedure %s();"
)
);
/** DROP TRIGGER statement */
private static final String DROP_TRIGGER = "drop trigger if exists %s ON %s;";
/** Default minimum interval in seconds between temp table vacuums */
private static final int DEFAULT_VACUUM_INTERVAL = 30;
/** Debug level logging enabled? */
private static final boolean debug = LOG.isLoggable(Level.FINE);
/** Flag indicating whether to inline a record limit in an SQL SELECT */
private static final ThreadLocal<Boolean> inlineLimit =
new ThreadLocal<Boolean>()
{
protected Boolean initialValue() { return Boolean.FALSE; }
};
/** Lowest date supported by PostgreSQL (01/01/4713 BC) */
private static final Date lowDate;
/** Highest date supported by PostgreSQL (12/31/32767 AD) */
private static final Date highDate;
/** Actual minimum interval in milliseconds between temp table vacuums */
private static long vacuumInterval = -1L;
/** Mapping between the FWD possible field/column types and their SQL counterparts. */
private static Map<String, String> fwd2sql = new HashMap<>();
/* Initialization of static data. */
static
{
fwd2sql.put("integer", "int4");
fwd2sql.put("Integer", "int4");
fwd2sql.put("recid", "int4");
fwd2sql.put("int64", "int8");
fwd2sql.put("Long", "int8");
fwd2sql.put("rowid", "int8");
fwd2sql.put("decimal", "numeric(50," + SCALE_TOK + ")");
fwd2sql.put("logical", "bool");
fwd2sql.put("character", "text");
fwd2sql.put("String", "text");
fwd2sql.put("handle", "int8");
fwd2sql.put("comhandle", "text");
fwd2sql.put("date", "date");
fwd2sql.put("datetime", "timestamp");
fwd2sql.put("datetimetz", "timestamp with time zone");
fwd2sql.put("blob", "oid"); // "bytea");
fwd2sql.put("clob", "text");
fwd2sql.put("object", "int8");
fwd2sql.put("raw", "bytea");
// 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.BC);
cal.set(Calendar.YEAR, 4713);
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DATE, 1);
lowDate = new Date(cal.getTime().getTime());
cal.clear();
cal.set(Calendar.ERA, GregorianCalendar.AD);
cal.set(Calendar.YEAR, 32767);
cal.set(Calendar.MONTH, Calendar.DECEMBER);
cal.set(Calendar.DATE, 31);
highDate = new Date(cal.getTime().getTime());
}
/**
* Default constructor. This constructor is for Hibernate's use. Use code
* should not construct instances of this class.
* <p>
* We provide this constructor to override the superclass' mapping of
* <code>Types.VARCHAR</code>, in order to force this type to map to
* PostgreSQL's <code>text</code> data type.
*/
public P2JPostgreSQLDialect()
{
super();
// Override superclass' VARCHAR mapping, since we always use text for
// string data.
// registerColumnType(Types.VARCHAR, "text");
// Override superclass' NUMERIC mapping, since the Hibernate PostgreSQL
// dialect doesn't honor precision and scale.
// registerColumnType(Types.NUMERIC, "numeric(50,$s)");
}
/**
* Get a collection of reserved keywords specific to this dialect.
*
* 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 PostgreSQL 9.3 reserved keywords in lowercase.
*
* @see <a href="http://www.postgresql.org/docs/9.3/static/sql-keywords-appendix.html">
* http://www.postgresql.org/docs/9.3/static/sql-keywords-appendix.html</a>
*/
public static List<String> getReservedKeywords()
{
String[] keywords = {
"all",
"analyse",
"analyze",
"and",
"any",
"array",
"as",
"asc",
"asymmetric",
"authorization",
"binary",
"both",
"case",
"cast",
"check",
"collate",
"collation",
"column",
"concurrently",
"constraint",
"create",
"cross",
"current_catalog",
"current_date",
"current_role",
"current_schema",
"current_time",
"current_timestamp",
"current_user",
"default",
"deferrable",
"desc",
"distinct",
"do",
"else",
"end",
"except",
"false",
"fetch",
"for",
"foreign",
"freeze",
"from",
"full",
"grant",
"group",
"having",
"ilike",
"in",
"initially",
"inner",
"intersect",
"interval",
"into",
"is",
"isnull",
"join",
"lateral",
"leading",
"left",
"like",
"limit",
"localtime",
"localtimestamp",
"natural",
"not",
"notnull",
"null",
"offset",
"on",
"only",
"or",
"order",
"outer",
"over",
"overlaps",
"placing",
"primary",
"references",
"returning",
"right",
"select",
"session_user",
"similar",
"some",
"symmetric",
"table",
"then",
"to",
"trailing",
"true",
"union",
"unique",
"user",
"using",
"variadic",
"verbose",
"when",
"where",
"window",
"with",
};
return Arrays.asList(keywords);
}
/**
* Report the minimum number of milliseconds between explicit vacuums.
* This number is retrieved from the directory, or if not configured there,
* is defaulted to a standard interval.
*
* @return Minimum number of milliseconds between vacuums.
*/
private static long getVacuumInterval()
{
if (vacuumInterval < 0)
{
// Attempt to read temp-table vacuum interval from temp-table section
// of directory. Missing value uses default interval, 0 disables,
// negative number is an error.
DirectoryService ds = DirectoryService.getInstance();
if (!ds.bind())
{
throw new RuntimeException("Directory bind failed");
}
try
{
String nodeID = Utils.findDirectoryNodePath(ds, "database", "container", false);
Integer value = null;
if (nodeID != null)
{
value = ds.getNodeInteger(nodeID + "/" + DatabaseManager.TEMP_TABLE_SCHEMA +
"/p2j/vacuum_interval",
"value");
}
int seconds = DEFAULT_VACUUM_INTERVAL;
if (value == null)
{
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO, "Temp table vacuum interval not specified; using default");
}
}
else
{
seconds = value;
}
if (seconds <= 0)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Temp table periodic vacuuming disabled!");
}
}
else if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO, "Temp table vacuum interval set to " + seconds + " seconds");
}
vacuumInterval = seconds * 1000;
}
finally
{
ds.unbind();
}
}
return vacuumInterval;
}
/**
* Get the name of the JDBC driver class associated with this dialect.
*
* @return Fully qualified driver class name.
*/
@Override
public String getDriverClassName()
{
return "org.postgresql.Driver";
}
/**
* Get the name of the JDBC database type (as in JBC URL).
*
* @return The name of the JDBC database type.
*/
public String getJdbcDatabaseType()
{
return "postgresql";
}
/**
* Indicate whether this dialect requires computed columns to participate
* within index definitions, instead of embedded expressions.
*
* @return <code>false</code> to indicate the backing database can embed
* an expression within an index definition.
*/
@Override
public boolean needsComputedColumns()
{
return false;
}
/**
* Indicate whether this dialect natively supports the overloading of user
* defined functions by parameter number and data type.
*
* @return <code>true</code>.
*/
@Override
public boolean supportsFunctionOverloading()
{
return true;
}
/**
* 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 Always {@code true}, all the above mentioned items are supported.
*/
@Override
public boolean supportsBooleanDatatype()
{
return true;
}
/**
* Create a data object, the purpose of which is specific to a particular
* dialect implementation, to be stored as a context-local object. This
* allows a dialect to establish context-local data which is stored by the
* persistence runtime framework, without the overhead of a separate,
* context-local variable. This object will be handed back to the dialect
* in callback methods when the services of certain hooks are required.
* <p>
* Since the context-local data is specific to a particular dialect
* implementation, it is optional. Dialect implementations which do not
* have a need for this data should simply return <code>null</code>.
*
* @param database
* The database of the persistence services.
*
* @return Dialect-specific, context-local data, or <code>null</code>.
*/
@Override
public Object createContext(Database database)
{
return (database.isTemporary() && getVacuumInterval() > 0) ? new Context() : null;
}
/**
* A dialect-specific callback hook which is invoked by the persistence
* runtime framework after a transaction is complete. The Hibernate
* session will have been closed by the time this method is invoked.
*
* @param persistence
* Provider of persistence services.
* @param context
* Optional, context-local data created previously by {@link
* #createContext}.
* @param rollback
* <code>true</code> if transaction ended with a rollback;
* <code>false</code> if transaction ended with a commit.
*
* @throws PersistenceException
* if any persistence-related error occurs.
*/
@Override
public void postTransaction(Persistence persistence, Object context, boolean rollback)
throws PersistenceException
{
if (context != null)
{
Context local = (Context) context;
long now = System.currentTimeMillis();
long diff = now - local.lastVacuum;
if (diff >= getVacuumInterval())
{
int count = 0;
try
{
StringBuilder buf = new StringBuilder();
Iterator<String> iter = TemporaryBuffer.activeTables();
for (count = 0; iter.hasNext(); count++, buf.setLength(0))
{
buf.append("vacuum analyze ");
buf.append(iter.next());
persistence.executeSQL(buf.toString(), Persistence.PRIVATE_CTX);
}
}
finally
{
long end = System.currentTimeMillis();
long elapsed = end - now;
local.vacuumTime += elapsed;
if (debug)
{
LOG.log(Level.FINE,
persistence.message("Vacuum " + count + " temp table(s) [" + diff +
":" + elapsed + ":" + local.vacuumTime + "]"));
}
local.lastVacuum = end;
}
}
}
}
/**
* Retrieve the earliest possible date value which is valid for the
* backing database.
*
* @return Earliest date supported by the backing database.
*/
@Override
public Date getLowDate()
{
return lowDate;
}
/**
* Retrieve the latest possible date value which is valid for the backing
* database.
*
* @return Latest date supported by the backing database.
*/
@Override
public Date getHighDate()
{
return highDate;
}
/**
* Format a date object as a string which is appropriate for use in an SQL
* statement.
*
* @param d
* Date to be formatted as a string.
*
* @return Formatted representation of the given date.
*/
@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)));
buf.append('-');
nf.setMinimumIntegerDigits(2);
buf.append(nf.format(d.getMonth()));
buf.append('-');
buf.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. For example, a column name as found within an index in metadata might be represented
* as:
* <pre>
* upper(rtrim("key"::text))
* </pre>
* We are interested only in the {@code key} root name, so we strip away the {@code upper()}
* and {@code rtrim()} functions the double quotes, and the {@code ::text} suffix.
*
* @param expr
* Expression containing column name as natively represented by the database dialect.
*
* @return Root column name.
*/
@Override
public String extractColumnName(String expr)
{
final String rtrim = "rtrim(";
final String upper = "upper(";
String root = expr.toLowerCase();
// Remove "upper" function if present (only applicable to text fields).
if (root.startsWith(upper))
{
int len = root.length();
root = root.substring(upper.length(), len - 1);
}
// Remove "rtrim" function if present (only applicable to text fields).
if (root.startsWith(rtrim))
{
int commaPos = root.lastIndexOf(",");
if (commaPos > 0)
{
root = root.substring(rtrim.length(), commaPos);
if (LOG.isLoggable(Level.INFO))
{
LOG.log(Level.INFO, "Old rtrim: [" + expr + "].");
}
}
else
{
root = root.substring(rtrim.length(), root.length() - 1);
}
}
// Remove data type suffix.
int delim = root.indexOf("::");
root = (delim >= 0 ? root.substring(0, delim) : root);
// Strip enclosing parentheses.
while (root.startsWith("(") && root.endsWith(")"))
{
root = root.substring(1, root.length() - 1);
}
// Strip enclosing double quotes or backticks.
root = StringHelper.stripEnclosing(root, '"');
root = StringHelper.stripEnclosing(root, '`');
return root;
}
/**
* Adjust a character column name appropriate to this dialect, such that it
* can be inserted into an index. This involves wrapping the name within
* an rtrim function, and if <code>ignoreCase</code> is <code>true</code>,
* an upper function.
*
* @param name
* Base column name.
* @param ignoreCase
* <code>true</code> to cause case to be ignored, else <code>false</code>.
*
* @return Processed column name.
*/
@Override
public String getProcessedCharacterColumnName(String name, boolean ignoreCase)
{
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. Assume 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</code> if <code>name</code> represents case-insensitive data.
*/
@Override
public boolean isCaseInsensitiveColumn(String name)
{
return name.toLowerCase().contains("upper(");
}
/**
* 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</code> is
* already determined to be the appropriate data type for textual data.
*
* @param name
* Column name, alias, or expression.
*
* @return <code>true</code> if <code>name</code> represents a computed column.
*/
@Override
public boolean isComputedColumn(String name)
{
return false; // PostgreSQL dialect does not use computed columns
}
/**
* Generate a DDL string which will create a global sequence.
*
* @param name
* Sequence name.
*
* @return Dialect-specific string to create the sequence, or {@code null} if not supported.
*/
@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.
*/
@Override
public String getCreateSequenceString(String name, long start)
{
return "create sequence " + name + " start " + 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</code> if
* not supported.
*/
@Override
public String getCreateSequenceString(String name,
Long init,
Long increment,
Long minVal,
Long maxVal,
Boolean cycle)
{
StringBuilder buf = new StringBuilder(getCreateSequenceString(name));
buf.append(" increment ").append(increment);
if (minVal != null)
{
buf.append(" minvalue ").append(minVal);
}
else
{
// default MINVALUE in PSQL is 1, must take care to avoid
// START value (n) cannot be less than MINVALUE (1) error
// if (increment > 0 /* && init < 1*/)
{
//default minvalue is: -9223372036854775807 !!
minVal = Long.MIN_VALUE;
buf.append(" minvalue ").append(minVal);
LOG.log(Level.WARNING, "Fixed MINVALUE = " + minVal + " for PSQL sequence " + name);
}
}
if (maxVal != null)
{
buf.append(" maxvalue ").append(maxVal);
}
else
{
// default MAXVALUE in PSQL is -1, must take care to avoid
// START value (n) cannot be greater than MAXVALUE (-1) error
// if (increment < 0 /* && init > -1 */)
{
maxVal = Long.MAX_VALUE;
buf.append(" maxvalue ").append(maxVal);
LOG.log(Level.WARNING, "Fixed MAXVALUE = " + maxVal + " for PSQL sequence " + name);
}
}
buf.append(" start ").append(init);
if (cycle != null && cycle)
{
buf.append(" cycle");
}
buf.append(" cache 1");
return buf.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.
*/
public String getDropSequenceString(String name)
{
return "drop sequence if exists " + name;
}
/**
* Generate the appropriate select statement to retrieve the next value of a sequence.
*
* @param sequenceName
* The name of the sequence.
* @param newVal
* The new value to be set.
*
* @return Dialect-specific string to query the sequence, or
* <code>null</code> if not supported.
*/
@Override
public String getSequenceSetValString(String sequenceName, long newVal)
{
return "select setval('" + sequenceName + "', " + newVal + ")";
}
/**
* Generate the appropriate select statement to retrieve the next value of a sequence.
*
* @param sequenceName
* The name of the sequence.
*
* @return Dialect-specific string to query the sequence, or
* <code>null</code> if not supported.
*/
@Override
public String getSequenceSetValString(String sequenceName)
{
return "select setval('" + sequenceName + "', ?)";
}
/**
* Generate the appropriate select statement to retrieve the next value of a sequence.
*
* @return Dialect-specific string to query the sequence, or
* <code>null</code> if not supported.
*/
@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.
*/
@Override
public String getSequenceNextValString(String sequenceName)
{
return "select nextval ('" + sequenceName + "')";
}
/**
* Generate the appropriate select statement to reset the 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</code> if not supported.
*/
@Override
public String getSequenceCurrValString(String sequenceName)
{
// The CURRVAL is relative to current session and will fail if called before a call to NEXTVAL or SETVAL
return "select last_value from " + sequenceName;
}
/**
* Returns <code>true</code> 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 true because PostgreSQL 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 Dialect-specific SQL expression, or {@code null} if this feature is not supported
* by the dialect.
*/
@Override
public String getSequencePrefetchString(String sequenceName)
{
return "select nextval('" + sequenceName + "') from generate_series(?, ?)";
}
/**
* Given an exception, this method will check if it indicates a database
* connectivity error.
* <p>
* For the PostgreSQL dialect, if the state indicates a 08xxx error code
* or 57P01 or 57P02 codes, the method will return <code>true</code>.
* <p>
* A complete list of defined error codes can be found here:
* https://www.postgresql.org/docs/current/errcodes-appendix.html
*
* @param exc
* The exception to be checked.
*
* @return <code>true</code> if the exception indicates a database
* connectivity error.
*/
@Override
public boolean isConnectionError(Exception exc)
{
boolean error = false;
Throwable next = exc;
while (next != null && !error)
{
if (next instanceof SQLException)
{
String state = ((SQLException) next).getSQLState();
error = state != null &&
(state.startsWith("08") ||
state.equals("57P01") ||
state.equals("57P02"));
}
next = next.getCause();
}
return error;
}
/**
* Given an exception, this method will check if it indicates a recoverable error.
* <p>
* For the PostgreSQL dialect, if the state indicates a 22P05 error code,
* the method will return <code>true</code>.
* <p>
* A complete list of defined error codes can be found
* <a href="https://www.postgresql.org/docs/current/errcodes-appendix.html">here</a>.
*
* @param exc
* The exception to be checked.
*
* @return {@code true} if the exception indicates a recoverable error, {@code false} otherwise.
*/
@Override
public boolean isRecoverableError(Exception exc)
{
boolean recoverable = false;
Throwable next = exc;
while (next != null && !recoverable)
{
if (next instanceof SQLException)
{
String state = ((SQLException) next).getSQLState();
recoverable = state != null && state.equals("22P05");
}
next = next.getCause();
}
return recoverable;
}
/**
* 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 true;
}
/**
* 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</code> to enable inlining; <code>false</code> to
* disable it.
*/
@Override
public void inlineLimit(boolean on)
{
inlineLimit.set(on);
}
/**
* Determines whether substitution parameters should be explicitly casted
* to their datatypes inside "then ? else ?" statement.
*
* @return <code>false</code>.
*/
@Override
public boolean requiresExplicitCastInsideTernary()
{
return true;
}
/**
* Override the parent's implementation to inline small (2 or less) limit
* values when there is no accompanying offset. In other cases, we fall
* back to the original implementation.
* <p>
* WARNING: the implementation of this override has an ugly dependency on
* the order of the logic in Hibernate's <code>Loader</code> class.
* Specifically, we rely on the fact that this method is invoked when a
* query is being prepared, which must happen before parameters are bound,
* at which point {@link #supportsVariableLimit()} is invoked. This
* method sets state in a context local variable which is later read when
* the latter method is invoked.
*
* @param sql
* Base select statement SQL without the limit/offset clause.
* @param offset
* Starting offset.
* @param limit
* Number of records to return.
*
* @return SQL with limit/offset clause appended.
*
* @see #supportsVariableLimit()
*/
@Override
public String getLimitString(String sql, int offset, int limit)
{
String stmt;
boolean inlinePermitted = inlineLimit.get();
if (inlinePermitted && limit <= 2 && offset == 0)
{
stmt = sql + " limit " + limit;
}
else
{
if (inlinePermitted)
{
// Override inline request.
inlineLimit.set(Boolean.FALSE);
}
stmt = super.getLimitString(sql, offset, limit);
}
return stmt;
}
/**
* Indicate whether a variable limit is supported by this dialect. The
* answer is dependent upon context local state set within {@link
* #getLimitString(String, int, int)}. See the applicable warning in that
* method's description. This method is consulted during parameter binding
* in Hibernate's <code>Loader</code> class.
*
* @return <code>true</code> if a bound parameter should be used in a
* SELECT statement's limit clause; <code>false</code> if the
* limit was inlined by {@link #getLimitString(String, int, int)}.
*/
@Override
public boolean supportsVariableLimit()
{
return inlineLimit.get() ? false : super.supportsVariableLimit();
}
/**
* 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.
*/
@Override
public String getDropIndexString(P2JIndex index)
{
return "drop index if exists " + quote(getSqlIndexName(index));
}
/**
* Build the DDL to create an index based upon the specified definition.
* The SQL is generated by using a database-specific dialect.
* <p>
* Use the INCLUDE syntax to also embed the PK into the secondary index; this is especially
* useful for unique indexes that do not have a trailing recid. Queries that are sorting
* on recid, query on recid or require the recid as a SELECT expression will be able to pick it up
* from the index entry directly, without an extra look-up into the index table
* <p>
* This addition is for the sake of performance and SHOULDN'T have any functional impact.
*
* @param index
* Definition of the index to be created.
* @param unique
* If <code>true</code>, create the index as a unique index,
* <code>false</code> to omit the unique constraint.
* @param toFile
* <code>true</code> if the string will be output to a file or
* <code>false</code> if it will be directly submitted via JDBC.
*
* @return The DDL which was created.
*/
@Override
public String getCreateIndexString(P2JIndex index, boolean unique, boolean toFile)
{
String fullIndex = super.getCreateIndexString(index, unique, toFile);
if (unique || index.isUnique())
{
fullIndex += " include (" + Configuration.getSchemaConfig().getPrimaryKeyName() + ")";
}
return fullIndex;
}
/**
* Returns the formula for character computed columns. This will be called in two cases: when
* generating the ddl for permanent databases schema (this will be written to a .sql file) and
* when creating temp-tables (the code will be sent directly to SQL).
*
* @param colName
* The base column name.
* @param ignoreCase
* {@code true} if this is a char field which has the "ignore case" flag set.
*
* @return Always {@code null} because this dialect does not use computed columns.
*/
@Override
public String getComputedColumnFormula(String colName, boolean ignoreCase)
{
return null;
}
/**
* Prepare the given metadata query parameter by making it lowercase.
*
* @param param
* The metadata query parameter.
*
* @return the lowercased parameter.
*/
@Override
public String prepareMetadataParameter(String param)
{
return param == null ? null : param.toLowerCase();
}
/**
* Process the given metadata query result, by making it lowercase.
*
* @param value
* The metadata query result.
*
* @return the lowercased result.
*/
@Override
public String processMetadataResult(String value)
{
return value == null ? null : value.toLowerCase();
}
/**
* Check if the given value is a descending clause for a column index.
*
* @param value
* The value to be checked.
*
* @return <code>true</code> if the value is the "D" or "d" string.
*/
@Override
public boolean isMetadataSortDesc(String value)
{
return "D".equalsIgnoreCase(value);
}
/**
* Check if the given value is an ascending clause for a column index.
*
* @param value
* The value to be checked.
*
* @return <code>true</code> if the value is the "A" or "a" string.
*/
@Override
public boolean isMetadataSortAsc(String value)
{
return "A".equalsIgnoreCase(value);
}
/**
* Get a list of database prepare DDL statements.
*
* @param database
* The database which needs to be prepared.
*
* @return an empty list.
*/
@Override
public List<String> getDatabasePrepareStatements(Database database)
{
return Collections.emptyList();
}
/**
* Check if the dialect needs to set an explicit collation during schema
* DDL generation.
*
* @return always <code>false</code>.
*/
@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</code> for a PostgresSQL database.
*
* @param ldbName
* The logical name of the database to create the <code>SequenceHandler</code> for.
*
* @return A <code>PostgreSQLSequenceHandler</code> instance.
*/
@Override
public SequenceHandler createSequenceHandler(String ldbName)
{
return new PostgreSQLSequenceHandler(ldbName);
}
/**
* Report whether the computed columns should be injected on table creating.
*
* @return <code>false</code> because PostgreSQL does not support computed columns.
*/
@Override
public boolean injectComputedColumns()
{
return false;
}
/**
* Checks if the SQLState of the SQLException matches the dialect specific UNIQUE_VIOLATION
* code for PostgreSQL (23505).
*
* @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 ("23505".equals(sqleState))
{
return true;
}
return false;
}
/**
* Constructs and returns the declaration of a computed column based on its name, type, scale, and
* case-sensitiveness.
*
* @param name
* The original column name.
* @param type
* The original column type.
* @param scale
* The optional scale, if needed.
* @param cs
* The case-sensitiveness.
* @param mandatory
* If the column is non-null.
*
* @return always {@code null}. This dialect does not use computed columns.
*/
@Override
public String getComputedColumnString(String name, String type, int scale, boolean cs, boolean mandatory)
{
return null;
}
/**
* Provides SQL to load sequences.
*
* @param database
* database object
* @return sequence SQL
*/
@Override
public String getSequenceSQL(Database database)
{
return "SELECT sequence_name " +
"FROM information_schema.sequences " +
"WHERE sequence_catalog = '" + database.getName() + "'";
}
/**
* Some dialects do have constraints about the maximum size of fields that compose an index.
* This method reports the dialect-specific index length limit, if any.
*
* @return always 0 as this dialect do not have any known constraints about the maximum size
* of the fields of an index.
*/
@Override
public int getIndexLengthLimit()
{
return 0;
}
/**
* Returns the size of the synthetic part of the index. At this moment only the <code>id</code>
* 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</code>. The synthetic components may differ
* between unique and non-unique indexes.
* <p>
* See {@link #getCreateIndexString(P2JIndex, boolean, boolean)}
*
* @param unique
* <code>true</code> if the index is unique
*
* @return 0 if index is unique, 8 = size of an pkid (int8)
*/
public int getSyntheticIndexSize(boolean unique)
{
return unique ? 0 : 8;
}
/**
* Composes a rtrim equivalent call for a specified expr. This is dialect specific.
* The returned value looks like:
* <pre>
* rtrim(expr)
* </pre>
* The rtrim'd expression is appended to the string builder.
*
* @param expr
* The expression to be 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 filed to.
*/
@Override
public void addRtrimmedExpression(String expr, StringBuilder sb)
{
sb.append("rtrim(").append(expr).append(")");
}
/**
* Obtain the number of parameters a query of this dialect is allowed by the database server.
*
* @return Always 0 because PostgreSQL has no such limitation.
*/
@Override
public int getMaxParameterCount()
{
return 0;
}
private enum Aggregator implements WeightAggregator
{
SUM
{
@Override
public BiConsumer<StringBuilder, Runnable> aggregate()
{
return (sb, r) -> {sb.append("sum(cast("); r.run(); sb.append(" as int))");};
}
@Override
public String minAggregatedValue()
{
return "-1";
}
},
MAX
{
@Override
public BiConsumer<StringBuilder, Runnable> aggregate()
{
return (sb, r) -> {sb.append("max(cast("); r.run(); sb.append(" as int))");};
}
@Override
public String minAggregatedValue()
{
return "-1";
}
},
OR
{
@Override
public BiConsumer<StringBuilder, Runnable> aggregate()
{
return (sb, r) -> {sb.append("bool_or("); r.run(); sb.append(")");};
}
@Override
public String minAggregatedValue()
{
return "false";
}
}
}
/**
* Return the beginning of the DDL statement that creates a temporary table.
*
* @param ifNotExist
* If {@code true} an 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.
*/
@Override
public String getCreateTemporaryTableString(boolean ifNotExist)
{
return "create temporary table ";
}
/**
* 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.
*/
@Override
public String getCreateTemporaryTablePostfix(boolean noUndo)
{
return " on commit drop";
}
/**
* 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.
*/
@Override
public String getDropIndexString(boolean ifExists, String idxName, String tableName)
{
return (ifExists ? "drop index if exists " : "drop index ") + idxName;
}
/**
* 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.
*/
@Override
public String getDropTableString(boolean ifExists, String tableName)
{
return (ifExists ? "drop table if exists " : "drop table ") + tableName + " cascade";
}
/**
* 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 "bool";
case Types.INTEGER: return "int4";
case Types.BIGINT: return "int8";
case Types.NUMERIC: return "numeric";
case Types.DATE: return "date";
case Types.VARBINARY: return "bytea";
case Types.TIMESTAMP: return "timestamp";
case Types.TIMESTAMP_WITH_TIMEZONE: return "timestamp with time zone";
case Types.ARRAY: return "array";
case Types.BLOB: return "oid";
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 "BOOL": return ParmType.LOG;
case "INT4": return ParmType.INT;
case "INT8": return ParmType.INT64;
case "NUMERIC": return ParmType.DEC;
case "TEXT": return ParmType.CHAR;
case "DATE": return ParmType.DATE;
case "TIMESTAMP": return ParmType.DT;
case "TIMESTAMPTZ": return ParmType.DTTZ;
case "OID": return ParmType.BLOB;
case "BYTEA": 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)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Unknown '" + fwdType + "' type for PostgreSQL dialect.");
}
}
else
{
int k = s.indexOf(SCALE_TOK);
if (k > 0)
{
s = s.substring(0, k) + scale + s.substring(k + SCALE_TOK.length());
}
}
return s;
}
/**
* 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.
*/
@Override
public String getAnalyzeString(String table)
{
return table == null ? "analyze" : "analyze " + table;
}
/**
* Get a new instance of a splitter for DDL scripts.
*
* @return the splitter for DDL scripts.
*/
@Override
public ScriptSplitter scriptSplitter()
{
return new PgScriptSplitter();
}
/**
* Get a ScriptRunner instance for the dialect.
*
* @return a ScriptRunner instance for the dialect.
*
*/
@Override
public ScriptRunner getScriptRunner()
{
return new PostgreSQLScriptRunner(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)
{
return new FWDBlob(bytes);
}
/**
* 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)
{
return new FWDClob(value);
}
/**
* Check if UDF should be used for converted CONTAINS operator
*
* @return <code>true</code> if UDF should be used for converted CONTAINS operator
*/
@Override
public boolean useUdf4Contains()
{
return useUdf4Contains;
}
/**
* Check if word tables should be used for converted CONTAINS operator
*
* @return <code>true</code> if tables should be used for converted CONTAINS operator
*/
public boolean useWordTables()
{
return !useUdf4Contains;
}
/**
* Check if native UDFs are supported for the dialect
*
* @return <code>true</code> if native UDFs are supported for the dialect
*/
@Override
public boolean isNativeUDFsSupported()
{
return true;
}
/**
* Get UDF schema name (if applicable).
*
* @return UDF schema name (if applicable).
*/
public String udfSchema()
{
return "udf.";
}
/**
* Check if the exception was caused by error in UDF.
* @param e
* the exception in question
* @return <code>true</code> if the exception was caused by error in UDF.
*/
@Override
public boolean isUdfOriginatedError(SQLException e)
{
return "FWD00".equals(e.getSQLState());
}
/**
* Check if the exception was caused by a warning in UDF.
* @param e
* the exception in question
* @return <code>true</code> if the exception was caused by error in UDF.
*/
public boolean isUdfOriginatedWarning(SQLException e)
{
return "FWD00".equals(e.getSQLState());
}
/**
* Create the error descriptor from the SQLException message
*
* @param message
* 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)), // was 4
matcher.group(2), !StringUtils.isBlank(matcher.group(1)));
}
catch (NumberFormatException e)
{
// no-op
}
}
return new ErrorEntry(-1, message, false);
}
/**
* Check if Common Table Expressions should be used for converted CONTAINS operator
*
* @return <code>true</code> if Common Table Expressions should be used for converted CONTAINS operator
*/
@Override
public boolean useCTE4Contains()
{
return useCte4Contains || useMixedMode4Contains;
}
/**
* Check if mixed mode re-write should be used for converted CONTAINS operator
*
* @return <code>true</code> if mixed mode re-write should be used
*/
@Override
public boolean useMixedNode4Contains()
{
return useMixedMode4Contains;
}
/**
* Get the WeightAggregator instance for the dialect if defined
* @return the WeightAggregator instance for the dialect if defined
*/
@Override
public Optional<WeightAggregator> weightAggregator()
{
return Optional.ofNullable(WEIGHT_AGGREGATOR);
}
/**
* Get dialect id.
*
* @return Dialect id
*/
@Override
public String id()
{
return "postgresql";
}
/**
* Get c3p0 ConnectionCustomizer class for the dialect.
* @param <T>
* Customizer class
* @param cfg
* Database configuration.
* @return c3p0 ConnectionCustomizer class for the dialect.
*/
@Override
public Class<? extends AbstractConnectionCustomizer> connectionCustomizer(DatabaseConfig cfg)
{
return cfg.isUseJavaUDFs() ? null : FWDPostgreSQLConnectionCustomizer.class;
}
/**
* 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
* word tables by name
*/
@Override
public void generateWordTablesDDLImpl(String dbName,
PrintStream out,
String eoln,
Collection<WordTable> wordTables)
{
String triggerFunc = TRIGGER_FUNC.stream().collect(Collectors.joining(eoln));
String triggerFuncExt = TRIGGER_FUNC_EXT.stream().collect(Collectors.joining(eoln));
String triggerFuncCExt = TRIGGER_FUNC_CEXT.stream().collect(Collectors.joining(eoln));
String insTriggerFuncCExt = INS_TRIGGER_FUNC_CEXT.stream().collect(Collectors.joining(eoln));
String afterInsertTrigger = AFTER_INSERT_TRIGGER.stream().collect(Collectors.joining(eoln));
String afterUpdateTrigger = AFTER_UPDATE_TRIGGER.stream().collect(Collectors.joining(eoln));
wordTables.stream().forEach(wt -> {
if (wt.customExtentFields == null)
{
if (wt.extent == 0)
{
out.printf(triggerFunc,
wt.triggerFunctionName, wt.tableName, wt.tableName, PK,
wt.tableName, PK, wt.fieldName, !wt.caseSensitive);
}
else
{
out.printf(triggerFuncExt,
wt.triggerFunctionName,
wt.tableName, wt.tableName, wt.tableName, wt.tableName,
wt.fieldName, !wt.caseSensitive);
}
out.print(eoln);
out.print(eoln);
out.printf(DROP_TRIGGER, wt.afterUpdateTriggerName, wt.parentTableName);
out.print(eoln);
out.print(eoln);
out.printf(afterUpdateTrigger,
wt.afterUpdateTriggerName, wt.fieldName, wt.parentTableName, wt.triggerFunctionName);
out.print(eoln);
out.print(eoln);
out.printf(DROP_TRIGGER, wt.afterInsertTriggerName, wt.parentTableName);
out.print(eoln);
out.print(eoln);
out.printf(afterInsertTrigger,
wt.afterInsertTriggerName, wt.parentTableName, wt.triggerFunctionName);
out.print(eoln);
out.print(eoln);
}
else
{
for (CustomExtentField f: wt.customExtentFields)
{
out.printf(triggerFuncCExt,
f.triggerFunctionName,
wt.tableName, wt.tableName, wt.tableName, f.pos,
wt.tableName, f.pos, f.fieldName, !wt.caseSensitive
);
out.print(eoln);
out.print(eoln);
out.printf(DROP_TRIGGER, f.afterUpdateTriggerName, wt.parentTableName);
out.print(eoln);
out.print(eoln);
out.printf(afterUpdateTrigger,
f.afterUpdateTriggerName, f.fieldName, wt.parentTableName,
f.triggerFunctionName
);
out.print(eoln);
out.print(eoln);
}
out.printf(DROP_TRIGGER, wt.afterInsertTriggerName, wt.parentTableName);
out.print(eoln);
out.print(eoln);
String lines = wt.customExtentFields.stream().map(f -> String.format(
INS_TRIGGER_LINE,
wt.tableName, f.pos, f.fieldName, !wt.caseSensitive )).
collect(Collectors.joining(eoln));
out.printf(insTriggerFuncCExt,
wt.triggerFunctionName,
wt.tableName, wt.tableName,
lines);
out.print(eoln);
out.print(eoln);
out.printf(afterInsertTrigger,
wt.afterInsertTriggerName, wt.parentTableName, wt.triggerFunctionName);
out.print(eoln);
out.print(eoln);
}
});
}
/**
* 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 null;
}
/**
* Obtain the name of an index. This dialect overrides this method in order to make sure the name is unique
* among tables because in PSQL the index names must be unique.
*
* @param index
* The index structure.
*
* @return the unique SQL name of the index.
*/
@Override
public String getSqlIndexName(P2JIndex index)
{
return "idx__" + index.getTable() + "_" + index.getName();
}
/** Obtains an object responsible with providing dialect specific SQL statements for tenant management. */
@Override
public 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 TEXT NOT NULL,\n" +
" tenant_description TEXT,\n" +
" tenant_info TEXT,\n" +
" status BOOL,\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 TEXT NOT NULL,\n" +
" pdb_name TEXT NOT NULL,\n" +
" ldb_name TEXT NOT NULL,\n" +
" url TEXT,\n" +
" username TEXT,\n" +
" password TEXT,\n" +
" " + C3P0_MAX_STMTS + " INT4,\n" +
" " + C3P0_MIN_POOL + " INT4,\n" +
" " + C3P0_MAX_POOL + " INT4,\n" +
" " + C3P0_ACQ_INC + " INT4,\n" +
" " + C3P0_MAX_IDLE + " INT4,\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)";
}
};
}
/*
* Returns the maximum admitted number of columns selected. For PSQL the number is 1664.
*
* @return limitation of the PSQL
*/
@Override
public int getMaxEntriesInSelect()
{
return 1664;
}
/**
* 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 datcollate FROM pg_database WHERE datname = current_database()");
ResultSet rs = ps.executeQuery();
if (rs.next())
{
return mapCP(rs.getString(1)); // datcollate
}
}
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 datcollate FROM pg_database WHERE datname = current_database()");
ResultSet rs = ps.executeQuery();
if (rs.next())
{
return mapCollation(rs.getString(1)); // datcollate
}
}
catch (SQLException e)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.warning("Failed to retrieve COLLATION from database.", e);
}
}
// fallback for all errors
return null;
}
/**
* Returns {@code true} if the dialect handles the optimization of an augmented construct
* which is resulted from comparing exactly two character fields.
*
* @return as described above.
*/
@Override
public boolean preferDisjunctiveForm()
{
return true;
}
/**
* Maps/extract from the PostgreSQL specific charset names to 4GL codepage names.
*
* @param collation
* A PostgreSQL specific collation name.
* (Ex: "en_US@p2j_basic", "en_US.utf8", "C", "en_US@iso88591_fwd_basic" (default))
*
* @return The 4GL codepage name of {@code h2Charset}.
*/
private static String mapCP(String collation)
{
if (collation == null)
{
return null;
}
if ("en_US@p2j_basic".equals(collation) || "C".equals(collation) || "POSIX".equals(collation))
{
return "basic"; // FWD 3.0 legacy
}
int i = collation.indexOf("_fwd_");
if (i > 0)
{
collation = collation.substring(0, i);
i = collation.indexOf('@');
if (i < 0)
{
i = collation.lastIndexOf('_');
}
if (i > 0)
{
collation = collation.substring(i + 1);
switch (collation.toUpperCase())
{
case "ISO88591": return "ISO8859-1";
case "ISO885915": return "ISO8859-15";
case "UTF8": return "UTF-8";
case "CP1252": return "1252";
// case "IBM850": return "IBM850";
// case "IBM852": return "IBM852";
default: return collation;
}
}
}
else
{
i = collation.lastIndexOf('.');
if (i > 0)
{
collation = collation.substring(i + 1);
switch (collation.toUpperCase())
{
case "ISO88591": return "ISO8859-1";
case "ISO885915": return "ISO8859-15";
case "UTF8": return "UTF-8";
case "CP1252": return "1252";
// case "IBM850": return "IBM850";
// case "IBM852": return "IBM852";
default: return collation;
}
}
}
return collation;
}
/**
* Maps the PostgreSQL specific collation names to 4GL collation names.
*
* @param collation
* A PostgreSQL specific collation name.
* (Ex: "en_US@p2j_basic", "en_US.utf8", "C", "en_US@iso88591_fwd_basic" (default))
*
* @return The 4GL codepage name of {@code h2Charset}.
*/
private static String mapCollation(String collation)
{
if (collation == null)
{
return null;
}
if ("en_US@p2j_basic".equals(collation) || "C".equals(collation) || "POSIX".equals(collation))
{
return "basic"; // FWD 3.0 legacy
}
int i = collation.indexOf("_fwd_");
if (i > 0)
{
return collation.substring(i + 5 /* "_fwd_".length */);
}
return collation;
}
/**
* Context local data needed by this object to perform dialect-specific
* processing. In this case, we track the last time a temp table vacuum
* was performed and how much accumulated time we have spent performing
* such vacuums in this context.
*/
private static class Context
{
/** Time of last temp table vacuum */
private long lastVacuum = System.currentTimeMillis();
/** Accumulated time spent vacuuming temp tables */
private long vacuumTime = 0L;
}
/**
* Read-only implementation of a BLOB instance, to be used by the JDBC driver.
*/
private static class FWDBlob
implements Blob
{
/** The data. */
private byte[] data;
/**
* Create a new CLOB instance.
*
* @param data
* The data.
*/
public FWDBlob(byte[] data)
{
this.data = data;
}
/**
* Get the {@link #data} length.
*
* @return See above.
*/
@Override
public long length()
throws SQLException
{
return data.length;
}
/**
* Get the sub-array starting from the given position, and with the specified length.
*
* @param pos
* The 1-based position.
* @param length
* The length.
*
* @return See above.
*/
@Override
public byte[] getBytes(long pos, int length)
throws SQLException
{
return Arrays.copyOfRange(data, (int) pos - 1, length);
}
/**
* Get the binary stream for the data.
*
* @return See above.
*/
@Override
public InputStream getBinaryStream()
throws SQLException
{
return new ByteArrayInputStream(data);
}
/**
* Get the binary stream starting from the given position, and with the specified length.
*
* @param pos
* The 1-based position.
* @param length
* The length.
*
* @return See above.
*/
@Override
public InputStream getBinaryStream(long pos, long length)
throws SQLException
{
return new ByteArrayInputStream(getBytes(pos, (int) length));
}
/**
* Release the {@link #data}.
*/
@Override
public void free() throws SQLException
{
this.data = null;
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public long position(byte[] pattern, long start)
throws SQLException
{
throw new SQLException("FWDBlob:position");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public long position(Blob pattern, long start)
throws SQLException
{
throw new SQLException("FWDBlob:position");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public int setBytes(long pos, byte[] bytes)
throws SQLException
{
throw new SQLException("FWDBlob:setBytes");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public int setBytes(long pos, byte[] bytes, int offset, int len)
throws SQLException
{
throw new SQLException("FWDBlob:setBytes");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public OutputStream setBinaryStream(long pos)
throws SQLException
{
throw new SQLException("FWDBlob:setBinaryStream");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public void truncate(long len)
throws SQLException
{
throw new SQLException("FWDBlob:truncate");
}
}
/**
* Read-only implementation of a CLOB instance, to be used by the JDBC driver.
*/
private static class FWDClob
implements Clob
{
/** The data. */
private String data;
/**
* Create a new CLOB instance.
*
* @param data
* The data.
*/
public FWDClob(String data)
{
this.data = data;
}
/**
* Get the {@link #data} length.
*
* @return See above.
*/
@Override
public long length()
throws SQLException
{
return data.length();
}
/**
* Get the substring starting from the given position, and with the specified length.
*
* @param pos
* The 1-based position.
* @param length
* The length.
*
* @return See above.
*/
@Override
public String getSubString(long pos, int length)
throws SQLException
{
return data.substring((int) pos, length);
}
/**
* Get the character stream.
*
* @return See above.
*/
@Override
public Reader getCharacterStream()
throws SQLException
{
return new CharSequenceReader(data);
}
/**
* Get the ASCII stream.
*
* @return See above.
*/
@Override
public InputStream getAsciiStream()
throws SQLException
{
return new ByteArrayInputStream(data.getBytes(Charset.defaultCharset()));
}
/**
* Get the character stream starting from the given position, and with the specified length.
*
* @param pos
* The 1-based position.
* @param length
* The length.
*
* @return See above.
*/
@Override
public Reader getCharacterStream(long pos, long length)
throws SQLException
{
return new CharSequenceReader(getSubString(pos, (int) length));
}
/**
* Release the {@link #data}.
*/
@Override
public void free()
throws SQLException
{
this.data = null;
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public long position(String searchstr, long start)
throws SQLException
{
throw new SQLException("FWDClob:position");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public long position(Clob searchstr, long start)
throws SQLException
{
throw new SQLException("FWDClob:position");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public int setString(long pos, String str)
throws SQLException
{
throw new SQLException("FWDClob:setString");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public int setString(long pos, String str, int offset, int len)
throws SQLException
{
throw new SQLException("FWDClob:setString");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public OutputStream setAsciiStream(long pos)
throws SQLException
{
throw new SQLException("FWDClob:setAsciiStream");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public Writer setCharacterStream(long pos)
throws SQLException
{
throw new SQLException("FWDClob:setCharacterStream");
}
/**
* Operation not allowed.
*
* @throws SQLException
* Always
*/
@Override
public void truncate(long len)
throws SQLException
{
throw new SQLException("FWDClob:truncate");
}
}
/**
* UDF types
*/
public static enum UDF_TYPE
{
/** Java UDFs */
JAVA,
/** SQL UDFs */
SQL;
/**
* Get configured UDF types.
* @param dbName
* database name
* @return list of configured UDF types.
*/
public static Set<UDF_TYPE> config(String dbName)
{
String udfTypes = Configuration.getSchemaConfig().
getNamespaceParameter(dbName, "postgresql/udf");
if (udfTypes == null)
{
return EnumSet.of(JAVA);
}
return Arrays.stream(udfTypes.split(",")).
map(String::toUpperCase).map(String::trim).map(UDF_TYPE::valueOf).
collect(Collectors.toSet());
}
/**
* Check if SQL UDFs should be used with the database.
* @param dbName
* database name
* @return <code>true</code>
*/
public static boolean useSqlUdfs(String dbName)
{
return config(dbName).contains(SQL);
}
}
/** Splitter for DDL scripts. */
public static class PgScriptSplitter
implements ScriptSplitter
{
/** The current state of the automaton. */
protected State currentState = State.GAP;
/**
* Forcibly set the splitter state
* @param state
* new state;
*/
public void state(State state)
{
this.currentState = state;
}
/**
* Process next line and return a new state
* @param line
* next sline to be processed
* @return new state
*/
public State state(String line)
{
if (line == null)
{
return (currentState = State.GAP);
}
String s = line.trim();
if (currentState == State.COMMENT)
{
if (s.endsWith("*/"))
{
return (currentState = State.GAP);
}
else
{
return currentState;
}
}
if (s.startsWith("/*"))
{
if (currentState == State.DDL)
{
return currentState;
}
return currentState = s.endsWith("*/") ? State.GAP : State.COMMENT;
}
if (s.isEmpty())
{
return currentState;
}
currentState = line.startsWith(";") ? State.GAP : State.DDL;
return currentState;
}
}
}