DDLGeneratorWorker.java
/*
** Module : DDLGeneratorWorker.java
** Abstract : Gathers schema information and constructs DDL statements for initializing permanent
** database (tables, indexes and sequences).
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20191009 Created initial version. Supported permanent tables and indexes DDLs.
** 20200618 Sorted tables alphabetically. Unique indexes are generated first.
** 002 OM 20200924 Index components carry multiple information to avoid map lookups for them.
** 003 IAS 20201203 Added generation database object for the word tables' support.
** IAS 20210219 Word tables support for custom extents
** IAS 20210315 Refactored for working with word tables for _temp database
** IAS 20210408 _MyConnection VST support
** OM 20210716 Final naming schema for custom collation. To be consistent with PG/Linux locale name,
** the DOT and AT are replaced by underscore in the background. Code maintenance.
** IAS 20210805 Added copyUDFs method.
** OM 20210908 Used SchemaDictionary.TEMP_TABLE_DB instead of hardcoded constant.
** CA 20211214 The AST manager plugin must be context-local, for runtime conversion, as the
** InMemoryRegistryPlugin is not thread-safe.
** OM 20220317 Added p2j_id_generator_sequence with default value at the end of the table DDLs.
** [dropFK] must be executed before [dropFkIndex] or else errors 42111 and 90085 are raised.
** OM 20220407 Looking for hints in the original folder, not in the volatile $cvtpath folder.
** ECF 20220630 Fixed hard-coded references to converted _MyConnection column names which have changed.
** OM 20220721 Added partial conversion-time MariaDb dialect support.
** OM 20220815 Word tables must be dialect sensitive (not finished).
** TJD 20220504 Upgrade do Java 11 minor changes
** OM 20220914 Use MAX-WIDTH to generate custom sized varchar columns.
** OM 20221026 Excluded table name from index name by default. It will be added as needed by dialects.
** OM 20221123 Fixed generation of SQL width for types different than decimal and character.
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
** performance.
** IAS 20230222 Fixed word tables generation to support denorm-extents=true.
** DDF 20230306 Added a parameter to getSqlMappedType() depending on the case sensitivity of the
** character (refs: #7108).
** DDF 20230309 Simplify if statement into a single variable.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 OM 20230615 Converting the MAX-WIDTH from size of UTF-16 encoding to number of characters.
** 006 OM 20230615 Clarified the semantics of maxWidth (MAX-WIDTH) parameter of addField() method.
** 007 OM 20230908 Used Dialect.checkIndexConstraints() to report possible errors.
** 008 IAS 20230910 Fixed word tables' generation.
** 009 OM 20231213 Regression fix: the computed columns for indexed fields other than character were
** not generated in create tables. Dialect dependant.
** 010 OM 20240410 DDL generation is disabled in runtime mode.
** 011 DDF 20240205 Added an additional parameter to addComponent which represents the non-null
** property value of the component.
** DDF 20240209 Add an additional parameter to getComputedColumnString method call, which already
** handles the character type depending on the dialect.
*/
/*
** 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.orm;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.Set;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.pattern.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
/**
* This worker gathers schema information from TRPL and constructs DDL statements for initializing
* the permanent database (tables, indexes and sequences).
* <p>
* The generated DDL is quite FWD-specific, the following particular-case processing are
* performed:
* <ul>
* <li>the {@code id} unique column is injected;</li>
* <li>the EXTENT fields are extracted to satellite tables with specific extent size. Their
* records are linked using foreign key to mother table. Constraints to keep data
* integrity are also generated. A dedicated index is also constructed for fast access
* to normalized data;</li>
* <li>the case-insensitive character fields that are index components are processed in a
* dialect specific. If the dialect support expressions in index components they are
* inlined, otherwise a computed column is created and used in index;</li>
* <li>all indexes are forced to be unique by adding the {@code id} column at the end if they
* are not already unique. As special case, for the SQL Server, we are forced to add
* it to all indexes. See inline comment;</li>
* <li>the {@code datetime-tz} ABL fields are generated with multiple (double) column. The
* supplementary integer column was added to compensate a PostgreSQL issue and is added
* in all cases in order to have a uniform management for this datatype;</li>
* </ul>
*
* TODO: implement support for merging in data from hand-written tables (parse some xml?).
*/
public final class DDLGeneratorWorker
extends AbstractPatternWorker
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(DDLGeneratorWorker.class);
/** Name of the primary key column. */
private static final String PK = Configuration.getSchemaConfig().getPrimaryKeyName();
/** Auxiliary database objects for _MyConnection VST */
private static final List<String> MY_CONNECTION_AUX = Collections.unmodifiableList(
Arrays.asList(
"create view meta_myconnection as",
"select * from meta_myconnection_",
"where my_conn_user_id = fwdSession_1();",
"",
"create trigger meta_myconnection_trg instead of insert, update, delete ",
"on meta_myconnection ",
"for each row call \"com.goldencode.p2j.persist.h2.UpdatableMyConnectionView\";"
)
);
/** This represents the surrogate {@code id} column, added to each SQL record. */
public static final P2JField ID = new P2JField(PK, ParmType.INT64, 0L,
null, null, null, null, false, null, null, false, null, null, null, null, true, 0, 0);
/** 'parent' field of the word table */
public static final P2JField PARENT = new P2JField("parent__id", ParmType.INT64, 0L,
null, null, null, null, false, null, null, false, null, null, null, null, true, 0, 0);
/** 'word' field of the word table */
public static final P2JField WORD = new P2JField("word", ParmType.CHAR, 0L,
null, null, null, null, false, null, null, false, null, null, null, null, true, 0, 0);
/**
* This column is used as foreign key in a normalized composite table. The value refer to
* the record to which the extent components belongs to.
*/
public static final P2JField parent__id = new P2JField("parent__id", ParmType.INT64, 0L,
null, null, null, null, false, null, null, false, null, null, null, null, true, 0, 0);
/** This column represents the index of the extent value. */
public static final P2JField list__index = new P2JField("list__index", ParmType.INT, 0L,
null, null, null, null, false, null, null, false, null, null, null, null, true, 0, 0);
/**
* The datetime-tz suffix for offset filed. This second field is used to compensate the fact
* that PostgreSQL SQL engine will drop this piece of information (the zone offset) to simplify
* the stored data. Although the time and date arithmetic is correct (due the timestamp is
* converted to GMT internally), we are unable to retrieve back the {@code offset}
*/
public static final String DTZ_OFFSET = "_offset";
/** Fixed size space used to make the output pretty. */
public static final String INDENT = " ";
/**
* The constructor configures the supporting library with exported functionality.
*/
public DDLGeneratorWorker()
{
setLibrary(new Helper());
}
/**
* Get the schema name based on database name.
*
* @param dbName
* The database name.
*
* @return The schema name.
*/
private String getSchemaName(String dbName)
{
if (MetadataManager.META_SCHEMA.equals(dbName))
{
return Configuration.getSchemaConfig().getMetadata().getName();
}
return dbName;
}
/**
* Dialect-independent data structure class for SQL tables. Holds all needed information
* related to a SQL table (name, fields, indexes). The structure is filled in a granular
* fashion and iterated when the DDL artifacts are created (table and index DDLs).
*/
private static class Table
{
/** The SQL table name. */
final String name;
/** The legacy table name. */
final String legacyName;
/**
* The set of fields of this table. This is an ordered list, each {@code P2JField} being
* mapped by its legacy name for quick access.
*/
final Map<String, P2JField> fields = new LinkedHashMap<>();
/** The set of indexes of this table. Also mapped by their legacy name. */
final Map<String, P2JIndex> indexes = new LinkedHashMap<>();
/**
* The indexes, after preprocessing (removed redundant and normalized unique). If {@code null} the
* map has not yet been preprocessed.
*/
List<P2JIndex> minimalUnique = null;
List<P2JIndex> nonRedundant = null;
Set<P2JIndex> remaining = null;
/**
* Creates a new {@code Table} identity.
*
* @param tableName
* The SQL name of the table.
* @param legacyName
* The legacy name of the table.
*/
Table(String tableName, String legacyName)
{
this.name = tableName;
this.legacyName = legacyName;
}
/**
* Creates a new {@code Table} identity.
*
* @param tableName
* The SQL name of the table.
*/
Table(String tableName)
{
this(tableName, tableName);
}
}
/** Dialect-independent data structure class for SQL sequences. */
private static class Sequence
{
/** The sql-legal name. */
private final String name;
/** The legacy name of the sequence used by dynamic forms. */
private final String legacyName;
/** The initial value of the sequence. */
private final Long initial;
/** The minimum value. */
private final Long min;
/** The maximum value. */
private final Long max;
/** The increment value, must not be 0. */
private final Long increment;
/** Flag for cycling sequences. */
private final Boolean cycle;
/**
* The only constructor creates the immutable object. Builds an object that has all static
* information for creating a sequence.
*
* @param sqlName
* The name of the sequence from database.
* @param legacyName
* The original name of the sequence in 4GL.
* @param init
* Initial value of the sequence.
* @param min
* Minimum value of the sequence.
* @param max
* Maximum value of the sequence.
* @param increment
* The increment of the sequence.
* @param cycle
* Cycling flag.
*/
Sequence(String legacyName,
String sqlName,
Long init,
Long min,
Long max,
Long increment,
Boolean cycle)
{
this.name = sqlName;
this.legacyName = legacyName;
this.initial = init;
this.min = min;
this.max = max;
this.increment = increment;
this.cycle = cycle;
}
}
/**
* The class that exposes its public methods as worker interface in TRPL. The internal data
* is acquired piece by piece using {@code add***()} methods. When the entire schema
* information is collected, the result can be obtained using {@code generate***()} methods.
*
*/
public class Helper
{
/** Max length of the database object name (most restrictive limit as for PostgreSQL)*/
private static final int MAX_OBJECT_NAME_LEN = 63;
/** The set of all tables mapped by their schema. */
private final Map<String, Map<String, Table>> tables = new TreeMap<>();
/** The set of all objects' names generated for word tables by mapped by their schema. */
private final Map<String, Set<String>> wordTablesNames = new TreeMap<>();
/** Word tables by name */
private final Map<String, Map<String, WordTable>> wordTables = new HashMap<>();
/** Word tables' keys DDLs by schema and word table name */
private final Map<String, Map<String, List<String>>> wordTablesKeys = new HashMap<>();
/** Schema hints by schema name */
private final Map<String, UastHints> hints = new HashMap<>();
/** The set of all sequences mapped by their schema. */
private final Map<String, Map<String, Sequence>> sequences = new HashMap<>();
/** The parser used by {@code getCharacterFormatLength()} method. */
private final CharacterFormatParser formatParser = new CharacterFormatParser();
/**
* Adds a new schema. This is the first method to be called. Without the schema declared,
* the rest of the structure cannot be build.
*
* @param dbName
* The name of the schema. TODO: clarify: legacy/sql
*/
public void addSchema(String dbName)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema here. DDL generation is disabled in runtime mode.
return;
}
if (tables.containsKey(dbName))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Schema for database [" + dbName + "] already added.");
}
return;
}
sequences.put(dbName, new TreeMap<>());
tables.put(dbName, new TreeMap<>());
}
/**
* Adds a new table to an existing schema (added previously with {@code addSchema()}). In
* case of validation error the method logs the event and returns without adding the table.
*
* @param dbName
* The target schema.
* @param tableName
* The SQL table name.
* @param legacy
* The legacy name.
*/
public void addTable(String dbName, String tableName, String legacy)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema. DDL generation is disabled in runtime mode.
return;
}
Map<String, Table> schema = tables.get(dbName);
if (schema == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Invalid schema [" + dbName + "].");
}
return;
}
if (schema.containsKey(legacy))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Table [" + tableName + "] already added in schema [" + dbName + "]");
}
return;
}
schema.put(legacy, new Table(tableName, legacy));
}
/**
* Adds a new field to an existing table (added previously with {@code addTable()}). In
* case of validation error the method logs the event and returns without adding the field.
*
* @param dbName
* The target schema.
* @param tableName
* The target (legacy) table name.
* @param legacy
* The legacy field name of the new field to be added.
* @param name
* The SQL name of the new field.
* @param type
* The type of the field.
* @param extent
* The extent of the field. If field is not an EXTENT, use 0.
* @param extentIndex
* In case of denormalized tables, this is the index of the extent field.
* @param cs
* Case sensitivity. Only taken into account when the current field is a {@code character@}.
* @param notNull
* Flag for {@code mandatory} fields.
* @param scale
* The scale. Only used when the declared field is {@code decimal}.
* @param maxWidth
* The SQL size reserved for the field. This is only used for variable datatypes of dialects
* which require this number. The unit is the bytes for binary data and characters for text /
* varchar. For the moment, only SQL Server and MariaDb dialect use this value.
*/
public void addField(String dbName,
String tableName,
String legacy,
String name,
String type,
int extent,
int extentIndex,
boolean cs,
boolean notNull,
int scale,
int maxWidth)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema. DDL generation is disabled in runtime mode.
return;
}
Map<String, Table> schema = tables.get(dbName);
if (schema == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Invalid schema [" + dbName + "].");
}
return;
}
Table table = schema.get(tableName);
if (table == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Invalid table [" + tableName + "] in schema [" + dbName + "]");
}
return;
}
String key = legacy;
if (extentIndex >= 0)
{
key += "~" + extentIndex;
}
if (table.fields.containsKey(key))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Field [" + legacy + "] already exists in table [" + tableName +
"] of schema [" + dbName + "]");
}
return;
}
P2JField val = new P2JField(name, ParmType.fromString(type), extent, "", null, null, null, cs,
null, null, false, null, null, null, null, notNull, scale, maxWidth);
table.fields.put(key, val);
}
/**
* Get the name of the word table for the word index
*
* @param dbName
* The target schema.
* @param tableName
* The target (legacy) table name.
* @param indexName
* The legacy name of the index.
*
* @return the name of the word table for the word index
*/
public String getWordTableName(String dbName, String tableName, String indexName)
{
if (Configuration.isRuntimeConfig() || SchemaDictionary.TEMP_TABLE_DB.equals(dbName))
{
return ""; // DDL generation is disabled in runtime mode.
}
Map<String, Table> tbl = tables.get(dbName);
if (tbl == null)
{
throw new IllegalArgumentException("Unknown database: [" + dbName + "]");
}
Table table = tbl.get(tableName);
if (table == null)
{
throw new IllegalArgumentException("Unknown table: [" + tableName + "]");
}
P2JIndex index = table.indexes.get(indexName);
if (index == null)
{
throw new IllegalArgumentException("Unknown index: [" + indexName + "]");
}
if (!index.isWord())
{
throw new IllegalArgumentException("Not a word index: [" + indexName + "]");
}
P2JIndexComponent component = index.components().get(0);
String fieldName = component.getColumnName();
String wordTableName = createDbObjectName(dbName, table.name, fieldName);
index.setWordTableName(wordTableName);
return wordTableName;
}
/**
* Adds a new index to an existing table (added previously with {@code addTable()}). In
* case of validation error the method logs the event and returns without adding the index.
*
* @param dbName
* The target schema.
* @param tableName
* The target (legacy) table name.
* @param legacy
* The legacy field name of the new index to be added.
* @param name
* The SQL name of the new index.
* @param primary
* Flag for PRIMARY indexes.
* @param unique
* Flag for UNIQUE indexes.
* @param word
* Flag for WORD indexes (not used ATM).
*/
public void addIndex(String dbName,
String tableName,
String legacy,
String name,
boolean primary,
boolean unique,
boolean word)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema. DDL generation is disabled in runtime mode.
return;
}
Map<String, Table> schema = tables.get(dbName);
if (schema == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Invalid schema [" + dbName + "].");
}
return;
}
Table table = schema.get(tableName);
if (table == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Invalid table [" + tableName + "] in schema [" + dbName + "]");
}
return;
}
P2JIndex index = new P2JIndex(table.name, name, unique, primary, word);
table.indexes.put(legacy, index);
}
/**
* Adds a new field to an existing table (added previously with {@code addTable()}). In
* case of validation error the method logs the event and returns without adding the table.
*
* @param dbName
* The target schema.
* @param tableName
* The target (legacy) table name.
* @param indexName
* The target (legacy) index name.
* @param property
* The property name of the field to be added as an index component.
* @param name
* The SQL column name of the field to be added as an index component.
* @param legacy
* The legacy field name of the field to be added as an index component.
* @param isChar
* Flag for {@code character} fields.
* @param isCS
* The case-sensitivity of the field. Only if {@code isChar} is {@code true}.
* @param isDesc
* The direction of sorting. If {@code true} the direction is descending.
* @param isMandatory
* If {@code true} the field is non-null.
*
* @return The added index component
*/
public P2JIndexComponent addIndexComponent(String dbName,
String tableName,
String indexName,
String property,
String name,
String legacy,
boolean isChar,
boolean isCS,
boolean isDesc,
boolean isMandatory)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema. DDL generation is disabled in runtime mode.
return null;
}
Map<String, Table> schema = tables.get(dbName);
if (schema == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Invalid schema [" + dbName + "].");
}
return null;
}
Table table = schema.get(tableName);
if (table == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Invalid table [" + tableName + "] in schema [" + dbName + "]");
}
return null;
}
P2JIndex index = table.indexes.get(indexName);
if (index == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Invalid index [" + indexName + "] in table [" + tableName +
"] in schema [" + dbName + "]");
}
return null;
}
int extent = 0;
if (index.isWord())
{
TableHints tableHints = hints.computeIfAbsent(dbName, dbn -> getHints()).
getTableHintsOrEmpty().get(tableName);
if (tableHints != null)
{
List<ExtentHintField> extentHintFields = tableHints.getCustomFieldExtent(legacy);
if (extentHintFields != null)
{
name = ClassDefinition.nconvert.convert(legacy, MatchPhraseConstants.TYPE_COLUMN);
extent = extentHintFields.size();
}
}
}
return index.addComponent(legacy,
extent,
property,
name,
isDesc,
isChar,
isChar && !isCS,
false,
isMandatory);
}
/**
* Records a new sequence.
*
* @param dbName
* The schema where the sequence belongs to.
* @param name
* The SQL name for the new sequence.
* @param legacy
* The legacy name of the sequence.
* @param init
* The initial value of the sequence.
* @param increment
* The increment step of the sequence.
* @param minVal
* The minimum value of the sequence.
* @param maxVal
* The minimum value of the sequence.
* @param cycle
* {@code true} when this is a cycling sequence.
*/
public void addSequence(String dbName,
String name,
String legacy,
Long init,
Long increment,
Long minVal,
Long maxVal,
Boolean cycle)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema. DDL generation is disabled in runtime mode.
return;
}
Map<String, Sequence> schema = sequences.get(dbName);
if (schema == null)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING, "Invalid schema [" + dbName + "].");
}
return;
}
if (schema.containsKey(legacy))
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Sequence [" + legacy + "] already added in schema [" + dbName + "]");
}
return;
}
schema.put(legacy, new Sequence(legacy, name, init, minVal, maxVal, increment, cycle));
}
/**
* Generates the DDLs needed to create all tables in a database with a specific schema. The
* temporary databases are not processed. If such schema was not defined, the method does
* nothing.
* <p>
* The method scans all dialects declared in {@code p2j.cfg.xml} and generates a DDL script
* for each one. The output is generated as {@code ddl/schema_table_<schema>_<dialect>.sql}.
*
* @param dbName
* The name of the schema to be processed.
*/
public void generateTableDDLs(String dbName)
{
if (Configuration.isRuntimeConfig() ||
DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName) ||
!tables.containsKey(dbName))
{
// quick out. Do not process _temp or other undefined schema. No DDL generation in runtime mode.
return;
}
File ddlFolder = getDDLFolder();
String eoln = getEndOfLine();
forAllDialects(dbName, dialect -> {
String fileName = "schema_table_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql";
File outFile = new File(ddlFolder, fileName);
try
{
PrintStream out = new PrintStream(outFile);
// H2: the collation is the very first statement executed. There must be absolutely no tables
// defined at the moment the collation is set
String coll = Configuration.getSchemaConfig().
getNamespaceParameter(dbName, dialect.id() + "/collation");
if (coll != null && dialect.explicitSetCollation())
{
// replacing DOT (.) and AT (@) with underscore (_) so that Linux - compatible schema name
// to be accepted by Java/H2. Example:
// "en_US@iso88591_fwd_basic" -> "en_US_iso88591_fwd_basic"
coll = coll.replace('.', '_').replace('@', '_');
out.print("set collation \"" + coll + "\"" + dialect.getDelimiter() + eoln);
}
List<String> preps = dialect.getDatabasePrepareStatements(new Database(dbName));
if (preps != null)
{
for (String prep : preps)
{
out.print(prep + eoln);
}
out.print(eoln);
}
generateTableDDLImpl(dbName, dialect, out, eoln);
if (!MetadataManager.META_SCHEMA.equals(dbName))
{
// the final thing: initialize the [p2j_id_generator_sequence] to default value so that the
// empty database can be used (without running the import tool)
out.print(dialect.getCreateSequenceString(Persistence.ID_GEN_SEQUENCE, 1) +
dialect.getDelimiter() + eoln + eoln);
}
out.close(); // make sure the file is closed
if (MetadataManager.META_SCHEMA.equals(dbName))
{
copySqlFile(outFile, MetadataManager.DDL_TABLE);
}
}
catch (IOException e)
{
LOG.severe("", e);
}
});
}
/**
* Generate triggers' DDL to support word tables.
*
* @param dbName
* The name of the schema to be processed.
*/
public void generateWordTablesDDLs(String dbName)
{
if (Configuration.isRuntimeConfig() ||
DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName) ||
!tables.containsKey(dbName))
{
// quick out. Do not process _temp or other undefined schema. No DDL generation in runtime mode.
return;
}
File ddlFolder = getDDLFolder();
String eoln = getEndOfLine();
forAllDialects(dbName, dialect -> {
String fileName = "schema_word_tables_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql";
if (!dialect.useWordTables())
{
return;
}
try (PrintStream out = new PrintStream(new File(ddlFolder, fileName)))
{
dialect.generateWordTablesDDLImpl(dbName, out, eoln,
wordTables.getOrDefault(dbName, Collections.emptyMap()).values());
wordTablesKeys.getOrDefault(dbName, Collections.emptyMap()).
values().stream().flatMap(List<String>::stream).forEach(s -> {
out.print(s);
out.print(eoln);
});
}
catch (IOException e)
{
LOG.severe("", e);
}
});
}
/**
* Generates the DDLs needed to create all sequences in a database with a specific schema.
* The sequence DDL statements are APPENDED at the end of table DDL definitions, for each
* dialect (i.e. {@code ddl/schema_table_<schema>_<dialect>.sql}).
*
* @param dbName
* The name of the schema to be processed.
*/
public void generateSequenceDDLs(String dbName)
{
if (Configuration.isRuntimeConfig() ||
DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName) ||
!tables.containsKey(dbName))
{
// quick out. Do not process _temp or other undefined schema. No DDL generation in runtime mode.
return;
}
File ddlFolder = getDDLFolder();
String eoln = getEndOfLine();
forAllDialects(dbName, dialect -> {
String fileName = "schema_table_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql";
try (PrintStream out = new PrintStream(
new FileOutputStream(new File(ddlFolder, fileName), true)))
{
generateSequenceDDLImpl(dbName, dialect, out, eoln);
}
catch (FileNotFoundException e)
{
LOG.severe("", e);
}
});
}
/**
* Generates the DDLs needed to create all indexes for the tables in a database with a
* specific schema. The temporary databases are not processed. If such schema was not
* defined, the method does nothing.
* <p>
* The method scans all dialects declared in {@code p2j.cfg.xml} and generates a DDL script
* for each one. The output is generated as {@code ddl/schema_index_<schema>_<dialect>.sql}.
*
* @param dbName
* The name of the schema to be processed.
*/
public void generateIndexDDLs(String dbName)
{
if (Configuration.isRuntimeConfig() || DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName))
{
// quick out. Do not process _temp schema. DDL generation is disabled in runtime mode.
return;
}
preprocessIndices(dbName);
File ddlFolder = getDDLFolder();
String eoln = getEndOfLine();
forAllDialects(dbName, dialect -> {
String fileName = "schema_index_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql";
File outFile = new File(ddlFolder, fileName);
try
{
PrintStream out = new PrintStream(outFile);
generateIndexDDLImpl(dbName, dialect, out, eoln);
out.close();
if (MetadataManager.META_SCHEMA.equals(dbName))
{
copySqlFile(outFile, MetadataManager.DDL_INDEX);
}
}
catch (IOException e)
{
LOG.severe("", e);
}
});
}
/**
* Copy UDFs script to the standard place.
*
* @param dbName
* The name of the database to be processed.
*/
public void copyUDFs(String dbName)
{
if (Configuration.isRuntimeConfig() ||
DatabaseManager.TEMP_TABLE_SCHEMA.equals(dbName) ||
!P2JPostgreSQLDialect.UDF_TYPE.useSqlUdfs(dbName))
{
return; // DDL processing is disabled in runtime mode.
}
File ddlFolder = getDDLFolder();
forAllDialects(dbName, dialect -> {
if (dialect instanceof P2JPostgreSQLDialect)
{
String fileName = "schema_udfs_" + getSchemaName(dbName) + "_" + dialect.id() + ".sql";
try
{
Path source = new File(Configuration.home() + "/p2j/build/udf/udfs.sql").toPath();
Path target = new File(ddlFolder, fileName).toPath();
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
}
catch (ConfigurationException | IOException e)
{
LOG.severe("", e);
}
}
});
}
/**
* Computes and returns the length of a format specifier.
*
* @param format
* The format to be parsed and its length returned.
*
* @return the number of character this format needs. If parsing operation fails, 0 is returned.
*/
public int getCharacterFormatLength(String format)
{
if (format == null || format.trim().isEmpty())
{
return 0;
}
try
{
return formatParser.parse(format);
}
catch (Exception e)
{
return 0;
}
}
/**
* Checks is the {@code ddl} folder exists and returns it after making sure it exists.
*
* @return the directory for storing DDL dialect specific statements.
*/
private File getDDLFolder()
{
File ddlFolder = new File("ddl");
if (!ddlFolder.exists())
{
ddlFolder.mkdirs(); // ignore result
}
return ddlFolder;
}
/**
* Get the EOLN sequence specific to target OS.
*
* @return The EOLN character or sequence that acts as a line terminator on the target OS.
*/
private String getEndOfLine()
{
return EnvironmentOps.OS_WIN.equalsIgnoreCase(Configuration.getParameter("opsys"))
? "\r\n" /* express to WINDOWS */ : "\n" /* default to Linux */;
}
/**
* Get UastHints for a current source.
*
* @return UastHints for a current source
*/
private UastHints getHints()
{
Aast src = getResolver().getSourceAst();
// locate the original file (.df, .p, .w, etc)
String filename = (String) src.getAncestor(-1).getAnnotation("srcfile");
if (filename == null)
{
// if that fails (expected annotation not present), locate the intermediary/artifact file
filename = src.getFilename();
}
if (filename.toLowerCase().endsWith(".p2o"))
{
// changing extensions from .p2o to .schema before applying the extra .hints token
filename = filename.substring(0, filename.length() - 4) + ".schema";
}
else if (filename.toLowerCase().endsWith(".df"))
{
// changing extensions from .df to .schema before applying the extra .hints token
filename = filename.substring(0, filename.length() - 3) + ".schema";
}
File file;
try
{
file = Configuration.toFile(filename);
}
catch (ConfigurationException e)
{
throw new RuntimeException("Failed to load hints from " + filename, e);
}
UastHints hints = UastHints.uastHints(file.getAbsolutePath());
return hints;
}
/**
* Execute a job for all configured dialects. The list of dialects is extracted from the
* {@code SchemaConfig} if none is declared, {@code H2} is assumed.
*
* @param dbName
* The database name.
* @param worker
* The job to be executed.
*/
private void forAllDialects(String dbName, Consumer<Dialect> worker)
{
SchemaConfig schemaConfig = Configuration.getSchemaConfig();
String dialectStr = schemaConfig.getNamespaceParameter(dbName, "ddl-dialects");
String[] dialects = dialectStr == null ? new String[] { "h2" } : dialectStr.split(",");
for (String dialectId : dialects)
{
Class<? extends Dialect> dialectCls = DialectHelper.getDialectClass(dialectId);
if (dialectCls != null)
{
try
{
Dialect dialect = dialectCls.getDeclaredConstructor().newInstance();
worker.accept(dialect);
}
catch (ReflectiveOperationException e)
{
LOG.severe("", e);
}
}
}
}
/**
* Preprocesses the indexes, dropping the UNIQUE option for those that are implicitly unique due to
* a previous index having the same components has already been validated as unique.
*
* @param dbName
* The name of the schema to be processed.
*/
private void preprocessIndices(String dbName)
{
Map<String, Table> schemaTables = this.tables.get(dbName);
Set<Map.Entry<String, Table>> tableEntries = schemaTables.entrySet();
for (Map.Entry<String, Table> tableEntry : tableEntries)
{
Table table = tableEntry.getValue();
if (table.minimalUnique != null)
{
// this table's indices were preprocessed and are now non-redundant and normalized
continue;
}
// extract the values in a list
List<P2JIndex> idxList = new ArrayList<>(table.indexes.size());
Set<Map.Entry<String, P2JIndex>> indexEntries = table.indexes.entrySet();
for (Map.Entry<String, P2JIndex> indexEntry : indexEntries)
{
if (!indexEntry.getValue().isWord())
{
idxList.add(indexEntry.getValue());
}
}
// do the processing:
table.minimalUnique = P2JIndex.normalizeUniqueIndexes(idxList, P2JIndexComponent.PROPERTY);
table.nonRedundant = P2JIndex.nonRedundantIndexes(idxList);
table.remaining = new LinkedHashSet<>(table.nonRedundant);
table.remaining.removeAll(table.minimalUnique);
}
}
/**
* Copies a generated SQL file to {@code $(output-root)/$(pkgroot)/dmo/_meta}.
*
* @param src
* The source file.
* @param destName
* The destination file name.
*
* @throws IOException
* When a problem wa detected during copy operation.
*/
private void copySqlFile(File src, String destName)
throws IOException
{
// create a copy in $(output-root)/$(pkgroot)/dmo/_meta
String dest = Configuration.getParameter("output-root") +
File.separator +
Configuration.getParameter("pkgroot").replace('.', File.separatorChar) +
File.separator +
"dmo" +
File.separator +
MetadataManager.META_SCHEMA;
new File(dest).mkdirs(); // make sure the dir exists
dest += File.separator + destName;
Files.copy(src.toPath(), new File(dest).toPath(), StandardCopyOption.REPLACE_EXISTING);
}
/**
* Worker method for {@code generateIndexDDLs}. It generates the DDLs needed to create all
* indexes for the tables in a database with a specific schema and using a certain dialect.
*
* @param dbName
* The target schema.
* @param dialect
* The {@code Dialect} to be used.
* @param out
* The {@code PrintStream} used for output.
* @param eoln
* OS-specific end of line terminator.
*/
private void generateIndexDDLImpl(String dbName, Dialect dialect, PrintStream out, String eoln)
{
Map<String, Table> schemaTables = this.tables.get(dbName);
Set<Map.Entry<String, Table>> tableEntries = schemaTables.entrySet();
for (Map.Entry<String, Table> tableEntry : tableEntries)
{
LOG.log(Level.SEVERE, "Generating INDEX DDL for table " + tableEntry.getKey() + "...");
Table table = tableEntry.getValue();
boolean isMeta = MetadataManager.META_SCHEMA.equals(dbName);
// generate drop indexes:
for (P2JIndex p2JIndex : table.nonRedundant)
{
if (isMeta && "meta_myconnection".equals(p2JIndex.getTable()))
{
p2JIndex = new P2JIndex(p2JIndex, "meta_myconnection_");
}
out.print(dialect.getDropIndexString(p2JIndex));
out.print(dialect.getDelimiter());
out.print(eoln);
}
// emit create statements for the minimum set of unique indexes first
for (P2JIndex p2JIndex : table.minimalUnique)
{
if (isMeta && "meta_myconnection".equals(p2JIndex.getTable()))
{
p2JIndex = new P2JIndex(p2JIndex, "meta_myconnection_");
}
dialect.checkIndexConstraints(p2JIndex, table.fields);
out.print(dialect.getCreateIndexString(p2JIndex, true, true));
out.print(dialect.getDelimiter());
out.print(eoln);
}
// then emit create statements for the rest of non-redundant indexes
for (P2JIndex p2JIndex : table.remaining)
{
out.print(dialect.getCreateIndexString(p2JIndex, false, true));
out.print(dialect.getDelimiter());
out.print(eoln);
}
}
}
/**
* Worker method for {@code generateSequencesDDLs()}. It generates the DDLs needed to create
* the sequences in a database with a specific schema and using a certain dialect.
*
* @param schema
* The target schema.
* @param dialect
* The {@code Dialect} to be used.
* @param out
* The {@code PrintStream} used for output.
* @param eoln
* OS-specific end of line terminator.
*/
private void generateSequenceDDLImpl(String schema,
Dialect dialect,
PrintStream out,
String eoln)
{
Map<String, Sequence> sequenceMap = this.sequences.get(schema);
Set<Map.Entry<String, Sequence>> entries = sequenceMap.entrySet();
for (Map.Entry<String, Sequence> entry : entries)
{
// first make sure the sequences are not present (by dropping them):
Sequence seq = entry.getValue();
out.print(dialect.getDropSequenceString(seq.name));
out.print(dialect.getDelimiter());
out.print(eoln);
// then (re-) create them
out.print(dialect.getCreateSequenceString(
seq.name, seq.initial, seq.increment, seq.min, seq.max, seq.cycle));
out.print(dialect.getDelimiter());
out.print(eoln);
}
}
/**
* Worker method for {@code generateTableDDLs()}. It generates the DDLs needed to create all
* tables in a database with a specific schema and using a certain dialect.
*
* @param dbName
* The target schema.
* @param dialect
* The {@code Dialect} to be used.
* @param out
* The {@code PrintStream} used for output.
* @param eoln
* OS-specific end of line terminator.
*/
private void generateTableDDLImpl(String dbName, Dialect dialect, PrintStream out, String eoln)
{
Map<String, Table> schemaTables = this.tables.get(dbName);
Set<Map.Entry<String, Table>> entries = schemaTables.entrySet();
List<String> addDDLs = new ArrayList<>();
for (Map.Entry<String, Table> entry : entries)
{
Table table = entry.getValue();
Map<Integer, StringBuilder> tableSBs = new HashMap<>();
String tableName = table.name;
boolean isMyConnectionTable = MetadataManager.META_SCHEMA.equals(dbName) &&
"meta_myconnection".equals(tableName);
if (isMyConnectionTable)
{
tableName += "_";
}
StringBuilder main = new StringBuilder();
main.append(dialect.getCreateTableString()).append(tableName).append(" (").append(eoln);
// injecting ID column as the first column
addFieldImpl(ID, dialect, table, main, eoln);
tableSBs.put(0, main);
Set<Map.Entry<String, P2JField>> fields = table.fields.entrySet();
for (Map.Entry<String, P2JField> fieldIt : fields)
{
P2JField field = fieldIt.getValue();
int extent = field.getExtent();
StringBuilder sb = tableSBs.get(extent);
if (sb == null)
{
sb = new StringBuilder();
String compositeTableName = table.name + "__" + extent;
sb.append(dialect.getCreateTableString()).append(compositeTableName)
.append(" (").append(eoln);
tableSBs.put(extent, sb);
// inject parent__id column at this place
addFieldImpl(parent__id, dialect, table, sb, eoln);
// add the foreign index:
String fkIndex = "create index " + compositeTableName + "_fkey on " +
compositeTableName + " (parent__id)";
addDDLs.add(fkIndex);
// add the constraint; the name is the FK prefix followed by the composite table name
addDDLs.add(dialect.getAddForeignKeyConstraintString(
compositeTableName,
"FK_" + compositeTableName.toUpperCase(),
"parent__id",
table.name,
PK,
INDENT,
eoln));
}
addFieldImpl(field, dialect, table, sb, eoln);
}
List<Integer> extents = new ArrayList<>(tableSBs.keySet());
extents.sort(Integer::compareTo);
for (Integer extent : extents)
{
// fixup table DDL before printing out
StringBuilder sb = tableSBs.get(extent);
if (extent != 0)
{
// NOTE: inject list__index column here
addFieldImpl(list__index, dialect, table, sb, eoln);
sb.append(INDENT).append("primary key (parent__id, list__index)").append(eoln);
}
else
{
sb.append(INDENT).append("primary key (").append(PK).append(")").append(eoln);
}
sb.append(")");
out.print(sb);
out.print(dialect.getDelimiter());
out.print(eoln);
out.print(eoln);
}
if (isMyConnectionTable)
{
for (String l: MY_CONNECTION_AUX)
{
out.print(l);
out.print(eoln);
}
out.print(eoln);
}
if (dialect.useWordTables())
{
generateWordTable(dbName, table, out, dialect, eoln);
}
}
for (String addDLL : addDDLs)
{
out.print(addDLL);
out.print(dialect.getDelimiter());
out.print(eoln);
out.print(eoln);
}
}
/**
* Generate auxiliary word table
*
* @param dbName
* The target schema.
* @param table
* The parent table.
* @param out
* The {@code PrintStream} used for output.
* @param dialect
* The {@code Dialect} to be used.
* @param eoln
* OS-specific end of line terminator.
*/
private void generateWordTable(String dbName,
Table table,
PrintStream out,
Dialect dialect,
String eoln)
{
String delimiter = dialect.getDelimiter();
TableHints tableHints = hints.computeIfAbsent(dbName, dbn -> getHints()).
getTableHintsOrEmpty().get(table.legacyName);
List<P2JIndex> wordIndexes = table.indexes.values().
stream().filter(P2JIndex::isWord).collect(Collectors.toList());
for (P2JIndex wordIndex: wordIndexes)
{
String tableName = wordIndex.getTable();
ArrayList<P2JIndexComponent> comps = wordIndex.components();
if (comps.size() == 0)
{
continue;
}
P2JIndexComponent indexComponent = comps.get(0);
boolean caseSensitive = !indexComponent.isIgnoreCase();
String fieldName = indexComponent.getColumnName();
String legacyName = indexComponent.getLegacyName();
P2JField field = table.fields.get(legacyName);
long extent = 0;
boolean customExtent = false;
WordTable.Builder builder;
if (field == null)
{
List<ExtentHintField> extentHintFields = null;
if (tableHints != null)
{
extentHintFields = tableHints.getCustomFieldExtent(legacyName);
}
else
{
String prefix = indexComponent.getLegacyName() + "~";
List<P2JField> extentFields= table.fields.entrySet().stream().
filter(e -> e.getKey().startsWith(prefix)).
map(Map.Entry::getValue).collect(Collectors.toList());
if (!extentFields.isEmpty())
{
extentHintFields = extentFields.stream().
map(p2jF -> new ExtentHintField(
p2jF.getName(), p2jF.getLabel(), null)).
collect(Collectors.toList());
}
}
if (extentHintFields == null)
{
LOG.warning(
String.format("No field data for the word index:%s.%s, field: %s",
table.name, wordIndex.getName(), legacyName)
);
continue;
}
extent = extentHintFields.size();
customExtent = true;
builder = new WordTable.Builder(parts -> createDbObjectName(dbName, parts),
tableName, fieldName, extent, caseSensitive).extentHintFields(extentHintFields);
}
else
{
extent = field.getExtent();
builder = new WordTable.Builder(parts -> createDbObjectName(dbName, parts),
tableName, fieldName, extent, caseSensitive);
}
// TODO: use hints to override default objects' names
WordTable wordTable = builder.build(dialect);
Table wtable = new Table(wordTable.tableName);
wordTables.computeIfAbsent(dbName, db -> new HashMap<>()).put(wordTable.tableName, wordTable);
StringBuilder wt = new StringBuilder();
wt.append(dialect.getCreateTableString()).
append(wordTable.tableName).
append(" (").append(eoln);
addFieldImpl(PARENT, dialect, wtable, wt, eoln);
if (extent > 0)
{
addFieldImpl(list__index, dialect, wtable, wt, eoln);
}
addLastFieldImpl(WORD, dialect, wtable, wt, eoln);
wt.append(")");
out.print(wt);
out.print(delimiter);
out.print(eoln);
out.print(eoln);
List<String> keys = new ArrayList<>();
keys.add(wordTable.dropPK("", delimiter));
keys.add(wordTable.createPK("", delimiter));
keys.add(wordTable.dropIndex(""));
keys.add(wordTable.createIndex("", delimiter));
// [dropFK] must be executed before [dropFkIndex]. See description of SQL errors 42111 and 90085.
keys.add(wordTable.dropFK("", delimiter));
keys.add(wordTable.dropFkIndex(""));
keys.add(wordTable.createFkIndex("", delimiter));
keys.add(wordTable.createFK(eoln));
wordTablesKeys.computeIfAbsent(dbName, db -> new HashMap<>()).put(wordTable.tableName, keys);
}
}
/**
* Appends a field to a table DDL statement. Usually one column is added but, if required
* (in case of {@code datetime-tz} and case-insensitive {@code character} fields), multiple
* column are added.
*
* @param field
* The field to be added.
* @param dialect
* The dialect to be used.
* @param table
* The field's parent table.
* @param sb
* The string builder used for concatenate string data.
* @param eoln
* OS-dependent end of line terminator.
*/
private void addFieldImpl(P2JField field,
Dialect dialect,
Table table,
StringBuilder sb,
String eoln)
{
String fwdType = field.getType().toString();
boolean isDecimal = "decimal".equals(fwdType);
int sqlWidth = isDecimal ? field.getScale() : field.getMaxWidth();
sb.append(INDENT).append(field.getName()).append(" ")
.append(dialect.getSqlMappedType(fwdType, sqlWidth, field.isCaseSensitive()))
.append(field.isMandatory() ? " not null," : ",").append(eoln);
// special handling for fields that are index components: we handle this in a dialect specific
if (dialect.injectComputedColumns() && isIndexComponent(field, table))
{
String ccDeclaration = dialect.getComputedColumnString(
field.getName(), fwdType, sqlWidth, field.isCaseSensitive(), field.isMandatory());
if (ccDeclaration != null)
{
sb.append(INDENT).append(ccDeclaration).append(",").append(eoln);
}
}
// special handling for datetime-tz: this is handled in a uniform, dialect-independent
// way (Note: this is because in spite of correct time/date arithmetic, the PSQL dialect
// loses the time-zone offset and it cannot be retrieved back)
if ("datetimetz".equals(fwdType))
{
sb.append(INDENT).append(field.getName()).append(DTZ_OFFSET)
.append(" ").append(dialect.getSqlMappedType("integer", 0, null))
.append(field.isMandatory() ? " not null," : ",").append(eoln);
}
}
/**
* Appends a last field to a table DDL statement. Usually one column is added but, if required
* (in case of {@code datetime-tz} and case-insensitive {@code character} fields), multiple
* column are added.
*
* @param field
* The field to be added.
* @param dialect
* The dialect to be used.
* @param table
* The field's parent table.
* @param sb
* The string builder used for concatenate string data.
* @param eoln
* OS-dependent end of line terminator.
*/
private void addLastFieldImpl(P2JField field,
Dialect dialect,
Table table,
StringBuilder sb,
String eoln)
{
String fwdType = field.getType().toString();
boolean isChar = "character".equals(fwdType);
boolean isDecimal = "decimal".equals(fwdType);
int sqlWidth = isDecimal ? field.getScale() : field.getMaxWidth();
sb.append(INDENT).append(field.getName()).append(" ")
.append(dialect.getSqlMappedType(fwdType, sqlWidth, field.isCaseSensitive()))
.append(field.isMandatory() ? " not null" : "").append(eoln);
// special handling for character fields that are index components:
// we handle this in a dialect specific
if (isChar &&
dialect.injectComputedColumns() &&
// dialect.needsComputedColumns() &&
isIndexComponent(field, table))
{
sb.append(INDENT)
.append(dialect.getComputedColumnPrefix(field.isCaseSensitive()))
.append(field.getName())
.append(" ")
.append(dialect.getSqlMappedType(fwdType, sqlWidth, field.isCaseSensitive()))
.append(" as ")
.append(dialect.getComputedColumnFormula(field.getName(), !field.isCaseSensitive()))
.append(eoln);
}
// special handling for datetime-tz: this is handled in a uniform, dialect-independent
// way (Note: this is because in spite of correct time/date arithmetic, the PSQL dialect
// loses the time-zone offset and it cannot be retrieved back)
if ("datetimetz".equals(fwdType))
{
sb.append(INDENT).append(field.getName()).append(DTZ_OFFSET)
.append(" ").append(dialect.getSqlMappedType("integer", 0, null))
.append(field.isMandatory() ? " not null" : "").append(eoln);
}
}
/**
* Checks whether a field is part of an index. The {@code character} case-insensitive fields
* index components need special handling.
*
* @param field
* The field name to be checked.
* @param table
* The parent table of the field.
*
* @return {@code true} only if the field is an index component.
*/
private boolean isIndexComponent(P2JField field, Table table)
{
Set<Map.Entry<String, P2JIndex>> entries = table.indexes.entrySet();
for (Map.Entry<String, P2JIndex> entry : entries)
{
P2JIndex index = entry.getValue();
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
if (comp.getColumnName().equals(field.getName()))
{
return true;
}
}
}
return false;
}
/**
* Construct database object name concatenating parts.
*
* @param dbName
* The target schema.
* @param parts
* The parts of the name.
*
* @return object name
*/
private String createDbObjectName(String dbName, String... parts)
{
Set<String> wordSupportName = wordTablesNames.computeIfAbsent(dbName, n -> new HashSet<>());
String name = NameBuilder.concat(parts);
int excess = name.length() - MAX_OBJECT_NAME_LEN;
if (excess > 0)
{
int np = 0, len = -1;
for (int i = 0; i < parts.length; i++)
{
if (parts[i].length() > len)
{
np = i;
len = parts[i].length();
}
}
String longest = parts[np];
int n = 0;
do
{
String s = String.valueOf(n++);
parts[np] = longest.substring(0, len - excess - s.length()) + s;
name = NameBuilder.concat(parts);
}
while (!wordSupportName.add(name));
}
return name;
}
}
}