/*
* Copyright <2021> Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*
*/
package software.amazon.documentdb.jdbc.metadata;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import org.bson.BsonType;
import software.amazon.documentdb.jdbc.common.utilities.JdbcType;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.calcite.sql.parser.SqlParser.DEFAULT_IDENTIFIER_MAX_LENGTH;
public class DocumentDbTableSchemaGeneratorHelper {
static final String EMPTY_STRING = "";
static final int KEY_COLUMN_NONE = 0;
private static final String PATH_SEPARATOR = ".";
private static final String ID_FIELD_NAME = "_id";
private static final int ID_PRIMARY_KEY_COLUMN = 1;
/**
* The map of data type promotions.
*
* @see
* Map Relational Schemas to DocumentDB - Scalar-Scalar Conflicts
*/
private static final ImmutableMap, JdbcType> PROMOTION_MAP =
new ImmutableMap.Builder, JdbcType>()
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.BOOLEAN), JdbcType.BOOLEAN)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.DATE_TIME), JdbcType.TIMESTAMP)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.DECIMAL128), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.DOUBLE), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.INT32), JdbcType.INTEGER)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.INT64), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.TIMESTAMP), JdbcType.TIMESTAMP)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.NULL), JdbcType.NULL)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.ARRAY), JdbcType.ARRAY)
.put(new SimpleEntry<>(JdbcType.NULL, BsonType.DOCUMENT), JdbcType.JAVA_OBJECT)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.BOOLEAN), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.DECIMAL128), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.DOUBLE), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.INT32), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.INT64), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.NULL), JdbcType.ARRAY)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.ARRAY), JdbcType.ARRAY)
.put(new SimpleEntry<>(JdbcType.ARRAY, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.BOOLEAN), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.DECIMAL128), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.DOUBLE), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.INT32), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.INT64), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.NULL), JdbcType.JAVA_OBJECT)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.JAVA_OBJECT, BsonType.DOCUMENT), JdbcType.JAVA_OBJECT)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.BOOLEAN), JdbcType.BOOLEAN)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.DECIMAL128), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.DOUBLE), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.INT32), JdbcType.INTEGER)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.INT64), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.NULL), JdbcType.BOOLEAN)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BOOLEAN, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.BOOLEAN), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.DECIMAL128), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.DOUBLE), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.INT32), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.INT64), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.NULL), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.BIGINT, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.BOOLEAN), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.DECIMAL128), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.DOUBLE), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.INT32), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.INT64), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.NULL), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DECIMAL, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.BOOLEAN), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.DECIMAL128), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.DOUBLE), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.INT32), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.INT64), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.NULL), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.DOUBLE, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.BOOLEAN), JdbcType.INTEGER)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.DECIMAL128), JdbcType.DECIMAL)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.DOUBLE), JdbcType.DOUBLE)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.INT32), JdbcType.INTEGER)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.INT64), JdbcType.BIGINT)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.NULL), JdbcType.INTEGER)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.INTEGER, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.BOOLEAN), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.DATE_TIME), JdbcType.TIMESTAMP)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.DECIMAL128), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.DOUBLE), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.INT32), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.INT64), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.TIMESTAMP), JdbcType.TIMESTAMP)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.NULL), JdbcType.TIMESTAMP)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.TIMESTAMP, BsonType.DOCUMENT), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.BOOLEAN), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.DATE_TIME), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.DECIMAL128), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.DOUBLE), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.INT32), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.INT64), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.TIMESTAMP), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.MAX_KEY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.MIN_KEY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.NULL), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.OBJECT_ID), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.STRING), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.ARRAY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARBINARY, BsonType.DOCUMENT), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.BOOLEAN), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.BINARY), JdbcType.VARBINARY)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.DATE_TIME), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.DECIMAL128), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.DOUBLE), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.INT32), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.INT64), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.TIMESTAMP), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.MAX_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.MIN_KEY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.NULL), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.OBJECT_ID), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.STRING), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.ARRAY), JdbcType.VARCHAR)
.put(new SimpleEntry<>(JdbcType.VARCHAR, BsonType.DOCUMENT), JdbcType.VARCHAR)
.build();
/**
* Combines two paths to form a new path.
*
* @param path the root/parent path.
* @param fieldName the field name to append to the path.
* @return a new path with the fieldName append to the root path separated by a period.
*/
public static String combinePath(final String path, final String fieldName) {
final boolean isPathEmpty = Strings.isNullOrEmpty(path);
final boolean isFieldNameEmpty = Strings.isNullOrEmpty(fieldName);
final String pathSeparator = !isPathEmpty && !isFieldNameEmpty ? PATH_SEPARATOR : EMPTY_STRING;
final String newPath = !isPathEmpty ? path : EMPTY_STRING;
final String newFieldName = !isFieldNameEmpty ? fieldName : EMPTY_STRING;
return String.format("%s%s%s", newPath, pathSeparator, newFieldName);
}
/**
* Gets the promoted SQL data type from previous SQL data type and the current BSON data type.
*
* @param bsonType the current BSON data type.
* @param prevSqlType the previous SQL data type.
* @return returns the promoted SQL data type.
*/
@VisibleForTesting
static JdbcType getPromotedSqlType(final BsonType bsonType, final JdbcType prevSqlType) {
final Entry key = new SimpleEntry<>(prevSqlType, bsonType);
return PROMOTION_MAP.getOrDefault(key, JdbcType.VARCHAR);
}
/**
* Gets whether the field is the "_id" field.
*
* @param fieldName the name of the field.
* @return returns {@code true} if the field name if "_id", {@code false} otherwise.
*/
static boolean isIdField(final String fieldName) {
return ID_FIELD_NAME.equals(fieldName);
}
/**
* Handles a complex to scalar conflict by removing the previous table map and clearing existing
* column map.
*
* @param tableMap the table map.
* @param path the path to the table.
* @param columnMap the column map.
*/
static void handleComplexScalarConflict(
final Map tableMap,
final String path,
final Map columnMap) {
tableMap.remove(path);
columnMap.clear();
}
/**
* Detects and handles the case were a conflict occurs at a lower lever in the array.
* It removes the index column for the higher level array index.
* It ensures the SQL type is set to VARCHAR.
*
* @param columnMap the column map to modify.
* @param level the current array level.
* @param sqlType the previous SQL type.
* @return if a conflict is detected, returns VARCHAR, otherwise, the original SQL type.
*/
static JdbcType handleArrayLevelConflict(
final Map columnMap,
final int level,
final JdbcType sqlType) {
JdbcType newSqlType = sqlType;
// Remove previously detect index columns at higher index level,
// if we now have scalars at a lower index level.
final Map origColumns = new LinkedHashMap<>(columnMap);
for (Entry entry : origColumns.entrySet()) {
final DocumentDbMetadataColumn column = (DocumentDbMetadataColumn) entry.getValue();
if (column.getArrayIndexLevel() != null && column.getArrayIndexLevel() > level) {
columnMap.remove(entry.getKey());
// We're collapsing an array level, so revert to VARCHAR/VARBINARY this and for the higher
// level array components.
newSqlType = getPromotedSqlType(BsonType.STRING, newSqlType);
}
}
return newSqlType;
}
/**
* Gets the primary key column number depending on whether we are whether it is the primary key.
*
* @param isPrimaryKey an indicator of whether we are dealing with the primary key.
* @return the value {@link #ID_PRIMARY_KEY_COLUMN} if the primary key, other it
* returns {@link #KEY_COLUMN_NONE}.
*/
static int getPrimaryKeyColumn(final boolean isPrimaryKey) {
// If primary key, then first column, zero indicates not part of primary key.
return isPrimaryKey ? ID_PRIMARY_KEY_COLUMN : KEY_COLUMN_NONE;
}
/**
* Gets the field name, depending on whether it is the primary key.
*
* @param path the path the field belongs to.
* @param fieldName the name of the field in the path.
* @param isPrimaryKey an indicator of whether this is the primary key.
* @param columnNameMap a map of unique column names.
* @return a column name for the field.
*/
static String getFieldNameIfIsPrimaryKey(
final String path, final String fieldName,
final boolean isPrimaryKey,
final Map columnNameMap) {
return isPrimaryKey
// For the primary key, qualify it with the parent name.
? toName(combinePath(getParentName(path), fieldName), columnNameMap)
: fieldName;
}
/**
* Gets the virtual table name, depending on whether this is the primary key.
*
* @param fieldPath the path the field belongs to.
* @param nextSqlType the next SQL type.
* @param isPrimaryKey an indicator of whether this is the primary key.
* @param collectionName a map of unique column names.
* @param tableNameMap a map of unique table names.
* @return the name of the virtual table if not a primary key (base table) and type is not
* ARRAY or JAVA_OBJECT. Otherwise, null.
*/
static String getVirtualTableNameIfIsPrimaryKey(
final String fieldPath,
final JdbcType nextSqlType,
final boolean isPrimaryKey,
final String collectionName,
final Map tableNameMap) {
return !isPrimaryKey && (nextSqlType == JdbcType.ARRAY || nextSqlType == JdbcType.JAVA_OBJECT)
? toName(combinePath(collectionName, fieldPath), tableNameMap)
: null;
}
/**
* Gets the SQL type, depending on whether this is the primary key.
*
* @param bsonType the underlying source data type.
* @param prevSqlType the previous SQL type detected.
* @param isPrimaryKey an indicator of whether this is the primary key.
* @return the SQL type to use.
*/
static JdbcType getSqlTypeIfIsPrimaryKey(
final BsonType bsonType,
final JdbcType prevSqlType,
final boolean isPrimaryKey) {
return isPrimaryKey && bsonType == BsonType.DOCUMENT
? JdbcType.VARCHAR
: getPromotedSqlType(bsonType, prevSqlType);
}
/**
* Gets the previous SQL data type.
*
* @param prevMetadataColumn the column to get the SQL type for. Can be null.
* @return the previous SQL data type if the column is not null, {@link JdbcType#NULL},
* otherwise.
*/
static JdbcType getPrevSqlTypeOrDefault(
final DocumentDbMetadataColumn prevMetadataColumn) {
return prevMetadataColumn != null
? prevMetadataColumn.getSqlType()
: JdbcType.NULL;
}
/**
* Gets whether the given SQL type is a complex type (ARRAY or JAVA_OBJECT).
*
* @param sqlType the SQL type to tests.
* @return {@code true} if a complex type, {@code false}, otherwise.
*/
static boolean isComplexType(final JdbcType sqlType) {
return sqlType == JdbcType.JAVA_OBJECT || sqlType == JdbcType.ARRAY;
}
/**
* Adds to the list of primary keys, if is a primary key.
*
* @param foreignKeys the list of foreign keys.
* @param isPrimaryKey an indicator of whether this is a primary key.
* @param metadataColumn the column to add.
*/
static void addToForeignKeysIfIsPrimary(
final List foreignKeys,
final boolean isPrimaryKey,
final DocumentDbMetadataColumn metadataColumn) {
// Add the key to the foreign keys for child tables.
if (isPrimaryKey) {
foreignKeys.add(metadataColumn);
}
}
/**
* Gets the previous index for this column.
*
* @param prevMetadataColumn the previous column to use. Can be null.
* @param defaultValue the default index value to use if the column is null.
* @return the index of the column, if not null. Otherwise, the default value.
*/
static int getPrevIndexOrDefault(final DocumentDbMetadataColumn prevMetadataColumn,
final int defaultValue) {
return prevMetadataColumn != null
? prevMetadataColumn.getIndex()
: defaultValue;
}
/**
* Checks and ensures consistency of SQL type between the primary key of the base table and any
* generated virtual tables.
*
* @param tableMap the map of tables.
* @param path the path of the collection.
* @param columnMap the column map of the base table.
* @param columnNameMap the map of unique column names.
*/
static void checkVirtualTablePrimaryKeys(
final Map tableMap,
final String path,
final LinkedHashMap columnMap,
final Map columnNameMap) {
final String primaryKeyColumnName = toName(combinePath(path, ID_FIELD_NAME), columnNameMap);
final DocumentDbMetadataColumn primaryKeyColumn = (DocumentDbMetadataColumn) columnMap
.get(primaryKeyColumnName);
for (DocumentDbSchemaTable table : tableMap.values()) {
final DocumentDbMetadataColumn column = (DocumentDbMetadataColumn) table
.getColumnMap().get(primaryKeyColumnName);
if (column != null && !column.getSqlType().equals(primaryKeyColumn.getSqlType())) {
column.setSqlType(primaryKeyColumn.getSqlType());
}
}
}
/**
* Converts the path to name swapping the period character for an underscore character. Unique
* names of maximum length {@link org.apache.calcite.sql.parser.SqlParser#DEFAULT_IDENTIFIER_MAX_LENGTH}
* are maintained in the uniqueNameMap parameter.
*
* @param path the path to convert.
* @param uniqueNameMap the map of unique names.
* @return a string the period character swapped for an underscore character, of correct maximum
* length and unique within the map of given paths
*/
@VisibleForTesting
static String toName(final String path, final Map uniqueNameMap) {
return toName(path, uniqueNameMap, DEFAULT_IDENTIFIER_MAX_LENGTH);
}
/**
* Converts the path to name swapping the period character for an underscore character. Unique
* names of maximum length given in identifierMaxLength parameter. are maintained in the
* uniqueNameMap parameter.
*
* @param path the path to convert.
* @param uniqueNameMap the map of unique names.
* @param identifierMaxLength the maximum length of identifier name.
* @return a string the period character swapped for an underscore character, of correct maximum
* length and unique within the map of given paths
*/
@VisibleForTesting
static String toName(
final String path,
final Map uniqueNameMap,
final int identifierMaxLength) {
final String fullPathName = path.replaceAll("\\.", "_");
// If already mapped, return the mapped value.
if (uniqueNameMap.containsKey(path)) {
return uniqueNameMap.get(path);
}
// If not greater the maximum allowed length, return value.
if (path.length() <= identifierMaxLength) {
return fullPathName;
}
// Shorten the name and ensure uniqueness.
final StringBuilder shortenedName = new StringBuilder(fullPathName);
final List matches = getSeparatorMatches(path);
if (matches.isEmpty()) {
// Only "base table"
shortenBaseName(
path,
uniqueNameMap,
identifierMaxLength,
shortenedName);
} else if (matches.get(0).start() < identifierMaxLength) {
// Base table shorter than max length - combine with trailing path.
shortenWithBaseNameLessThanMaxLength(
path,
uniqueNameMap,
identifierMaxLength,
shortenedName,
matches);
} else {
// Base table too long. Combine on trailing path.
shortenWithBaseNameLongerThanMaxLength(
path,
uniqueNameMap,
identifierMaxLength,
shortenedName,
matches);
}
return shortenedName.toString();
}
private static void shortenWithBaseNameLongerThanMaxLength(
final String path,
final Map uniqueNameMap,
final int identifierMaxLength,
final StringBuilder shortenedName,
final List matches) {
int lastMatchIndex = 0;
for (int matchIndex = matches.size() - 1; matchIndex > 0; matchIndex--) {
if ((path.length() - matches.get(matchIndex).start()) >= identifierMaxLength) {
break;
}
lastMatchIndex = matchIndex;
}
if (lastMatchIndex > 0) {
shortenedName.delete(0, matches.get(lastMatchIndex).start());
} else {
shortenedName.delete(0, shortenedName.length() - identifierMaxLength);
}
ensureUniqueName(uniqueNameMap, shortenedName, path);
}
private static void shortenWithBaseNameLessThanMaxLength(
final String path,
final Map uniqueNameMap,
final int identifierMaxLength,
final StringBuilder shortenedName,
final List matches) {
int lastMatchIndex = 0;
for (int matchIndex = matches.size() - 1; matchIndex > 0; matchIndex--) {
if ((path.length() - matches.get(matchIndex).start()) + matches.get(0).start()
>= identifierMaxLength) {
break;
}
lastMatchIndex = matchIndex;
}
final int deleteChars;
if (lastMatchIndex > 0) {
deleteChars = matches.get(lastMatchIndex).start() - matches.get(0).start();
} else {
deleteChars = path.length() - identifierMaxLength;
}
shortenedName.delete(matches.get(0).start(), matches.get(0).start() + deleteChars);
ensureUniqueName(uniqueNameMap, shortenedName, path);
}
private static void shortenBaseName(
final String path,
final Map uniqueNameMap,
final int identifierMaxLength,
final StringBuilder shortenedName) {
shortenedName.delete(identifierMaxLength, shortenedName.length());
ensureUniqueName(uniqueNameMap, shortenedName, path);
}
private static void ensureUniqueName(
final Map uniqueNameMap,
final StringBuilder shortenedName,
final String path) {
int counter = 0;
final StringBuilder tempName = new StringBuilder(shortenedName);
while (uniqueNameMap.values().stream().anyMatch(s -> tempName.toString().equals(s))) {
counter++;
final String counterString = String.valueOf(counter);
tempName.setLength(0);
tempName.append(
shortenedName.substring(0, shortenedName.length() - counterString.length()))
.append(counterString);
}
shortenedName.setLength(0);
shortenedName.append(tempName);
uniqueNameMap.put(path, shortenedName.toString());
}
private static List getSeparatorMatches(final String path) {
final List matches = new ArrayList<>();
final Pattern separatorPattern = Pattern.compile("\\.");
final Matcher separatorMatcher = separatorPattern.matcher(path);
separatorMatcher.reset();
while (separatorMatcher.find()) {
matches.add(separatorMatcher.toMatchResult());
}
return matches;
}
/**
* Gets the parent name (last node) in the path.
*
* @param path the path to read.
* @return the last node in the path.
*/
private static String getParentName(final String path) {
return path.substring(path.lastIndexOf('.') + 1);
}
}