blob: 8d76aaf49879e160694624e33f04f17a7ff96d02 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2009, 2011 Oracle. All rights reserved.
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0, which accompanies this distribution
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*
* Contributors:
* Oracle - initial API and implementation
******************************************************************************/
package org.eclipse.jpt.jpa.db.internal.driver;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.jpt.common.utility.internal.ArrayTools;
import org.eclipse.jpt.common.utility.internal.StringTools;
import org.eclipse.jpt.common.utility.internal.Tools;
import org.eclipse.jpt.jpa.db.Database;
import org.eclipse.jpt.jpa.db.DatabaseObject;
import org.eclipse.jpt.jpa.db.Schema;
import org.eclipse.jpt.jpa.db.Table;
/**
* MySQL is a bit unusual in that it does <em>not</em> fold <em>regular</em>
* identifiers and its identifiers are <em>not</em> case-sensitive
* (except in special cases, described below). Even <em>delimited</em> identifiers
* are case-<em>in</em>sensitive. (Delimiters are only needed for identifiers
* that contain special characters or are reserved words.)
* <p>
* MySQL handles the case-sensitivity of databases and tables according to the
* value of the system variable <code>lower_case_table_names</code>:<ul>
* <li>0 (Unix/Linux default): database and table names are <em>not</em> folded
* and they are case-sensitive
* <li>1 (Windows default): database and table names are folded to lowercase
* and they are <em>not</em> case-sensitive
* <li>2 (Mac OS X default): database and table names are <em>not</em> folded
* and they are <em>not</em> case-sensitive
* </ul>
* These values will not work perfectly on all platforms because databases are
* stored as directories and tables are stored as files in the underlying
* O/S; and the actual case-sensitivity of the names will ulimately be
* determined by the behavior of filenames on the O/S.
* <p>
* See <a href=http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html>
* Identifier Case Sensitivity</a>.
*/
class MySQL
extends AbstractDTPDriverAdapter
{
/** lazy-initialized; value comes from the database server */
private int lower_case_table_names = -1;
/** lazy-initialized; value comes from the database server */
private Boolean ANSI_QUOTES = null;
MySQL(Database database) {
super(database);
}
/**
* The DTP model for MySQL has a database that contains no catalogs;
* but, instead, directly holds a single schema with the same name as
* the database. (This is hard-coded in
* {@link org.eclipse.datatools.enablement.mysql.catalog.MySqlCatalogDatabase#getSchemas()}.)
* Although you can qualify identifiers with a database name
* in MySQL, only the database specified at login seems to be available
* in the DTP model....
* <p>
* <strong>NB:</strong> In MySQL DDL, <code>SCHEMA</code> is a synonym
* for <code>DATABASE</code>; but the JDBC
* method {@link java.sql.DatabaseMetaData#getSchemas()} returns an empty list,
* while {@link java.sql.DatabaseMetaData#getCatalogs()} returns a list of the
* available databases.
* You can also use the JDBC method {@link java.sql.Connection#setCatalog(String)}
* to set the default database.
*/
@Override
CatalogStrategy buildCatalogStrategy() {
return new NoCatalogStrategy(this.database.getDTPDatabase());
}
/**
* There are some cases where MySQL folds identifiers, but using the
* non-folding strategy should work:<ul>
* <li>We only fold identifiers when
* converting them to names; but MySQL is case-<em>in</em>sensitive
* when it is folding database and table names
* (<code>lower_case_table_names</code>=1), so lookups will work
* fine without any folding anyways.
* <li>We only test whether a name is already folded when determining
* whether it must be delimited; and MySQL ignores delimiters
* when it comes to case-sensitivity, so we can leave any <em>regular</em>
* mixed-case names undelimited. (MySQL only requires delimiters for
* special characters and reserved words.)
* </ul>
*/
@Override
FoldingStrategy buildFoldingStrategy() {
return NonFoldingStrategy.instance();
}
@Override
void addDefaultSchemaNamesTo(ArrayList<String> names) {
names.add(this.database.getName());
}
// ********** identifiers/names **********
/**
* MySQL is the only vendor that allows a digit.
* Although, the name cannnot be <em>all</em> digits
* (unless delimited).
*/
@Override
boolean characterIsRegularNameStart(char c) {
return Character.isDigit(c) || super.characterIsRegularNameStart(c);
}
@Override
char[] getExtendedRegularNameStartCharacters() {
return EXTENDED_REGULAR_NAME_START_CHARACTERS;
}
private static final char[] EXTENDED_REGULAR_NAME_START_CHARACTERS = new char[] { '_', '$' };
@Override
String delimitName(String name) {
return StringTools.delimit(name, BACKTICK);
}
/**
* By default, MySQL delimits identifiers with backticks (<code>`</code>);
* but it can also be configured to use double-quotes.
*/
@Override
boolean identifierIsDelimited(String identifier) {
return StringTools.stringIsDelimited(identifier, BACKTICK)
|| (StringTools.stringIsQuoted(identifier) && this.doubleQuoteIsIdentifierDelimiter());
}
private static final char BACKTICK = '`';
/**
* This method should only affect whether entity
* generation will generate a specified table or column identifier when the
* default identifier is not a match. Worst case: Entity generation will
* generate explicit identifiers unnecessarily for columns when tables are
* case-sensitive. The generated annotation will be valid but the column
* name will be redundant.
*/
@Override
boolean regularNamesMatch(String name1, String name2) {
return this.tableNamesAreCaseSensitive() ?
name1.equals(name2) :
name1.equalsIgnoreCase(name2);
}
// ********** selecting database objects **********
/**
* DTP schemata are MySQL databases(?). Of course, we only have one per
* connection....
*/
@Override
public Schema selectSchemaForIdentifier(Iterable<Schema> schemata, String identifier) {
return this.tableNamesAreCaseSensitive() ?
this.selectDatabaseObjectForIdentifierRespectCase(schemata, identifier) :
super.selectSchemaForIdentifier(schemata, identifier);
}
@Override
public Table selectTableForIdentifier(Iterable<Table> tables, String identifier) {
return this.tableNamesAreCaseSensitive() ?
this.selectDatabaseObjectForIdentifierRespectCase(tables, identifier) :
super.selectTableForIdentifier(tables, identifier);
}
private <T extends DatabaseObject> T selectDatabaseObjectForIdentifierRespectCase(Iterable<T> databaseObjects, String identifier) {
return this.selectDatabaseObjectNamedRespectCase(databaseObjects, this.convertIdentifierToName(identifier));
}
/**
* MySQL database object names are usually case-insensitive;
* the exception being databases (schemata) and tables when the system
* variable <code>lower_case_table_names</code> is 0 (the default on
* UNIX/Linux). (For some odd reason, trigger names are also
* case-sensitive.... Fortunately, we don't care yet.)
*/
@Override
<T extends DatabaseObject> T selectDatabaseObjectNamed(Iterable<T> databaseObjects, String name) {
return this.selectDatabaseObjectNamedIgnoreCase(databaseObjects, name);
}
// ********** lower_case_table_names **********
private boolean tableNamesAreCaseSensitive() {
return this.getLowerCaseTableNames() == 0;
}
private int getLowerCaseTableNames() {
if (this.lower_case_table_names == -1) {
this.lower_case_table_names = this.buildLowerCaseTableNames();
}
return this.lower_case_table_names;
}
/**
* If we don't have a live connection (i.e. we are working off-line),
* use the current O/S default setting(?).
*/
private int buildLowerCaseTableNames() {
int dbValue = this.getLowerCaseTableNamesFromDatabase();
return (dbValue != -1) ? dbValue : this.getLowerCaseTableNamesFromOS();
}
/**
* See <a href=http://dev.mysql.com/doc/refman/5.0/en/identifier-case-sensitivity.html>
* Identifier Case Sensitivity</a>.
*/
private int getLowerCaseTableNamesFromDatabase() {
if (this.getConnectionProfile().isDisconnected()) {
return -1;
}
// the underscore is a wild character on MySQL, so we need to escape it
List<Map<String, Object>> rows = this.execute("SHOW VARIABLES LIKE 'lower\\_case\\_table\\_names'"); //$NON-NLS-1$
if (rows.isEmpty()) {
return -1;
}
Map<String, Object> row = rows.get(0);
if (Tools.valuesAreEqual(row.get("Variable_name"), "lower_case_table_names")) { //$NON-NLS-1$ //$NON-NLS-2$
return Integer.valueOf((String) row.get("Value")).intValue(); //$NON-NLS-1$
}
return -1;
}
/**
* Make a best guess at what the setting might be:
* Return the default value for the current O/S (unfortunately this is the
* client O/S, not the MySQL Server O/S...).
*/
private int getLowerCaseTableNamesFromOS() {
if (Tools.osIsMac()) {
return 2;
}
if (Tools.osIsWindows()) {
return 1;
}
return 0; // Linux etc.
}
// ********** ANSI_QUOTES **********
private boolean doubleQuoteIsIdentifierDelimiter() {
return this.getANSIQuotes().booleanValue();
}
private Boolean getANSIQuotes() {
if (this.ANSI_QUOTES == null) {
this.ANSI_QUOTES = this.buildANSIQuotes();
}
return this.ANSI_QUOTES;
}
/**
* If we don't have a live connection (i.e. we are working off-line),
* return <code>FALSE</code> (the default setting).
*/
private Boolean buildANSIQuotes() {
Boolean dbValue = this.getANSIQuotesFromDatabase();
return (dbValue != null) ? dbValue : Boolean.FALSE;
}
/**
* See <a href=http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_ansi_quotes>
* ANSI_QUOTES</a>.
*/
private Boolean getANSIQuotesFromDatabase() {
if (this.getConnectionProfile().isDisconnected()) {
return null;
}
List<Map<String, Object>> rows = this.execute("SELECT @@SESSION.sql_mode"); //$NON-NLS-1$
if (rows.isEmpty()) {
return null;
}
Map<String, Object> row = rows.get(0);
String sql_mode = (String) row.get("@@SESSION.sql_mode"); //$NON-NLS-1$
if (sql_mode == null) {
return null;
}
String[] modes = sql_mode.split(","); //$NON-NLS-1$
return Boolean.valueOf(ArrayTools.contains(modes, "ANSI_QUOTES")); //$NON-NLS-1$
}
// ********** factory **********
static class Factory implements DTPDriverAdapterFactory {
private static final String[] VENDORS = {
"MySql" //$NON-NLS-1$
};
public String[] getSupportedVendors() {
return VENDORS;
}
public DTPDriverAdapter buildAdapter(Database database) {
return new MySQL(database);
}
}
}