package er.extensions.jdbc;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.Types;
import java.text.MessageFormat;
import java.text.ParseException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.eoaccess.EOAdaptor;
import com.webobjects.eoaccess.EOAdaptorChannel;
import com.webobjects.eoaccess.EOAdaptorOperation;
import com.webobjects.eoaccess.EOAttribute;
import com.webobjects.eoaccess.EODatabase;
import com.webobjects.eoaccess.EODatabaseChannel;
import com.webobjects.eoaccess.EODatabaseContext;
import com.webobjects.eoaccess.EOEntity;
import com.webobjects.eoaccess.EOGeneralAdaptorException;
import com.webobjects.eoaccess.EOModel;
import com.webobjects.eoaccess.EOQualifierSQLGeneration;
import com.webobjects.eoaccess.EORelationship;
import com.webobjects.eoaccess.EOSQLExpression;
import com.webobjects.eoaccess.EOSQLExpressionFactory;
import com.webobjects.eoaccess.EOSchemaGeneration;
import com.webobjects.eoaccess.EOSynchronizationFactory;
import com.webobjects.eoaccess.EOUtilities;
import com.webobjects.eocontrol.EOEditingContext;
import com.webobjects.eocontrol.EOFetchSpecification;
import com.webobjects.eocontrol.EOObjectStoreCoordinator;
import com.webobjects.eocontrol.EOQualifier;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSForwardException;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSSelector;
import com.webobjects.foundation.NSSet;
import com.webobjects.foundation.NSTimestamp;
import com.webobjects.foundation._NSUtilities;
import com.webobjects.jdbcadaptor.JDBCAdaptor;
import com.webobjects.jdbcadaptor.JDBCAdaptorException;
import com.webobjects.jdbcadaptor.JDBCPlugIn;
import er.extensions.eof.ERXConstant;
import er.extensions.eof.ERXEC;
import er.extensions.eof.ERXEOAccessUtilities;
import er.extensions.eof.ERXModelGroup;
import er.extensions.eof.qualifiers.ERXFullTextQualifier;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXStringUtilities;
import er.extensions.validation.ERXValidationException;
import er.extensions.validation.ERXValidationFactory;
/**
* ERXSQLHelper provides support for additional database-vender-specific
* operations that JDBCPlugIn does not cover.
*
* By default this will try to load the class
* er.extensions.ERXSQLHelper$DatabaseVendorSQLHelper. For instance,
* er.extensions.ERXSQLHelper$FrontBaseSQLHelper. If you want to change the
* helper that is used for a particular database vendor, then override
* FrontBase.SQLHelper, Oracle.SQLHelper, etc. Case is important (because the
* vendor name is prepended to the class name), and should match what your
* JDBCPlugIn.databaseProductName() returns.
*
* @property databaseProductName.SQLHelper the class name of the SQLHelper for
* the database product name
*
* @author mschrag
*/
public class ERXSQLHelper {
/** custom JDBC types */
public interface CustomTypes {
public static final int INET = 9001;
}
private static final Logger log = LoggerFactory.getLogger(ERXSQLHelper.class);
private static Map<String, ERXSQLHelper> _sqlHelperMap = new HashMap<>();
public void prepareConnectionForSchemaChange(EOEditingContext ec, EOModel model) {
// do nothing by default
}
public void restoreConnectionSettingsAfterSchemaChange(EOEditingContext ec, EOModel model) {
// do nothing by default
}
public boolean shouldExecute(String sql) {
return true;
}
/**
* creates SQL to create tables for the specified Entities. This can be used
* with EOUtilities rawRowsForSQL method to create the tables.
*
* @param entities
* a NSArray containing the entities for which create table
* statements should be generated or <code>null</code> if all entities
* in the model should be used.
* @param modelName
* the name of the EOModel
* @param optionsCreate
*
* @return a <code>String</code> containing SQL statements to create
* tables
*/
public String createSchemaSQLForEntitiesInModelWithNameAndOptions(NSArray<EOEntity> entities, String modelName, NSDictionary optionsCreate) {
EOModel m = ERXEOAccessUtilities.modelGroup(null).modelNamed(modelName);
return createSchemaSQLForEntitiesInModelAndOptions(entities, m, optionsCreate);
}
/**
* Reimplementation that does not try to the shared objects. You should exit
* soon after calling this, as it may or may not leave channels open. It is
* simply to generate sql.
*
* @param model
* @param coordinator
* @return the database context for the given model
*/
private EODatabaseContext databaseContextForModel(EOModel model, EOObjectStoreCoordinator coordinator) {
EODatabaseContext dbc = null;
NSArray objectStores = coordinator.cooperatingObjectStores();
int i = 0;
for (int c = objectStores.count(); i < c; i++) {
Object objectStore = objectStores.objectAtIndex(i);
if ((objectStore instanceof EODatabaseContext) && ((EODatabaseContext) objectStore).database().addModelIfCompatible(model)) {
dbc = (EODatabaseContext) objectStore;
}
}
if (dbc == null) {
dbc = (EODatabaseContext) _NSUtilities.instantiateObject(EODatabaseContext.contextClassToRegister(), new Class[] { com.webobjects.eoaccess.EODatabase.class }, new Object[] { new EODatabase(model) }, true, false);
coordinator.addCooperatingObjectStore(dbc);
}
return dbc;
}
/**
* creates SQL to create tables for the specified Entities. This can be used
* with EOUtilities rawRowsForSQL method to create the tables.
*
* @param entities
* a NSArray containing the entities for which create table
* statements should be generated or null if all entities in the
* model should be used.
* @param model
* the EOModel
* @param optionsCreate
* a NSDictionary containing the different options
*
* @return a <code>String</code> containing SQL statements to create
* tables
*/
@SuppressWarnings("unchecked")
public String createSchemaSQLForEntitiesInModelAndOptions(NSArray<EOEntity> entities, EOModel model, NSDictionary optionsCreate) {
EOEditingContext ec = ERXEC.newEditingContext();
ec.lock();
try {
EODatabaseContext databaseContext = databaseContextForModel(model, (EOObjectStoreCoordinator) ec.rootObjectStore());
// AK the default implementation loads the shared objects, and when
// they don't exist, throw an an error
// which is not very useful for schema generation
// But you would probably want to exit soon after calling this....
// EODatabaseContext databaseContext =
// EODatabaseContext.registeredDatabaseContextForModel(model, ec);
if (entities == null) {
Enumeration<EOEntity> e = model.entities().objectEnumerator();
NSMutableArray<EOEntity> ar = new NSMutableArray<>();
while (e.hasMoreElements()) {
EOEntity currentEntity = e.nextElement();
if (ERXModelGroup.isPrototypeEntity(currentEntity)) {
// we do not want to add EOXXXPrototypes entities
continue;
}
if (!ERXEOAccessUtilities.entityUsesSeparateTable(currentEntity)) {
continue;
}
ar.addObject(currentEntity);
}
entities = ar;
}
String result = createSchemaSQLForEntitiesWithOptions(entities, databaseContext, optionsCreate);
return result;
}
finally {
ec.unlock();
}
}
/**
* Creates the schema sql for a set of entities.
*
* @param entities
* the entities to create sql for
* @param databaseContext
* the database context to use
* @param optionsCreate
* the options (@see
* createSchemaSQLForEntitiesInModelWithNameAndOptions)
* @return a sql script
*/
public String createSchemaSQLForEntitiesWithOptions(NSArray<EOEntity> entities, EODatabaseContext databaseContext, NSDictionary<String, String> optionsCreate) {
return createSchemaSQLForEntitiesWithOptions(entities, databaseContext.adaptorContext().adaptor(), optionsCreate);
}
/**
* Generates table create statements for a set of entities, then finds all the entities that those entities depend on (in other models) and generates
* foreign key statements for those, so you can generate sql for cross-model.
*
* @param entities the entities to generate for
* @param adaptor the adaptor to use
* @return the sql script
*/
public String createDependentSchemaSQLForEntities(NSArray<EOEntity> entities, EOAdaptor adaptor) {
NSMutableDictionary<String, String> optionsCreateTables = new NSMutableDictionary<>();
optionsCreateTables.setObjectForKey("NO", EOSchemaGeneration.DropTablesKey);
optionsCreateTables.setObjectForKey("NO", EOSchemaGeneration.DropPrimaryKeySupportKey);
optionsCreateTables.setObjectForKey("YES", EOSchemaGeneration.CreateTablesKey);
optionsCreateTables.setObjectForKey("YES", EOSchemaGeneration.CreatePrimaryKeySupportKey);
optionsCreateTables.setObjectForKey("YES", EOSchemaGeneration.PrimaryKeyConstraintsKey);
optionsCreateTables.setObjectForKey("NO", EOSchemaGeneration.ForeignKeyConstraintsKey);
optionsCreateTables.setObjectForKey("NO", EOSchemaGeneration.CreateDatabaseKey);
optionsCreateTables.setObjectForKey("NO", EOSchemaGeneration.DropDatabaseKey);
StringBuilder sqlBuffer = new StringBuilder();
EOSynchronizationFactory sf = ((JDBCAdaptor) adaptor).plugIn().synchronizationFactory();
String creationScript = sf.schemaCreationScriptForEntities(entities, optionsCreateTables);
sqlBuffer.append(creationScript);
NSMutableArray<EOEntity> foreignKeyEntities = entities.mutableClone();
for (EOEntity entity : entities) {
for (EORelationship relationship : entity.relationships()) {
if (!relationship.isToMany()) {
EOEntity destinationEntity = relationship.destinationEntity();
if (destinationEntity.model() != entity.model()) {
foreignKeyEntities.addObject(destinationEntity);
}
}
}
}
NSMutableDictionary<String, String> optionsCreateForeignKeys = new NSMutableDictionary<>();
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.DropTablesKey);
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.DropPrimaryKeySupportKey);
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.CreateTablesKey);
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.CreatePrimaryKeySupportKey);
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.PrimaryKeyConstraintsKey);
optionsCreateForeignKeys.setObjectForKey("YES", EOSchemaGeneration.ForeignKeyConstraintsKey);
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.CreateDatabaseKey);
optionsCreateForeignKeys.setObjectForKey("NO", EOSchemaGeneration.DropDatabaseKey);
String foreignKeyScript = sf.schemaCreationScriptForEntities(foreignKeyEntities, optionsCreateForeignKeys);
sqlBuffer.append(foreignKeyScript);
return sqlBuffer.toString();
}
/**
* Creates the schema sql for a set of entities.
*
* @param entities
* the entities to create sql for
* @param adaptor
* the adaptor to use
* @param optionsDictionary
* the options (@see
* createSchemaSQLForEntitiesInModelWithNameAndOptions)
* @return a sql script
*/
public String createSchemaSQLForEntitiesWithOptions(NSArray<EOEntity> entities, EOAdaptor adaptor, NSDictionary<String, String> optionsDictionary) {
EOSynchronizationFactory sf = ((JDBCAdaptor) adaptor).plugIn().synchronizationFactory();
String creationScript = sf.schemaCreationScriptForEntities(entities, optionsDictionary);
return creationScript;
}
/**
* creates SQL to create tables for the specified Entities. This can be used
* with EOUtilities rawRowsForSQL method to create the tables.
*
* @param entities
* a NSArray containing the entities for which create table
* statements should be generated or null if all entities in the
* model should be used.
* @param modelName
* the name of the EOModel
* @return a <code>String</code> containing SQL statements to create
* tables
*/
public String createSchemaSQLForEntitiesInModelWithName(NSArray<EOEntity> entities, String modelName) {
EOModel model = ERXEOAccessUtilities.modelGroup(null).modelNamed(modelName);
return createSchemaSQLForEntitiesInModel(entities, model);
}
/**
* Creates SQL to create tables for the specified Entities. This can be used
* with EOUtilities rawRowsForSQL method to create the tables.
*
* @param entities
* a NSArray containing the entities for which create table
* statements should be generated or null if all entities in the
* model should be used.
* @param model
* the EOModel
*
* @return a <code>String</code> containing SQL statements to create
* tables
*/
public String createSchemaSQLForEntitiesInModel(NSArray<EOEntity> entities, EOModel model) {
return createSchemaSQLForEntitiesInModelAndOptions(entities, model, defaultOptionDictionary(true, true));
}
/**
* Creates an option dictionary to use with the other methods
*
* @param create
* add create statements
* @param drop
* add drop statements
* <p>
* This method uses the following
* defaults options:
* <ul>
* <li>EOSchemaGeneration.DropTablesKey=YES if drop</li>
* <li>EOSchemaGeneration.DropPrimaryKeySupportKey=YES if drop</li>
* <li>EOSchemaGeneration.CreateTablesKey=YES if create</li>
* <li>EOSchemaGeneration.CreatePrimaryKeySupportKey=YES if
* create</li>
* <li>EOSchemaGeneration.PrimaryKeyConstraintsKey=YES if create</li>
* <li>EOSchemaGeneration.ForeignKeyConstraintsKey=YES if create</li>
* <li>EOSchemaGeneration.CreateDatabaseKey=NO</li>
* <li>EOSchemaGeneration.DropDatabaseKey=NO</li>
* </ul>
* Possible values are <code>YES</code> and <code>NO</code>
*
* @return a <code>String</code> containing SQL statements to create
* tables
*/
public NSMutableDictionary<String, String> defaultOptionDictionary(boolean create, boolean drop) {
NSMutableDictionary<String, String> optionsCreate = new NSMutableDictionary<>();
optionsCreate.setObjectForKey((drop) ? "YES" : "NO", EOSchemaGeneration.DropTablesKey);
optionsCreate.setObjectForKey((drop) ? "YES" : "NO", EOSchemaGeneration.DropPrimaryKeySupportKey);
optionsCreate.setObjectForKey((create) ? "YES" : "NO", EOSchemaGeneration.CreateTablesKey);
optionsCreate.setObjectForKey((create) ? "YES" : "NO", EOSchemaGeneration.CreatePrimaryKeySupportKey);
optionsCreate.setObjectForKey((create) ? "YES" : "NO", EOSchemaGeneration.PrimaryKeyConstraintsKey);
optionsCreate.setObjectForKey((create) ? "YES" : "NO", EOSchemaGeneration.ForeignKeyConstraintsKey);
optionsCreate.setObjectForKey("NO", EOSchemaGeneration.CreateDatabaseKey);
optionsCreate.setObjectForKey("NO", EOSchemaGeneration.DropDatabaseKey);
return optionsCreate;
}
/**
* creates SQL to create tables for the specified Entities. This can be used
* with EOUtilities rawRowsForSQL method to create the tables.
*
* @param entities
* a NSArray containing the entities for which create table
* statements should be generated or null if all entities in the
* model should be used.
* @param databaseContext
* the databaseContext
*
* @param create
* if true, tables and keys are created
* @param drop
* if true, tables and keys are dropped
* @return a <code>String</code> containing SQL statements to create
* tables
*/
public String createSchemaSQLForEntitiesInDatabaseContext(NSArray<EOEntity> entities, EODatabaseContext databaseContext, boolean create, boolean drop) {
return createSchemaSQLForEntitiesWithOptions(entities, databaseContext, defaultOptionDictionary(create, drop));
}
public String createIndexSQLForEntities(NSArray<EOEntity> entities) {
return createIndexSQLForEntities(entities, null);
}
@SuppressWarnings("unchecked")
public String createIndexSQLForEntities(NSArray<EOEntity> entities, NSArray<String> externalTypesToIgnore) {
if (externalTypesToIgnore == null) {
externalTypesToIgnore = NSArray.EmptyArray;
}
if (entities == null || entities.count() == 0) {
return "";
}
int i = 0;
String oldIndexName = null;
String lineSeparator = System.getProperty("line.separator");
StringBuilder buf = new StringBuilder();
String commandSeparator = commandSeparatorString();
for (Enumeration entitiesEnum = entities.objectEnumerator(); entitiesEnum.hasMoreElements();) {
EOEntity entity = (EOEntity) entitiesEnum.nextElement();
// only use this entity if it has its own table
if (!ERXEOAccessUtilities.entityUsesSeparateTable(entity)) {
continue;
}
NSDictionary<String, Object> d = entity.userInfo();
NSMutableArray<String> usedColumns = new NSMutableArray<>();
for (Enumeration<String> keys = d.keyEnumerator(); keys.hasMoreElements();) {
String key = keys.nextElement();
if (key.startsWith("index")) {
String numbers = key.substring("index".length());
if (StringUtils.isNumeric(numbers)) {
String attributeNames = (String) d.objectForKey(key);
if (ERXStringUtilities.stringIsNullOrEmpty(attributeNames)) {
continue;
}
String indexName = "c" + System.currentTimeMillis() + new NSTimestamp().getNanos();
String newIndexName = i == 0 ? indexName : indexName + "_" + i;
if (oldIndexName == null) {
oldIndexName = indexName;
}
else if (oldIndexName.equals(newIndexName)) {
indexName += "_" + ++i;
}
else {
i = 0;
}
oldIndexName = indexName;
StringBuilder localBuf = new StringBuilder();
StringBuilder columnBuf = new StringBuilder();
boolean validIndex = false;
localBuf.append("create index " + indexName + " on " + entity.externalName() + "(");
for (Enumeration<String> attributes = NSArray.componentsSeparatedByString(attributeNames, ",").objectEnumerator(); attributes.hasMoreElements();) {
String attributeName = attributes.nextElement();
attributeName = attributeName.trim();
EOAttribute attribute = entity.attributeNamed(attributeName);
if (attribute == null) {
attribute = ERXEOAccessUtilities.attributeWithColumnNameFromEntity(attributeName, entity);
}
if (attribute != null && externalTypesToIgnore.indexOfObject(attribute.externalType()) != NSArray.NotFound) {
continue;
}
validIndex = true;
String columnName = attribute == null ? attributeName : attribute.columnName();
columnBuf.append(columnName);
if (attributes.hasMoreElements()) {
columnBuf.append(", ");
}
}
if (validIndex) {
String l = columnBuf.toString();
if (l.endsWith(", ")) {
l = l.substring(0, l.length() - 2);
}
if (usedColumns.indexOfObject(l) == NSArray.NotFound) {
buf.append(localBuf).append(l);
usedColumns.addObject(l);
buf.append(')').append(commandSeparator).append(lineSeparator);
}
}
}
}
else if (key.equals("additionalIndexes")) {
// this is a space separated list of column or attribute
// names
String value = (String) d.objectForKey(key);
for (Enumeration indexes = NSArray.componentsSeparatedByString(value, " ").objectEnumerator(); indexes.hasMoreElements();) {
String indexValues = (String) indexes.nextElement();
if (ERXStringUtilities.stringIsNullOrEmpty(indexValues)) {
continue;
}
// this might be a comma separate list
String indexName = "c" + System.currentTimeMillis() + new NSTimestamp().getNanos();
String newIndexName = i == 0 ? indexName : indexName + "_" + i;
if (oldIndexName == null) {
oldIndexName = indexName;
}
else if (oldIndexName.equals(newIndexName)) {
indexName += "_" + ++i;
}
else {
i = 0;
}
oldIndexName = indexName;
StringBuffer localBuf = new StringBuffer();
StringBuffer columnBuf = new StringBuffer();
boolean validIndex = false;
localBuf.append("create index " + indexName + " on " + entity.externalName() + "(");
for (Enumeration e = NSArray.componentsSeparatedByString(indexValues, ",").objectEnumerator(); e.hasMoreElements();) {
String attributeName = (String) e.nextElement();
attributeName = attributeName.trim();
EOAttribute attribute = entity.attributeNamed(attributeName);
if (attribute == null) {
attribute = ERXEOAccessUtilities.attributeWithColumnNameFromEntity(attributeName, entity);
}
if (attribute != null && externalTypesToIgnore.indexOfObject(attribute.externalType()) != NSArray.NotFound) {
continue;
}
validIndex = true;
String columnName = attribute == null ? attributeName : attribute.columnName();
columnBuf.append(columnName);
if (e.hasMoreElements()) {
columnBuf.append(", ");
}
}
if (validIndex) {
String l = columnBuf.toString();
if (l.endsWith(", ")) {
l = l.substring(0, l.length() - 2);
}
if (usedColumns.indexOfObject(l) == NSArray.NotFound) {
buf.append(localBuf).append(l);
usedColumns.addObject(l);
buf.append(')').append(commandSeparator).append(lineSeparator);
}
}
}
}
}
}
return buf.toString();
}
/**
* Returns the list of attributes to fetch for a fetch spec. The entity is
* passed in here because it has likely already been looked up for the
* particular fetch spec.
*
* @param fetchSpec
* the fetch spec
* @param entity
* the entity (which should match fetchSpec.entityName())
* @return the list of attributes to fetch
*/
@SuppressWarnings("unchecked")
public NSArray<EOAttribute> attributesToFetchForEntity(EOFetchSpecification fetchSpec, EOEntity entity) {
NSArray<EOAttribute> attributes;
if (!fetchSpec.fetchesRawRows()) {
attributes = entity.attributesToFetch();
}
else {
NSMutableArray<EOAttribute> rawRowAttributes = new NSMutableArray<>();
for (String rawRowKeyPath : fetchSpec.rawRowKeyPaths()) {
rawRowAttributes.addObject(entity.anyAttributeNamed(rawRowKeyPath));
}
attributes = rawRowAttributes.immutableClone();
}
return attributes;
}
/**
* Creates the SQL which is used by the provided EOFetchSpecification,
* limited by the given range.
*
* @param ec
* the EOEditingContext
* @param spec
* the EOFetchSpecification in question
* @param start
* start of rows to fetch
* @param end
* end of rows to fetch (-1 if not used)
*
* @return the EOSQLExpression which the EOFetchSpecification would use
*/
public EOSQLExpression sqlExpressionForFetchSpecification(EOEditingContext ec, EOFetchSpecification spec, long start, long end) {
return sqlExpressionForFetchSpecification(ec, spec, start, end, null);
}
/**
* Returns the custom query expression hint as a String. At the moment, if it's an EOSQLExpression, it just returns .statement().
*
* @param hint the hint to convert to a String
* @return the hint as a String
*/
public String customQueryExpressionHintAsString(Object hint) {
String sql;
if (hint instanceof String) {
sql = (String) hint;
}
else if (hint instanceof EOSQLExpression) {
sql = ((EOSQLExpression)hint).statement();
if (sql == null) {
throw new IllegalArgumentException("This EOSQLExpression's statement was null (" + hint + ").");
}
}
else {
sql = null;
}
return sql;
}
/**
* Creates the SQL which is used by the provided EOFetchSpecification,
* limited by the given range.
*
* @param ec
* the EOEditingContext
* @param spec
* the EOFetchSpecification in question
* @param start
* start of rows to fetch
* @param end
* end of rows to fetch (-1 if not used)
* @param attributes
* the attributes to fetch from the given entity
*
* @return the EOSQLExpression which the EOFetchSpecification would use
*/
public EOSQLExpression sqlExpressionForFetchSpecification(EOEditingContext ec, EOFetchSpecification spec, long start, long end, NSArray<EOAttribute> attributes) {
EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, spec.entityName());
EOModel model = entity.model();
EODatabaseContext dbc = EOUtilities.databaseContextForModelNamed(ec, model.name());
EOAdaptor adaptor = dbc.adaptorContext().adaptor();
EOSQLExpressionFactory sqlFactory = adaptor.expressionFactory();
spec = (EOFetchSpecification) spec.clone();
EOQualifier qualifier = spec.qualifier();
if (qualifier != null) {
qualifier = EOQualifierSQLGeneration.Support._schemaBasedQualifierWithRootEntity(qualifier, entity);
}
if (qualifier != spec.qualifier()) {
spec.setQualifier(qualifier);
}
if (spec.fetchLimit() > 0) {
spec.setFetchLimit(0);
spec.setPromptsAfterFetchLimit(false);
}
spec = ERXEOAccessUtilities.localizeFetchSpecification(ec, spec);
if (attributes == null) {
attributes = attributesToFetchForEntity(spec, entity);
}
EOSQLExpression sqlExpr = sqlFactory.selectStatementForAttributes(attributes, false, spec, entity);
String sql = sqlExpr.statement();
if (spec.hints() != null && !spec.hints().isEmpty() && spec.hints().valueForKey(EODatabaseContext.CustomQueryExpressionHintKey) != null) {
Object hint = spec.hints().valueForKey(EODatabaseContext.CustomQueryExpressionHintKey);
sql = customQueryExpressionHintAsString(hint);
}
if (end >= 0) {
sql = limitExpressionForSQL(sqlExpr, spec, sql, start, end);
sqlExpr.setStatement(sql);
}
return sqlExpr;
}
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
throw new UnsupportedOperationException("There is no " + getClass().getSimpleName() + " implementation for generating limit expressions.");
}
/**
* Removes an attribute from the select list.
*
* @param attribute
* the attribute to remove from the select list
* @param sqlExpression
* the expression to remove from
*/
public void removeSelectFromExpression(EOAttribute attribute, EOSQLExpression sqlExpression) {
// MS: This is a bit brute force, but there's not really a nicer way to
// do this, unfortunately
String sql = sqlExpression.statement();
String attributeSql = sqlExpression.sqlStringForAttribute(attribute);
String replaceSql = sql.replaceFirst(", " + attributeSql, "");
if (replaceSql.length() == sql.length()) {
replaceSql = sql.replaceFirst(attributeSql + ", ", "");
}
sqlExpression.setStatement(replaceSql);
}
/**
* Returns the attribute read format for an aggregate function for a
* particular column with a name.
*
* @param functionName
* the aggregate function to generate
* @param columnName
* the column name to aggregate on
* @param aggregateName
* the name to assign to the aggregate result
* @return the generated read format
*/
public String readFormatForAggregateFunction(String functionName, String columnName, String aggregateName) {
return readFormatForAggregateFunction(functionName, columnName, aggregateName, false);
}
/**
* Returns the attribute read format for an aggregate function for a
* particular column with a name.
*
* @param functionName
* the aggregate function to generate
* @param columnName
* the column name to aggregate on
* @param aggregateName
* the name to assign to the aggregate result
* @param usesDistinct
* <code>true</code> if function should be used on distinct values
* @return the generated read format
*/
public String readFormatForAggregateFunction(String functionName, String columnName, String aggregateName, boolean usesDistinct) {
StringBuilder sb = new StringBuilder();
sb.append(functionName);
sb.append('(');
if (usesDistinct) {
sb.append("distinct ");
}
sb.append(columnName);
sb.append(')');
if (aggregateName != null) {
sb.append(" AS ");
sb.append(aggregateName);
}
return sb.toString();
}
/**
* Adds itemString to a comma-separated list. If listString already has
* entries, this method appends a comma followed by itemString. There is no
* good way to hook in and use EOSQLExpression's version of this, so we have
* our own copy of it.
*
* @param itemString
* the item to append
* @param listString
* the list buffer
*/
public void appendItemToListString(String itemString, StringBuffer listString) {
if (listString.length() > 0) {
listString.append(", ");
}
listString.append(itemString);
}
/**
* Adds a group-by clause to the given SQL Expression based on the list of
* attributes defined in the given fetch spec.
*
* @param editingContext
* the editing context to lookup entities with
* @param fetchSpec
* the fetch spec to retrieve attributes from
* @param expression
* the sql expression to add a "group by" clause to
*/
public void addGroupByClauseToExpression(EOEditingContext editingContext, EOFetchSpecification fetchSpec, EOSQLExpression expression) {
EOEntity entity = ERXEOAccessUtilities.entityNamed(editingContext, fetchSpec.entityName());
addGroupByClauseToExpression(attributesToFetchForEntity(fetchSpec, entity), expression);
}
/**
* Returns the index in the expression's statement where order by clauses
* should be inserted.
*
* @param expression
* the expression to look into
* @return the index into statement where the order by should be inserted
*/
public int _orderByIndex(EOSQLExpression expression) {
String sql = expression.statement();
int orderByInsertIndex = sql.lastIndexOf(" LIMIT ");
if (orderByInsertIndex == -1) {
orderByInsertIndex = sql.length();
}
return orderByInsertIndex;
}
/**
* Returns the index in the expression's statement where group by and having
* clauses should be inserted.
*
* @param expression
* the expression to look into
* @return the index into statement where the group by should be inserted
*/
public int _groupByOrHavingIndex(EOSQLExpression expression) {
String sql = expression.statement();
int groupByInsertIndex = sql.lastIndexOf(" ORDER BY ");
if (groupByInsertIndex == -1) {
groupByInsertIndex = sql.lastIndexOf(" LIMIT ");
if (groupByInsertIndex == -1) {
groupByInsertIndex = sql.length();
}
}
return groupByInsertIndex;
}
/**
* Adds a group-by clause to the given SQL Expression based on the given
* list of attributes.
*
* @param attributes
* the list of attributes to group by
* @param expression
* the sql expression to add a "group by" clause to
*/
public void addGroupByClauseToExpression(NSArray<EOAttribute> attributes, EOSQLExpression expression) {
StringBuffer groupByBuffer = new StringBuffer();
for (EOAttribute attribute : attributes) {
String attributeSqlString = expression.sqlStringForAttribute(attribute);
attributeSqlString = expression.formatSQLString(attributeSqlString, attribute.readFormat());
appendItemToListString(attributeSqlString, groupByBuffer);
}
groupByBuffer.insert(0, " GROUP BY ");
StringBuilder sqlBuffer = new StringBuilder(expression.statement());
sqlBuffer.insert(_groupByOrHavingIndex(expression), groupByBuffer);
expression.setStatement(sqlBuffer.toString());
}
/**
* Adds a " having count(*) > x" clause to a group by expression.
*
* @param selector
* the comparison selector -- just like EOKeyValueQualifier
* @param value
* the value to compare against
* @param expression
* the expression to modify
*/
public void addHavingCountClauseToExpression(NSSelector selector, int value, EOSQLExpression expression) {
Integer integerValue = Integer.valueOf(value);
String operatorString = expression.sqlStringForSelector(selector, integerValue);
StringBuilder havingBuffer = new StringBuilder();
havingBuffer.append(" HAVING COUNT(*) ");
havingBuffer.append(operatorString);
havingBuffer.append(' ');
havingBuffer.append(integerValue);
StringBuilder sqlBuffer = new StringBuilder(expression.statement());
sqlBuffer.insert(_groupByOrHavingIndex(expression), havingBuffer);
expression.setStatement(sqlBuffer.toString());
}
/**
* Returns the SQL expression for a regular expression query.
*
* @param key
* @param value
* @return the regex SQL
*/
public String sqlForRegularExpressionQuery(String key, String value) {
throw new UnsupportedOperationException("There is no " + getClass().getSimpleName() + " implementation for generating regex expressions.");
}
/**
* Returns the SQL expression for a full text search query.
*
* @param qualifier
* the full text qualifier
* @param expression
* the EOSQLExpression context
* @return a SQL expression
*/
public String sqlForFullTextQuery(ERXFullTextQualifier qualifier, EOSQLExpression expression) {
throw new UnsupportedOperationException("There is no " + getClass().getSimpleName() + " implementation for generating full text expressions.");
}
/**
* Returns the SQL expression for creating a unique index on the given set
* of columns
*
* @param indexName
* the name of the index to create
* @param tableName the name of the containing table
* @param columnNames
* the list of column names to index on
* @return a SQL expression
*/
public String sqlForCreateUniqueIndex(String indexName, String tableName, String... columnNames) {
return sqlForCreateUniqueIndex(indexName, tableName, columnIndexesFromColumnNames(columnNames));
}
/**
* Returns the SQL expression for creating a unique index on the given set
* of columns
*
* @param indexName
* the name of the index to create
* @param tableName the name of the containing table
* @param columnIndexes
* the list of columns to index on
* @return a SQL expression
*/
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "ALTER TABLE \"" + tableName + "\" ADD CONSTRAINT \"" + indexName + "\" UNIQUE(\"" + new NSArray<>(columnNames).componentsJoinedByString("\", \"") + "\")";
}
/**
* Returns the SQL expression for creating an index on the given set
* of columns
*
* @param indexName
* the name of the index to create
* @param tableName the name of the containing table
* @param columnNames
* the list of column names to index on
* @return a SQL expression
*/
public String sqlForCreateIndex(String indexName, String tableName, String... columnNames) {
return sqlForCreateIndex(indexName, tableName, columnIndexesFromColumnNames(columnNames));
}
protected ColumnIndex[] columnIndexesFromColumnNames(String... columnNames) {
NSMutableArray<ColumnIndex> columnIndexes = new NSMutableArray<>();
for (String columnName : columnNames) {
columnIndexes.addObject(new ColumnIndex(columnName));
}
return columnIndexes.toArray(new ColumnIndex[columnIndexes.count()]);
}
/**
* Returns the SQL expression for creating an index on the given set
* of columns
*
* @param indexName
* the name of the index to create
* @param tableName the name of the containing table
* @param columnIndexes
* the list of columns to index on
* @return a SQL expression
*/
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
throw new UnsupportedOperationException("There is no " + getClass().getSimpleName() + " implementation for generating index expressions.");
}
/**
* IndexLimit represents the reference to a column for use in an index
* definition along with an optional limit.
*
* @author mschrag
*/
public static class ColumnIndex {
private String _columnName;
private int _length;
public ColumnIndex(String columnName) {
this(columnName, 0);
}
public ColumnIndex(String columnName, int length) {
_columnName = columnName;
_length = length;
}
public String columnName() {
return _columnName;
}
public int length() {
return _length;
}
public boolean hasLength() {
return _length > 0;
}
@Override
public String toString() {
return "[ColumnIndex: columnName = " + _columnName + "; length = " + _length + "]";
}
}
/**
* Returns the JDBCType that should be used for a varcharLarge column in migrations.
*
* @return the JDBCType that should be used for a varcharLarge column in migrations
*/
public int varcharLargeJDBCType() {
return Types.VARCHAR;
}
/**
* Returns the width that should be used for a varcharLarge column in migrations.
*
* @return the width that should be used for a varcharLarge column in migrations
*/
public int varcharLargeColumnWidth() {
return 10000000;
}
/**
* Returns the name of the table to use for database migrations.
*
* @return the name of the table to use for database migrations
*/
public String migrationTableName() {
return "_dbupdater";
}
/**
* Returns the JDBC type to use for a given ERXSQLHelper custom type
*
* @param jdbcType
* the ERXSQLHelper custom type
* @return the JDBC type to use
*/
public int jdbcTypeForCustomType(int jdbcType) {
int result = jdbcType;
if (jdbcType == CustomTypes.INET) {
result = Types.VARCHAR;
}
return result;
}
/**
* JDBCAdaptor.externalTypeForJDBCType just returns the first type it finds
* instead of trying to find a best match. This can still fail, mind you,
* but it should be much better than the EOF default impl.
*
* @param adaptor
* the adaptor to retrieve an external type for
* @param jdbcType
* the JDBC type number
* @return a guess at the external type name to use
*/
@SuppressWarnings("unchecked")
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType = null;
NSArray<String> defaultJDBCTypes = null;
jdbcType = jdbcTypeForCustomType(jdbcType);
try {
// MS: This is super dirty, but we can deadlock if we end up trying
// to request jdbc2Info during a migration. We have to be able to
// use the adaptor's cached version of this method and so we just
// have to go in through the backdoor here ...
Method typeInfoMethod = adaptor.getClass().getDeclaredMethod("typeInfo");
boolean oldAccessible = typeInfoMethod.isAccessible();
typeInfoMethod.setAccessible(true);
try {
NSDictionary typeInfo = (NSDictionary) typeInfoMethod.invoke(adaptor);
if (typeInfo != null) {
String jdbcStringRep = JDBCAdaptor.stringRepresentationForJDBCType(jdbcType);
String typeInfoStringRep = jdbcStringRep;
// MS: We need to do a case-insensitive lookup of the type
// info string representation, because some databases say
// "VARCHAR" and some "varchar".
// Awesome.
for (String possibleTypeInfoStringRep : (NSArray<String>) typeInfo.allKeys()) {
if (typeInfoStringRep.equalsIgnoreCase(possibleTypeInfoStringRep)) {
typeInfoStringRep = possibleTypeInfoStringRep;
break;
}
}
// We're going to guess that the jdbc string rep is a valid
// type in this adaptor. If it is, then we can use that and
// it will probably be a better guess than just the first
// type we run across.
NSDictionary typeDescription = (NSDictionary) typeInfo.objectForKey(typeInfoStringRep);
if (typeDescription != null) {
defaultJDBCTypes = (NSArray<String>) typeDescription.objectForKey("defaultJDBCType");
if (defaultJDBCTypes != null && defaultJDBCTypes.containsObject(jdbcStringRep)) {
externalType = typeInfoStringRep;
}
}
if (externalType == null) {
externalType = adaptor.externalTypeForJDBCType(jdbcType);
}
}
}
finally {
typeInfoMethod.setAccessible(oldAccessible);
}
}
catch (Exception e) {
log.error("Failed to sneakily execute adaptor.typeInfo().", e);
}
if (externalType == null) {
externalType = adaptor.externalTypeForJDBCType(jdbcType);
}
// OK .. So we didn't find an exact match, and the superclass
// basically gave up. So we're going to take what should be a
// decent guess. If we found a type info that matched the name,
// but we didn't find a JDBC type name that matched, let's just
// guess that it's PROBABLY one of the entries from the
// JDBC type names list. We're really not any worse off than
// the complete failure we were 2 lines ago.
if (externalType == null && defaultJDBCTypes != null) {
int defaultJDBCTypesCount = defaultJDBCTypes.count();
if (defaultJDBCTypesCount == 1) {
externalType = defaultJDBCTypes.objectAtIndex(0);
}
else if (defaultJDBCTypesCount == 0) {
throw new IllegalArgumentException("There is no type that could be found in your database that maps to JDBC Type #" + jdbcType + ".");
}
else {
externalType = defaultJDBCTypes.objectAtIndex(0);
log.warn("There was more than one type that in your database that maps to JDBC Type #{}: {}. We guessed '{}'. Cross your fingers.", jdbcType, defaultJDBCTypes, externalType);
}
}
return externalType;
}
/**
* Returns the number of rows the supplied EOFetchSpecification would
* return.
*
* @param ec
* the EOEditingContext
* @param spec
* the EOFetchSpecification in question
* @return the number of rows
*/
public int rowCountForFetchSpecification(EOEditingContext ec, EOFetchSpecification spec) {
int rowCount = -1;
EOEntity entity = ERXEOAccessUtilities.entityNamed(ec, spec.entityName());
EOModel model = entity.model();
NSArray result = null;
String sql;
if (spec.hints() == null || spec.hints().isEmpty() || spec.hints().valueForKey(EODatabaseContext.CustomQueryExpressionHintKey) == null) {
// no hints
if (spec.fetchLimit() > 0 || spec.sortOrderings() != null) {
boolean usesDistinct = spec.usesDistinct();
spec = new EOFetchSpecification(spec.entityName(), spec.qualifier(), null);
spec.setUsesDistinct(usesDistinct);
}
EOSQLExpression sqlExpression = sqlExpressionForFetchSpecification(ec, spec, 0, -1);
String statement = sqlExpression.statement();
String listString = sqlExpression.listString();
String countExpression;
if (spec.usesDistinct()) {
countExpression = sqlForCountDistinct(entity);
} else {
countExpression = "count(*) ";
}
statement = statement.replace(listString, countExpression);
sqlExpression.setStatement(statement);
sql = statement;
result = ERXEOAccessUtilities.rawRowsForSQLExpression(ec, model.name(), sqlExpression);
}
else {
// we have hints
Object hint = spec.hints().valueForKey(EODatabaseContext.CustomQueryExpressionHintKey);
sql = ERXSQLHelper.newSQLHelper(model).customQueryExpressionHintAsString(hint);
// MS: This looks super sketchy ...
if (sql.endsWith(";")) {
sql = sql.substring(0, sql.length() - 1);
}
sql = "select count(*) from " + sqlForSubquery(sql, "result_count_temp_table");
result = EOUtilities.rawRowsForSQL(ec, model.name(), sql, null);
}
if (result.count() > 0) {
NSDictionary dict = (NSDictionary) result.objectAtIndex(0);
NSArray values = dict.allValues();
if (values.count() > 0) {
Object value = values.objectAtIndex(0);
if (value instanceof Number) {
return ((Number) value).intValue();
}
try {
int c = Integer.parseInt(value.toString());
rowCount = c;
}
catch (NumberFormatException e) {
throw new IllegalStateException("sql " + sql + " returned a wrong result, could not convert " + value + " into an int!");
}
}
else {
throw new IllegalStateException("sql " + sql + " returned no result!");
}
}
else {
throw new IllegalStateException("sql " + sql + " returned no result!");
}
return rowCount;
}
/**
* Returns the SQL to count the distinct number of rows. The general implementation doesn't
* support composite primary keys and chooses only one primary key column when formatting the
* SQL expression.
* <p>
* Concrete classes may override this implementation to add support for composite
* primary keys according to their database specific SQL syntax.
*
* @param entity the base entity used in this query
* @return the formatted SQL count using distinct for the given entity
*/
protected String sqlForCountDistinct(EOEntity entity) {
NSArray<String> primaryKeyAttributeNames = entity.primaryKeyAttributeNames();
if (primaryKeyAttributeNames.count() > 1) {
log.warn("Composite primary keys are currently unsupported in rowCountForFetchSpecification, when the spec uses distinct");
}
String pkAttributeName = primaryKeyAttributeNames.lastObject();
String pkColumnName = entity.attributeNamed(pkAttributeName).columnName();
return "count(distinct " + quoteColumnName("t0." + pkColumnName) + ") ";
}
/**
* Returns the syntax for using the given query as an aliased subquery in a from-clause.
*
* @param subquery the subquery to wrap
* @param alias the alias to use
* @return the formatted subquery expression
*/
protected String sqlForSubquery(String subquery, String alias) {
return "(" + subquery + ") as " + alias;
}
/**
* Returns the SQL required to select the next value from the given sequence. This should
* return a single row with a single column.
*
* @param sequenceName the name of the sequence
* @return the next sequence value
*/
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
throw new UnsupportedOperationException("There is no " + getClass().getSimpleName() + " implementation for sequences.");
}
/**
* Convenience method to get the next unique ID from a sequence.
*
* @param ec
* editing context
* @param modelName
* name of the model which connects to the database that has the
* sequence in it
* @param sequenceName
* name of the sequence
* @return next value in the sequence
*/
// ENHANCEME: Need a non-oracle specific way of doing this. Should poke
// around at
// the adaptor level and see if we can't find something better.
public Number getNextValFromSequenceNamed(EOEditingContext ec, String modelName, String sequenceName) {
NSArray array = EOUtilities.rawRowsForSQL(ec, modelName, sqlForGetNextValFromSequencedNamed(sequenceName), null);
if (array.count() == 0) {
throw new RuntimeException("Unable to generate value from sequence named: " + sequenceName + " in model: " + modelName);
}
NSDictionary dictionary = (NSDictionary) array.objectAtIndex(0);
NSArray valuesArray = dictionary.allValues();
return (Number) valuesArray.objectAtIndex(0);
}
/**
* Creates a where clause string " someKey IN ( someValue1,...)". Can
* migrate keyPaths.
*
* @param e the SQL expression
* @param key the name of the key
* @param valueArray an array of values to generate an "in" clause for
* @return the where clause for the given key
*/
public String sqlWhereClauseStringForKey(EOSQLExpression e, String key, NSArray valueArray) {
if (valueArray.count() == 0) {
return "0=1";
}
StringBuilder sb = new StringBuilder();
NSArray attributePath = ERXEOAccessUtilities.attributePathForKeyPath(e.entity(), key);
EOAttribute attribute = (EOAttribute) attributePath.lastObject();
String sqlName;
if (attributePath.count() > 1) {
sqlName = e.sqlStringForAttributePath(attributePath);
}
else {
sqlName = e.sqlStringForAttribute(attribute);
}
int maxPerQuery = maximumElementPerInClause(e.entity());
// Need to wrap this SQL in parenthesis if there are multiple groups
if (valueArray.count() > maxPerQuery) {
sb.append(" ( ");
}
for (int j = 0; j < valueArray.count(); j += maxPerQuery) {
int currentSize = (j + (maxPerQuery - 1) < valueArray.count() ? maxPerQuery : ((valueArray.count() % maxPerQuery)));
sb.append(sqlName);
sb.append(" IN ");
sb.append('(');
for (int i = j; i < j + currentSize; i++) {
if (i > j) {
sb.append(", ");
}
Object value = valueArray.objectAtIndex(i);
// AK : crude hack for queries with number constants.
// Apparently
// EOAttribute.adaptorValueByConvertingAttributeValue() doesn't
// actually return a suitable value
if (value instanceof ERXConstant.NumberConstant) {
value = Long.valueOf(((Number) value).longValue());
}
else {
value = formatValueForAttribute(e, value, attribute, key);
}
sb.append(value);
}
sb.append(')');
if (j < valueArray.count() - maxPerQuery) {
sb.append(" OR ");
}
}
if (valueArray.count() > maxPerQuery) {
sb.append(" ) ");
}
return sb.toString();
}
/**
* The database specific limit, or or most efficient number, of elements in an IN clause in a statement. If there
* are more that this number of elements, additional IN clauses will be generated, ORed to the others.
*
* @param entity EOEntity that can be used to fine-tune the result
* @return database specific limit, or or most efficient number, of elements in an IN clause in a statement
*/
protected int maximumElementPerInClause(EOEntity entity) {
return 256;
}
protected String formatValueForAttribute(EOSQLExpression expression, Object value, EOAttribute attribute, String key) {
return expression.sqlStringForValue(value, key);
}
/**
* Splits semicolon-separate sql statements into an array of strings
*
* @param sql
* a multi-line sql statement
* @return an array of sql statements
*/
public NSArray<String> splitSQLStatements(String sql) {
NSMutableArray<String> statements = new NSMutableArray<>();
if (sql != null) {
char commandSeparatorChar = commandSeparatorChar();
Pattern commentPattern = commentPattern();
StringBuilder statementBuffer = new StringBuilder();
BufferedReader reader = new BufferedReader(new StringReader(sql));
boolean inQuotes = false;
try {
String nextLine = reader.readLine();
while (nextLine != null) {
if(!inQuotes) {
nextLine = nextLine.trim(); // trim only if we not inQuotes
} else {
statementBuffer.append('\n'); // we are in Quotes but got a new Line
}
// Skip blank lines and new lines starting with the comment pattern
if (nextLine.length() == 0 ||
(statementBuffer.length() == 0 && commentPattern.matcher(nextLine).find())) {
nextLine = reader.readLine();
continue;
}
// Determine if the line ends inside a single quoted string
int length = nextLine.length();
char ch = 0;
char prev = 0;
for (int i = 0; i < length; i++) {
ch = nextLine.charAt(i);
// Determine if we are in a quoted string, but ignore escaped apostrophes, e.g. 'Mike\'s Code'
if (inQuotes && ch == '\\') {
i++;
}
else if (ch == '\'') {
inQuotes = !inQuotes;
}
else if (ch == '-' && prev == '-' && !inQuotes) {
statementBuffer.deleteCharAt(statementBuffer.length() - 1);
break;
}
if (inQuotes || ch != commandSeparatorChar) {
statementBuffer.append(ch);
}
prev = ch;
}
// If we are not in a quoted string, either this is the end of the command or we need to
// add some whitespace before the continuation of this command
if (!inQuotes) {
if (ch == commandSeparatorChar) {
statements.addObject(statementBuffer.toString().trim());
statementBuffer.setLength(0);
}
else {
statementBuffer.append(' ');
}
}
nextLine = reader.readLine();
}
String finalStatement = statementBuffer.toString().trim();
if (finalStatement.length() > 0) {
statements.addObject(finalStatement);
}
}
catch (IOException e) {
throw NSForwardException._runtimeExceptionForThrowable(e);
}
}
return statements;
}
/**
* Splits the SQL statements from the given input stream
*
* @param is
* the input stream to read from
* @return an array of SQL statements
* @throws IOException
* if there is a problem reading the stream
*/
public NSArray<String> splitSQLStatementsFromInputStream(InputStream is) throws IOException {
String encoding = System.getProperty("file.encoding");
return splitSQLStatements(ERXStringUtilities.stringIsNullOrEmpty(encoding) ? ERXStringUtilities.stringFromInputStream(is) : ERXStringUtilities.stringFromInputStream(is, encoding));
}
/**
* Splits the SQL statements from the given file.
*
* @param f
* the file to read from
* @return an array of SQL statements
* @throws IOException
* if there is a problem reading the stream
*/
public NSArray<String> splitSQLStatementsFromFile(File f) throws IOException {
try (FileInputStream fis = new FileInputStream(f); BufferedInputStream bis = new BufferedInputStream(fis)){
return splitSQLStatementsFromInputStream(bis);
}
}
/**
* This is totally cheating ... But I just need the separator character for now. We
* can rewrite the script parser later. Actually, somewhere on earth there is already
* a sql parser or two. Probably worth getting that one.
*
* @return the separator character used by this database
*/
protected char commandSeparatorChar() {
return ';';
}
protected String commandSeparatorString() {
String lineSeparator = System.getProperty("line.separator");
return ";" + lineSeparator;
}
/**
* Returns a pattern than matches only blank lines. Subclasses should implement this to return a pattern
* matching the vendor specific comment indicator(s).
*
* @return regex pattern that indicates this line is an SQL comment
*/
protected Pattern commentPattern() {
return Pattern.compile("^$");
}
public NSMutableArray<String> columnNamesFromColumnIndexes(ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<String>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return columnNames;
}
public boolean reassignExternalTypeForValueTypeOverride(EOAttribute attribute) {
return true;
}
public String quoteColumnName(String columnName){
// just pass through by default
return columnName;
}
/**
* Returns whether or not this database can always perform the a distinct operation
* when sort orderings are applied. Oracle, for instance, will fail if you try to
* sort on a key that isn't in the list of fetched keys.
*
* @return whether or not this database can always perform the a distinct operation
* when sort orderings are applied
*/
protected boolean canReliablyPerformDistinctWithSortOrderings() {
return true;
}
/**
* Returns whether or not this database should perform the distinct portion of the
* given fetch spec in memory or not.
*
* @param fetchSpecification the fetch spec to check
* @return whether or not this database should perform the distinct portion of the
* given fetch spec in memory or not
*/
public boolean shouldPerformDistinctInMemory(EOFetchSpecification fetchSpecification) {
boolean shouldPerformDistinctInMemory = false;
if (!canReliablyPerformDistinctWithSortOrderings()) {
NSArray<EOSortOrdering> sortOrderings = fetchSpecification.sortOrderings();
if (fetchSpecification.usesDistinct() && sortOrderings != null && sortOrderings.count() > 0) {
shouldPerformDistinctInMemory = true;
// MS: We might be able to restrict this check further at some point ...
// for (EOSortOrdering sortOrdering : sortOrderings) {
// sortOrdering.key();
// }
}
}
return shouldPerformDistinctInMemory;
}
/**
* Returns true if the SQL helper can handle the exception. Typical uses are
* morphing unique constraints to NSValidation exceptions.
*
* @param databaseContext
* @param throwable
* @return whether or not the SQL helper can handl this exception
*/
public boolean handleDatabaseException(EODatabaseContext databaseContext, Throwable throwable) {
return false;
}
public static ERXSQLHelper newSQLHelper(EOSQLExpression expression) {
// This is REALLY hacky.
String className = expression.getClass().getName();
int dotIndex = className.lastIndexOf('$');
if (dotIndex == -1) {
dotIndex = className.lastIndexOf('.');
}
int expressionIndex = className.lastIndexOf("Expression");
if (expressionIndex == -1) {
throw new RuntimeException("Failed to create sql helper for expression " + expression + ".");
}
String databaseProductName = className.substring(dotIndex + 1, expressionIndex);
return ERXSQLHelper.newSQLHelper(databaseProductName);
}
public static ERXSQLHelper newSQLHelper(EOEditingContext ec, String modelName) {
return ERXSQLHelper.newSQLHelper(EOUtilities.databaseContextForModelNamed(ec, modelName));
}
public static ERXSQLHelper newSQLHelper(EOEditingContext ec, EOEntity entity) {
return ERXSQLHelper.newSQLHelper(EODatabaseContext.registeredDatabaseContextForModel(entity.model(), ec));
}
public static ERXSQLHelper newSQLHelper(EOEditingContext ec, EOModel model) {
return ERXSQLHelper.newSQLHelper(EODatabaseContext.registeredDatabaseContextForModel(model, ec));
}
public static ERXSQLHelper newSQLHelper(EODatabaseContext databaseContext) {
EOAdaptor adaptor = databaseContext.database().adaptor();
return ERXSQLHelper.newSQLHelper(adaptor);
}
public static ERXSQLHelper newSQLHelper(EODatabaseChannel databaseChannel) {
EOAdaptor adaptor = databaseChannel.adaptorChannel().adaptorContext().adaptor();
return ERXSQLHelper.newSQLHelper(adaptor);
}
public static ERXSQLHelper newSQLHelper(EOAdaptor adaptor) {
if (adaptor instanceof JDBCAdaptor) {
return ERXSQLHelper.newSQLHelper((JDBCAdaptor)adaptor);
}
// MS: Hack to support non JDBC adaptor migrations
return new NoSQLHelper();
}
public static ERXSQLHelper newSQLHelper(EOAdaptorChannel adaptorChannel) {
EOAdaptor adaptor = adaptorChannel.adaptorContext().adaptor();
return ERXSQLHelper.newSQLHelper(adaptor);
}
public static ERXSQLHelper newSQLHelper(JDBCAdaptor adaptor) {
JDBCPlugIn plugin = adaptor.plugIn();
return ERXSQLHelper.newSQLHelper(plugin);
}
public static ERXSQLHelper newSQLHelper(JDBCPlugIn plugin) {
String databaseProductName = plugin.databaseProductName();
return ERXSQLHelper.newSQLHelper(databaseProductName);
}
public static ERXSQLHelper newSQLHelper(EOEntity entity) {
return ERXSQLHelper.newSQLHelper(entity.model());
}
public static ERXSQLHelper newSQLHelper(EOModel model) {
EOAdaptor adaptor = EOAdaptor.adaptorWithModel(model);
return ERXSQLHelper.newSQLHelper(adaptor);
}
public static ERXSQLHelper newSQLHelper(String databaseProductName) {
synchronized (_sqlHelperMap) {
ERXSQLHelper sqlHelper = _sqlHelperMap.get(databaseProductName);
if (sqlHelper == null) {
try {
String sqlHelperClassName = ERXProperties.stringForKey(databaseProductName + ".SQLHelper");
if (sqlHelperClassName == null) {
if (databaseProductName == null) {
// If there is no plugin then product name will be null
sqlHelper = new ERXSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("frontbase")) {
sqlHelper = new FrontBaseSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("mysql")) {
sqlHelper = new MySQLSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("oracle")) {
sqlHelper = new OracleSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("postgresql")) {
sqlHelper = new PostgresqlSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("openbase")) {
sqlHelper = new OpenBaseSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("derby")) {
sqlHelper = new DerbySQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("microsoft")) {
sqlHelper = new MicrosoftSQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("h2")) {
sqlHelper = new H2SQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("db2")) {
sqlHelper = new DB2SQLHelper();
}
else if (databaseProductName.equalsIgnoreCase("firebird")) {
sqlHelper = new FirebirdSQLHelper();
}
else {
try {
sqlHelper = (ERXSQLHelper) Class.forName(ERXSQLHelper.class.getName() + "$" + databaseProductName + "SQLHelper").newInstance();
}
catch (ClassNotFoundException e) {
sqlHelper = new ERXSQLHelper();
}
}
}
else {
sqlHelper = (ERXSQLHelper) Class.forName(sqlHelperClassName).newInstance();
}
_sqlHelperMap.put(databaseProductName, sqlHelper);
}
catch (Exception e) {
throw new NSForwardException(e, "Failed to create sql helper for the database with the product name '" + databaseProductName + "'.");
}
}
return sqlHelper;
}
}
public static class EROracleSQLHelper extends ERXSQLHelper.OracleSQLHelper {
}
public static class OracleSQLHelper extends ERXSQLHelper {
@Override
protected String sqlForSubquery(String subquery, String alias) {
return "(" + subquery + ") " + alias;
}
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
String sqlString = "select " + sequenceName + ".nextVal from dual";
return sqlString;
}
/**
* oracle 9 has a maximum length of 30 characters for table names,
* column names and constraint names Foreign key constraint names are
* defined like this from the plugin:
*
* <pre><code>TABLENAME_FOEREIGNKEYNAME_FK</code></pre>
*
* The whole statement looks like this:
*
* <pre><code>ALTER TABLE [TABLENAME] ADD CONSTRAINT [CONSTRAINTNAME] FOREIGN KEY
* ([FK]) REFERENCES [DESTINATION_TABLE] ([PK]) DEFERRABLE INITIALLY
* DEFERRED</code></pre>
*
* THIS means that the tablename and the columnname together cannot be
* longer than 26 characters.
* <p>
* This method checks each foreign key constraint name and if it is
* longer than 30 characters its replaced with a unique name.
*
* @see ERXSQLHelper#createSchemaSQLForEntitiesInModelWithNameAndOptions(NSArray, String, NSDictionary)
*/
@Override
public String createSchemaSQLForEntitiesInModelWithNameAndOptions(NSArray<EOEntity> entities, String modelName, NSDictionary optionsCreate) {
String oldConstraintName = null;
int i = 0;
String s = super.createSchemaSQLForEntitiesInModelWithNameAndOptions(entities, modelName, optionsCreate);
NSArray a = NSArray.componentsSeparatedByString(s, "/");
StringBuilder buf = new StringBuilder(s.length());
Pattern pattern = Pattern.compile(".*ALTER TABLE .* ADD CONSTRAINT (.*) FOREIGN KEY .* REFERENCES .* \\(.*\\) DEFERRABLE INITIALLY DEFERRED.*");
Pattern pattern2 = Pattern.compile("(.*ALTER TABLE .* ADD CONSTRAINT ).*( FOREIGN KEY .* REFERENCES .* \\(.*\\) DEFERRABLE INITIALLY DEFERRED.*)");
String lineSeparator = System.getProperty("line.separator");
for (Enumeration e = a.objectEnumerator(); e.hasMoreElements();) {
String statementLine = (String) e.nextElement();
NSArray b = NSArray.componentsSeparatedByString(statementLine, lineSeparator);
for (Enumeration e1 = b.objectEnumerator(); e1.hasMoreElements();) {
String statement = (String) e1.nextElement();
if (!pattern.matcher(statement).matches()) {
buf.append(statement);
buf.append(lineSeparator);
continue;
}
String constraintName = pattern.matcher(statement).replaceAll("$1");
if (constraintName.length() <= 30) {
buf.append(statement);
buf.append(lineSeparator);
continue;
}
constraintName = "fk" + System.currentTimeMillis() + new NSTimestamp().getNanos();
String newConstraintName = i == 0 ? constraintName : constraintName + "_" + i;
if (oldConstraintName == null) {
oldConstraintName = constraintName;
}
else if (oldConstraintName.equals(newConstraintName)) {
constraintName += "_" + ++i;
}
else {
i = 0;
}
oldConstraintName = constraintName;
String newConstraint = pattern2.matcher(statement).replaceAll("$1" + constraintName + "$2");
buf.append(newConstraint);
buf.append(lineSeparator);
}
if (e.hasMoreElements()) {
buf.append('/');
}
}
return buf.toString();
}
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
String limitSQL;
/*
* Oracle can make you puke... These are grabbed from tips all over
* the net and I can't test them as it doesn't even install on OSX.
* Pick your poison.
*/
int debug = ERXProperties.intForKeyWithDefault("OracleBatchMode", 3);
if (debug == 1) {
// this only works for the first page
limitSQL = "select * from (" + sql + ") where rownum between " + (start + 1) + " and " + end;
}
else if (debug == 2) {
// this doesn't work at all when have have *no* order by
limitSQL = "select * from (" + "select " + expression.listString() + ", row_number() over (" + expression.orderByString() + ") as eo_rownum from (" + sql + ")) where eo_rownum between " + (start + 1) + " and " + end;
}
else if (debug == 3) {
// this works, but breaks with horizontal inheritance
if (expression != null) {
limitSQL = "select * from (" + "select " + expression.listString().replaceAll("[Tt]\\d\\.", "") + ", rownum eo_rownum from (" + sql + ")) where eo_rownum between " + (start + 1) + " and " + end;
}
else {
limitSQL = "select * from (select a.*, rownum eo_rownum from (" + sql + ") a where rownum <= " + end + ") where eo_rownum >= " + (start + 1);
}
}
else {
// this might work, too, but only if we have an ORDER BY
limitSQL = "select * from (" + "select " + (fetchSpecification.usesDistinct() ? " distinct " : "") + expression.listString() + ", row_number() over (" + expression.orderByString() + ") eo_rownum" + " from " + expression.joinClauseString() + " where " + expression.whereClauseString() + ") where eo_rownum between " + (start + 1) + " and " + end;
}
return limitSQL;
}
@Override
protected char commandSeparatorChar() {
return '/';
}
@Override
protected String commandSeparatorString() {
String lineSeparator = System.getProperty("line.separator");
String commandSeparator = lineSeparator + "/" + lineSeparator;
return commandSeparator;
}
@Override
public String createIndexSQLForEntities(NSArray<EOEntity> entities, NSArray<String> externalTypesToIgnore) {
NSMutableArray<String> oracleExternalTypesToIgnore = new NSMutableArray<String>();
if (externalTypesToIgnore != null) {
oracleExternalTypesToIgnore.addObjectsFromArray(externalTypesToIgnore);
}
oracleExternalTypesToIgnore.addObject("BLOB");
oracleExternalTypesToIgnore.addObject("CLOB");
return super.createIndexSQLForEntities(entities, oracleExternalTypesToIgnore);
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE UNIQUE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForRegularExpressionQuery(String key, String value) {
return "REGEXP_LIKE(" + key + ", " + value + ")";
}
@Override
public String migrationTableName() {
return "dbupdater";
}
@Override
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType;
if (jdbcType == Types.TIMESTAMP) {
externalType = "DATE";
}
else {
externalType = super.externalTypeForJDBCType(adaptor, jdbcType);
}
return externalType;
}
@Override
public boolean reassignExternalTypeForValueTypeOverride(EOAttribute attribute) {
return false;
}
@Override
protected boolean canReliablyPerformDistinctWithSortOrderings() {
return false;
}
/**
* For Oracle, it seems the right thing to do for varcharLarge is to use a CLOB column.
* CLOB is limited to 8TB where as VARCHAR is limited to 4000 bytes.
*/
@Override
public int varcharLargeJDBCType() {
return Types.CLOB;
}
@Override
public int varcharLargeColumnWidth() {
return -1;
}
}
public static class OpenBaseSQLHelper extends ERXSQLHelper {
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
// Openbase support for limiting result set
return sql + " return results " + start + " to " + end;
}
}
public static class H2SQLHelper extends ERXSQLHelper {
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
return sql + " LIMIT " + (end - start) + " OFFSET " + start;
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "ALTER TABLE " + tableName + " ADD CONSTRAINT \"" + indexName + "\" UNIQUE(" + new NSArray<>(columnNames).componentsJoinedByString(", ") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "CREATE INDEX \""+indexName+"\" ON "+tableName+" ("+new NSArray<>(columnNames).componentsJoinedByString(", ")+")";
}
/**
* @see er.extensions.jdbc.ERXSQLHelper#sqlForGetNextValFromSequencedNamed(java.lang.String)
*/
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
return "select NEXTVAL('" + sequenceName + "') as key";
}
@Override
public String sqlForRegularExpressionQuery(String key, String value) {
return key + " REGEXP " + value + "";
}
@Override
public int varcharLargeJDBCType() {
return Types.LONGVARCHAR;
}
@Override
public int varcharLargeColumnWidth() {
return -1;
}
}
public static class DerbySQLHelper extends ERXSQLHelper {
@Override
public boolean shouldExecute(String sql) {
return sql != null && !sql.startsWith("--");
}
@Override
public int varcharLargeJDBCType() {
return Types.CLOB;
}
@Override
public int varcharLargeColumnWidth() {
return 0;
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "CREATE UNIQUE INDEX \""+indexName+"\" ON "+tableName+" (" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "CREATE INDEX \""+indexName+"\" ON "+tableName+ " ("+columnNames.componentsJoinedByString(", ")+")";
}
/**
* @see er.extensions.jdbc.ERXSQLHelper#sqlForGetNextValFromSequencedNamed(java.lang.String)
*/
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
return "VALUES (NEXT VALUE FOR " + sequenceName + ")";
}
@Override
public String migrationTableName() {
return "dbupdater";
}
@Override
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType;
if (jdbcType == Types.TIMESTAMP) {
externalType = "DATE";
}
else {
externalType = super.externalTypeForJDBCType(adaptor, jdbcType);
}
return externalType;
}
}
public static class FrontBaseSQLHelper extends ERXSQLHelper {
private static final String PREFIX_ISOLATION_LEVEL = "isolation=";
private static final String PREFIX_LOCKING = "locking=";
@Override
public boolean reassignExternalTypeForValueTypeOverride(EOAttribute attribute) {
boolean reassignExternalTypeForValueTypeOverride = super.reassignExternalTypeForValueTypeOverride(attribute);
if ("DATE".equalsIgnoreCase(attribute.externalType()) && attribute.valueType() == null) {
reassignExternalTypeForValueTypeOverride = false;
}
return reassignExternalTypeForValueTypeOverride;
}
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
return "select unique from " + sequenceName;
}
@Override
public boolean shouldExecute(String sql) {
boolean shouldExecute = true;
if (sql.startsWith("SET TRANSACTION ISOLATION LEVEL")) {
shouldExecute = false;
}
else if (sql.startsWith("COMMIT")) {
// shouldExecute = false;
}
return shouldExecute;
}
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
// add TOP(start, (end - start)) after the SELECT word
int index = sql.indexOf("select");
if (index == -1) {
index = sql.indexOf("SELECT");
}
index += 6;
String limitSQL = sql.substring(0, index) + " TOP(" + start + ", " + (end - start) + ") " + sql.substring(index + 1, sql.length());
return limitSQL;
}
@Override
public String sqlForFullTextQuery(ERXFullTextQualifier qualifier, EOSQLExpression expression) {
StringBuilder sb = new StringBuilder();
sb.append("satisfies(");
sb.append(qualifier.indexName());
sb.append(", '");
ERXFullTextQualifier.MatchType matchType = qualifier.matchType();
NSArray<String> terms = qualifier.terms();
for (String term : terms) {
String[] termWords = term.split(" ");
for (String termWord : termWords) {
sb.append(termWord);
if (matchType == ERXFullTextQualifier.MatchType.ALL) {
sb.append('&');
}
else if (matchType == ERXFullTextQualifier.MatchType.ANY) {
sb.append('|');
}
}
}
// Lop off the last '&' or '|'
if (terms.count() > 0) {
sb.setLength(sb.length() - 1);
}
sb.append("')");
return sb.toString();
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "ALTER TABLE \"" + tableName + "\" ADD CONSTRAINT \"" + indexName + "\" UNIQUE(\"" + new NSArray<>(columnNames).componentsJoinedByString("\", \"") + "\") DEFERRABLE INITIALLY DEFERRED";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = columnNamesFromColumnIndexes(columnIndexes);
return "CREATE INDEX \""+indexName+"\" ON \""+tableName+"\" (\""+new NSArray<>(columnNames).componentsJoinedByString("\", \"")+"\")";
}
@Override
public void prepareConnectionForSchemaChange(EOEditingContext ec, EOModel model) {
ERXEOAccessUtilities.ChannelAction action = new ERXEOAccessUtilities.ChannelAction() {
@Override
protected int doPerform(EOAdaptorChannel channel) {
try {
ERXJDBCUtilities.executeUpdate(channel, "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, LOCKING PESSIMISTIC");
}
catch (SQLException e) {
throw new NSForwardException(e);
}
return 0;
}
};
action.perform(ec, model.name());
}
@Override
@SuppressWarnings("unchecked")
public void restoreConnectionSettingsAfterSchemaChange(EOEditingContext ec, EOModel model) {
// Default settings
String transactionIsolationLevel = "SERIALIZABLE";
String lockingDiscipline = "PESSIMISTIC";
// Guess settings from looking at the url
String url = (String) model.connectionDictionary().valueForKey("URL");
NSArray<String> urlComponents = NSArray.componentsSeparatedByString(url, "/");
for (String urlComponent : urlComponents) {
if (urlComponent.toLowerCase().startsWith(PREFIX_LOCKING)) {
lockingDiscipline = urlComponent.substring(PREFIX_LOCKING.length()).toUpperCase();
}
else if (urlComponent.toLowerCase().startsWith(PREFIX_ISOLATION_LEVEL)) {
transactionIsolationLevel = urlComponent.substring(PREFIX_ISOLATION_LEVEL.length()).toUpperCase().replaceAll("_", " ");
}
}
final String sql = "SET TRANSACTION ISOLATION LEVEL " + transactionIsolationLevel + ", LOCKING " + lockingDiscipline;
ERXEOAccessUtilities.ChannelAction action = new ERXEOAccessUtilities.ChannelAction() {
@Override
protected int doPerform(EOAdaptorChannel channel) {
try {
ERXJDBCUtilities.executeUpdate(channel, sql);
}
catch (SQLException e) {
throw new NSForwardException(e);
}
return 0;
}
};
action.perform(ec, model.name());
}
/**
* Returns a pattern than matches lines that start with "--".
*
* @return regex pattern that indicates this line is an SQL comment
*/
@Override
protected Pattern commentPattern() {
return Pattern.compile("^--");
}
@Override
public String quoteColumnName(String columnName){
if (columnName == null)
return null;
int i = columnName.lastIndexOf(46);
if (i == -1)
return "\"" + columnName + "\"";
return "\"" + columnName.substring(0, i) + "\".\"" + columnName.substring(i + 1, columnName.length()) + "\"";
}
/**
* FrontBase is exceedingly inefficient in processing OR clauses. A query like this:
* <pre><code>SELECT * FROM "Foo" t0 WHERE ( t0."oid" IN (431, 437, ...) OR t0."oid" IN (1479, 1480, 1481,...)...</code></pre>
* Completely KILLS FrontBase (30+ seconds of 100%+ CPU usage). The same query rendered as:
* <pre><code>SELECT * FROM "Foo" t0 WHERE t0."oid" IN (431, 437, ...) UNION SELECT * FROM "Foo" t0 WHERE t0."oid" IN (1479, 1480, 1481, ...)...</code></pre>
* executes in less than a tenth of the time with less high CPU load. Collapse all the ORs and INs into one and it is faster
* still. This has been tested with over 17,000 elements, so 15,000 seemed like a safe maximum. I don't know what the actual
* theoretical maximum is.
* <p>
* But... It looks to like the query optimizer will choose to NOT use an index if the number of elements in the IN gets close to,
* or exceeds, the number of rows (as in the case of a select based on FK with a large number of keys that don't match any rows). In
* this case it seems to fall back to table scanning (or something dreadfully slow). This only seems to have an impact when the number
* of elements in the IN is greater than 1,000. For larger sizes, the correct number for this method to return seems to depend on the
* number of rows in the tables. 1/5th of the table size may be a good place to start looking for the upper bound.
*
* @see ERXSQLHelper#maximumElementPerInClause(EOEntity)
*
* @param entity EOEntity that can be used to fine-tune the result
* @return database specific limit, or or most efficient number, of elements in an IN clause in a statement
*/
@Override
protected int maximumElementPerInClause(EOEntity entity) {
return 15000;
}
/**
* For BOOLEAN we take 'boolean' as external type. For any other
* case, we pass it up to the default impl.
*
* @param adaptor
* the adaptor to retrieve an external type for
* @param jdbcType
* the JDBC type number
* @return a guess at the external type name to use
*/
@Override
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType;
if (jdbcType == Types.BOOLEAN) {
externalType = "boolean";
}
else if (jdbcType == Types.BINARY) {
externalType = "byte";
}
else {
externalType = super.externalTypeForJDBCType(adaptor, jdbcType);
}
return externalType;
}
}
public static class MySQLSQLHelper extends ERXSQLHelper {
/**
* Returns a pattern than matches lines that start with "--".
*
* @return regex pattern that indicates this line is an SQL comment
*/
@Override
protected Pattern commentPattern() {
return Pattern.compile("^--");
}
/**
* We know better than EOF.
*
* For any other case, we pass it up to the default impl.
*
* @param adaptor
* the adaptor to retrieve an external type for
* @param jdbcType
* the JDBC type number
* @return a guess at the external type name to use
*/
@Override
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType;
if (jdbcType == Types.LONGVARCHAR || jdbcType == Types.CLOB) {
externalType = "longtext";
}
else {
externalType = super.externalTypeForJDBCType(adaptor, jdbcType);
}
return externalType;
}
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
return sql + " LIMIT " + start + ", " + (end - start);
}
@Override
public String sqlForRegularExpressionQuery(String key, String value) {
return key + " REGEXP " + value + "";
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
StringBuffer sql = new StringBuffer();
sql.append("ALTER TABLE `" + tableName + "` ADD UNIQUE `" + indexName + "` (");
_appendIndexColNames(sql, columnIndexes);
sql.append(')');
return sql.toString();
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
StringBuffer sql = new StringBuffer();
sql.append("CREATE INDEX `"+ indexName + "` ON `"+tableName+"` (");
_appendIndexColNames(sql, columnIndexes);
sql.append(')');
return sql.toString();
}
private void _appendIndexColNames(StringBuffer sql, ColumnIndex... columnIndexes) {
for (int columnIndexNum = 0; columnIndexNum < columnIndexes.length; columnIndexNum++) {
ColumnIndex columnIndex = columnIndexes[columnIndexNum];
sql.append("`" + columnIndex.columnName() + "`");
if (columnIndex.hasLength()) {
// index limit of 767 bytes for InnoDB, 999 bytes for MyISAM
// which maps to up to 255 and 333 utf8 characters
int length = Math.min(columnIndex.length(), 255);
sql.append("(" + length + ")");
}
if (columnIndexNum < columnIndexes.length - 1) {
sql.append(", ");
}
}
}
@Override
public int varcharLargeJDBCType() {
return Types.LONGVARCHAR;
}
@Override
public int varcharLargeColumnWidth() {
return -1;
}
}
public static class PostgresqlSQLHelper extends ERXSQLHelper {
/**
* The exception state string for unique constraint exceptions.
*
* @see <a href="http://www.postgresql.org/docs/9.1/static/errcodes-appendix.html">Error codes</a>
*/
public static final String UNIQUE_CONSTRAINT_EXCEPTION_STATE = "23505";
public static final String UNIQUE_CONSTRAINT_MESSAGE_FORMAT = "ERROR: duplicate key value violates unique constraint \"{0}\"\n Detail: Key ({1})=({2}) already exists.";
/**
* Overriden to prevent the external time types set in
* {@link #externalTypeForJDBCType(JDBCAdaptor, int)} from being reset.
*/
@Override
public boolean reassignExternalTypeForValueTypeOverride(EOAttribute attr) {
if(attr != null && attr.adaptorValueType() == EOAttribute.AdaptorDateType) {
return false;
}
return super.reassignExternalTypeForValueTypeOverride(attr);
}
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
return "select NEXTVAL('" + sequenceName + "') as key";
}
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
return sql + " LIMIT " + (end - start) + " OFFSET " + start;
}
@Override
public String sqlForRegularExpressionQuery(String key, String value) {
return key + " ~* " + value + "";
}
/**
* For most types, finding the type in jdbc2Info's typeInfo will provide
* us with a correct type mapping. For Postgresql, it has the honor of
* not actually having a type named "integer," so EOF goes on a hunt for
* a type that MIGHT match (which is just bad, btw) and comes up with
* "serial".
*
* cug: There seems to be also nothing useful for "BLOB", so we return
* bytea for Type.BLOB; int8 for BIGINT; numeric for DECIMAL; bool for
* BOOLEAN
*
* We know better than EOF.
*
* For any other case, we pass it up to the default impl.
*
* @param adaptor
* the adaptor to retrieve an external type for
* @param jdbcType
* the JDBC type number
* @return a guess at the external type name to use
*/
@Override
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType;
if (jdbcType == Types.INTEGER) {
externalType = "int4";
}
else if (jdbcType == Types.BIGINT) {
externalType = "int8";
}
else if (jdbcType == Types.FLOAT) {
externalType = "float4";
}
else if (jdbcType == Types.DOUBLE) {
externalType = "float8";
}
else if (jdbcType == Types.BLOB) {
externalType = "bytea";
}
else if (jdbcType == Types.BOOLEAN) {
externalType = "bool";
}
else if (jdbcType == Types.DECIMAL) {
externalType = "numeric";
}
else if (jdbcType == CustomTypes.INET) {
externalType = "inet";
}
else if (jdbcType == Types.DATE) {
externalType = "date";
}
else if (jdbcType == Types.TIME) {
externalType = "time";
}
else if (jdbcType == Types.LONGVARCHAR || jdbcType == Types.CLOB) {
externalType = "text";
}
else {
externalType = super.externalTypeForJDBCType(adaptor, jdbcType);
}
return externalType;
}
/**
* Creates unique index; stolen from the derby helper
*
* @author cug - Jun 24, 2008
* @see ERXSQLHelper#sqlForCreateUniqueIndex(String, String, ColumnIndex...)
*/
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
indexName = indexName.replace('.', '_');
return "CREATE UNIQUE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public int varcharLargeJDBCType() {
return Types.LONGVARCHAR;
}
@Override
public int varcharLargeColumnWidth() {
return -1;
}
@Override
public boolean handleDatabaseException(EODatabaseContext databaseContext, Throwable throwable) {
if (throwable instanceof EOGeneralAdaptorException) {
EOGeneralAdaptorException gae = (EOGeneralAdaptorException) throwable;
if (gae.userInfo() != null) {
EOAdaptorOperation failedOperation = (EOAdaptorOperation) gae.userInfo().objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey);
if (failedOperation != null) {
Throwable t = failedOperation.exception();
if (t instanceof JDBCAdaptorException) {
JDBCAdaptorException jdbcEx = (JDBCAdaptorException) t;
SQLException sqlEx = jdbcEx.sqlException();
if (sqlEx != null && UNIQUE_CONSTRAINT_EXCEPTION_STATE.equals(sqlEx.getSQLState())) {
String message = sqlEx.getMessage();
MessageFormat format = new MessageFormat(UNIQUE_CONSTRAINT_MESSAGE_FORMAT);
try {
Object[] objs = format.parse(message);
String idx = (String) objs[0];
ERXValidationFactory factory = ERXValidationFactory.defaultFactory();
String method = "UniqueConstraintException." + idx;
ERXValidationException ex = factory.createCustomException(null, method);
databaseContext.rollbackChanges();
throw ex;
} catch (ParseException e) {
log.warn("Error parsing unique constraint exception message: {}", message, e);
}
}
}
}
}
}
return false;
}
@Override
protected String sqlForCountDistinct(EOEntity entity) {
NSArray<String> primaryKeyAttributeNames = entity.primaryKeyAttributeNames();
NSMutableArray<String> pkColumnNames = new NSMutableArray<>(primaryKeyAttributeNames.size());
for (String pkAttributeName : primaryKeyAttributeNames) {
pkColumnNames.add(quoteColumnName("t0." + entity.attributeNamed(pkAttributeName).columnName()));
}
return "count(distinct (" + StringUtils.join(pkColumnNames, ", ") + ")) ";
}
}
public static class FirebirdSQLHelper extends ERXSQLHelper {
@Override
public String externalTypeForJDBCType(JDBCAdaptor adaptor, int jdbcType) {
String externalType;
if (jdbcType == Types.BOOLEAN) {
externalType = "SMALLINT";
} else {
externalType = super.externalTypeForJDBCType(adaptor, jdbcType);
}
return externalType;
}
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
return "select Gen_ID(" + sequenceName + ", 1) FROM RDB$Database";
}
@Override
protected int maximumElementPerInClause(EOEntity entity) {
return 1500;
}
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
return sql + " ROWS " + start + " TO " + end;
}
private static final NSSet reservedWords = new NSSet(new String[] {
"active",
"password"
});
@Override
public String quoteColumnName(String columnName){
if (columnName == null)
return null;
if (columnName.startsWith("\""))
return columnName;
if (!reservedWords.contains(columnName))
return columnName;
int i = columnName.lastIndexOf(".");
if (i == -1)
return "\"" + columnName + "\"";
return "\"" + columnName.substring(0, i) + "\".\"" + columnName.substring(i + 1, columnName.length()) + "\"";
}
/**
* Creates unique index; stolen from the derby helper
* @see ERXSQLHelper#sqlForCreateUniqueIndex(String, String, ColumnIndex...)
*/
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
indexName = indexName.replace('.', '_');
return "CREATE UNIQUE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
}
public static class MicrosoftSQLHelper extends ERXSQLHelper {
/**
* Returns a pattern than matches lines that start with "--".
*
* @return regex pattern that indicates this line is an SQL comment
*/
@Override
protected Pattern commentPattern() {
return Pattern.compile("^--");
}
@Override
public String externalTypeForJDBCType( JDBCAdaptor adaptor, int type ) {
if( type == Types.BLOB ) {
return "binary";
}
return super.externalTypeForJDBCType( adaptor, type );
}
@Override
public String limitExpressionForSQL( EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end ) {
if( sql == null || "".equals( sql ) )
{
return sql;
}
String originalSql = sql.toLowerCase();
String orderBy;
int indexOfOrderByClause = originalSql.indexOf( " order by " );
if( indexOfOrderByClause > 0)
{
orderBy = originalSql.substring( indexOfOrderByClause + 1, originalSql.length() );
originalSql = originalSql.substring( 0, indexOfOrderByClause );
}
else
{
String columns = originalSql.substring( originalSql.indexOf( "select " ) + 7, originalSql.indexOf( " from " ) );
orderBy = "order by " + columns.split( "," )[0];
}
StringBuilder limitSqlBuilder = new StringBuilder( originalSql );
limitSqlBuilder.insert( 0, "select * from (" );
String rowNumberClause = ", row_number() over (" + orderBy + ") eo_rownum";
limitSqlBuilder.insert( limitSqlBuilder.lastIndexOf( " from " ), rowNumberClause );
limitSqlBuilder.append( ") as temp_row_number where eo_rownum >= " );
limitSqlBuilder.append( start + 1 );
limitSqlBuilder.append( " and eo_rownum < " );
limitSqlBuilder.append( end + 1 );
limitSqlBuilder.append( " order by eo_rownum" );
return limitSqlBuilder.toString();
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE UNIQUE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
}
public static class NoSQLHelper extends ERXSQLHelper {
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
return null;
}
}
public static class DB2SQLHelper extends ERXSQLHelper {
@Override
protected String sqlForSubquery(String subquery, String alias) {
return "(" + subquery + ") " + alias;
}
@Override
protected String sqlForGetNextValFromSequencedNamed(String sequenceName) {
String sqlString = "select next value for " + sequenceName + " from SYSIBM.SYSDUMMY1";
return sqlString;
}
@Override
public String limitExpressionForSQL(EOSQLExpression expression, EOFetchSpecification fetchSpecification, String sql, long start, long end) {
// this might work, too, but only if we have an ORDER BY
// remove order by clause
String orderBy = expression.orderByString();
String innerSql = sql.replace(orderBy, " ");
innerSql = innerSql.replace(" ORDER BY ", " ");
String rownum = ", row_number() over ( order by " + orderBy + ") eo_rownum" + " FROM ";
innerSql = innerSql.replace(" FROM ", rownum);
innerSql = innerSql.replaceAll("FETCH FIRST\\W+[0-9]+\\W+ROWS ONLY", " "); // this removes any limit that may be on may want to keep it it does work
String limitSQL = "select * from (" + innerSql + ") as inner_select where eo_rownum between " + (start + 1) + " and " + end;
return limitSQL;
}
@Override
protected char commandSeparatorChar() {
return ';';
}
@Override
protected String commandSeparatorString() {
String lineSeparator = System.getProperty("line.separator");
String commandSeparator = lineSeparator + ";" + lineSeparator;
return commandSeparator;
}
@Override
public String createIndexSQLForEntities(NSArray<EOEntity> entities, NSArray<String> externalTypesToIgnore) {
NSMutableArray<String> db2ExternalTypesToIgnore = new NSMutableArray<>();
if (externalTypesToIgnore != null) {
db2ExternalTypesToIgnore.addObjectsFromArray(externalTypesToIgnore);
}
db2ExternalTypesToIgnore.addObject("BLOB");
db2ExternalTypesToIgnore.addObject("CLOB");
db2ExternalTypesToIgnore.addObject("DBCLOB");
db2ExternalTypesToIgnore.addObject("LONG VARCHAR");
db2ExternalTypesToIgnore.addObject("LONG VARGRAPHIC");
return super.createIndexSQLForEntities(entities, db2ExternalTypesToIgnore);
}
@Override
public String sqlForCreateUniqueIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE UNIQUE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String sqlForCreateIndex(String indexName, String tableName, ColumnIndex... columnIndexes) {
NSMutableArray<String> columnNames = new NSMutableArray<>();
for (ColumnIndex columnIndex : columnIndexes) {
columnNames.addObject(columnIndex.columnName());
}
return "CREATE INDEX " + indexName + " ON " + tableName + "(" + columnNames.componentsJoinedByString(",") + ")";
}
@Override
public String migrationTableName() {
return "dbupdater";
}
@Override
public boolean reassignExternalTypeForValueTypeOverride(EOAttribute attribute) {
return false;
}
@Override
protected boolean canReliablyPerformDistinctWithSortOrderings() {
return false;
}
/**
* For DB2, it seems the right thing to do for varcharLarge is to use a Clob column.
* CLOB is limited to 2GB where as VARCHAR is limited to 32672 bytes and a LONG VARCHAR to 32700
*/
@Override
public int varcharLargeJDBCType() {
return Types.CLOB;
}
}
}