From 7ebbc94dd9f640cf95390567acf1ba5a5a0f16ba Mon Sep 17 00:00:00 2001 From: v-reye Date: Tue, 11 Jun 2019 14:22:52 -0700 Subject: [PATCH] Feature | Add new connection property 'useFmtOnly' to retrieve Parameter Metadata Adds a new way to query metadata for instances where sp_describe_undeclared_parameters fail. --- build.gradle | 3 +- pom.xml | 9 + .../sqlserver/jdbc/ISQLServerConnection.java | 15 + .../sqlserver/jdbc/ISQLServerDataSource.java | 17 +- .../jdbc/ISQLServerPreparedStatement.java | 20 +- .../sqlserver/jdbc/SQLServerConnection.java | 62 ++- .../jdbc/SQLServerConnectionPoolProxy.java | 10 + .../sqlserver/jdbc/SQLServerDataSource.java | 12 + .../sqlserver/jdbc/SQLServerDriver.java | 8 +- .../sqlserver/jdbc/SQLServerFMTQuery.java | 113 ++++ .../sqlserver/jdbc/SQLServerLexer.java | 433 ++++++++++++++++ .../jdbc/SQLServerParameterMetaData.java | 419 ++------------- .../sqlserver/jdbc/SQLServerParser.java | 485 ++++++++++++++++++ .../jdbc/SQLServerPreparedStatement.java | 14 + .../sqlserver/jdbc/SQLServerResource.java | 11 +- .../JDBCEncryptionDecryptionTest.java | 19 + .../microsoft/sqlserver/jdbc/ParserUtils.java | 63 +++ .../microsoft/sqlserver/jdbc/TestUtils.java | 15 + .../RequestBoundaryMethodsTest.java | 31 +- .../sqlserver/jdbc/fmtOnly/APITest.java | 79 +++ .../sqlserver/jdbc/fmtOnly/DeleteTest.java | 112 ++++ .../sqlserver/jdbc/fmtOnly/InsertTest.java | 265 ++++++++++ .../sqlserver/jdbc/fmtOnly/LexerTest.java | 170 ++++++ .../jdbc/fmtOnly/ParameterMetaDataTest.java | 186 +++++++ .../sqlserver/jdbc/fmtOnly/SelectTest.java | 233 +++++++++ .../sqlserver/jdbc/fmtOnly/UpdateTest.java | 237 +++++++++ .../jdbc/unit/statement/PQImpsTest.java | 8 +- 27 files changed, 2634 insertions(+), 415 deletions(-) create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerLexer.java create mode 100644 src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/ParserUtils.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/APITest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/DeleteTest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/InsertTest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/LexerTest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/SelectTest.java create mode 100644 src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/UpdateTest.java diff --git a/build.gradle b/build.gradle index 9b3f84ab7..c9f4ac7fa 100644 --- a/build.gradle +++ b/build.gradle @@ -113,7 +113,8 @@ dependencies { compileOnly 'com.microsoft.azure:azure-keyvault:1.2.0', 'com.microsoft.azure:azure-keyvault-webkey:1.2.0', 'com.microsoft.rest:client-runtime:1.6.5', - 'com.microsoft.azure:adal4j:1.6.3' + 'com.microsoft.azure:adal4j:1.6.3', + 'org.antlr:antlr4-runtime:4.7.2' testCompile 'org.junit.platform:junit-platform-console:1.4.0', 'org.junit.platform:junit-platform-commons:1.4.0', 'org.junit.platform:junit-platform-engine:1.4.0', diff --git a/pom.xml b/pom.xml index 96a4346b3..06ff594e0 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ 1.6.7 6.0.0 5.0.0 + 4.7.2 [1.3.2, 1.4.2] @@ -93,6 +94,14 @@ ${rest.client.version} true + + + + org.antlr + antlr4-runtime + ${antlr.runtime.version} + true + diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java index 44d991782..19b9fe2f9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java @@ -343,4 +343,19 @@ public CallableStatement prepareCall(String sql, int nType, int nConcur, int nHo * @return true if statement pooling is disabled, false if it is enabled. */ public boolean getDisableStatementPooling(); + + /** + * Returns the current flag value for useFmtOnly. + * + * @return 'useFmtOnly' property value. + */ + public boolean getUseFmtOnly(); + + /** + * Specifies the flag to use FMTONLY for parameter metadata queries. + * + * @param useFmtOnly + * boolean value for 'useFmtOnly'. + */ + public void setUseFmtOnly(boolean useFmtOnly); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java index 8317a0759..b1c15c048 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java @@ -843,7 +843,7 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * Client Key of Azure Key Vault (AKV) Provider to be used for column encryption. */ void setKeyVaultProviderClientKey(String keyVaultProviderClientKey); - + /** * Sets the 'domain' connection property used for NTLM Authentication. * @@ -858,4 +858,19 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource { * @return 'domain' property value */ String getDomain(); + + /** + * Returns the current flag value for useFmtOnly. + * + * @return 'useFmtOnly' property value. + */ + public boolean getUseFmtOnly(); + + /** + * Specifies the flag to use FMTONLY for parameter metadata queries. + * + * @param useFmtOnly + * boolean value for 'useFmtOnly'. + */ + public void setUseFmtOnly(boolean useFmtOnly); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerPreparedStatement.java index 187d20821..e8eb211db 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerPreparedStatement.java @@ -755,12 +755,28 @@ public void setTimestamp(int parameterIndex, java.sql.Timestamp x, java.util.Cal * * @param forceRefresh: * If true the cache will not be used to retrieve the metadata. - * * @return Per the description. - * * @throws SQLServerException * when an error occurs */ public ParameterMetaData getParameterMetaData(boolean forceRefresh) throws SQLServerException; + /** + * Returns the current flag value for useFmtOnly. + * + * @return 'useFmtOnly' property value. + * @throws SQLServerException + * when the connection is closed. + */ + public boolean getUseFmtOnly() throws SQLServerException; + + /** + * Specifies the flag to use FMTONLY for parameter metadata queries. + * + * @param useFmtOnly + * boolean value for 'useFmtOnly'. + * @throws SQLServerException + * when the connection is closed. + */ + public void setUseFmtOnly(boolean useFmtOnly) throws SQLServerException; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index 46d8c2d27..1e2c633d7 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -575,6 +575,7 @@ public void setUseBulkCopyForBatchInsert(boolean useBulkCopyForBatchInsert) { boolean userSetTNIR = true; private boolean sendTimeAsDatetime = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue(); + private boolean useFmtOnly = SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue(); @Override public final boolean getSendTimeAsDatetime() { @@ -1093,10 +1094,8 @@ void checkClosed() throws SQLServerException { * Returns if Federated Authentication is in use or is about to expire soon * * @return true/false - * @throws SQLServerException - * if an error occurs. */ - protected boolean needsReconnect() throws SQLServerException { + protected boolean needsReconnect() { return (null != fedAuthToken && Util.checkIfNeedNewAccessToken(this, fedAuthToken.expiresOn)); } @@ -1299,7 +1298,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.USER.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = SQLServerDriverStringProperty.USER.getDefaultValue(); activeConnectionProperties.setProperty(sPropKey, sPropValue); } @@ -1307,7 +1306,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.PASSWORD.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = SQLServerDriverStringProperty.PASSWORD.getDefaultValue(); activeConnectionProperties.setProperty(sPropKey, sPropValue); } @@ -1340,7 +1339,7 @@ Connection connectInternal(Properties propsIn, // operation of RFC 3490. sPropKey = SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SERVER_NAME_AS_ACE.getDefaultValue()); activeConnectionProperties.setProperty(sPropKey, sPropValue); } @@ -1351,7 +1350,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.SERVER_NAME.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = "localhost"; } @@ -1401,14 +1400,14 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.APPLICATION_NAME.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue != null) + if (null != sPropValue) validateMaxSQLLoginName(sPropKey, sPropValue); else activeConnectionProperties.setProperty(sPropKey, SQLServerDriver.DEFAULT_APP_NAME); sPropKey = SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.LAST_UPDATE_COUNT.getDefaultValue()); activeConnectionProperties.setProperty(sPropKey, sPropValue); } @@ -1461,7 +1460,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.MULTI_SUBNET_FAILOVER.getDefaultValue()); activeConnectionProperties.setProperty(sPropKey, sPropValue); } @@ -1469,7 +1468,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { userSetTNIR = false; sPropValue = Boolean .toString(SQLServerDriverBooleanProperty.TRANSPARENT_NETWORK_IP_RESOLUTION.getDefaultValue()); @@ -1479,7 +1478,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.ENCRYPT.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.ENCRYPT.getDefaultValue()); activeConnectionProperties.setProperty(sPropKey, sPropValue); } @@ -1489,7 +1488,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = Boolean .toString(SQLServerDriverBooleanProperty.TRUST_SERVER_CERTIFICATE.getDefaultValue()); activeConnectionProperties.setProperty(sPropKey, sPropValue); @@ -1504,7 +1503,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.SELECT_METHOD.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = SQLServerDriverStringProperty.SELECT_METHOD.getDefaultValue(); } @@ -1520,7 +1519,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.RESPONSE_BUFFERING.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = SQLServerDriverStringProperty.RESPONSE_BUFFERING.getDefaultValue(); } @@ -1534,7 +1533,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.APPLICATION_INTENT.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = SQLServerDriverStringProperty.APPLICATION_INTENT.getDefaultValue(); } @@ -1543,13 +1542,21 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue()); activeConnectionProperties.setProperty(sPropKey, sPropValue); } sendTimeAsDatetime = isBooleanPropertyOn(sPropKey, sPropValue); + sPropKey = SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString(); + sPropValue = activeConnectionProperties.getProperty(sPropKey); + if (null == sPropValue) { + sPropValue = Boolean.toString(SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue()); + activeConnectionProperties.setProperty(sPropKey, sPropValue); + } + useFmtOnly = isBooleanPropertyOn(sPropKey, sPropValue); + // Must be set before DISABLE_STATEMENT_POOLING sPropKey = SQLServerDriverIntProperty.STATEMENT_POOLING_CACHE_SIZE.toString(); if (activeConnectionProperties.getProperty(sPropKey) != null @@ -1574,7 +1581,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue != null) { + if (null != sPropValue) { integratedSecurity = isBooleanPropertyOn(sPropKey, sPropValue); } @@ -1582,7 +1589,7 @@ Connection connectInternal(Properties propsIn, if (integratedSecurity) { sPropKey = SQLServerDriverStringProperty.AUTHENTICATION_SCHEME.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue != null) { + if (null != sPropValue) { intAuthScheme = AuthenticationScheme.valueOfString(sPropValue); } } @@ -1617,7 +1624,7 @@ Connection connectInternal(Properties propsIn, sPropKey = SQLServerDriverStringProperty.AUTHENTICATION.toString(); sPropValue = activeConnectionProperties.getProperty(sPropKey); - if (sPropValue == null) { + if (null == sPropValue) { sPropValue = SQLServerDriverStringProperty.AUTHENTICATION.getDefaultValue(); } authenticationString = SqlAuthentication.valueOfString(sPropValue).toString().trim(); @@ -5445,6 +5452,16 @@ public void setSendTimeAsDatetime(boolean sendTimeAsDateTimeValue) { sendTimeAsDatetime = sendTimeAsDateTimeValue; } + @Override + public void setUseFmtOnly(boolean useFmtOnly) { + this.useFmtOnly = useFmtOnly; + } + + @Override + public final boolean getUseFmtOnly() { + return useFmtOnly; + } + @Override public java.sql.Array createArrayOf(String typeName, Object[] elements) throws SQLException { SQLServerException.throwNotSupportedException(this, null); @@ -5645,6 +5662,7 @@ public T unwrap(Class iface) throws SQLException { private boolean originalUseBulkCopyForBatchInsert; private volatile SQLWarning originalSqlWarnings; private List openStatements; + private boolean originalUseFmtOnly; protected void beginRequestInternal() throws SQLException { loggerExternal.entering(getClassNameLogging(), "beginRequest", this); @@ -5663,6 +5681,7 @@ protected void beginRequestInternal() throws SQLException { originalUseBulkCopyForBatchInsert = getUseBulkCopyForBatchInsert(); originalSqlWarnings = sqlWarnings; openStatements = new LinkedList(); + originalUseFmtOnly = useFmtOnly; requestStarted = true; } } @@ -5691,6 +5710,9 @@ protected void endRequestInternal() throws SQLException { if (sendTimeAsDatetime != originalSendTimeAsDatetime) { setSendTimeAsDatetime(originalSendTimeAsDatetime); } + if (useFmtOnly != originalUseFmtOnly) { + setUseFmtOnly(originalUseFmtOnly); + } if (statementPoolingCacheSize != originalStatementPoolingCacheSize) { setStatementPoolingCacheSize(originalStatementPoolingCacheSize); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java index 69f642f1e..9aa915967 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java @@ -583,4 +583,14 @@ public void setDisableStatementPooling(boolean value) { public boolean getDisableStatementPooling() { return wrappedConnection.getDisableStatementPooling(); } + + @Override + public void setUseFmtOnly(boolean useFmtOnly) { + wrappedConnection.setUseFmtOnly(useFmtOnly); + } + + @Override + public boolean getUseFmtOnly() { + return wrappedConnection.getUseFmtOnly(); + } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java index e53ee2301..3473dfd7f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java @@ -478,7 +478,19 @@ public boolean getSendTimeAsDatetime() { return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.toString(), SQLServerDriverBooleanProperty.SEND_TIME_AS_DATETIME.getDefaultValue()); } + + @Override + public void setUseFmtOnly(boolean useFmtOnly) { + setBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString(), + useFmtOnly); + } + @Override + public boolean getUseFmtOnly() { + return getBooleanProperty(connectionProps, SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString(), + SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue()); + } + /** * Sets whether string parameters are sent to the server in UNICODE format. * diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java index 183f9f27d..f0850a46e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java @@ -357,7 +357,8 @@ enum SQLServerDriverBooleanProperty { XOPEN_STATES("xopenStates", false), FIPS("fips", false), ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT("enablePrepareOnFirstPreparedStatementCall", SQLServerConnection.DEFAULT_ENABLE_PREPARE_ON_FIRST_PREPARED_STATEMENT_CALL), - USE_BULK_COPY_FOR_BATCH_INSERT("useBulkCopyForBatchInsert", false); + USE_BULK_COPY_FOR_BATCH_INSERT("useBulkCopyForBatchInsert", false), + USE_FMT_ONLY("useFmtOnly", false); private final String name; private final boolean defaultValue; @@ -529,7 +530,10 @@ public final class SQLServerDriver implements java.sql.Driver { new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_ID.toString(), SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_ID.getDefaultValue(), false, null), new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.toString(), - SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.getDefaultValue(), false, null)}; + SQLServerDriverStringProperty.KEY_VAULT_PROVIDER_CLIENT_KEY.getDefaultValue(), false, null), + new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.USE_FMT_ONLY.toString(), + Boolean.toString(SQLServerDriverBooleanProperty.USE_FMT_ONLY.getDefaultValue()), false, + TRUE_FALSE),}; /** * Properties that can only be set by using Properties. Cannot set in connection string diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java new file mode 100644 index 000000000..93f67b32d --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerFMTQuery.java @@ -0,0 +1,113 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.Token; + + +class SQLServerFMTQuery { + + private static final String FMT_ON = "SET FMTONLY ON;"; + private static final String SELECT = "SELECT "; + private static final String FROM = " FROM "; + private static final String FMT_OFF = ";SET FMTONLY OFF;"; + + private String prefix = ""; + private ArrayList tokenList = null; + private List userColumns = new ArrayList<>(); + private List tableTarget = new ArrayList<>(); + private List possibleAliases = new ArrayList<>(); + private List> valuesList = new ArrayList<>(); + + List getColumns() { + return userColumns; + } + + List getTableTarget() { + return tableTarget; + } + + List> getValuesList() { + return valuesList; + } + + List getAliases() { + return possibleAliases; + } + + /** + * Takes the list of user parameters ('?') and appends their respective parsed column names together. In the case of + * an INSERT INTO table VALUES(?,?,?...), we need to wait for the server to reply to know the column names. The + * parser uses an '*' followed by placeholder '?'s to indicate these unknown columns, and we can't include the '?'s + * in column targets. This method is used to generate the column targets in the FMT Select query: SELECT + * {constructColumnTargets} FROM ... . + */ + String constructColumnTargets() { + if (userColumns.contains("?")) { + return userColumns.stream().filter(s -> !s.equals("?")).map(s -> s.equals("") ? "NULL" : s) + .collect(Collectors.joining(",")); + } else { + return userColumns.isEmpty() ? "*" : userColumns.stream().map(s -> s.equals("") ? "NULL" : s) + .collect(Collectors.joining(",")); + } + } + + String constructTableTargets() { + return tableTarget.stream().distinct().filter(s -> !possibleAliases.contains(s)) + .collect(Collectors.joining(",")); + } + + String getFMTQuery() { + StringBuilder sb = new StringBuilder(FMT_ON); + if (prefix != "") { + sb.append(prefix); + } + sb.append(SELECT); + sb.append(constructColumnTargets()); + if (!tableTarget.isEmpty()) { + sb.append(FROM); + sb.append(constructTableTargets()); + } + sb.append(FMT_OFF); + return sb.toString(); + } + + // Do not allow default instantiation, class must be used with sql query + @SuppressWarnings("unused") + private SQLServerFMTQuery() {}; + + SQLServerFMTQuery(String userSql) throws SQLServerException { + if (null == userSql || userSql.length() == 0) { + SQLServerException.makeFromDriverError(null, this, + SQLServerResource.getResource("R_noTokensFoundInUserQuery"), "", false); + } + InputStream stream = new ByteArrayInputStream(userSql.getBytes(StandardCharsets.UTF_8)); + SQLServerLexer lexer = null; + try { + lexer = new SQLServerLexer(CharStreams.fromStream(stream)); + } catch (IOException e) { + SQLServerException.makeFromDriverError(null, userSql, e.getLocalizedMessage(), "", false); + } + + this.tokenList = (ArrayList) lexer.getAllTokens(); + if (tokenList.size() <= 0) { + SQLServerException.makeFromDriverError(null, this, + SQLServerResource.getResource("R_noTokensFoundInUserQuery"), "", false); + } + SQLServerTokenIterator iter = new SQLServerTokenIterator(tokenList); + this.prefix = SQLServerParser.getCTE(iter); + SQLServerParser.parseQuery(iter, this); + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerLexer.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerLexer.java new file mode 100644 index 000000000..7fa435dba --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerLexer.java @@ -0,0 +1,433 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import org.antlr.v4.runtime.atn.ATN; +import org.antlr.v4.runtime.atn.ATNDeserializer; +import org.antlr.v4.runtime.atn.LexerATNSimulator; +import org.antlr.v4.runtime.atn.PredictionContextCache; +import org.antlr.v4.runtime.dfa.DFA; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.RuntimeMetaData; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.Vocabulary; +import org.antlr.v4.runtime.VocabularyImpl; + + +@SuppressWarnings({"all", "warnings", "unchecked", "unused", "cast"}) +class SQLServerLexer extends Lexer { + static { + RuntimeMetaData.checkVersion("4.7.2", RuntimeMetaData.VERSION); + } + + protected static final DFA[] _decisionToDFA; + protected static final PredictionContextCache _sharedContextCache = new PredictionContextCache(); + static final int SELECT = 1, INSERT = 2, DELETE = 3, UPDATE = 4, FROM = 5, INTO = 6, EXECUTE = 7, WHERE = 8, + HAVING = 9, GROUP = 10, ORDER = 11, OPTION = 12, BY = 13, VALUES = 14, OUTPUT = 15, OJ = 16, WITH = 17, + AS = 18, DEFAULT = 19, SET = 20, OPENQUERY = 21, OPENJSON = 22, OPENDATASOURCE = 23, OPENROWSET = 24, + OPENXML = 25, TOP = 26, DISCTINCT = 27, PERCENT = 28, TIES = 29, LIKE = 30, IN = 31, IS = 32, NOT = 33, + BETWEEN = 34, AND = 35, SPACE = 36, COMMENT = 37, LINE_COMMENT = 38, DOUBLE_QUOTE = 39, SINGLE_QUOTE = 40, + LOCAL_ID = 41, DECIMAL = 42, ID = 43, STRING = 44, DOUBLE_LITERAL = 45, SQUARE_LITERAL = 46, BINARY = 47, + FLOAT = 48, REAL = 49, EQUAL = 50, GREATER = 51, LESS = 52, GREATER_EQUAL = 53, LESS_EQUAL = 54, + NOT_EQUAL = 55, EXCLAMATION = 56, PLUS_ASSIGN = 57, MINUS_ASSIGN = 58, MULT_ASSIGN = 59, DIV_ASSIGN = 60, + MOD_ASSIGN = 61, AND_ASSIGN = 62, XOR_ASSIGN = 63, OR_ASSIGN = 64, DOUBLE_BAR = 65, DOT = 66, + UNDERLINE = 67, AT = 68, SHARP = 69, DOLLAR = 70, LR_BRACKET = 71, RR_BRACKET = 72, LS_BRACKET = 73, + RS_BRACKET = 74, LC_BRACKET = 75, RC_BRACKET = 76, COMMA = 77, SEMI = 78, COLON = 79, STAR = 80, + DIVIDE = 81, MODULE = 82, PLUS = 83, MINUS = 84, BIT_NOT = 85, BIT_OR = 86, BIT_AND = 87, BIT_XOR = 88, + PARAMETER = 89; + static String[] channelNames = {"DEFAULT_TOKEN_CHANNEL", "HIDDEN"}; + + static String[] modeNames = {"DEFAULT_MODE"}; + + private static String[] makeRuleNames() { + return new String[] {"SELECT", "INSERT", "DELETE", "UPDATE", "FROM", "INTO", "EXECUTE", "WHERE", "HAVING", + "GROUP", "ORDER", "OPTION", "BY", "VALUES", "OUTPUT", "OJ", "WITH", "AS", "DEFAULT", "SET", "OPENQUERY", + "OPENJSON", "OPENDATASOURCE", "OPENROWSET", "OPENXML", "TOP", "DISCTINCT", "PERCENT", "TIES", "LIKE", + "IN", "IS", "NOT", "BETWEEN", "AND", "SPACE", "COMMENT", "LINE_COMMENT", "DOUBLE_QUOTE", "SINGLE_QUOTE", + "LOCAL_ID", "DECIMAL", "ID", "STRING", "DOUBLE_LITERAL", "SQUARE_LITERAL", "BINARY", "FLOAT", "REAL", + "EQUAL", "GREATER", "LESS", "GREATER_EQUAL", "LESS_EQUAL", "NOT_EQUAL", "EXCLAMATION", "PLUS_ASSIGN", + "MINUS_ASSIGN", "MULT_ASSIGN", "DIV_ASSIGN", "MOD_ASSIGN", "AND_ASSIGN", "XOR_ASSIGN", "OR_ASSIGN", + "DOUBLE_BAR", "DOT", "UNDERLINE", "AT", "SHARP", "DOLLAR", "LR_BRACKET", "RR_BRACKET", "LS_BRACKET", + "RS_BRACKET", "LC_BRACKET", "RC_BRACKET", "COMMA", "SEMI", "COLON", "STAR", "DIVIDE", "MODULE", "PLUS", + "MINUS", "BIT_NOT", "BIT_OR", "BIT_AND", "BIT_XOR", "PARAMETER", "DEC_DOT_DEC", "HEX_DIGIT", + "DEC_DIGIT", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", + "S", "T", "U", "V", "W", "X", "Y", "Z", "FullWidthLetter"}; + } + + static final String[] ruleNames = makeRuleNames(); + + private static String[] makeLiteralNames() { + return new String[] {null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, "'\"'", "'''", null, null, null, null, null, null, null, null, + null, "'='", "'>'", "'<'", "'>='", "'<='", "'!='", "'!'", "'+='", "'-='", "'*='", "'/='", "'%='", + "'&='", "'^='", "'|='", "'||'", "'.'", "'_'", "'@'", "'#'", "'$'", "'('", "')'", "'['", "']'", "'{'", + "'}'", "','", "';'", "':'", "'*'", "'/'", "'%'", "'+'", "'-'", "'~'", "'|'", "'&'", "'^'", "'?'"}; + } + + private static final String[] _LITERAL_NAMES = makeLiteralNames(); + + private static String[] makeSymbolicNames() { + return new String[] {null, "SELECT", "INSERT", "DELETE", "UPDATE", "FROM", "INTO", "EXECUTE", "WHERE", "HAVING", + "GROUP", "ORDER", "OPTION", "BY", "VALUES", "OUTPUT", "OJ", "WITH", "AS", "DEFAULT", "SET", "OPENQUERY", + "OPENJSON", "OPENDATASOURCE", "OPENROWSET", "OPENXML", "TOP", "DISCTINCT", "PERCENT", "TIES", "LIKE", + "IN", "IS", "NOT", "BETWEEN", "AND", "SPACE", "COMMENT", "LINE_COMMENT", "DOUBLE_QUOTE", "SINGLE_QUOTE", + "LOCAL_ID", "DECIMAL", "ID", "STRING", "DOUBLE_LITERAL", "SQUARE_LITERAL", "BINARY", "FLOAT", "REAL", + "EQUAL", "GREATER", "LESS", "GREATER_EQUAL", "LESS_EQUAL", "NOT_EQUAL", "EXCLAMATION", "PLUS_ASSIGN", + "MINUS_ASSIGN", "MULT_ASSIGN", "DIV_ASSIGN", "MOD_ASSIGN", "AND_ASSIGN", "XOR_ASSIGN", "OR_ASSIGN", + "DOUBLE_BAR", "DOT", "UNDERLINE", "AT", "SHARP", "DOLLAR", "LR_BRACKET", "RR_BRACKET", "LS_BRACKET", + "RS_BRACKET", "LC_BRACKET", "RC_BRACKET", "COMMA", "SEMI", "COLON", "STAR", "DIVIDE", "MODULE", "PLUS", + "MINUS", "BIT_NOT", "BIT_OR", "BIT_AND", "BIT_XOR", "PARAMETER"}; + } + + private static final String[] _SYMBOLIC_NAMES = makeSymbolicNames(); + static final Vocabulary VOCABULARY = new VocabularyImpl(_LITERAL_NAMES, _SYMBOLIC_NAMES); + + /** + * @deprecated Use {@link #VOCABULARY} instead. + */ + @Deprecated + static final String[] tokenNames; + static { + tokenNames = new String[_SYMBOLIC_NAMES.length]; + for (int i = 0; i < tokenNames.length; i++) { + tokenNames[i] = VOCABULARY.getLiteralName(i); + if (tokenNames[i] == null) { + tokenNames[i] = VOCABULARY.getSymbolicName(i); + } + + if (tokenNames[i] == null) { + tokenNames[i] = ""; + } + } + } + + @Override + @Deprecated + public String[] getTokenNames() { + return tokenNames; + } + + @Override + public Vocabulary getVocabulary() { + return VOCABULARY; + } + + SQLServerLexer(CharStream input) { + super(input); + _interp = new LexerATNSimulator(this, _ATN, _decisionToDFA, _sharedContextCache); + } + + @Override + public String getGrammarFileName() { + return "SQLServerLexer.g4"; + } + + @Override + public String[] getRuleNames() { + return ruleNames; + } + + @Override + public String getSerializedATN() { + return _serializedATN; + } + + @Override + public String[] getChannelNames() { + return channelNames; + } + + @Override + public String[] getModeNames() { + return modeNames; + } + + @Override + public ATN getATN() { + return _ATN; + } + + static final String _serializedATN = "\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2[\u02fa\b\1\4\2\t" + + "\2\4\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7\4\b\t\b\4\t\t\t\4\n\t\n\4\13" + + "\t\13\4\f\t\f\4\r\t\r\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\4\22\t\22" + + "\4\23\t\23\4\24\t\24\4\25\t\25\4\26\t\26\4\27\t\27\4\30\t\30\4\31\t\31" + + "\4\32\t\32\4\33\t\33\4\34\t\34\4\35\t\35\4\36\t\36\4\37\t\37\4 \t \4!" + + "\t!\4\"\t\"\4#\t#\4$\t$\4%\t%\4&\t&\4\'\t\'\4(\t(\4)\t)\4*\t*\4+\t+\4" + + ",\t,\4-\t-\4.\t.\4/\t/\4\60\t\60\4\61\t\61\4\62\t\62\4\63\t\63\4\64\t" + + "\64\4\65\t\65\4\66\t\66\4\67\t\67\48\t8\49\t9\4:\t:\4;\t;\4<\t<\4=\t=" + + "\4>\t>\4?\t?\4@\t@\4A\tA\4B\tB\4C\tC\4D\tD\4E\tE\4F\tF\4G\tG\4H\tH\4I" + + "\tI\4J\tJ\4K\tK\4L\tL\4M\tM\4N\tN\4O\tO\4P\tP\4Q\tQ\4R\tR\4S\tS\4T\tT" + + "\4U\tU\4V\tV\4W\tW\4X\tX\4Y\tY\4Z\tZ\4[\t[\4\\\t\\\4]\t]\4^\t^\4_\t_\4" + + "`\t`\4a\ta\4b\tb\4c\tc\4d\td\4e\te\4f\tf\4g\tg\4h\th\4i\ti\4j\tj\4k\t" + + "k\4l\tl\4m\tm\4n\tn\4o\to\4p\tp\4q\tq\4r\tr\4s\ts\4t\tt\4u\tu\4v\tv\4" + + "w\tw\4x\tx\3\2\3\2\3\2\3\2\3\2\3\2\3\2\3\3\3\3\3\3\3\3\3\3\3\3\3\3\3\4" + + "\3\4\3\4\3\4\3\4\3\4\3\4\3\5\3\5\3\5\3\5\3\5\3\5\3\5\3\6\3\6\3\6\3\6\3" + + "\6\3\7\3\7\3\7\3\7\3\7\3\b\3\b\3\b\3\b\3\b\3\b\3\b\3\b\5\b\u0120\n\b\3" + + "\t\3\t\3\t\3\t\3\t\3\t\3\n\3\n\3\n\3\n\3\n\3\n\3\n\3\13\3\13\3\13\3\13" + + "\3\13\3\13\3\f\3\f\3\f\3\f\3\f\3\f\3\r\3\r\3\r\3\r\3\r\3\r\3\r\3\16\3" + + "\16\3\16\3\17\3\17\3\17\3\17\3\17\3\17\3\17\3\20\3\20\3\20\3\20\3\20\3" + + "\20\3\20\3\21\3\21\3\21\3\22\3\22\3\22\3\22\3\22\3\23\3\23\3\23\3\24\3" + + "\24\3\24\3\24\3\24\3\24\3\24\3\24\3\25\3\25\3\25\3\25\3\26\3\26\3\26\3" + + "\26\3\26\3\26\3\26\3\26\3\26\3\26\3\27\3\27\3\27\3\27\3\27\3\27\3\27\3" + + "\27\3\27\3\30\3\30\3\30\3\30\3\30\3\30\3\30\3\30\3\30\3\30\3\30\3\30\3" + + "\30\3\30\3\30\3\31\3\31\3\31\3\31\3\31\3\31\3\31\3\31\3\31\3\31\3\31\3" + + "\32\3\32\3\32\3\32\3\32\3\32\3\32\3\32\3\33\3\33\3\33\3\33\3\34\3\34\3" + + "\34\3\34\3\34\3\34\3\34\3\34\3\34\3\35\3\35\3\35\3\35\3\35\3\35\3\35\3" + + "\35\3\36\3\36\3\36\3\36\3\36\3\37\3\37\3\37\3\37\3\37\3 \3 \3 \3!\3!\3" + + "!\3\"\3\"\3\"\3\"\3#\3#\3#\3#\3#\3#\3#\3#\3$\3$\3$\3$\3%\6%\u01d5\n%\r" + + "%\16%\u01d6\3%\3%\3&\3&\3&\3&\3&\7&\u01e0\n&\f&\16&\u01e3\13&\3&\3&\3" + + "&\3&\3&\3\'\3\'\3\'\3\'\7\'\u01ee\n\'\f\'\16\'\u01f1\13\'\3\'\3\'\3(\3" + + "(\3)\3)\3*\3*\3*\6*\u01fc\n*\r*\16*\u01fd\3+\6+\u0201\n+\r+\16+\u0202" + + "\3,\3,\5,\u0207\n,\3,\3,\7,\u020b\n,\f,\16,\u020e\13,\3-\5-\u0211\n-\3" + + "-\3-\3-\3-\7-\u0217\n-\f-\16-\u021a\13-\3-\3-\3.\3.\3.\3.\7.\u0222\n." + + "\f.\16.\u0225\13.\3.\3.\3/\3/\3/\3/\7/\u022d\n/\f/\16/\u0230\13/\3/\3" + + "/\3\60\3\60\3\60\7\60\u0237\n\60\f\60\16\60\u023a\13\60\3\61\3\61\3\62" + + "\3\62\5\62\u0240\n\62\3\62\3\62\5\62\u0244\n\62\3\62\6\62\u0247\n\62\r" + + "\62\16\62\u0248\3\63\3\63\3\64\3\64\3\65\3\65\3\66\3\66\3\66\3\67\3\67" + + "\3\67\38\38\38\39\39\3:\3:\3:\3;\3;\3;\3<\3<\3<\3=\3=\3=\3>\3>\3>\3?\3" + + "?\3?\3@\3@\3@\3A\3A\3A\3B\3B\3B\3C\3C\3D\3D\3E\3E\3F\3F\3G\3G\3H\3H\3" + + "I\3I\3J\3J\3K\3K\3L\3L\3M\3M\3N\3N\3O\3O\3P\3P\3Q\3Q\3R\3R\3S\3S\3T\3" + + "T\3U\3U\3V\3V\3W\3W\3X\3X\3Y\3Y\3Z\3Z\3[\6[\u02a8\n[\r[\16[\u02a9\3[\3" + + "[\6[\u02ae\n[\r[\16[\u02af\3[\6[\u02b3\n[\r[\16[\u02b4\3[\3[\3[\3[\6[" + + "\u02bb\n[\r[\16[\u02bc\5[\u02bf\n[\3\\\3\\\3]\3]\3^\3^\3_\3_\3`\3`\3a" + + "\3a\3b\3b\3c\3c\3d\3d\3e\3e\3f\3f\3g\3g\3h\3h\3i\3i\3j\3j\3k\3k\3l\3l" + + "\3m\3m\3n\3n\3o\3o\3p\3p\3q\3q\3r\3r\3s\3s\3t\3t\3u\3u\3v\3v\3w\3w\3x" + + "\3x\3\u01e1\2y\3\3\5\4\7\5\t\6\13\7\r\b\17\t\21\n\23\13\25\f\27\r\31\16" + + "\33\17\35\20\37\21!\22#\23%\24\'\25)\26+\27-\30/\31\61\32\63\33\65\34" + + "\67\359\36;\37= ?!A\"C#E$G%I&K\'M(O)Q*S+U,W-Y.[/]\60_\61a\62c\63e\64g" + + "\65i\66k\67m8o9q:s;u{?}@\177A\u0081B\u0083C\u0085D\u0087E\u0089F" + + "\u008bG\u008dH\u008fI\u0091J\u0093K\u0095L\u0097M\u0099N\u009bO\u009d" + + "P\u009fQ\u00a1R\u00a3S\u00a5T\u00a7U\u00a9V\u00abW\u00adX\u00afY\u00b1" + + "Z\u00b3[\u00b5\2\u00b7\2\u00b9\2\u00bb\2\u00bd\2\u00bf\2\u00c1\2\u00c3" + + "\2\u00c5\2\u00c7\2\u00c9\2\u00cb\2\u00cd\2\u00cf\2\u00d1\2\u00d3\2\u00d5" + + "\2\u00d7\2\u00d9\2\u00db\2\u00dd\2\u00df\2\u00e1\2\u00e3\2\u00e5\2\u00e7" + + "\2\u00e9\2\u00eb\2\u00ed\2\u00ef\2\3\2\'\5\2\13\f\16\17\"\"\4\2\f\f\17" + + "\17\7\2%&\62;B\\aac|\6\2%%C\\aac|\3\2))\3\2$$\3\2__\4\2--//\4\2\62;CH" + + "\3\2\62;\4\2CCcc\4\2DDdd\4\2EEee\4\2FFff\4\2GGgg\4\2HHhh\4\2IIii\4\2J" + + "Jjj\4\2KKkk\4\2LLll\4\2MMmm\4\2NNnn\4\2OOoo\4\2PPpp\4\2QQqq\4\2RRrr\4" + + "\2SSss\4\2TTtt\4\2UUuu\4\2VVvv\4\2WWww\4\2XXxx\4\2YYyy\4\2ZZzz\4\2[[{" + + "{\4\2\\\\||\f\2\u00c2\u00d8\u00da\u00f8\u00fa\u2001\u2c02\u3001\u3042" + + "\u3191\u3302\u3381\u3402\u4001\u4e02\ud801\uf902\ufb01\uff02\ufff2\2\u02f7" + + "\2\3\3\2\2\2\2\5\3\2\2\2\2\7\3\2\2\2\2\t\3\2\2\2\2\13\3\2\2\2\2\r\3\2" + + "\2\2\2\17\3\2\2\2\2\21\3\2\2\2\2\23\3\2\2\2\2\25\3\2\2\2\2\27\3\2\2\2" + + "\2\31\3\2\2\2\2\33\3\2\2\2\2\35\3\2\2\2\2\37\3\2\2\2\2!\3\2\2\2\2#\3\2" + + "\2\2\2%\3\2\2\2\2\'\3\2\2\2\2)\3\2\2\2\2+\3\2\2\2\2-\3\2\2\2\2/\3\2\2" + + "\2\2\61\3\2\2\2\2\63\3\2\2\2\2\65\3\2\2\2\2\67\3\2\2\2\29\3\2\2\2\2;\3" + + "\2\2\2\2=\3\2\2\2\2?\3\2\2\2\2A\3\2\2\2\2C\3\2\2\2\2E\3\2\2\2\2G\3\2\2" + + "\2\2I\3\2\2\2\2K\3\2\2\2\2M\3\2\2\2\2O\3\2\2\2\2Q\3\2\2\2\2S\3\2\2\2\2" + + "U\3\2\2\2\2W\3\2\2\2\2Y\3\2\2\2\2[\3\2\2\2\2]\3\2\2\2\2_\3\2\2\2\2a\3" + + "\2\2\2\2c\3\2\2\2\2e\3\2\2\2\2g\3\2\2\2\2i\3\2\2\2\2k\3\2\2\2\2m\3\2\2" + + "\2\2o\3\2\2\2\2q\3\2\2\2\2s\3\2\2\2\2u\3\2\2\2\2w\3\2\2\2\2y\3\2\2\2\2" + + "{\3\2\2\2\2}\3\2\2\2\2\177\3\2\2\2\2\u0081\3\2\2\2\2\u0083\3\2\2\2\2\u0085" + + "\3\2\2\2\2\u0087\3\2\2\2\2\u0089\3\2\2\2\2\u008b\3\2\2\2\2\u008d\3\2\2" + + "\2\2\u008f\3\2\2\2\2\u0091\3\2\2\2\2\u0093\3\2\2\2\2\u0095\3\2\2\2\2\u0097" + + "\3\2\2\2\2\u0099\3\2\2\2\2\u009b\3\2\2\2\2\u009d\3\2\2\2\2\u009f\3\2\2" + + "\2\2\u00a1\3\2\2\2\2\u00a3\3\2\2\2\2\u00a5\3\2\2\2\2\u00a7\3\2\2\2\2\u00a9" + + "\3\2\2\2\2\u00ab\3\2\2\2\2\u00ad\3\2\2\2\2\u00af\3\2\2\2\2\u00b1\3\2\2" + + "\2\2\u00b3\3\2\2\2\3\u00f1\3\2\2\2\5\u00f8\3\2\2\2\7\u00ff\3\2\2\2\t\u0106" + + "\3\2\2\2\13\u010d\3\2\2\2\r\u0112\3\2\2\2\17\u0117\3\2\2\2\21\u0121\3" + + "\2\2\2\23\u0127\3\2\2\2\25\u012e\3\2\2\2\27\u0134\3\2\2\2\31\u013a\3\2" + + "\2\2\33\u0141\3\2\2\2\35\u0144\3\2\2\2\37\u014b\3\2\2\2!\u0152\3\2\2\2" + + "#\u0155\3\2\2\2%\u015a\3\2\2\2\'\u015d\3\2\2\2)\u0165\3\2\2\2+\u0169\3" + + "\2\2\2-\u0173\3\2\2\2/\u017c\3\2\2\2\61\u018b\3\2\2\2\63\u0196\3\2\2\2" + + "\65\u019e\3\2\2\2\67\u01a2\3\2\2\29\u01ab\3\2\2\2;\u01b3\3\2\2\2=\u01b8" + + "\3\2\2\2?\u01bd\3\2\2\2A\u01c0\3\2\2\2C\u01c3\3\2\2\2E\u01c7\3\2\2\2G" + + "\u01cf\3\2\2\2I\u01d4\3\2\2\2K\u01da\3\2\2\2M\u01e9\3\2\2\2O\u01f4\3\2" + + "\2\2Q\u01f6\3\2\2\2S\u01f8\3\2\2\2U\u0200\3\2\2\2W\u0206\3\2\2\2Y\u0210" + + "\3\2\2\2[\u021d\3\2\2\2]\u0228\3\2\2\2_\u0233\3\2\2\2a\u023b\3\2\2\2c" + + "\u023f\3\2\2\2e\u024a\3\2\2\2g\u024c\3\2\2\2i\u024e\3\2\2\2k\u0250\3\2" + + "\2\2m\u0253\3\2\2\2o\u0256\3\2\2\2q\u0259\3\2\2\2s\u025b\3\2\2\2u\u025e" + + "\3\2\2\2w\u0261\3\2\2\2y\u0264\3\2\2\2{\u0267\3\2\2\2}\u026a\3\2\2\2\177" + + "\u026d\3\2\2\2\u0081\u0270\3\2\2\2\u0083\u0273\3\2\2\2\u0085\u0276\3\2" + + "\2\2\u0087\u0278\3\2\2\2\u0089\u027a\3\2\2\2\u008b\u027c\3\2\2\2\u008d" + + "\u027e\3\2\2\2\u008f\u0280\3\2\2\2\u0091\u0282\3\2\2\2\u0093\u0284\3\2" + + "\2\2\u0095\u0286\3\2\2\2\u0097\u0288\3\2\2\2\u0099\u028a\3\2\2\2\u009b" + + "\u028c\3\2\2\2\u009d\u028e\3\2\2\2\u009f\u0290\3\2\2\2\u00a1\u0292\3\2" + + "\2\2\u00a3\u0294\3\2\2\2\u00a5\u0296\3\2\2\2\u00a7\u0298\3\2\2\2\u00a9" + + "\u029a\3\2\2\2\u00ab\u029c\3\2\2\2\u00ad\u029e\3\2\2\2\u00af\u02a0\3\2" + + "\2\2\u00b1\u02a2\3\2\2\2\u00b3\u02a4\3\2\2\2\u00b5\u02be\3\2\2\2\u00b7" + + "\u02c0\3\2\2\2\u00b9\u02c2\3\2\2\2\u00bb\u02c4\3\2\2\2\u00bd\u02c6\3\2" + + "\2\2\u00bf\u02c8\3\2\2\2\u00c1\u02ca\3\2\2\2\u00c3\u02cc\3\2\2\2\u00c5" + + "\u02ce\3\2\2\2\u00c7\u02d0\3\2\2\2\u00c9\u02d2\3\2\2\2\u00cb\u02d4\3\2" + + "\2\2\u00cd\u02d6\3\2\2\2\u00cf\u02d8\3\2\2\2\u00d1\u02da\3\2\2\2\u00d3" + + "\u02dc\3\2\2\2\u00d5\u02de\3\2\2\2\u00d7\u02e0\3\2\2\2\u00d9\u02e2\3\2" + + "\2\2\u00db\u02e4\3\2\2\2\u00dd\u02e6\3\2\2\2\u00df\u02e8\3\2\2\2\u00e1" + + "\u02ea\3\2\2\2\u00e3\u02ec\3\2\2\2\u00e5\u02ee\3\2\2\2\u00e7\u02f0\3\2" + + "\2\2\u00e9\u02f2\3\2\2\2\u00eb\u02f4\3\2\2\2\u00ed\u02f6\3\2\2\2\u00ef" + + "\u02f8\3\2\2\2\u00f1\u00f2\5\u00dfp\2\u00f2\u00f3\5\u00c3b\2\u00f3\u00f4" + + "\5\u00d1i\2\u00f4\u00f5\5\u00c3b\2\u00f5\u00f6\5\u00bf`\2\u00f6\u00f7" + + "\5\u00e1q\2\u00f7\4\3\2\2\2\u00f8\u00f9\5\u00cbf\2\u00f9\u00fa\5\u00d5" + + "k\2\u00fa\u00fb\5\u00dfp\2\u00fb\u00fc\5\u00c3b\2\u00fc\u00fd\5\u00dd" + + "o\2\u00fd\u00fe\5\u00e1q\2\u00fe\6\3\2\2\2\u00ff\u0100\5\u00c1a\2\u0100" + + "\u0101\5\u00c3b\2\u0101\u0102\5\u00d1i\2\u0102\u0103\5\u00c3b\2\u0103" + + "\u0104\5\u00e1q\2\u0104\u0105\5\u00c3b\2\u0105\b\3\2\2\2\u0106\u0107\5" + + "\u00e3r\2\u0107\u0108\5\u00d9m\2\u0108\u0109\5\u00c1a\2\u0109\u010a\5" + + "\u00bb^\2\u010a\u010b\5\u00e1q\2\u010b\u010c\5\u00c3b\2\u010c\n\3\2\2" + + "\2\u010d\u010e\5\u00c5c\2\u010e\u010f\5\u00ddo\2\u010f\u0110\5\u00d7l" + + "\2\u0110\u0111\5\u00d3j\2\u0111\f\3\2\2\2\u0112\u0113\5\u00cbf\2\u0113" + + "\u0114\5\u00d5k\2\u0114\u0115\5\u00e1q\2\u0115\u0116\5\u00d7l\2\u0116" + + "\16\3\2\2\2\u0117\u0118\5\u00c3b\2\u0118\u0119\5\u00e9u\2\u0119\u011a" + + "\5\u00c3b\2\u011a\u011f\5\u00bf`\2\u011b\u011c\5\u00e3r\2\u011c\u011d" + + "\5\u00e1q\2\u011d\u011e\5\u00c3b\2\u011e\u0120\3\2\2\2\u011f\u011b\3\2" + + "\2\2\u011f\u0120\3\2\2\2\u0120\20\3\2\2\2\u0121\u0122\5\u00e7t\2\u0122" + + "\u0123\5\u00c9e\2\u0123\u0124\5\u00c3b\2\u0124\u0125\5\u00ddo\2\u0125" + + "\u0126\5\u00c3b\2\u0126\22\3\2\2\2\u0127\u0128\5\u00c9e\2\u0128\u0129" + + "\5\u00bb^\2\u0129\u012a\5\u00e5s\2\u012a\u012b\5\u00cbf\2\u012b\u012c" + + "\5\u00d5k\2\u012c\u012d\5\u00c7d\2\u012d\24\3\2\2\2\u012e\u012f\5\u00c7" + + "d\2\u012f\u0130\5\u00ddo\2\u0130\u0131\5\u00d7l\2\u0131\u0132\5\u00e3" + + "r\2\u0132\u0133\5\u00d9m\2\u0133\26\3\2\2\2\u0134\u0135\5\u00d7l\2\u0135" + + "\u0136\5\u00ddo\2\u0136\u0137\5\u00c1a\2\u0137\u0138\5\u00c3b\2\u0138" + + "\u0139\5\u00ddo\2\u0139\30\3\2\2\2\u013a\u013b\5\u00d7l\2\u013b\u013c" + + "\5\u00d9m\2\u013c\u013d\5\u00e1q\2\u013d\u013e\5\u00cbf\2\u013e\u013f" + + "\5\u00d7l\2\u013f\u0140\5\u00d5k\2\u0140\32\3\2\2\2\u0141\u0142\5\u00bd" + + "_\2\u0142\u0143\5\u00ebv\2\u0143\34\3\2\2\2\u0144\u0145\5\u00e5s\2\u0145" + + "\u0146\5\u00bb^\2\u0146\u0147\5\u00d1i\2\u0147\u0148\5\u00e3r\2\u0148" + + "\u0149\5\u00c3b\2\u0149\u014a\5\u00dfp\2\u014a\36\3\2\2\2\u014b\u014c" + + "\5\u00d7l\2\u014c\u014d\5\u00e3r\2\u014d\u014e\5\u00e1q\2\u014e\u014f" + + "\5\u00d9m\2\u014f\u0150\5\u00e3r\2\u0150\u0151\5\u00e1q\2\u0151 \3\2\2" + + "\2\u0152\u0153\5\u00d7l\2\u0153\u0154\5\u00cdg\2\u0154\"\3\2\2\2\u0155" + + "\u0156\5\u00e7t\2\u0156\u0157\5\u00cbf\2\u0157\u0158\5\u00e1q\2\u0158" + + "\u0159\5\u00c9e\2\u0159$\3\2\2\2\u015a\u015b\5\u00bb^\2\u015b\u015c\5" + + "\u00dfp\2\u015c&\3\2\2\2\u015d\u015e\5\u00c1a\2\u015e\u015f\5\u00c3b\2" + + "\u015f\u0160\5\u00c5c\2\u0160\u0161\5\u00bb^\2\u0161\u0162\5\u00e3r\2" + + "\u0162\u0163\5\u00d1i\2\u0163\u0164\5\u00e1q\2\u0164(\3\2\2\2\u0165\u0166" + + "\5\u00dfp\2\u0166\u0167\5\u00c3b\2\u0167\u0168\5\u00e1q\2\u0168*\3\2\2" + + "\2\u0169\u016a\5\u00d7l\2\u016a\u016b\5\u00d9m\2\u016b\u016c\5\u00c3b" + + "\2\u016c\u016d\5\u00d5k\2\u016d\u016e\5\u00dbn\2\u016e\u016f\5\u00e3r" + + "\2\u016f\u0170\5\u00c3b\2\u0170\u0171\5\u00ddo\2\u0171\u0172\5\u00ebv" + + "\2\u0172,\3\2\2\2\u0173\u0174\5\u00d7l\2\u0174\u0175\5\u00d9m\2\u0175" + + "\u0176\5\u00c3b\2\u0176\u0177\5\u00d5k\2\u0177\u0178\5\u00cdg\2\u0178" + + "\u0179\5\u00dfp\2\u0179\u017a\5\u00d7l\2\u017a\u017b\5\u00d5k\2\u017b" + + ".\3\2\2\2\u017c\u017d\5\u00d7l\2\u017d\u017e\5\u00d9m\2\u017e\u017f\5" + + "\u00c3b\2\u017f\u0180\5\u00d5k\2\u0180\u0181\5\u00c1a\2\u0181\u0182\5" + + "\u00bb^\2\u0182\u0183\5\u00e1q\2\u0183\u0184\5\u00bb^\2\u0184\u0185\5" + + "\u00dfp\2\u0185\u0186\5\u00d7l\2\u0186\u0187\5\u00e3r\2\u0187\u0188\5" + + "\u00ddo\2\u0188\u0189\5\u00bf`\2\u0189\u018a\5\u00c3b\2\u018a\60\3\2\2" + + "\2\u018b\u018c\5\u00d7l\2\u018c\u018d\5\u00d9m\2\u018d\u018e\5\u00c3b" + + "\2\u018e\u018f\5\u00d5k\2\u018f\u0190\5\u00ddo\2\u0190\u0191\5\u00d7l" + + "\2\u0191\u0192\5\u00e7t\2\u0192\u0193\5\u00dfp\2\u0193\u0194\5\u00c3b" + + "\2\u0194\u0195\5\u00e1q\2\u0195\62\3\2\2\2\u0196\u0197\5\u00d7l\2\u0197" + + "\u0198\5\u00d9m\2\u0198\u0199\5\u00c3b\2\u0199\u019a\5\u00d5k\2\u019a" + + "\u019b\5\u00e9u\2\u019b\u019c\5\u00d3j\2\u019c\u019d\5\u00d1i\2\u019d" + + "\64\3\2\2\2\u019e\u019f\5\u00e1q\2\u019f\u01a0\5\u00d7l\2\u01a0\u01a1" + + "\5\u00d9m\2\u01a1\66\3\2\2\2\u01a2\u01a3\5\u00c1a\2\u01a3\u01a4\5\u00cb" + + "f\2\u01a4\u01a5\5\u00dfp\2\u01a5\u01a6\5\u00e1q\2\u01a6\u01a7\5\u00cb" + + "f\2\u01a7\u01a8\5\u00d5k\2\u01a8\u01a9\5\u00bf`\2\u01a9\u01aa\5\u00e1" + + "q\2\u01aa8\3\2\2\2\u01ab\u01ac\5\u00d9m\2\u01ac\u01ad\5\u00c3b\2\u01ad" + + "\u01ae\5\u00ddo\2\u01ae\u01af\5\u00bf`\2\u01af\u01b0\5\u00c3b\2\u01b0" + + "\u01b1\5\u00d5k\2\u01b1\u01b2\5\u00e1q\2\u01b2:\3\2\2\2\u01b3\u01b4\5" + + "\u00e1q\2\u01b4\u01b5\5\u00cbf\2\u01b5\u01b6\5\u00c3b\2\u01b6\u01b7\5" + + "\u00dfp\2\u01b7<\3\2\2\2\u01b8\u01b9\5\u00d1i\2\u01b9\u01ba\5\u00cbf\2" + + "\u01ba\u01bb\5\u00cfh\2\u01bb\u01bc\5\u00c3b\2\u01bc>\3\2\2\2\u01bd\u01be" + + "\5\u00cbf\2\u01be\u01bf\5\u00d5k\2\u01bf@\3\2\2\2\u01c0\u01c1\5\u00cb" + + "f\2\u01c1\u01c2\5\u00dfp\2\u01c2B\3\2\2\2\u01c3\u01c4\5\u00d5k\2\u01c4" + + "\u01c5\5\u00d7l\2\u01c5\u01c6\5\u00e1q\2\u01c6D\3\2\2\2\u01c7\u01c8\5" + + "\u00bd_\2\u01c8\u01c9\5\u00c3b\2\u01c9\u01ca\5\u00e1q\2\u01ca\u01cb\5" + + "\u00e7t\2\u01cb\u01cc\5\u00c3b\2\u01cc\u01cd\5\u00c3b\2\u01cd\u01ce\5" + + "\u00d5k\2\u01ceF\3\2\2\2\u01cf\u01d0\5\u00bb^\2\u01d0\u01d1\5\u00d5k\2" + + "\u01d1\u01d2\5\u00c1a\2\u01d2H\3\2\2\2\u01d3\u01d5\t\2\2\2\u01d4\u01d3" + + "\3\2\2\2\u01d5\u01d6\3\2\2\2\u01d6\u01d4\3\2\2\2\u01d6\u01d7\3\2\2\2\u01d7" + + "\u01d8\3\2\2\2\u01d8\u01d9\b%\2\2\u01d9J\3\2\2\2\u01da\u01db\7\61\2\2" + + "\u01db\u01dc\7,\2\2\u01dc\u01e1\3\2\2\2\u01dd\u01e0\5K&\2\u01de\u01e0" + + "\13\2\2\2\u01df\u01dd\3\2\2\2\u01df\u01de\3\2\2\2\u01e0\u01e3\3\2\2\2" + + "\u01e1\u01e2\3\2\2\2\u01e1\u01df\3\2\2\2\u01e2\u01e4\3\2\2\2\u01e3\u01e1" + + "\3\2\2\2\u01e4\u01e5\7,\2\2\u01e5\u01e6\7\61\2\2\u01e6\u01e7\3\2\2\2\u01e7" + + "\u01e8\b&\2\2\u01e8L\3\2\2\2\u01e9\u01ea\7/\2\2\u01ea\u01eb\7/\2\2\u01eb" + + "\u01ef\3\2\2\2\u01ec\u01ee\n\3\2\2\u01ed\u01ec\3\2\2\2\u01ee\u01f1\3\2" + + "\2\2\u01ef\u01ed\3\2\2\2\u01ef\u01f0\3\2\2\2\u01f0\u01f2\3\2\2\2\u01f1" + + "\u01ef\3\2\2\2\u01f2\u01f3\b\'\2\2\u01f3N\3\2\2\2\u01f4\u01f5\7$\2\2\u01f5" + + "P\3\2\2\2\u01f6\u01f7\7)\2\2\u01f7R\3\2\2\2\u01f8\u01fb\7B\2\2\u01f9\u01fc" + + "\t\4\2\2\u01fa\u01fc\5\u00efx\2\u01fb\u01f9\3\2\2\2\u01fb\u01fa\3\2\2" + + "\2\u01fc\u01fd\3\2\2\2\u01fd\u01fb\3\2\2\2\u01fd\u01fe\3\2\2\2\u01feT" + + "\3\2\2\2\u01ff\u0201\5\u00b9]\2\u0200\u01ff\3\2\2\2\u0201\u0202\3\2\2" + + "\2\u0202\u0200\3\2\2\2\u0202\u0203\3\2\2\2\u0203V\3\2\2\2\u0204\u0207" + + "\t\5\2\2\u0205\u0207\5\u00efx\2\u0206\u0204\3\2\2\2\u0206\u0205\3\2\2" + + "\2\u0207\u020c\3\2\2\2\u0208\u020b\t\4\2\2\u0209\u020b\5\u00efx\2\u020a" + + "\u0208\3\2\2\2\u020a\u0209\3\2\2\2\u020b\u020e\3\2\2\2\u020c\u020a\3\2" + + "\2\2\u020c\u020d\3\2\2\2\u020dX\3\2\2\2\u020e\u020c\3\2\2\2\u020f\u0211" + + "\7P\2\2\u0210\u020f\3\2\2\2\u0210\u0211\3\2\2\2\u0211\u0212\3\2\2\2\u0212" + + "\u0218\7)\2\2\u0213\u0217\n\6\2\2\u0214\u0215\7)\2\2\u0215\u0217\7)\2" + + "\2\u0216\u0213\3\2\2\2\u0216\u0214\3\2\2\2\u0217\u021a\3\2\2\2\u0218\u0216" + + "\3\2\2\2\u0218\u0219\3\2\2\2\u0219\u021b\3\2\2\2\u021a\u0218\3\2\2\2\u021b" + + "\u021c\7)\2\2\u021cZ\3\2\2\2\u021d\u0223\7$\2\2\u021e\u0222\n\7\2\2\u021f" + + "\u0220\7$\2\2\u0220\u0222\7$\2\2\u0221\u021e\3\2\2\2\u0221\u021f\3\2\2" + + "\2\u0222\u0225\3\2\2\2\u0223\u0221\3\2\2\2\u0223\u0224\3\2\2\2\u0224\u0226" + + "\3\2\2\2\u0225\u0223\3\2\2\2\u0226\u0227\7$\2\2\u0227\\\3\2\2\2\u0228" + + "\u022e\7]\2\2\u0229\u022d\n\b\2\2\u022a\u022b\7_\2\2\u022b\u022d\7_\2" + + "\2\u022c\u0229\3\2\2\2\u022c\u022a\3\2\2\2\u022d\u0230\3\2\2\2\u022e\u022c" + + "\3\2\2\2\u022e\u022f\3\2\2\2\u022f\u0231\3\2\2\2\u0230\u022e\3\2\2\2\u0231" + + "\u0232\7_\2\2\u0232^\3\2\2\2\u0233\u0234\7\62\2\2\u0234\u0238\7Z\2\2\u0235" + + "\u0237\5\u00b7\\\2\u0236\u0235\3\2\2\2\u0237\u023a\3\2\2\2\u0238\u0236" + + "\3\2\2\2\u0238\u0239\3\2\2\2\u0239`\3\2\2\2\u023a\u0238\3\2\2\2\u023b" + + "\u023c\5\u00b5[\2\u023cb\3\2\2\2\u023d\u0240\5U+\2\u023e\u0240\5\u00b5" + + "[\2\u023f\u023d\3\2\2\2\u023f\u023e\3\2\2\2\u0240\u0241\3\2\2\2\u0241" + + "\u0243\7G\2\2\u0242\u0244\t\t\2\2\u0243\u0242\3\2\2\2\u0243\u0244\3\2" + + "\2\2\u0244\u0246\3\2\2\2\u0245\u0247\5\u00b9]\2\u0246\u0245\3\2\2\2\u0247" + + "\u0248\3\2\2\2\u0248\u0246\3\2\2\2\u0248\u0249\3\2\2\2\u0249d\3\2\2\2" + + "\u024a\u024b\7?\2\2\u024bf\3\2\2\2\u024c\u024d\7@\2\2\u024dh\3\2\2\2\u024e" + + "\u024f\7>\2\2\u024fj\3\2\2\2\u0250\u0251\7@\2\2\u0251\u0252\7?\2\2\u0252" + + "l\3\2\2\2\u0253\u0254\7>\2\2\u0254\u0255\7?\2\2\u0255n\3\2\2\2\u0256\u0257" + + "\7#\2\2\u0257\u0258\7?\2\2\u0258p\3\2\2\2\u0259\u025a\7#\2\2\u025ar\3" + + "\2\2\2\u025b\u025c\7-\2\2\u025c\u025d\7?\2\2\u025dt\3\2\2\2\u025e\u025f" + + "\7/\2\2\u025f\u0260\7?\2\2\u0260v\3\2\2\2\u0261\u0262\7,\2\2\u0262\u0263" + + "\7?\2\2\u0263x\3\2\2\2\u0264\u0265\7\61\2\2\u0265\u0266\7?\2\2\u0266z" + + "\3\2\2\2\u0267\u0268\7\'\2\2\u0268\u0269\7?\2\2\u0269|\3\2\2\2\u026a\u026b" + + "\7(\2\2\u026b\u026c\7?\2\2\u026c~\3\2\2\2\u026d\u026e\7`\2\2\u026e\u026f" + + "\7?\2\2\u026f\u0080\3\2\2\2\u0270\u0271\7~\2\2\u0271\u0272\7?\2\2\u0272" + + "\u0082\3\2\2\2\u0273\u0274\7~\2\2\u0274\u0275\7~\2\2\u0275\u0084\3\2\2" + + "\2\u0276\u0277\7\60\2\2\u0277\u0086\3\2\2\2\u0278\u0279\7a\2\2\u0279\u0088" + + "\3\2\2\2\u027a\u027b\7B\2\2\u027b\u008a\3\2\2\2\u027c\u027d\7%\2\2\u027d" + + "\u008c\3\2\2\2\u027e\u027f\7&\2\2\u027f\u008e\3\2\2\2\u0280\u0281\7*\2" + + "\2\u0281\u0090\3\2\2\2\u0282\u0283\7+\2\2\u0283\u0092\3\2\2\2\u0284\u0285" + + "\7]\2\2\u0285\u0094\3\2\2\2\u0286\u0287\7_\2\2\u0287\u0096\3\2\2\2\u0288" + + "\u0289\7}\2\2\u0289\u0098\3\2\2\2\u028a\u028b\7\177\2\2\u028b\u009a\3" + + "\2\2\2\u028c\u028d\7.\2\2\u028d\u009c\3\2\2\2\u028e\u028f\7=\2\2\u028f" + + "\u009e\3\2\2\2\u0290\u0291\7<\2\2\u0291\u00a0\3\2\2\2\u0292\u0293\7,\2" + + "\2\u0293\u00a2\3\2\2\2\u0294\u0295\7\61\2\2\u0295\u00a4\3\2\2\2\u0296" + + "\u0297\7\'\2\2\u0297\u00a6\3\2\2\2\u0298\u0299\7-\2\2\u0299\u00a8\3\2" + + "\2\2\u029a\u029b\7/\2\2\u029b\u00aa\3\2\2\2\u029c\u029d\7\u0080\2\2\u029d" + + "\u00ac\3\2\2\2\u029e\u029f\7~\2\2\u029f\u00ae\3\2\2\2\u02a0\u02a1\7(\2" + + "\2\u02a1\u00b0\3\2\2\2\u02a2\u02a3\7`\2\2\u02a3\u00b2\3\2\2\2\u02a4\u02a5" + + "\7A\2\2\u02a5\u00b4\3\2\2\2\u02a6\u02a8\5\u00b9]\2\u02a7\u02a6\3\2\2\2" + + "\u02a8\u02a9\3\2\2\2\u02a9\u02a7\3\2\2\2\u02a9\u02aa\3\2\2\2\u02aa\u02ab" + + "\3\2\2\2\u02ab\u02ad\7\60\2\2\u02ac\u02ae\5\u00b9]\2\u02ad\u02ac\3\2\2" + + "\2\u02ae\u02af\3\2\2\2\u02af\u02ad\3\2\2\2\u02af\u02b0\3\2\2\2\u02b0\u02bf" + + "\3\2\2\2\u02b1\u02b3\5\u00b9]\2\u02b2\u02b1\3\2\2\2\u02b3\u02b4\3\2\2" + + "\2\u02b4\u02b2\3\2\2\2\u02b4\u02b5\3\2\2\2\u02b5\u02b6\3\2\2\2\u02b6\u02b7" + + "\7\60\2\2\u02b7\u02bf\3\2\2\2\u02b8\u02ba\7\60\2\2\u02b9\u02bb\5\u00b9" + + "]\2\u02ba\u02b9\3\2\2\2\u02bb\u02bc\3\2\2\2\u02bc\u02ba\3\2\2\2\u02bc" + + "\u02bd\3\2\2\2\u02bd\u02bf\3\2\2\2\u02be\u02a7\3\2\2\2\u02be\u02b2\3\2" + + "\2\2\u02be\u02b8\3\2\2\2\u02bf\u00b6\3\2\2\2\u02c0\u02c1\t\n\2\2\u02c1" + + "\u00b8\3\2\2\2\u02c2\u02c3\t\13\2\2\u02c3\u00ba\3\2\2\2\u02c4\u02c5\t" + + "\f\2\2\u02c5\u00bc\3\2\2\2\u02c6\u02c7\t\r\2\2\u02c7\u00be\3\2\2\2\u02c8" + + "\u02c9\t\16\2\2\u02c9\u00c0\3\2\2\2\u02ca\u02cb\t\17\2\2\u02cb\u00c2\3" + + "\2\2\2\u02cc\u02cd\t\20\2\2\u02cd\u00c4\3\2\2\2\u02ce\u02cf\t\21\2\2\u02cf" + + "\u00c6\3\2\2\2\u02d0\u02d1\t\22\2\2\u02d1\u00c8\3\2\2\2\u02d2\u02d3\t" + + "\23\2\2\u02d3\u00ca\3\2\2\2\u02d4\u02d5\t\24\2\2\u02d5\u00cc\3\2\2\2\u02d6" + + "\u02d7\t\25\2\2\u02d7\u00ce\3\2\2\2\u02d8\u02d9\t\26\2\2\u02d9\u00d0\3" + + "\2\2\2\u02da\u02db\t\27\2\2\u02db\u00d2\3\2\2\2\u02dc\u02dd\t\30\2\2\u02dd" + + "\u00d4\3\2\2\2\u02de\u02df\t\31\2\2\u02df\u00d6\3\2\2\2\u02e0\u02e1\t" + + "\32\2\2\u02e1\u00d8\3\2\2\2\u02e2\u02e3\t\33\2\2\u02e3\u00da\3\2\2\2\u02e4" + + "\u02e5\t\34\2\2\u02e5\u00dc\3\2\2\2\u02e6\u02e7\t\35\2\2\u02e7\u00de\3" + + "\2\2\2\u02e8\u02e9\t\36\2\2\u02e9\u00e0\3\2\2\2\u02ea\u02eb\t\37\2\2\u02eb" + + "\u00e2\3\2\2\2\u02ec\u02ed\t \2\2\u02ed\u00e4\3\2\2\2\u02ee\u02ef\t!\2" + + "\2\u02ef\u00e6\3\2\2\2\u02f0\u02f1\t\"\2\2\u02f1\u00e8\3\2\2\2\u02f2\u02f3" + + "\t#\2\2\u02f3\u00ea\3\2\2\2\u02f4\u02f5\t$\2\2\u02f5\u00ec\3\2\2\2\u02f6" + + "\u02f7\t%\2\2\u02f7\u00ee\3\2\2\2\u02f8\u02f9\t&\2\2\u02f9\u00f0\3\2\2" + + "\2\36\2\u011f\u01d6\u01df\u01e1\u01ef\u01fb\u01fd\u0202\u0206\u020a\u020c" + + "\u0210\u0216\u0218\u0221\u0223\u022c\u022e\u0238\u023f\u0243\u0248\u02a9" + + "\u02af\u02b4\u02bc\u02be\3\b\2\2"; + static final ATN _ATN = new ATNDeserializer().deserialize(_serializedATN.toCharArray()); + static { + _decisionToDFA = new DFA[_ATN.getNumberOfDecisions()]; + for (int i = 0; i < _ATN.getNumberOfDecisions(); i++) { + _decisionToDFA[i] = new DFA(_ATN.getDecisionState(i), i); + } + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java index 1efbd8572..87c91c1ce 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java @@ -14,7 +14,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -49,9 +48,6 @@ public final class SQLServerParameterMetaData implements ParameterMetaData { final private String traceID = " SQLServerParameterMetaData:" + nextInstanceID(); boolean isTVP = false; - private String stringToParse = null; - private int indexToBeginParse = -1; - // Returns unique id for each instance. private static int nextInstanceID() { return baseID.incrementAndGet(); @@ -67,126 +63,6 @@ final public String toString() { return traceID; } - /** - * Parses the columns in a column set. - * - * @param columnSet - * the list of columns - * @param columnStartToken - * the token that prfixes the column set - * @throws SQLServerException - */ - private String parseColumns(String columnSet, String columnStartToken) throws SQLServerException { - StringTokenizer st = new StringTokenizer(columnSet, " =?<>!\r\n\t\f", true); - final int START = 0; - final int PARAMNAME = 1; - final int PARAMVALUE = 2; - - int nState = 0; - String sLastField = null; - StringBuilder sb = new StringBuilder(); - - int sTokenIndex = 0; - while (st.hasMoreTokens()) { - - String sToken = st.nextToken(); - sTokenIndex = sTokenIndex + sToken.length(); - - if (sToken.equalsIgnoreCase(columnStartToken)) { - nState = PARAMNAME; - continue; - } - if (nState == START) - continue; - if ((sToken.charAt(0) == '=') || sToken.equalsIgnoreCase("is") || (sToken.charAt(0) == '<') - || (sToken.charAt(0) == '>') || sToken.equalsIgnoreCase("like") || sToken.equalsIgnoreCase("not") - || sToken.equalsIgnoreCase("in") || (sToken.charAt(0) == '!')) { - nState = PARAMVALUE; - continue; - } - if (sToken.charAt(0) == '?' && sLastField != null) { - if (sb.length() != 0) { - sb.append(", "); - } - sb.append(sLastField); - nState = PARAMNAME; - sLastField = null; - continue; - } - if (nState == PARAMNAME) { - // space get the next token. - if (sToken.equals(" ")) - continue; - String paramN = escapeParse(st, sToken); - if (paramN.length() > 0) { - sLastField = paramN; - } - } - } - - indexToBeginParse = sTokenIndex; - return sb.toString(); - } - - /** - * Parses the column set in an insert syntax. - * - * @param sql - * the sql syntax - * @param columnMarker - * the token that denotes the start of the column set - * @throws SQLServerException - */ - private String parseInsertColumns(String sql, String columnMarker) throws SQLServerException { - StringTokenizer st = new StringTokenizer(sql, " (),", true); - int nState = 0; - String sLastField = null; - StringBuilder sb = new StringBuilder(); - - int sTokenIndex = 0; - while (st.hasMoreTokens()) { - String sToken = st.nextToken(); - sTokenIndex = sTokenIndex + sToken.length(); - - if (sToken.equalsIgnoreCase(columnMarker)) { - nState = 1; - continue; - } - if (nState == 0) - continue; - if (sToken.charAt(0) == '=') { - nState = 2; - continue; - } - if ((sToken.charAt(0) == ',' || sToken.charAt(0) == ')' || sToken.charAt(0) == ' ') && sLastField != null) { - if (sb.length() != 0) - sb.append(", "); - sb.append(sLastField); - nState = 1; - sLastField = null; - } - if (sToken.charAt(0) == ')') { - nState = 0; - break; - } - if (nState == 1) { - if (sToken.trim().length() > 0) { - if (sToken.charAt(0) != ',') { - sLastField = escapeParse(st, sToken); - - // in case the parameter has braces in its name, e.g. [c2_nvarchar(max)], the original sToken - // variable just - // contains [c2_nvarchar, sLastField actually has the whole name [c2_nvarchar(max)] - sTokenIndex = sTokenIndex + (sLastField.length() - sToken.length()); - } - } - } - } - - indexToBeginParse = sTokenIndex; - return sb.toString(); - } - /* Used for prepared statement meta data */ class QueryMeta { String parameterClassName = null; @@ -304,24 +180,41 @@ private void parseQueryMeta(ResultSet rsQueryMeta) throws SQLServerException { } } - private void parseQueryMetaFor2008(ResultSet rsQueryMeta) throws SQLServerException { - ResultSetMetaData md; + private void parseFMTQueryMeta(ResultSetMetaData md, SQLServerFMTQuery f) throws SQLServerException { try { - if (null != rsQueryMeta) { - md = rsQueryMeta.getMetaData(); - - for (int i = 1; i <= md.getColumnCount(); i++) { - QueryMeta qm = new QueryMeta(); - - qm.parameterClassName = md.getColumnClassName(i); - qm.parameterType = md.getColumnType(i); - qm.parameterTypeName = md.getColumnTypeName(i); - qm.precision = md.getPrecision(i); - qm.scale = md.getScale(i); - qm.isNullable = md.isNullable(i); - qm.isSigned = md.isSigned(i); - - queryMetaMap.put(i, qm); + // Gets the list of parsed column names/targets + List columns = f.getColumns(); + // Gets VALUES(?,?,?...) list. The internal list corresponds to the parameters in the bracket after VALUES. + List> params = f.getValuesList(); + int valueListOffset = 0; + int mdIndex = 1; + int mapIndex = 1; + for (int i = 0; i < columns.size(); i++) { + /** + * For INSERT table VALUES(?,?,?...) scenarios where the column names are not specifically defined after + * the table name, the parser adds a '*' followed by '?'s equal to the number of parameters in the + * values bracket. The '*' will retrieve all values from the table and we'll use the '?'s to match their + * position here + */ + if (columns.get(i).equals("*")) { + for (int j = 0; j < params.get(valueListOffset).size(); j++) { + if (params.get(valueListOffset).get(j).equals("?")) { + if (!md.isAutoIncrement(mdIndex + j)) { + QueryMeta qm = getQueryMetaFromResultSetMetaData(md, mdIndex + j); + queryMetaMap.put(mapIndex++, qm); + i++; + } + } + } + mdIndex += params.get(valueListOffset).size(); + valueListOffset++; + } else { + /* + * If this is not a INSERT table VALUES(...) situation, just add the entry. + */ + QueryMeta qm = getQueryMetaFromResultSetMetaData(md, mdIndex); + queryMetaMap.put(mapIndex++, qm); + mdIndex++; } } } catch (SQLException e) { @@ -329,184 +222,16 @@ private void parseQueryMetaFor2008(ResultSet rsQueryMeta) throws SQLServerExcept } } - /** - * Parses escaped strings properly e.g.[Table Name, ] using tokenizer. - * - * @param st - * string tokenizer - * @param firstToken - * @throws SQLServerException - * @returns the full token - */ - private String escapeParse(StringTokenizer st, String firstToken) throws SQLServerException { - - if (null == firstToken) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NullValue")); - Object[] msgArgs1 = {"firstToken"}; - throw new SQLServerException(form.format(msgArgs1), null); - } - - // skip spaces - while ((0 == firstToken.trim().length()) && st.hasMoreTokens()) { - firstToken = st.nextToken(); - } - - String fullName = firstToken; - if (firstToken.charAt(0) == '[' && firstToken.charAt(firstToken.length() - 1) != ']') { - while (st.hasMoreTokens()) { - firstToken = st.nextToken(); - fullName = fullName.concat(firstToken); - if (firstToken.charAt(firstToken.length() - 1) == ']') { - break; - } - - } - } - fullName = fullName.trim(); - return fullName; - } - - private class MetaInfo { - String table; - String fields; - - MetaInfo(String table, String fields) { - this.table = table; - this.fields = fields; - } - } - - /** - * Parses a SQL syntax. - * - * @param sql - * String - * @param sTableMarker - * the location of the table in the syntax - * @throws SQLServerException - */ - private MetaInfo parseStatement(String sql, String sTableMarker) throws SQLServerException { - StringTokenizer st = new StringTokenizer(sql, " ,\r\n\t\f(", true); - - /* Find the table */ - - String metaTable = null; - String metaFields = ""; - while (st.hasMoreTokens()) { - String sToken = st.nextToken().trim(); - - if (sToken.contains("*/")) { - sToken = removeCommentsInTheBeginning(sToken, 0, 0, "/*", "*/"); - } - - if (sToken.equalsIgnoreCase(sTableMarker)) { - if (st.hasMoreTokens()) { - metaTable = escapeParse(st, st.nextToken()); - break; - } - } - } - - if (null != metaTable) { - if (sTableMarker.equalsIgnoreCase("UPDATE")) { - metaFields = parseColumns(sql, "SET"); // Get the set fields - stringToParse = ""; - } else if (sTableMarker.equalsIgnoreCase("INTO")) { // insert - metaFields = parseInsertColumns(sql, "("); // Get the value fields - stringToParse = sql.substring(indexToBeginParse); // the index of ')' - - // skip VALUES() clause - if (stringToParse.trim().toLowerCase().startsWith("values")) { - parseInsertColumns(stringToParse, "("); - stringToParse = stringToParse.substring(indexToBeginParse); // the index of ')' - } - } else { - metaFields = parseColumns(sql, "WHERE"); // Get the where fields - stringToParse = ""; - } - - return new MetaInfo(metaTable, metaFields); - } - - return null; - } - - /** - * Parses a SQL syntax. - * - * @param sql - * the syntax - * @throws SQLServerException - */ - private MetaInfo parseStatement(String sql) throws SQLServerException { - StringTokenizer st = new StringTokenizer(sql, " \r\n\t\f"); - if (st.hasMoreTokens()) { - String sToken = st.nextToken().trim(); - - // filter out multiple line comments in the beginning of the query - if (sToken.contains("/*")) { - String sqlWithoutCommentsInBeginning = removeCommentsInTheBeginning(sql, 0, 0, "/*", "*/"); - return parseStatement(sqlWithoutCommentsInBeginning); - } - - // filter out single line comments in the beginning of the query - if (sToken.contains("--")) { - String sqlWithoutCommentsInBeginning = removeCommentsInTheBeginning(sql, 0, 0, "--", "\n"); - return parseStatement(sqlWithoutCommentsInBeginning); - } - - if (sToken.equalsIgnoreCase("INSERT")) - return parseStatement(sql, "INTO"); // INTO marks the table name - - if (sToken.equalsIgnoreCase("UPDATE")) - return parseStatement(sql, "UPDATE"); - - if (sToken.equalsIgnoreCase("SELECT")) - return parseStatement(sql, "FROM"); - - if (sToken.equalsIgnoreCase("DELETE")) - return parseStatement(sql, "FROM"); - } - - return null; - } - - private String removeCommentsInTheBeginning(String sql, int startCommentMarkCount, int endCommentMarkCount, - String startMark, String endMark) { - int startCommentMarkIndex = sql.indexOf(startMark); - int endCommentMarkIndex = sql.indexOf(endMark); - - if (-1 == startCommentMarkIndex) { - startCommentMarkIndex = Integer.MAX_VALUE; - } - if (-1 == endCommentMarkIndex) { - endCommentMarkIndex = Integer.MAX_VALUE; - } - - // Base case. startCommentMarkCount is guaranteed to be bigger than 0 because the method is called when /* - // occurs - if (startCommentMarkCount == endCommentMarkCount) { - if (startCommentMarkCount != 0 && endCommentMarkCount != 0) { - return sql; - } - } - - // filter out first start comment mark - if (startCommentMarkIndex < endCommentMarkIndex) { - String sqlWithoutCommentsInBeginning = sql.substring(startCommentMarkIndex + startMark.length()); - return removeCommentsInTheBeginning(sqlWithoutCommentsInBeginning, ++startCommentMarkCount, - endCommentMarkCount, startMark, endMark); - } - // filter out first end comment mark - else { - if (Integer.MAX_VALUE == endCommentMarkIndex) { - return sql; - } - - String sqlWithoutCommentsInBeginning = sql.substring(endCommentMarkIndex + endMark.length()); - return removeCommentsInTheBeginning(sqlWithoutCommentsInBeginning, startCommentMarkCount, - ++endCommentMarkCount, startMark, endMark); - } + private QueryMeta getQueryMetaFromResultSetMetaData(ResultSetMetaData md, int index) throws SQLException { + QueryMeta qm = new QueryMeta(); + qm.parameterClassName = md.getColumnClassName(index); + qm.parameterType = md.getColumnType(index); + qm.parameterTypeName = md.getColumnTypeName(index); + qm.precision = md.getPrecision(index); + qm.scale = md.getScale(index); + qm.isNullable = md.isNullable(index); + qm.isSigned = md.isSigned(index); + return qm; } String parseProcIdentifier(String procIdentifier) throws SQLServerException { @@ -609,9 +334,7 @@ private void checkClosed() throws SQLServerException { // if SQL server version is 2008, then use FMTONLY else { queryMetaMap = new HashMap<>(); - - if (con.getServerMajorVersion() >= SQL_SERVER_2012_VERSION) { - // new implementation for SQL verser 2012 and above + if (con.getServerMajorVersion() >= SQL_SERVER_2012_VERSION && !st.getUseFmtOnly()) { String preparedSQL = con.replaceParameterMarkers(stmtParent.userSQL, stmtParent.userSQLParamPositions, stmtParent.inOutParam, stmtParent.bReturnValueSyntax); @@ -621,56 +344,10 @@ private void checkClosed() throws SQLServerException { parseQueryMeta(cstmt.executeQueryInternal()); } } else { - // old implementation for SQL server 2008 - stringToParse = sProcString; - ArrayList metaInfoList = new ArrayList<>(); - - while (stringToParse.length() > 0) { - MetaInfo metaInfo = parseStatement(stringToParse); - if (null == metaInfo) { - MessageFormat form = new MessageFormat( - SQLServerException.getErrString("R_cantIdentifyTableMetadata")); - Object[] msgArgs = {stringToParse}; - SQLServerException.makeFromDriverError(con, stmtParent, form.format(msgArgs), null, false); - } - - metaInfoList.add(metaInfo); - } - if (metaInfoList.size() <= 0 || metaInfoList.get(0).fields.length() <= 0) { - return; - } - - StringBuilder sbColumns = new StringBuilder(); - - for (MetaInfo mi : metaInfoList) { - sbColumns = sbColumns.append(mi.fields).append(","); - } - sbColumns.deleteCharAt(sbColumns.length() - 1); - - String columns = sbColumns.toString(); - - StringBuilder sbTablesAndJoins = new StringBuilder(); - for (int i = 0; i < metaInfoList.size(); i++) { - if (i == 0) { - sbTablesAndJoins = sbTablesAndJoins.append(metaInfoList.get(i).table); - } else { - if (metaInfoList.get(i).table.equals(metaInfoList.get(i - 1).table) - && metaInfoList.get(i).fields.equals(metaInfoList.get(i - 1).fields)) { - continue; - } - sbTablesAndJoins = sbTablesAndJoins.append(" LEFT JOIN ").append(metaInfoList.get(i).table) - .append(" ON ").append(metaInfoList.get(i - 1).table).append(".") - .append(metaInfoList.get(i - 1).fields).append("=") - .append(metaInfoList.get(i).table).append(".").append(metaInfoList.get(i).fields); - } - } - - String sCom = "sp_executesql N'SET FMTONLY ON SELECT " + columns + " FROM " - + Util.escapeSingleQuotes(sbTablesAndJoins.toString()) + " '"; - + SQLServerFMTQuery f = new SQLServerFMTQuery(sProcString); try (SQLServerStatement stmt = (SQLServerStatement) con.createStatement(); - ResultSet rs = stmt.executeQuery(sCom)) { - parseQueryMetaFor2008(rs); + ResultSet rs = stmt.executeQuery(f.getFMTQuery())) { + parseFMTQueryMeta(rs.getMetaData(), f); } } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java new file mode 100644 index 000000000..c6b7e1181 --- /dev/null +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParser.java @@ -0,0 +1,485 @@ +/* + * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made + * available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ + +package com.microsoft.sqlserver.jdbc; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.ListIterator; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicInteger; + +import org.antlr.v4.runtime.Token; + + +final class SQLServerParser { + + private static final List SELECT_DELIMITING_WORDS = Arrays.asList(SQLServerLexer.WHERE, + SQLServerLexer.GROUP, SQLServerLexer.HAVING, SQLServerLexer.ORDER, SQLServerLexer.OPTION); + private static final List INSERT_DELIMITING_WORDS = Arrays.asList(SQLServerLexer.VALUES, + SQLServerLexer.OUTPUT, SQLServerLexer.LR_BRACKET, SQLServerLexer.SELECT, SQLServerLexer.EXECUTE, + SQLServerLexer.WITH, SQLServerLexer.DEFAULT); + private static final List DELETE_DELIMITING_WORDS = Arrays.asList(SQLServerLexer.OPTION, + SQLServerLexer.WHERE, SQLServerLexer.OUTPUT, SQLServerLexer.FROM); + private static final List UPDATE_DELIMITING_WORDS = Arrays.asList(SQLServerLexer.SET, + SQLServerLexer.OUTPUT, SQLServerLexer.WHERE, SQLServerLexer.OPTION); + private static final List FROM_DELIMITING_WORDS = Arrays.asList(SQLServerLexer.WHERE, SQLServerLexer.GROUP, + SQLServerLexer.HAVING, SQLServerLexer.ORDER, SQLServerLexer.OPTION, SQLServerLexer.AND); + + /* + * Retrieves the table target from a single query. + */ + static void parseQuery(SQLServerTokenIterator iter, SQLServerFMTQuery query) throws SQLServerException { + Token t = null; + while (iter.hasNext()) { + t = iter.next(); + switch (t.getType()) { + case SQLServerLexer.SELECT: + t = skipTop(iter); + while (t.getType() != SQLServerLexer.SEMI) { + if (t.getType() == SQLServerLexer.PARAMETER) { + String columnName = SQLServerParser.findColumnAroundParameter(iter); + query.getColumns().add(columnName); + } + if (t.getType() == SQLServerLexer.FROM) { + query.getTableTarget() + .add(getTableTargetChunk(iter, query.getAliases(), SELECT_DELIMITING_WORDS)); + break; + } + if (iter.hasNext()) { + t = iter.next(); + } else { + break; + } + } + break; + case SQLServerLexer.INSERT: + t = skipTop(iter); + // Check for optional keyword INTO and move the iterator back if it isn't there + if (t.getType() != SQLServerLexer.INTO) { + t = iter.previous(); + } + query.getTableTarget().add(getTableTargetChunk(iter, query.getAliases(), INSERT_DELIMITING_WORDS)); + + List tableValues = getValuesList(iter); + // VALUES case + boolean valuesFound = false; + int valuesMarker = iter.nextIndex(); + while (!valuesFound && iter.hasNext()) { + t = iter.next(); + if (t.getType() == SQLServerLexer.VALUES) { + valuesFound = true; + do { + query.getValuesList().add(getValuesList(iter)); + } while (iter.hasNext() && iter.next().getType() == SQLServerLexer.COMMA); + iter.previous(); + } + } + if (!valuesFound) { + resetIteratorIndex(iter, valuesMarker); + } + if (!query.getValuesList().isEmpty()) { + for (List ls : query.getValuesList()) { + if (tableValues.isEmpty()) { + query.getColumns().add("*"); + } + for (int i = 0; i < ls.size(); i++) { + if (ls.get(i).equalsIgnoreCase("?")) { + query.getColumns().add((tableValues.size() == 0) ? "?" : tableValues.get(i)); + } + } + } + } + break; + case SQLServerLexer.DELETE: + t = skipTop(iter); + // Check for optional keyword FROM and move the iterator back if it isn't there + if (t.getType() != SQLServerLexer.FROM) { + t = iter.previous(); + } + query.getTableTarget().add(getTableTargetChunk(iter, query.getAliases(), DELETE_DELIMITING_WORDS)); + break; + case SQLServerLexer.UPDATE: + skipTop(iter); + t = iter.previous(); + // Get next chunk + query.getTableTarget().add(getTableTargetChunk(iter, query.getAliases(), UPDATE_DELIMITING_WORDS)); + break; + case SQLServerLexer.FROM: + query.getTableTarget().add(getTableTargetChunk(iter, query.getAliases(), FROM_DELIMITING_WORDS)); + break; + case SQLServerLexer.PARAMETER: + int parameterIndex = iter.nextIndex(); + String columnName = SQLServerParser.findColumnAroundParameter(iter); + query.getColumns().add(columnName); + resetIteratorIndex(iter, parameterIndex); + break; + default: + // skip, we only support SELECT/UPDATE/INSERT/DELETE + break; + } + } + } + + static void resetIteratorIndex(SQLServerTokenIterator iter, int index) { + if (iter.nextIndex() < index) { + while (iter.nextIndex() != index) { + iter.next(); + } + } else if (iter.nextIndex() > index) { + while (iter.nextIndex() != index) { + iter.previous(); + } + } + } + + private static String getRoundBracketChunk(SQLServerTokenIterator iter, Token t) { + StringBuilder sb = new StringBuilder(); + sb.append('('); + Stack s = new Stack<>(); + s.push("("); + while (!s.empty()) { + t = iter.next(); + if (t.getType() == SQLServerLexer.RR_BRACKET) { + sb.append(")"); + s.pop(); + } else if (t.getType() == SQLServerLexer.LR_BRACKET) { + sb.append("("); + s.push("("); + } else { + sb.append(t.getText()).append(" "); + } + } + return sb.toString(); + } + + private static String getRoundBracketChunkBefore(SQLServerTokenIterator iter, Token t) { + StringBuilder sb = new StringBuilder(); + sb.append('('); + Stack s = new Stack<>(); + s.push(")"); + while (!s.empty()) { + t = iter.previous(); + if (t.getType() == SQLServerLexer.RR_BRACKET) { + sb.append("("); + s.push(")"); + } else if (t.getType() == SQLServerLexer.LR_BRACKET) { + sb.append(")"); + s.pop(); + } else { + sb.append(t.getText()).append(" "); + } + } + return sb.toString(); + } + + private static final List OPERATORS = Arrays.asList(SQLServerLexer.EQUAL, SQLServerLexer.GREATER, + SQLServerLexer.LESS, SQLServerLexer.GREATER_EQUAL, SQLServerLexer.LESS_EQUAL, SQLServerLexer.NOT_EQUAL, + SQLServerLexer.PLUS_ASSIGN, SQLServerLexer.MINUS_ASSIGN, SQLServerLexer.MULT_ASSIGN, + SQLServerLexer.DIV_ASSIGN, SQLServerLexer.MOD_ASSIGN, SQLServerLexer.AND_ASSIGN, SQLServerLexer.XOR_ASSIGN, + SQLServerLexer.OR_ASSIGN, SQLServerLexer.STAR, SQLServerLexer.DIVIDE, SQLServerLexer.MODULE, + SQLServerLexer.PLUS, SQLServerLexer.MINUS, SQLServerLexer.LIKE, SQLServerLexer.IN, SQLServerLexer.BETWEEN); + + static String findColumnAroundParameter(SQLServerTokenIterator iter) { + int index = iter.nextIndex(); + iter.previous(); + String value = findColumnBeforeParameter(iter); + resetIteratorIndex(iter, index); + if (value.equalsIgnoreCase("")) { + value = findColumnAfterParameter(iter); + resetIteratorIndex(iter, index); + } + return value; + } + + private static String findColumnAfterParameter(SQLServerTokenIterator iter) { + StringBuilder sb = new StringBuilder(); + while (sb.length() == 0 && iter.hasNext()) { + Token t = iter.next(); + if (t.getType() == SQLServerLexer.NOT) { + t = iter.next(); // skip NOT + } + if (OPERATORS.contains(t.getType()) && iter.hasNext()) { + t = iter.next(); + if (t.getType() != SQLServerLexer.PARAMETER) { + if (t.getType() == SQLServerLexer.LR_BRACKET) { + sb.append(getRoundBracketChunk(iter, t)); + } else { + sb.append(t.getText()); + } + for (int i = 0; i < 3 && iter.hasNext(); i++) { + t = iter.next(); + if (t.getType() == SQLServerLexer.DOT) { + sb.append("."); + t = iter.next(); + sb.append(t.getText()); + } + } + } + } else { + return ""; + } + } + return sb.toString(); + } + + private static String findColumnBeforeParameter(SQLServerTokenIterator iter) { + StringBuilder sb = new StringBuilder(); + while (sb.length() == 0 && iter.hasPrevious()) { + Token t = iter.previous(); + if (t.getType() == SQLServerLexer.DOLLAR) { + t = iter.previous(); // skip if it's a $ sign + } + if (t.getType() == SQLServerLexer.AND) { + if (iter.hasPrevious()) { + t = iter.previous(); + if (iter.hasPrevious()) { + t = iter.previous(); // try to find BETWEEN + if (t.getType() == SQLServerLexer.BETWEEN) { + iter.next(); + continue; + } else { + return ""; + } + } + } + } + if (OPERATORS.contains(t.getType()) && iter.hasPrevious()) { + t = iter.previous(); + if (t.getType() == SQLServerLexer.NOT) { + t = iter.previous(); // skip NOT if found + } + if (t.getType() != SQLServerLexer.PARAMETER) { + Deque d = new ArrayDeque<>(); + if (t.getType() == SQLServerLexer.RR_BRACKET) { + d.push(getRoundBracketChunkBefore(iter, t)); + } else { + d.push(t.getText()); + } + // Linked-servers can have a maximum of 4 parts + for (int i = 0; i < 3; i++) { + t = iter.previous(); + if (t.getType() == SQLServerLexer.DOT) { + d.push("."); + t = iter.previous(); + d.push(t.getText()); + } + } + d.stream().forEach(sb::append); + } + } else { + return ""; + } + } + return sb.toString(); + } + + static List getValuesList(SQLServerTokenIterator iter) { + Token t = iter.next(); + if (t.getType() == SQLServerLexer.LR_BRACKET) { + ArrayList parameterColumns = new ArrayList<>(); + Deque d = new ArrayDeque<>(); + StringBuilder sb = new StringBuilder(); + do { + switch (t.getType()) { + case SQLServerLexer.LR_BRACKET: + if (!d.isEmpty()) { + sb.append('('); + } + d.push(SQLServerLexer.LR_BRACKET); + break; + case SQLServerLexer.RR_BRACKET: + if (d.peek() == SQLServerLexer.LR_BRACKET) { + d.pop(); + } + if (!d.isEmpty()) { + sb.append(')'); + } else { + parameterColumns.add(sb.toString().trim()); + } + break; + case SQLServerLexer.COMMA: + if (d.size() == 1) { + parameterColumns.add(sb.toString().trim()); + sb = new StringBuilder(); + } else { + sb.append(','); + } + break; + default: + sb.append(t.getText()); + break; + } + if (iter.hasNext() && !d.isEmpty()) { + t = iter.next(); + } + } while (!d.isEmpty()); + return parameterColumns; + } else { + iter.previous(); + return new ArrayList<>(); + } + } + + /* + * Moves the iterator past the TOP clause to the next token. Returns the first token after the TOP clause. + */ + static Token skipTop(SQLServerTokenIterator iter) throws SQLServerException { + // Look for the TOP token + Token t = iter.next(); + if (t.getType() == SQLServerLexer.TOP) { + t = iter.next(); + if (t.getType() == SQLServerLexer.LR_BRACKET) { + getRoundBracketChunk(iter, t); + } + t = iter.next(); + + // Look for PERCENT, must come before WITH TIES + if (t.getType() == SQLServerLexer.PERCENT) { + t = iter.next(); + } + + // Look for WITH TIES + if (t.getType() == SQLServerLexer.WITH) { + t = iter.next(); + if (t.getType() == SQLServerLexer.TIES) { + t = iter.next(); + } else { + /* + * It's not a WITH TIES clause, move the iterator back. Note: this clause is probably not a valid + * T-SQL clause. + */ + t = iter.previous(); + } + } + } + return t; + } + + static String getCTE(SQLServerTokenIterator iter) throws SQLServerException { + Token t = iter.next(); + if (t.getType() == SQLServerLexer.WITH) { + StringBuilder sb = new StringBuilder("WITH "); + getCTESegment(iter, sb); + return sb.toString(); + } else { + iter.previous(); + return ""; + } + } + + static void getCTESegment(SQLServerTokenIterator iter, StringBuilder sb) throws SQLServerException { + sb.append(getTableTargetChunk(iter, null, Arrays.asList(SQLServerLexer.AS))); + iter.next(); + Token t = iter.next(); + sb.append(" AS "); + if (t.getType() != SQLServerLexer.LR_BRACKET) { + SQLServerException.makeFromDriverError(null, null, SQLServerResource.getResource("R_invalidCTEFormat"), "", + false); + } + int leftRoundBracketCount = 0; + do { + sb.append(t.getText()).append(' '); + if (t.getType() == SQLServerLexer.LR_BRACKET) { + leftRoundBracketCount++; + } else if (t.getType() == SQLServerLexer.RR_BRACKET) { + leftRoundBracketCount--; + } + t = iter.next(); + } while (leftRoundBracketCount > 0); + + if (t.getType() == SQLServerLexer.COMMA) { + sb.append(", "); + getCTESegment(iter, sb); + } else { + iter.previous(); + } + } + + private static String getTableTargetChunk(SQLServerTokenIterator iter, List possibleAliases, + List delimiters) throws SQLServerException { + StringBuilder sb = new StringBuilder(); + Token t = iter.next(); + do { + switch (t.getType()) { + case SQLServerLexer.LR_BRACKET: + sb.append(getRoundBracketChunk(iter, t)); + break; + case SQLServerLexer.OPENDATASOURCE: + case SQLServerLexer.OPENJSON: + case SQLServerLexer.OPENQUERY: + case SQLServerLexer.OPENROWSET: + case SQLServerLexer.OPENXML: + sb.append(t.getText()); + t = iter.next(); + if (t.getType() != SQLServerLexer.LR_BRACKET) { + SQLServerException.makeFromDriverError(null, null, + SQLServerResource.getResource("R_invalidOpenqueryCall"), "", false); + } + sb.append(getRoundBracketChunk(iter, t)); + break; + case SQLServerLexer.AS: + sb.append(t.getText()); + if (iter.hasNext()) { + String s = iter.next().getText(); + possibleAliases.add(s); + sb.append(" ").append(s); + } + break; + default: + sb.append(t.getText()); + break; + } + if (iter.hasNext()) { + sb.append(' '); + t = iter.next(); + } else { + break; + } + } while (!delimiters.contains(t.getType()) && t.getType() != SQLServerLexer.SEMI); + if (iter.hasNext()) { + iter.previous(); + } + return sb.toString().trim(); + } +} + + +final class SQLServerTokenIterator { + private final AtomicInteger index; + private final int listSize; + private final ListIterator iter; + + SQLServerTokenIterator(ArrayList tokenList) { + this.iter = tokenList.listIterator(); + this.index = new AtomicInteger(0); + this.listSize = tokenList.size(); + } + + Token next() { + index.incrementAndGet(); + return iter.next(); + } + + Token previous() { + index.decrementAndGet(); + return iter.previous(); + } + + boolean hasNext() { + return (index.intValue() < listSize); + } + + boolean hasPrevious() { + return (index.intValue() > 0); + } + + int nextIndex() { + return index.intValue() + 1; + } +} diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index 5fea2bad0..1a534004a 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -92,6 +92,8 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS /** Set to true if the statement is a stored procedure call that expects a return value */ final boolean bReturnValueSyntax; + private boolean useFmtOnly = this.connection.getUseFmtOnly(); + /** * The number of OUT parameters to skip in the response to get to the first app-declared OUT parameter. * @@ -2864,6 +2866,18 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th } } + @Override + public final void setUseFmtOnly(boolean useFmtOnly) throws SQLServerException { + checkClosed(); + this.useFmtOnly = useFmtOnly; + } + + @Override + public final boolean getUseFmtOnly() throws SQLServerException { + checkClosed(); + return this.useFmtOnly; + } + @Override public final void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 7132ad7fe..fd99f83b3 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -583,5 +583,12 @@ protected Object[][] getContents() { {"R_ntlmMessageTypeError", "NTLM Challenge Message type error: {0}"}, {"R_ntlmAuthenticateError", "NTLM error constructing Authenticate Message: {0}"}, {"R_ntlmNoTargetInfo", "NTLM Challenge Message is missing target info."}, - {"R_ntlmUnknownValue", "NTLM Challenge Message target info error: unknown value \"{0}\""}}; -} + {"R_ntlmUnknownValue", "NTLM Challenge Message target info error: unknown value \"{0}\""}, + {"R_useFmtOnlyPropertyDescription", + "Determines whether to enable/disable use of SET FMTONLY to retrieve parameter metadata."}, + {"R_invalidOpenqueryCall", + "Invalid syntax: OPENQUERY/OPENJSON/OPENDATASOURCE/OPENROWSET/OPENXML must be preceded by round brackets"}, + {"R_invalidCTEFormat", + "Invalid syntax: AS must be followed by round brackets in Common Table Expressions."}, + {"R_noTokensFoundInUserQuery", "Invalid query: No tokens were parsed from the SQL provided."}}; +}; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java index e7f2da284..06fb0e4a1 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java @@ -7,6 +7,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; @@ -601,6 +603,23 @@ public void testNumericNormalization() throws SQLException { } } + @Test + public void testAEFMTOnly() throws SQLException { + try (SQLServerConnection c = PrepUtil.getConnection(AETestConnectionString + ";useFmtOnly=true", AEInfo); + Statement s = c.createStatement()) { + dropTables(s); + createNumericTable(); + String sql = "insert into " + AbstractSQLGenerator.escapeIdentifier(Constants.NUMERIC_TABLE_AE) + + " values( " + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?," + "?,?,?" + + ")"; + try (PreparedStatement p = c.prepareStatement(sql)) { + ParameterMetaData pmd = p.getParameterMetaData(); + assertTrue(pmd.getParameterCount() == 48); + } + } + } + private void testChar(SQLServerStatement stmt, String[] values) throws SQLException { String sql = "select * from " + AbstractSQLGenerator.escapeIdentifier(Constants.CHAR_TABLE_AE); try (SQLServerConnection con = PrepUtil.getConnection(AETestConnectionString, AEInfo); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/ParserUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/ParserUtils.java new file mode 100644 index 000000000..702cf75fe --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/ParserUtils.java @@ -0,0 +1,63 @@ +package com.microsoft.sqlserver.jdbc; + +import static org.junit.Assert.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.ListIterator; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.Token; + + +public class ParserUtils { + + private static String getTableName(String s) { + try { + return new SQLServerFMTQuery(s).constructTableTargets(); + } catch (SQLServerException e) { + return e.getLocalizedMessage(); + } + } + + private static String getCTE(String s) { + InputStream stream = new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8)); + SQLServerLexer lexer = null; + try { + lexer = new SQLServerLexer(CharStreams.fromStream(stream)); + invokeANTLRMethods(lexer); + ArrayList tokenList = (ArrayList) lexer.getAllTokens(); + SQLServerTokenIterator iter = new SQLServerTokenIterator(tokenList); + return SQLServerParser.getCTE(iter); + } catch (IOException | SQLServerException e) { + return e.getLocalizedMessage(); + } + } + + public static void compareTableName(String tsql, String expected) { + // Verbose to make debugging more friendly + String extractedTableName = ParserUtils.getTableName(tsql).trim(); + assertEquals(expected, extractedTableName); + } + + public static void compareCommonTableExpression(String tsql, String expected) { + // Verbose to make debugging more friendly + String extractedTableName = ParserUtils.getCTE(tsql).trim(); + assertEquals(expected, extractedTableName); + } + + @SuppressWarnings("deprecation") + private static void invokeANTLRMethods(SQLServerLexer s) { + s.getTokenNames(); + s.getVocabulary(); + s.getGrammarFileName(); + s.getRuleNames(); + s.getSerializedATN(); + s.getChannelNames(); + s.getModeNames(); + s.getATN(); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java index d99ddf837..1e257dec3 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java @@ -291,6 +291,17 @@ public static void dropTableIfExists(String tableName, java.sql.Statement stmt) dropObjectIfExists(tableName, "U", stmt); } + /** + * mimic "DROP View ..." + * + * @param tableName + * @param stmt + * @throws SQLException + */ + public static void dropViewIfExists(String tableName, java.sql.Statement stmt) throws SQLException { + dropObjectIfExists(tableName, "V", stmt); + } + /** * mimic "DROP PROCEDURE ..." * @@ -392,6 +403,10 @@ private static void dropObjectIfExists(String objectName, String objectType, break; case "U": typeName = "TABLE"; + break; + case "V": + typeName = "VIEW"; + break; default: break; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java index 8b2560bb4..2a9023b19 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/connection/RequestBoundaryMethodsTest.java @@ -66,6 +66,7 @@ public void testModifiableConnectionProperties() throws SQLException { boolean enablePrepareOnFirstPreparedStatementCall1 = false; String sCatalog1 = "master"; boolean useBulkCopyForBatchInsert1 = true; + boolean useFmtOnly1 = true; boolean autoCommitMode2 = false; int transactionIsolationLevel2 = SQLServerConnection.TRANSACTION_SERIALIZABLE; @@ -78,6 +79,7 @@ public void testModifiableConnectionProperties() throws SQLException { boolean enablePrepareOnFirstPreparedStatementCall2 = true; String sCatalog2 = RandomUtil.getIdentifier("RequestBoundaryDatabase"); boolean useBulkCopyForBatchInsert2 = false; + boolean useFmtOnly2 = false; try (SQLServerConnection con = getConnection(); Statement stmt = con.createStatement()) { if (TestUtils.isJDBC43OrGreater(con)) { @@ -88,53 +90,53 @@ public void testModifiableConnectionProperties() throws SQLException { setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, - useBulkCopyForBatchInsert1); + useBulkCopyForBatchInsert1, useFmtOnly1); con.beginRequest(); // Call setters with the second set of values inside beginRequest()/endRequest() block. setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2); + useBulkCopyForBatchInsert2, useFmtOnly2); con.endRequest(); // Test if endRequest() resets the SQLServerConnection properties back to the first set of values. compareValuesAgainstConnection(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, - useBulkCopyForBatchInsert1); + useBulkCopyForBatchInsert1, useFmtOnly1); // Multiple calls to beginRequest() without an intervening call to endRequest() are no-op. setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2); + useBulkCopyForBatchInsert2, useFmtOnly2); con.beginRequest(); setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, - useBulkCopyForBatchInsert1); + useBulkCopyForBatchInsert1, useFmtOnly1); con.beginRequest(); con.endRequest(); // Same values as before the first beginRequest() compareValuesAgainstConnection(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2); + useBulkCopyForBatchInsert2, useFmtOnly2); // A call to endRequest() without an intervening call to beginRequest() is no-op. setConnectionFields(con, autoCommitMode1, transactionIsolationLevel1, networkTimeout1, holdability1, sendTimeAsDatetime1, statementPoolingCacheSize1, disableStatementPooling1, serverPreparedStatementDiscardThreshold1, enablePrepareOnFirstPreparedStatementCall1, sCatalog1, - useBulkCopyForBatchInsert1); + useBulkCopyForBatchInsert1, useFmtOnly1); setConnectionFields(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2); + useBulkCopyForBatchInsert2, useFmtOnly2); con.endRequest(); // No change. compareValuesAgainstConnection(con, autoCommitMode2, transactionIsolationLevel2, networkTimeout2, holdability2, sendTimeAsDatetime2, statementPoolingCacheSize2, disableStatementPooling2, serverPreparedStatementDiscardThreshold2, enablePrepareOnFirstPreparedStatementCall2, sCatalog2, - useBulkCopyForBatchInsert2); + useBulkCopyForBatchInsert2, useFmtOnly2); } } finally { TestUtils.dropDatabaseIfExists(sCatalog2, connectionString); @@ -386,8 +388,8 @@ public void testNewMethods() { private void setConnectionFields(SQLServerConnection con, boolean autoCommitMode, int transactionIsolationLevel, int networkTimeout, int holdability, boolean sendTimeAsDatetime, int statementPoolingCacheSize, boolean disableStatementPooling, int serverPreparedStatementDiscardThreshold, - boolean enablePrepareOnFirstPreparedStatementCall, String sCatalog, - boolean useBulkCopyForBatchInsert) throws SQLException { + boolean enablePrepareOnFirstPreparedStatementCall, String sCatalog, boolean useBulkCopyForBatchInsert, + boolean useFmtOnly) throws SQLException { con.setAutoCommit(autoCommitMode); con.setTransactionIsolation(transactionIsolationLevel); con.setNetworkTimeout(null, networkTimeout); @@ -399,13 +401,14 @@ private void setConnectionFields(SQLServerConnection con, boolean autoCommitMode con.setEnablePrepareOnFirstPreparedStatementCall(enablePrepareOnFirstPreparedStatementCall); con.setCatalog(sCatalog); con.setUseBulkCopyForBatchInsert(useBulkCopyForBatchInsert); + con.setUseFmtOnly(useFmtOnly); } private void compareValuesAgainstConnection(SQLServerConnection con, boolean autoCommitMode, int transactionIsolationLevel, int networkTimeout, int holdability, boolean sendTimeAsDatetime, int statementPoolingCacheSize, boolean disableStatementPooling, int serverPreparedStatementDiscardThreshold, - boolean enablePrepareOnFirstPreparedStatementCall, String sCatalog, - boolean useBulkCopyForBatchInsert) throws SQLException { + boolean enablePrepareOnFirstPreparedStatementCall, String sCatalog, boolean useBulkCopyForBatchInsert, + boolean useFmtOnly) throws SQLException { final String description = " values do not match."; assertEquals(autoCommitMode, con.getAutoCommit(), "autoCommitmode" + description); assertEquals(transactionIsolationLevel, con.getTransactionIsolation(), @@ -424,6 +427,7 @@ private void compareValuesAgainstConnection(SQLServerConnection con, boolean aut assertEquals(sCatalog, con.getCatalog(), "sCatalog" + description); assertEquals(useBulkCopyForBatchInsert, con.getUseBulkCopyForBatchInsert(), "useBulkCopyForBatchInsert" + description); + assertEquals(useFmtOnly, con.getUseFmtOnly(), "useFmtOnly" + description); } private void generateWarning(Connection con) throws SQLException { @@ -478,6 +482,7 @@ private List getVerifiedMethodNames() { verifiedMethodNames.add("isWrapperFor"); verifiedMethodNames.add("setTypeMap"); verifiedMethodNames.add("createArrayOf"); + verifiedMethodNames.add("setUseFmtOnly"); return verifiedMethodNames; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/APITest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/APITest.java new file mode 100644 index 000000000..ccc0a92e2 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/APITest.java @@ -0,0 +1,79 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import static org.junit.Assert.assertEquals; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.PrepUtil; + + +public class APITest extends AbstractTest { + + private static final String tableName = RandomUtil.getIdentifier("FMT_API_Test"); + + @BeforeAll + private static void setupTest() throws SQLException { + try (Statement s = connection.createStatement()) { + s.execute("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(tableName) + + " (c1 int identity, c2 float, c3 real, c4 bigint, c5 nvarchar(4000))"); + } + } + + @AfterAll + private static void cleanupTest() throws SQLException { + try (Statement s = connection.createStatement()) { + TestUtils.dropTableIfExists(tableName, s); + } + } + + @Test + public void publicAPITest() throws SQLException { + String sql = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(tableName) + " VALUES(?,?,?,?)"; + + ds.setUseFmtOnly(true); + try (Connection cStringConnection = PrepUtil.getConnection(AbstractTest.connectionString + ";useFmtOnly=true;"); + Connection statementConnection = getConnection(); + Connection dsConnection = ((DataSource) ds).getConnection()) { + try (PreparedStatement cStringPstmt = cStringConnection.prepareStatement(sql); + PreparedStatement statementPstmt = statementConnection.prepareStatement(sql); + PreparedStatement dsPstmt = dsConnection.prepareStatement(sql)) { + ((SQLServerPreparedStatement) statementPstmt).setUseFmtOnly(true); + ParameterMetaData cStringMD = cStringPstmt.getParameterMetaData(); + ParameterMetaData statementMD = statementPstmt.getParameterMetaData(); + ParameterMetaData dsMD = dsPstmt.getParameterMetaData(); + compare(cStringMD.getParameterCount(), statementMD.getParameterCount(), dsMD.getParameterCount()); + for (int i = 1; i <= cStringMD.getParameterCount(); i++) { + compare(cStringMD.getParameterClassName(i), statementMD.getParameterClassName(i), + dsMD.getParameterClassName(i)); + compare(cStringMD.getParameterMode(i), statementMD.getParameterMode(i), dsMD.getParameterMode(i)); + compare(cStringMD.getParameterType(i), statementMD.getParameterType(i), dsMD.getParameterType(i)); + compare(cStringMD.getParameterTypeName(i), statementMD.getParameterTypeName(i), + dsMD.getParameterTypeName(i)); + compare(cStringMD.getPrecision(i), statementMD.getPrecision(i), dsMD.getPrecision(i)); + compare(cStringMD.getScale(i), statementMD.getScale(i), dsMD.getScale(i)); + } + } + } + } + + private void compare(Object a, Object b, Object c) { + assertEquals(a, b); + assertEquals(b, c); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/DeleteTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/DeleteTest.java new file mode 100644 index 000000000..669244f3b --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/DeleteTest.java @@ -0,0 +1,112 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ParserUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class DeleteTest extends AbstractTest { + /* + * A collection of DELETE T-SQL statements from the Microsoft docs. + * @link https://docs.microsoft.com/en-us/sql/t-sql/statements/delete-transact-sql?view=sql-server-2017#examples + */ + @Test + public void deleteExamplesTest() { + // A. Using DELETE with no WHERE clause + ParserUtils.compareTableName("DELETE FROM Sales.SalesPersonQuotaHistory; ", "Sales . SalesPersonQuotaHistory"); + // B. Using the WHERE clause to delete a set of rows + ParserUtils.compareTableName( + "DELETE FROM Production.ProductCostHistory \r\n" + "WHERE StandardCost > 1000.00;", + "Production . ProductCostHistory"); + ParserUtils.compareTableName( + "DELETE Production.ProductCostHistory \r\n" + "WHERE StandardCost BETWEEN 12.00 AND 14.00 \r\n" + + " AND EndDate IS NULL; \r\n" + + "PRINT 'Number of rows deleted is ' + CAST(@@ROWCOUNT as char(3)); ", + "Production . ProductCostHistory"); + + // C. Using a cursor to determine the row to delete + ParserUtils.compareTableName( + "DELETE FROM HumanResources.EmployeePayHistory \r\n" + "WHERE CURRENT OF complex_cursor; \r\n" + + "CLOSE complex_cursor; \r\n" + "DEALLOCATE complex_cursor; ", + "HumanResources . EmployeePayHistory"); + + // D. Using joins and subqueries to data in one table to delete rows in another table + ParserUtils.compareTableName( + "DELETE FROM Sales.SalesPersonQuotaHistory \r\n" + "WHERE BusinessEntityID IN \r\n" + + " (SELECT BusinessEntityID \r\n" + " FROM Sales.SalesPerson \r\n" + + " WHERE SalesYTD > 2500000.00); ", + "Sales . SalesPersonQuotaHistory,Sales . SalesPerson"); + ParserUtils.compareTableName( + "DELETE FROM Sales.SalesPersonQuotaHistory \r\n" + "FROM Sales.SalesPersonQuotaHistory AS spqh \r\n" + + "INNER JOIN Sales.SalesPerson AS sp \r\n" + + "ON spqh.BusinessEntityID = sp.BusinessEntityID \r\n" + "WHERE sp.SalesYTD > 2500000.00; ", + "Sales . SalesPersonQuotaHistory,Sales . SalesPersonQuotaHistory AS spqh INNER JOIN Sales . SalesPerson AS sp ON spqh . BusinessEntityID = sp . BusinessEntityID"); + + ParserUtils.compareTableName("DELETE spqh \r\n" + " FROM \r\n" + + " Sales.SalesPersonQuotaHistory AS spqh \r\n" + " INNER JOIN Sales.SalesPerson AS sp \r\n" + + " ON spqh.BusinessEntityID = sp.BusinessEntityID \r\n" + " WHERE sp.SalesYTD > 2500000.00;", + "Sales . SalesPersonQuotaHistory AS spqh INNER JOIN Sales . SalesPerson AS sp ON spqh . BusinessEntityID = sp . BusinessEntityID"); + + // E. Using TOP to limit the number of rows deleted + ParserUtils.compareTableName( + "DELETE TOP (20) \r\n" + "FROM Purchasing.PurchaseOrderDetail \r\n" + "WHERE DueDate < '20020701';", + "Purchasing . PurchaseOrderDetail"); + ParserUtils.compareTableName( + "DELETE FROM Purchasing.PurchaseOrderDetail \r\n" + "WHERE PurchaseOrderDetailID IN \r\n" + + " (SELECT TOP 10 PurchaseOrderDetailID \r\n" + + " FROM Purchasing.PurchaseOrderDetail \r\n" + " ORDER BY DueDate ASC); ", + "Purchasing . PurchaseOrderDetail"); + + // F. Deleting data from a remote table by using a linked server + ParserUtils.compareTableName( + "DELETE MyLinkServer.AdventureWorks2012.HumanResources.Department \r\n" + "WHERE DepartmentID > 16; ", + "MyLinkServer . AdventureWorks2012 . HumanResources . Department"); + + // G. Deleting data from a remote table by using the OPENQUERY function + ParserUtils.compareTableName( + "DELETE OPENQUERY (MyLinkServer, 'SELECT Name, GroupName FROM AdventureWorks2012.HumanResources.Department WHERE DepartmentID = 18');", + "OPENQUERY(MyLinkServer , 'SELECT Name, GroupName FROM AdventureWorks2012.HumanResources.Department WHERE DepartmentID = 18' )"); + + // H. Deleting data from a remote table by using the OPENDATASOURCE function + ParserUtils.compareTableName( + "DELETE FROM OPENDATASOURCE('SQLNCLI', \r\n" + + " 'Data Source= ; Integrated Security=SSPI') \r\n" + + " .AdventureWorks2012.HumanResources.Department \r\n" + "WHERE DepartmentID = 17;'", + "OPENDATASOURCE('SQLNCLI' , 'Data Source= ; Integrated Security=SSPI' ) . AdventureWorks2012 . HumanResources . Department"); + + // I. Using DELETE with the OUTPUT clause + ParserUtils.compareTableName("DELETE Sales.ShoppingCartItem OUTPUT DELETED.* WHERE ShoppingCartID = 20621;", + "Sales . ShoppingCartItem"); + + // J. Using OUTPUT with in a DELETE statement + ParserUtils.compareTableName("DELETE Production.ProductProductPhoto \r\n" + "OUTPUT DELETED.ProductID, \r\n" + + " p.Name, \r\n" + " p.ProductModelID, \r\n" + " DELETED.ProductPhotoID \r\n" + + " INTO @MyTableVar \r\n" + "FROM Production.ProductProductPhoto AS ph \r\n" + + "JOIN Production.Product as p \r\n" + " ON ph.ProductID = p.ProductID \r\n" + + " WHERE p.ProductModelID BETWEEN 120 and 130; ", + "Production . ProductProductPhoto,Production . ProductProductPhoto AS ph JOIN Production . Product as p ON ph . ProductID = p . ProductID"); + + // K. Delete all rows from a table + ParserUtils.compareTableName("DELETE FROM Table1;", "Table1"); + + // L. DELETE a set of rows from a table + ParserUtils.compareTableName("DELETE FROM Table1 WHERE StandardCost > 1000.00;", "Table1"); + + // M. Using LABEL with a DELETE statement + ParserUtils.compareTableName("DELETE FROM Table1 OPTION ( LABEL = N'label1' );", "Table1"); + + // N. Using a label and a query hint with the DELETE statement + ParserUtils.compareTableName( + "DELETE FROM dbo.FactInternetSales \r\n" + "WHERE ProductKey IN ( \r\n" + + " SELECT T1.ProductKey FROM dbo.DimProduct T1 \r\n" + + " JOIN dbo.DimProductSubcategory T2 \r\n" + + " ON T1.ProductSubcategoryKey = T2.ProductSubcategoryKey \r\n" + + " WHERE T2.EnglishProductSubcategoryName = 'Road Bikes' ) \r\n" + + "OPTION ( LABEL = N'CustomJoin', HASH JOIN );", + "dbo . FactInternetSales,dbo . DimProduct T1 JOIN dbo . DimProductSubcategory T2 ON T1 . ProductSubcategoryKey = T2 . ProductSubcategoryKey"); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/InsertTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/InsertTest.java new file mode 100644 index 000000000..d5c92394c --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/InsertTest.java @@ -0,0 +1,265 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ParserUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class InsertTest extends AbstractTest { + + @Test + public void basicInsertTest() { + // minor case sensitivity checking + ParserUtils.compareTableName("insert into jdbctest values (1)", "jdbctest"); + ParserUtils.compareTableName("InSerT IntO jdbctest VALUES (2)", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES (3)", "jdbctest"); + + // escape sequence + ParserUtils.compareTableName("INSERT INTO [jdbctest]]] VALUES (1)", "[jdbctest]]]"); + ParserUtils.compareTableName("INSERT INTO [jdb]]ctest] VALUES (1)", "[jdb]]ctest]"); + ParserUtils.compareTableName("INSERT INTO [j[db]]ctest] VALUES (1)", "[j[db]]ctest]"); + + // basic cases + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES (?,?,?)", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES (?,?,?);", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO /*hello this is a comment*/jdbctest VALUES (1);", "jdbctest"); + + // double quote literal + ParserUtils.compareTableName("INSERT INTO \"jdbc test\" VALUES (1)", "\"jdbc test\""); + ParserUtils.compareTableName("INSERT INTO \"jdbc /*test*/\" VALUES (1)", "\"jdbc /*test*/\""); + ParserUtils.compareTableName("INSERT INTO \"jdbc //test\" VALUES (1)", "\"jdbc //test\""); + ParserUtils.compareTableName("INSERT INTO \"dbo\".\"jdbcDB\".\"jdbctest\" VALUES (1)", + "\"dbo\" . \"jdbcDB\" . \"jdbctest\""); + ParserUtils.compareTableName("INSERT INTO \"jdbctest\" VALUES (1)", "\"jdbctest\""); + + // square bracket literal + ParserUtils.compareTableName("INSERT INTO [jdbctest] VALUES (1)", "[jdbctest]"); + ParserUtils.compareTableName("INSERT INTO [dbo].[jdbcDB].[jdbctest] VALUES (1)", + "[dbo] . [jdbcDB] . [jdbctest]"); + ParserUtils.compareTableName("INSERT INTO [dbo].\"jdbcDB\".\"jdbctest\" VALUES (1)", + "[dbo] . \"jdbcDB\" . \"jdbctest\""); + ParserUtils.compareTableName("INSERT INTO [jdbc test] VALUES (1)", "[jdbc test]"); + ParserUtils.compareTableName("INSERT INTO [jdbc /*test*/] VALUES (1)", "[jdbc /*test*/]"); + ParserUtils.compareTableName("INSERT INTO [jdbc //test] VALUES (1)", "[jdbc //test]"); + + // with parameters + ParserUtils.compareTableName("INSERT jdbctest VALUES (c1,c2,c3)", "jdbctest"); + ParserUtils.compareTableName("INSERT jdbctest VALUES (c1,c2,c3);", "jdbctest"); + ParserUtils.compareTableName("INSERT jdbctest VALUES (?,?,?)", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES (c1,?,c3)", "jdbctest"); + + // with special parameters + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES ([c1],\"c2\")", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES ([c1]]],\"c2\")", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES ([c]]1],\"c2\")", "jdbctest"); + ParserUtils.compareTableName("INSERT INTO jdbctest VALUES ([c1],\"[c2]\")", "jdbctest"); + ParserUtils.compareTableName("INSERT jdbctest VALUES ([\"c1],\"FROM\")", "jdbctest"); + ParserUtils.compareTableName("INSERT jdbctest VALUES ([\"c\"1\"],\"c2\")", "jdbctest"); + ParserUtils.compareTableName("INSERT jdbctest VALUES (['FROM'1)],\"c2\")", "jdbctest"); + ParserUtils.compareTableName("INSERT jdbctest VALUES ([((c)1{}],\"{{c}2)(\")", "jdbctest"); + + // with sub queries + ParserUtils.compareTableName("INSERT INTO jdbctest SELECT c1,c2,c3 FROM jdbctest2 " + "WHERE c1 > 4.0", + "jdbctest,jdbctest2"); + + // Multiple Selects + ParserUtils.compareTableName("INSERT INTO table1 VALUES (1);INSERT INTO table2 VALUES (1)", "table1,table2"); + ParserUtils.compareTableName("INSERT INTO table1 VALUES (1);INSERT INTO table1 VALUES (1)", "table1"); + + // Special cases + ParserUtils.compareTableName("INSERT INTO dbo.FastCustomers2009\r\n" + + "SELECT T1.FirstName, T1.LastName, T1.YearlyIncome, T1.MaritalStatus FROM Insured_Customers T1 JOIN CarSensor_Data T2\r\n" + + "ON (T1.CustomerKey = T2.CustomerKey)\r\n" + "WHERE T2.YearMeasured = ? and T2.Speed > ?;", + "dbo . FastCustomers2009,Insured_Customers T1 JOIN CarSensor_Data T2 ON (T1 . CustomerKey = T2 . CustomerKey )"); + } + + /* + * A collection of INSERT T-SQL statements from the Microsoft docs. + * @link https://docs.microsoft.com/en-us/sql/t-sql/statements/insert-transact-sql?view=sql-server-2017 + */ + @Test + public void insertExamplesTest() { + // A. Inserting a single row of data + ParserUtils.compareTableName("INSERT INTO Production.UnitMeasure VALUES (N'FT', N'Feet', '20080414');", + "Production . UnitMeasure"); + + // B. Inserting multiple rows of data + ParserUtils.compareTableName("INSERT INTO Production.UnitMeasure " + + "VALUES (N'FT2', N'Square Feet ', '20080923'), (N'Y', N'Yards', '20080923') " + + ", (N'Y3', N'Cubic Yards', '20080923');", "Production . UnitMeasure"); + + // C. Inserting data that is not in the same order as the table columns + ParserUtils.compareTableName("INSERT INTO Production.UnitMeasure (Name, UnitMeasureCode, " + "ModifiedDate) " + + "VALUES (N'Square Yards', N'Y2', GETDATE());", "Production . UnitMeasure"); + + // D. Inserting data into a table with columns that have default values + ParserUtils.compareTableName("INSERT INTO T1 DEFAULT VALUES;", "T1"); + + // E. Inserting data into a table with an identity column + ParserUtils.compareTableName("INSERT INTO T1 (column_1,column_2) VALUES (-99, 'Explicit identity value'); ", + "T1"); + + // F. Inserting data into a uniqueidentifier column by using NEWID() + ParserUtils.compareTableName("INSERT INTO dbo.T1 (column_2) VALUES (NEWID());", "dbo . T1"); + + // G. Inserting data into user-defined type columns + ParserUtils.compareTableName("INSERT INTO dbo.Points (PointValue) VALUES (CAST ('1,99' AS Point));", + "dbo . Points"); + + // H. Using the SELECT and EXECUTE options to insert data from other tables + ParserUtils.compareTableName("INSERT INTO dbo.EmployeeSales \r\n" + + " SELECT 'SELECT', sp.BusinessEntityID, c.LastName, sp.SalesYTD \r\n" + + " FROM Sales.SalesPerson AS sp \r\n" + " INNER JOIN Person.Person AS c \r\n" + + " ON sp.BusinessEntityID = c.BusinessEntityID \r\n" + + " WHERE sp.BusinessEntityID LIKE '2%' \r\n" + " ORDER BY sp.BusinessEntityID, c.LastName;", + "dbo . EmployeeSales,Sales . SalesPerson AS sp INNER JOIN Person . Person AS c ON sp . BusinessEntityID = c . BusinessEntityID"); + ParserUtils.compareTableName("INSERT INTO dbo.EmployeeSales \r\n" + "EXECUTE dbo.uspGetEmployeeSales;", + "dbo . EmployeeSales"); + ParserUtils.compareTableName("INSERT INTO dbo.EmployeeSales \r\n" + "EXECUTE \r\n" + "(' \r\n" + + "SELECT ''EXEC STRING'', sp.BusinessEntityID, c.LastName, \r\n" + " sp.SalesYTD \r\n" + + " FROM Sales.SalesPerson AS sp \r\n" + " INNER JOIN Person.Person AS c \r\n" + + " ON sp.BusinessEntityID = c.BusinessEntityID \r\n" + + " WHERE sp.BusinessEntityID LIKE ''2%'' \r\n" + + " ORDER BY sp.BusinessEntityID, c.LastName \r\n" + "');", "dbo . EmployeeSales"); + + // I. Using WITH common table expression to define the data inserted + ParserUtils.compareTableName("WITH EmployeeTemp (EmpID, LastName, FirstName, Phone, \r\n" + + " Address, City, StateProvince, \r\n" + + " PostalCode, CurrentFlag) \r\n" + "AS (SELECT \r\n" + + " e.BusinessEntityID, c.LastName, c.FirstName, pp.PhoneNumber, \r\n" + + " a.AddressLine1, a.City, sp.StateProvinceCode, \r\n" + + " a.PostalCode, e.CurrentFlag \r\n" + " FROM HumanResources.Employee e \r\n" + + " INNER JOIN Person.BusinessEntityAddress AS bea \r\n" + + " ON e.BusinessEntityID = bea.BusinessEntityID \r\n" + + " INNER JOIN Person.Address AS a \r\n" + " ON bea.AddressID = a.AddressID \r\n" + + " INNER JOIN Person.PersonPhone AS pp \r\n" + + " ON e.BusinessEntityID = pp.BusinessEntityID \r\n" + + " INNER JOIN Person.StateProvince AS sp \r\n" + + " ON a.StateProvinceID = sp.StateProvinceID \r\n" + + " INNER JOIN Person.Person as c \r\n" + + " ON e.BusinessEntityID = c.BusinessEntityID \r\n" + " ) \r\n" + + "INSERT INTO HumanResources.NewEmployee \r\n" + + " SELECT EmpID, LastName, FirstName, Phone, \r\n" + + " Address, City, StateProvince, PostalCode, CurrentFlag \r\n" + " FROM EmployeeTemp;", + "HumanResources . NewEmployee,EmployeeTemp"); + + // J. Using TOP to limit the data inserted from the source table + ParserUtils.compareTableName( + "INSERT TOP(5)INTO dbo.EmployeeSales \r\n" + " OUTPUT inserted.EmployeeID, inserted.FirstName, \r\n" + + " inserted.LastName, inserted.YearlySales \r\n" + + " SELECT sp.BusinessEntityID, c.LastName, c.FirstName, sp.SalesYTD \r\n" + + " FROM Sales.SalesPerson AS sp \r\n" + " INNER JOIN Person.Person AS c \r\n" + + " ON sp.BusinessEntityID = c.BusinessEntityID \r\n" + + " WHERE sp.SalesYTD > 250000.00 \r\n" + " ORDER BY sp.SalesYTD DESC;", + "dbo . EmployeeSales,Sales . SalesPerson AS sp INNER JOIN Person . Person AS c ON sp . BusinessEntityID = c . BusinessEntityID"); + ParserUtils.compareTableName( + "INSERT INTO dbo.EmployeeSales \r\n" + " OUTPUT inserted.EmployeeID, inserted.FirstName, \r\n" + + " inserted.LastName, inserted.YearlySales \r\n" + + " SELECT TOP (5) sp.BusinessEntityID, c.LastName, c.FirstName, sp.SalesYTD \r\n" + + " FROM Sales.SalesPerson AS sp \r\n" + " INNER JOIN Person.Person AS c \r\n" + + " ON sp.BusinessEntityID = c.BusinessEntityID \r\n" + + " WHERE sp.SalesYTD > 250000.00 \r\n" + " ORDER BY sp.SalesYTD DESC;", + "dbo . EmployeeSales,Sales . SalesPerson AS sp INNER JOIN Person . Person AS c ON sp . BusinessEntityID = c . BusinessEntityID"); + + // K. Inserting data by specifying a view + ParserUtils.compareTableName("INSERT INTO V1 VALUES ('Row 1',1); ", "V1"); + + // L. Inserting data into a table variable + ParserUtils.compareTableName("INSERT INTO @MyTableVar (LocationID, CostRate, ModifiedDate) \r\n" + + " SELECT LocationID, CostRate, GETDATE() \r\n" + " FROM Production.Location \r\n" + + " WHERE CostRate > 0; ", "@MyTableVar,Production . Location"); + + // M. Inserting data into a remote table by using a linked server + ParserUtils.compareTableName( + "INSERT INTO MyLinkServer.AdventureWorks2012.HumanResources.Department (Name, GroupName) \r\n" + + "VALUES (N'Public Relations', N'Executive General and Administration'); ", + "MyLinkServer . AdventureWorks2012 . HumanResources . Department"); + + // N. Inserting data into a remote table by using the OPENQUERY function + ParserUtils.compareTableName( + "INSERT OPENQUERY (MyLinkServer, 'SELECT Name, GroupName FROM AdventureWorks2012.HumanResources.Department') VALUES ('Environmental Impact', 'Engineering');", + "OPENQUERY(MyLinkServer , 'SELECT Name, GroupName FROM AdventureWorks2012.HumanResources.Department' )"); + + // P. Inserting into an external table created using PolyBase + ParserUtils.compareTableName("INSERT INTO dbo.FastCustomer2009 " + + "SELECT T.* FROM Insured_Customers T1 JOIN CarSensor_Data T2 \r\n" + + "ON (T1.CustomerKey = T2.CustomerKey) \r\n" + "WHERE T2.YearMeasured = 2009 and T2.Speed > 40;", + "dbo . FastCustomer2009,Insured_Customers T1 JOIN CarSensor_Data T2 ON (T1 . CustomerKey = T2 . CustomerKey )"); + + // Q. Inserting data into a heap with minimal logging + ParserUtils.compareTableName("INSERT INTO Sales.SalesHistory WITH (TABLOCK) \r\n" + " (SalesOrderID, \r\n" + + " SalesOrderDetailID, \r\n" + " CarrierTrackingNumber, \r\n" + " OrderQty, \r\n" + + " ProductID, \r\n" + " SpecialOfferID, \r\n" + " UnitPrice, \r\n" + + " UnitPriceDiscount, \r\n" + " LineTotal, \r\n" + " rowguid, \r\n" + + " ModifiedDate) \r\n" + "SELECT * FROM Sales.SalesOrderDetail; ", + "Sales . SalesHistory,Sales . SalesOrderDetail"); + + // R. Using the OPENROWSET function with BULK to bulk load data into a table + ParserUtils.compareTableName( + "INSERT INTO HumanResources.Department WITH (IGNORE_TRIGGERS) (Name, GroupName) \r\n" + + "SELECT b.Name, b.GroupName \r\n" + "FROM OPENROWSET ( \r\n" + + " BULK 'C:SQLFilesDepartmentData.txt', \r\n" + + " FORMATFILE = 'C:SQLFilesBulkloadFormatFile.xml', \r\n" + + " ROWS_PER_BATCH = 15000)AS b;", + "HumanResources . Department,OPENROWSET(BULK 'C:SQLFilesDepartmentData.txt' , FORMATFILE = 'C:SQLFilesBulkloadFormatFile.xml' , ROWS_PER_BATCH = 15000 ) AS b"); + + // S. Using the TABLOCK hint to specify a locking method + ParserUtils.compareTableName("INSERT INTO Production.Location WITH (XLOCK) \r\n" + "(Name, CostRate, Availability) \r\n" + + "VALUES ( N'Final Inventory', 15.00, 80.00); ", "Production . Location"); + + // T. Using OUTPUT with an INSERT statement TODO: FIX + ParserUtils.compareTableName( + "INSERT Production.ScrapReason \r\n" + + " OUTPUT INSERTED.ScrapReasonID, INSERTED.Name, INSERTED.ModifiedDate \r\n" + + " INTO @MyTableVar \r\n" + "VALUES (N'Operator error', GETDATE());", + "Production . ScrapReason"); + + // U. Using OUTPUT with identity and computed columns + ParserUtils.compareTableName( + "INSERT INTO dbo.EmployeeSales (LastName, FirstName, CurrentSales) \r\n" + + " OUTPUT INSERTED.LastName, \r\n" + " INSERTED.FirstName, \r\n" + + " INSERTED.CurrentSales \r\n" + " INTO @MyTableVar \r\n" + + " SELECT c.LastName, c.FirstName, sp.SalesYTD \r\n" + + " FROM Sales.SalesPerson AS sp \r\n" + " INNER JOIN Person.Person AS c \r\n" + + " ON sp.BusinessEntityID = c.BusinessEntityID \r\n" + + " WHERE sp.BusinessEntityID LIKE '2%' \r\n" + " ORDER BY c.LastName, c.FirstName;", + "dbo . EmployeeSales,Sales . SalesPerson AS sp INNER JOIN Person . Person AS c ON sp . BusinessEntityID = c . BusinessEntityID"); + + /* + * V. Inserting data returned from an OUTPUT clause. TODO: Table name extraction is actually working, but this + * query won't work on SSMS because of MERGE clause. + * ParserUtils.compareTableName("INSERT INTO Production.ZeroInventory (DeletedProductID, RemovedOnDate) \r\n" + + * "SELECT ProductID, GETDATE() \r\n" + "FROM \r\n" + "( MERGE Production.ProductInventory AS pi \r\n" + + * " USING (SELECT ProductID, SUM(OrderQty) FROM Sales.SalesOrderDetail AS sod \r\n" + + * " JOIN Sales.SalesOrderHeader AS soh \r\n" + " ON sod.SalesOrderID = soh.SalesOrderID \r\n" + + * " AND soh.OrderDate = '20070401' \r\n" + " GROUP BY ProductID) AS src (ProductID, OrderQty) \r\n" + + * " ON (pi.ProductID = src.ProductID) \r\n" + " WHEN MATCHED AND pi.Quantity - src.OrderQty <= 0 \r\n" + + * " THEN DELETE \r\n" + " WHEN MATCHED \r\n" + " THEN UPDATE SET pi.Quantity = pi.Quantity - src.OrderQty \r\n" + * + " OUTPUT $action, deleted.ProductID) AS Changes (Action, ProductID) \r\n" + "WHERE Action = 'DELETE'; ", + * "Production . ZeroInventory,(MERGE Production . ProductInventory AS pi USING (SELECT ProductID , SUM (OrderQty )FROM Sales . SalesOrderDetail AS sod JOIN Sales . SalesOrderHeader AS soh ON sod . SalesOrderID = soh . SalesOrderID AND soh . OrderDate = '20070401' GROUP BY ProductID )AS src (ProductID , OrderQty )ON (pi . ProductID = src . ProductID )WHEN MATCHED AND pi . Quantity - src . OrderQty <= 0 THEN DELETE WHEN MATCHED THEN UPDATE SET pi . Quantity = pi . Quantity - src . OrderQty OUTPUT $ action , deleted . ProductID ) AS Changes (Action , ProductID )" + * )); + */ + + // W. Inserting data using the SELECT option + ParserUtils.compareTableName( + "INSERT INTO EmployeeTitles \r\n" + " SELECT EmployeeKey, LastName, Title \r\n" + + " FROM ssawPDW.dbo.DimEmployee \r\n" + " WHERE EndDate IS NULL; ", + "EmployeeTitles,ssawPDW . dbo . DimEmployee"); + + // X. Specifying a label with the INSERT statement + ParserUtils.compareTableName("INSERT INTO DimCurrency \r\n" + "VALUES (500, N'C1', N'Currency1') \r\n" + + "OPTION ( LABEL = N'label1' ); ", "DimCurrency"); + + // Y. Using a label and a query hint with the INSERT statement + ParserUtils.compareTableName("INSERT INTO DimCustomer (CustomerKey, CustomerAlternateKey, \r\n" + + " FirstName, MiddleName, LastName ) \r\n" + + "SELECT ProspectiveBuyerKey, ProspectAlternateKey, \r\n" + " FirstName, MiddleName, LastName \r\n" + + "FROM ProspectiveBuyer p JOIN DimGeography g ON p.PostalCode = g.PostalCode \r\n" + + "WHERE g.CountryRegionCode = 'FR' \r\n" + "OPTION ( LABEL = 'Add French Prospects', HASH JOIN);", + "DimCustomer,ProspectiveBuyer p JOIN DimGeography g ON p . PostalCode = g . PostalCode"); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/LexerTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/LexerTest.java new file mode 100644 index 000000000..167068917 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/LexerTest.java @@ -0,0 +1,170 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ParserUtils; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class LexerTest extends AbstractTest { + + /* + * A collection of Common Table Expression T-SQL statements from the Microsoft docs. + * @link + * https://docs.microsoft.com/en-us/sql/t-sql/queries/with-common-table-expression-transact-sql?view=sql-server-2017 + */ + @Test + public void testCTE() { + // A. Creating a simple common table expression + ParserUtils.compareCommonTableExpression("-- Define the CTE expression name and column list. \r\n" + + "WITH Sales_CTE (SalesPersonID, SalesOrderID, SalesYear) \r\n" + "AS \r\n" + + "-- Define the CTE query. \r\n" + "( \r\n" + + " SELECT SalesPersonID, SalesOrderID, YEAR(OrderDate) AS SalesYear \r\n" + + " FROM Sales.SalesOrderHeader \r\n" + " WHERE SalesPersonID IS NOT NULL \r\n" + ") \r\n" + + "-- Define the outer query referencing the CTE name. \r\n" + + "SELECT SalesPersonID, COUNT(SalesOrderID) AS TotalSales, SalesYear \r\n" + "FROM Sales_CTE \r\n" + + "GROUP BY SalesYear, SalesPersonID \r\n" + "ORDER BY SalesPersonID, SalesYear; ", + "WITH Sales_CTE (SalesPersonID , SalesOrderID , SalesYear ) AS ( SELECT SalesPersonID , SalesOrderID , YEAR ( OrderDate ) AS SalesYear FROM Sales . SalesOrderHeader WHERE SalesPersonID IS NOT NULL )"); + + // B. Using a common table expression to limit counts and report averages + ParserUtils.compareCommonTableExpression("WITH Sales_CTE (SalesPersonID, NumberOfOrders) \r\n" + "AS \r\n" + + "( \r\n" + " SELECT SalesPersonID, COUNT(*) \r\n" + " FROM Sales.SalesOrderHeader \r\n" + + " WHERE SalesPersonID IS NOT NULL \r\n" + " GROUP BY SalesPersonID \r\n" + ") \r\n" + + "SELECT AVG(NumberOfOrders) AS \"Average Sales Per Person\" \r\n" + "FROM Sales_CTE; \r\n" + "", + "WITH Sales_CTE (SalesPersonID , NumberOfOrders ) AS ( SELECT SalesPersonID , COUNT ( * ) FROM Sales . SalesOrderHeader WHERE SalesPersonID IS NOT NULL GROUP BY SalesPersonID )"); + + // C. Using multiple CTE definitions in a single query + ParserUtils.compareCommonTableExpression("WITH Sales_CTE (SalesPersonID, TotalSales, SalesYear) \r\n" + + "AS \r\n" + "-- Define the first CTE query. \r\n" + "( \r\n" + + " SELECT SalesPersonID, SUM(TotalDue) AS TotalSales, YEAR(OrderDate) AS SalesYear \r\n" + + " FROM Sales.SalesOrderHeader \r\n" + " WHERE SalesPersonID IS NOT NULL \r\n" + + " GROUP BY SalesPersonID, YEAR(OrderDate) \r\n" + " \r\n" + ") \r\n" + + ", -- Use a comma to separate multiple CTE definitions. \r\n" + " \r\n" + + "-- Define the second CTE query, which returns sales quota data by year for each sales person. \r\n" + + "Sales_Quota_CTE (BusinessEntityID, SalesQuota, SalesQuotaYear) \r\n" + "AS \r\n" + "( \r\n" + + " SELECT BusinessEntityID, SUM(SalesQuota)AS SalesQuota, YEAR(QuotaDate) AS SalesQuotaYear \r\n" + + " FROM Sales.SalesPersonQuotaHistory \r\n" + + " GROUP BY BusinessEntityID, YEAR(QuotaDate) \r\n" + ") \r\n" + " \r\n" + + "-- Define the outer query by referencing columns from both CTEs. \r\n" + + "SELECT SalesPersonID \r\n" + " , SalesYear \r\n" + + " , FORMAT(TotalSales,'C','en-us') AS TotalSales \r\n" + " , SalesQuotaYear \r\n" + + " , FORMAT (SalesQuota,'C','en-us') AS SalesQuota \r\n" + + " , FORMAT (TotalSales -SalesQuota, 'C','en-us') AS Amt_Above_or_Below_Quota \r\n" + + "FROM Sales_CTE \r\n" + + "JOIN Sales_Quota_CTE ON Sales_Quota_CTE.BusinessEntityID = Sales_CTE.SalesPersonID \r\n" + + " AND Sales_CTE.SalesYear = Sales_Quota_CTE.SalesQuotaYear \r\n" + + "ORDER BY SalesPersonID, SalesYear;", + "WITH Sales_CTE (SalesPersonID , TotalSales , SalesYear ) AS ( SELECT SalesPersonID , SUM ( TotalDue ) AS TotalSales , YEAR ( OrderDate ) AS SalesYear FROM Sales . SalesOrderHeader WHERE SalesPersonID IS NOT NULL GROUP BY SalesPersonID , YEAR ( OrderDate ) ) , Sales_Quota_CTE (BusinessEntityID , SalesQuota , SalesQuotaYear ) AS ( SELECT BusinessEntityID , SUM ( SalesQuota ) AS SalesQuota , YEAR ( QuotaDate ) AS SalesQuotaYear FROM Sales . SalesPersonQuotaHistory GROUP BY BusinessEntityID , YEAR ( QuotaDate ) )"); + + // D. Using a recursive common table expression to display multiple levels of recursion + ParserUtils.compareCommonTableExpression( + "WITH DirectReports(ManagerID, EmployeeID, Title, EmployeeLevel) AS \r\n" + "( \r\n" + + " SELECT ManagerID, EmployeeID, Title, 0 AS EmployeeLevel \r\n" + + " FROM dbo.MyEmployees \r\n" + " WHERE ManagerID IS NULL \r\n" + + " UNION ALL \r\n" + + " SELECT e.ManagerID, e.EmployeeID, e.Title, EmployeeLevel + 1 \r\n" + + " FROM dbo.MyEmployees AS e \r\n" + " INNER JOIN DirectReports AS d \r\n" + + " ON e.ManagerID = d.EmployeeID \r\n" + ") \r\n" + + "SELECT ManagerID, EmployeeID, Title, EmployeeLevel \r\n" + "FROM DirectReports \r\n" + + "ORDER BY ManagerID;", + "WITH DirectReports (ManagerID , EmployeeID , Title , EmployeeLevel ) AS ( SELECT ManagerID , EmployeeID , Title , 0 AS EmployeeLevel FROM dbo . MyEmployees WHERE ManagerID IS NULL UNION ALL SELECT e . ManagerID , e . EmployeeID , e . Title , EmployeeLevel + 1 FROM dbo . MyEmployees AS e INNER JOIN DirectReports AS d ON e . ManagerID = d . EmployeeID )"); + + // E. Using a recursive common table expression to display two levels of recursion + ParserUtils.compareCommonTableExpression( + "WITH DirectReports(ManagerID, EmployeeID, Title, EmployeeLevel) AS \r\n" + "( \r\n" + + " SELECT ManagerID, EmployeeID, Title, 0 AS EmployeeLevel \r\n" + + " FROM dbo.MyEmployees \r\n" + " WHERE ManagerID IS NULL \r\n" + + " UNION ALL \r\n" + + " SELECT e.ManagerID, e.EmployeeID, e.Title, EmployeeLevel + 1 \r\n" + + " FROM dbo.MyEmployees AS e \r\n" + " INNER JOIN DirectReports AS d \r\n" + + " ON e.ManagerID = d.EmployeeID \r\n" + ") \r\n" + + "SELECT ManagerID, EmployeeID, Title, EmployeeLevel \r\n" + "FROM DirectReports \r\n" + + "WHERE EmployeeLevel <= 2 ;", + "WITH DirectReports (ManagerID , EmployeeID , Title , EmployeeLevel ) AS ( SELECT ManagerID , EmployeeID , Title , 0 AS EmployeeLevel FROM dbo . MyEmployees WHERE ManagerID IS NULL UNION ALL SELECT e . ManagerID , e . EmployeeID , e . Title , EmployeeLevel + 1 FROM dbo . MyEmployees AS e INNER JOIN DirectReports AS d ON e . ManagerID = d . EmployeeID )"); + + // G. Using MAXRECURSION to cancel a statement + ParserUtils.compareCommonTableExpression( + "WITH cte (EmployeeID, ManagerID, Title) \r\n" + "AS \r\n" + "( \r\n" + + " SELECT EmployeeID, ManagerID, Title \r\n" + " FROM dbo.MyEmployees \r\n" + + " WHERE ManagerID IS NOT NULL \r\n" + " UNION ALL \r\n" + + " SELECT e.EmployeeID, e.ManagerID, e.Title \r\n" + " FROM dbo.MyEmployees AS e \r\n" + + " JOIN cte ON e.ManagerID = cte.EmployeeID \r\n" + ") \r\n" + + "SELECT EmployeeID, ManagerID, Title \r\n" + "FROM cte;", + "WITH cte (EmployeeID , ManagerID , Title ) AS ( SELECT EmployeeID , ManagerID , Title FROM dbo . MyEmployees WHERE ManagerID IS NOT NULL UNION ALL SELECT e . EmployeeID , e . ManagerID , e . Title FROM dbo . MyEmployees AS e JOIN cte ON e . ManagerID = cte . EmployeeID )"); + + // H. Using a common table expression to selectively step through a recursive relationship in a SELECT statement + ParserUtils.compareCommonTableExpression( + "WITH Parts(AssemblyID, ComponentID, PerAssemblyQty, EndDate, ComponentLevel) AS \r\n" + "( \r\n" + + " SELECT b.ProductAssemblyID, b.ComponentID, b.PerAssemblyQty, \r\n" + + " b.EndDate, 0 AS ComponentLevel \r\n" + + " FROM Production.BillOfMaterials AS b \r\n" + " WHERE b.ProductAssemblyID = 800 \r\n" + + " AND b.EndDate IS NULL \r\n" + " UNION ALL \r\n" + + " SELECT bom.ProductAssemblyID, bom.ComponentID, p.PerAssemblyQty, \r\n" + + " bom.EndDate, ComponentLevel + 1 \r\n" + + " FROM Production.BillOfMaterials AS bom \r\n" + " INNER JOIN Parts AS p \r\n" + + " ON bom.ProductAssemblyID = p.ComponentID \r\n" + + " AND bom.EndDate IS NULL \r\n" + ") \r\n" + + "SELECT AssemblyID, ComponentID, Name, PerAssemblyQty, EndDate, \r\n" + + " ComponentLevel \r\n" + "FROM Parts AS p \r\n" + + " INNER JOIN Production.Product AS pr \r\n" + " ON p.ComponentID = pr.ProductID \r\n" + + "ORDER BY ComponentLevel, AssemblyID, ComponentID;", + "WITH Parts (AssemblyID , ComponentID , PerAssemblyQty , EndDate , ComponentLevel ) AS ( SELECT b . ProductAssemblyID , b . ComponentID , b . PerAssemblyQty , b . EndDate , 0 AS ComponentLevel FROM Production . BillOfMaterials AS b WHERE b . ProductAssemblyID = 800 AND b . EndDate IS NULL UNION ALL SELECT bom . ProductAssemblyID , bom . ComponentID , p . PerAssemblyQty , bom . EndDate , ComponentLevel + 1 FROM Production . BillOfMaterials AS bom INNER JOIN Parts AS p ON bom . ProductAssemblyID = p . ComponentID AND bom . EndDate IS NULL )"); + + // I. Using a recursive CTE in an UPDATE statement + ParserUtils.compareCommonTableExpression( + "WITH Parts(AssemblyID, ComponentID, PerAssemblyQty, EndDate, ComponentLevel) AS \r\n" + "( \r\n" + + " SELECT b.ProductAssemblyID, b.ComponentID, b.PerAssemblyQty, \r\n" + + " b.EndDate, 0 AS ComponentLevel \r\n" + + " FROM Production.BillOfMaterials AS b \r\n" + " WHERE b.ProductAssemblyID = 800 \r\n" + + " AND b.EndDate IS NULL \r\n" + " UNION ALL \r\n" + + " SELECT bom.ProductAssemblyID, bom.ComponentID, p.PerAssemblyQty, \r\n" + + " bom.EndDate, ComponentLevel + 1 \r\n" + + " FROM Production.BillOfMaterials AS bom \r\n" + " INNER JOIN Parts AS p \r\n" + + " ON bom.ProductAssemblyID = p.ComponentID \r\n" + + " AND bom.EndDate IS NULL \r\n" + ") \r\n" + + "UPDATE Production.BillOfMaterials \r\n" + "SET PerAssemblyQty = c.PerAssemblyQty * 2 \r\n" + + "FROM Production.BillOfMaterials AS c \r\n" + + "JOIN Parts AS d ON c.ProductAssemblyID = d.AssemblyID \r\n" + "WHERE d.ComponentLevel = 0;", + "WITH Parts (AssemblyID , ComponentID , PerAssemblyQty , EndDate , ComponentLevel ) AS ( SELECT b . ProductAssemblyID , b . ComponentID , b . PerAssemblyQty , b . EndDate , 0 AS ComponentLevel FROM Production . BillOfMaterials AS b WHERE b . ProductAssemblyID = 800 AND b . EndDate IS NULL UNION ALL SELECT bom . ProductAssemblyID , bom . ComponentID , p . PerAssemblyQty , bom . EndDate , ComponentLevel + 1 FROM Production . BillOfMaterials AS bom INNER JOIN Parts AS p ON bom . ProductAssemblyID = p . ComponentID AND bom . EndDate IS NULL )"); + + // J. Using multiple anchor and recursive members + ParserUtils.compareCommonTableExpression("-- Create the recursive CTE to find all of Bonnie's ancestors. \r\n" + + "WITH Generation (ID) AS \r\n" + "( \r\n" + "-- First anchor member returns Bonnie's mother. \r\n" + + " SELECT Mother \r\n" + " FROM dbo.Person \r\n" + " WHERE Name = 'Bonnie' \r\n" + + "UNION \r\n" + "-- Second anchor member returns Bonnie's father. \r\n" + " SELECT Father \r\n" + + " FROM dbo.Person \r\n" + " WHERE Name = 'Bonnie' \r\n" + "UNION ALL \r\n" + + "-- First recursive member returns male ancestors of the previous generation. \r\n" + + " SELECT Person.Father \r\n" + " FROM Generation, Person \r\n" + + " WHERE Generation.ID=Person.ID \r\n" + "UNION ALL \r\n" + + "-- Second recursive member returns female ancestors of the previous generation. \r\n" + + " SELECT Person.Mother \r\n" + " FROM Generation, dbo.Person \r\n" + + " WHERE Generation.ID=Person.ID \r\n" + ") \r\n" + + "SELECT Person.ID, Person.Name, Person.Mother, Person.Father \r\n" + + "FROM Generation, dbo.Person \r\n" + "WHERE Generation.ID = Person.ID;", + "WITH Generation (ID ) AS ( SELECT Mother FROM dbo . Person WHERE Name = 'Bonnie' UNION SELECT Father FROM dbo . Person WHERE Name = 'Bonnie' UNION ALL SELECT Person . Father FROM Generation , Person WHERE Generation . ID = Person . ID UNION ALL SELECT Person . Mother FROM Generation , dbo . Person WHERE Generation . ID = Person . ID )"); + + // K. Using analytical functions in a recursive CTE + ParserUtils.compareCommonTableExpression("WITH vw AS \r\n" + " ( \r\n" + " SELECT itmIDComp, itmID \r\n" + + " FROM @t1 \r\n" + " \r\n" + " UNION ALL \r\n" + " \r\n" + + " SELECT itmIDComp, itmID \r\n" + " FROM @t2 \r\n" + ") \r\n" + ",r AS \r\n" + " ( \r\n" + + " SELECT t.itmID AS itmIDComp \r\n" + " , NULL AS itmID \r\n" + + " ,CAST(0 AS bigint) AS N \r\n" + " ,1 AS Lvl \r\n" + + " FROM (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) AS t (itmID) \r\n" + + " \r\n" + "UNION ALL \r\n" + " \r\n" + "SELECT t.itmIDComp \r\n" + " , t.itmID \r\n" + + " , ROW_NUMBER() OVER(PARTITION BY t.itmIDComp ORDER BY t.itmIDComp, t.itmID) AS N \r\n" + + " , Lvl + 1 \r\n" + "FROM r \r\n" + " JOIN vw AS t ON t.itmID = r.itmIDComp \r\n" + + ") \r\n" + " \r\n" + "SELECT Lvl, N FROM r;", + "WITH vw AS ( SELECT itmIDComp , itmID FROM @t1 UNION ALL SELECT itmIDComp , itmID FROM @t2 ) , r AS ( SELECT t . itmID AS itmIDComp , NULL AS itmID , CAST ( 0 AS bigint ) AS N , 1 AS Lvl FROM ( SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 ) AS t ( itmID ) UNION ALL SELECT t . itmIDComp , t . itmID , ROW_NUMBER ( ) OVER ( PARTITION BY t . itmIDComp ORDER BY t . itmIDComp , t . itmID ) AS N , Lvl + 1 FROM r JOIN vw AS t ON t . itmID = r . itmIDComp )"); + } + + @Test + public void testEmptyString() { + ParserUtils.compareTableName("", TestUtils.R_BUNDLE.getString("R_noTokensFoundInUserQuery")); + ParserUtils.compareTableName(null, TestUtils.R_BUNDLE.getString("R_noTokensFoundInUserQuery")); + } + +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java new file mode 100644 index 000000000..d39f1b962 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/ParameterMetaDataTest.java @@ -0,0 +1,186 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ParameterMetaData; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import com.microsoft.sqlserver.jdbc.TestResource; +import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.Constants; +import com.microsoft.sqlserver.testframework.PrepUtil; + + +public class ParameterMetaDataTest extends AbstractTest { + + private static final String tableName = "[test_jdbc_" + UUID.randomUUID() + "]"; + + @BeforeEach + public void setupTests() throws SQLException { + try (Connection c = getConnection(); Statement s = c.createStatement()) { + s.execute("CREATE TABLE " + tableName + + "(cBigint bigint, cNumeric numeric, cBit bit, cSmallint smallint, cDecimal decimal, " + + "cSmallmoney smallmoney, cInt int, cTinyint tinyint, cMoney money, cFloat float, cReal real, " + + "cDate date, cDatetimeoffset datetimeoffset, cDatetime2 datetime2, cSmalldatetime smalldatetime, " + + "cDatetime datetime, cTime time, cChar char, cVarchar varchar(8000), cNchar nchar, " + + "cNvarchar nvarchar(4000), cBinary binary, cVarbinary varbinary(8000))"); + } + } + + @AfterEach + public void cleanupTests() throws SQLException { + try (Connection c = getConnection(); Statement s = c.createStatement()) { + TestUtils.dropTableIfExists(tableName, s); + } + } + + @Test + @Tag(Constants.xAzureSQLDW) + public void compareStoredProcTest() throws SQLException { + List l = Arrays.asList("SELECT * FROM " + tableName + " WHERE cBigint > ?", + "SELECT TOP(20) PERCENT * FROM " + tableName + " WHERE cBigint > ?", + "SELECT TOP(20) * FROM " + tableName + " WHERE cFloat < ?", + "SELECT TOP 20 * FROM " + tableName + " WHERE cReal = ?", + "SELECT TOP(20) PERCENT WITH TIES * FROM " + tableName + + " WHERE cInt BETWEEN ? AND ? AND ? = cChar ORDER BY cDecimal ASC", + "INSERT " + tableName + " VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + "INSERT " + tableName + "(cInt,cVarchar,cNvarchar) VALUES(?,?,?)", + "INSERT " + tableName + "(cInt,cVarchar,cNvarchar) VALUES(?,?,?),(?,?,?),(?,?,?)", + "DELETE " + tableName + " WHERE cFloat >= ? AND ? <= cInt", + "DELETE FROM " + tableName + " WHERE cReal BETWEEN ? AND ?;", + "DELETE " + tableName + " WHERE cFloat IN (SELECT cFloat FROM " + tableName + " WHERE cReal = ?)", + "DELETE " + tableName + " WHERE cFloat IN (SELECT cFloat FROM " + tableName + " t WHERE t.cReal = ?)", + "UPDATE TOP (10) " + tableName + " SET cInt = cInt + ?;", + "UPDATE TOP (10) " + tableName + " SET cInt = cInt + ? FROM (SELECT TOP 10 cFloat,cReal,cNvarchar FROM " + + tableName + " ORDER BY cFloat ASC) AS b WHERE b.cReal > ? AND b.cNvarchar LIKE ?", + "WITH t1(cInt,cReal,cVarchar,cMoney) AS (SELECT cInt,cReal,cVarchar,cMoney FROM " + tableName + + ") UPDATE a SET a.cInt = ? * a.cInt FROM " + tableName + + " AS a JOIN t1 AS b ON a.cInt = b.cInt WHERE b.cInt = ?", + "SELECT cInt FROM " + tableName + " WHERE ? = (cInt + (3 - 5))", + "SELECT cInt FROM " + tableName + " WHERE (cInt + (3 - 5)) = ?", + "SELECT cInt FROM " + tableName + " WHERE " + tableName + ".[cInt] = ?", + "SELECT cInt FROM " + tableName + " WHERE ? = " + tableName + ".[cInt]", + "WITH t1(cInt) AS (SELECT 1), t2(cInt) AS (SELECT 2) SELECT * FROM t1 JOIN t2 ON [t1].\"cInt\" = \"t2\".[cInt] WHERE \"t1\".[cInt] = [t2].\"cInt\" + ?", + "INSERT INTO " + tableName + "(cInt,cFloat) SELECT 1,1.5 WHERE 1 > ?", + "WITH t1(cInt) AS (SELECT 1), t2(cInt) AS (SELECT 2), t3(cInt) AS (SELECT 3) SELECT * FROM t1,t2,t3 WHERE t1.cInt >= ?", + "SELECT (1),2,[cInt],\"cFloat\" FROM " + tableName + " WHERE cNvarchar LIKE ?", + "WITH t1(cInt) AS (SELECT 1) SELECT * FROM \"t1\""); + l.forEach(this::compareFmtAndSp); + } + + @Test + public void noStoredProcTest() throws SQLException { + List l = Arrays.asList("SELECT cMoney FROM " + tableName + " WHERE cMoney > $?", + "SELECT cVarchar FROM " + tableName + " WHERE " + tableName + ".[cVarchar] NOT LIKE ?", + "SELECT cVarchar FROM " + tableName + " WHERE ? NOT LIKE " + tableName + ".[cVarchar]", + "INSERT " + tableName + "(cInt) VALUES((1+?)),(3*(?+3))", "SELECT ?"); + l.forEach(this::executeFmt); + } + + @Test + public void exceptionTest() throws SQLException { + executeInvalidFmt("SELECT FROM OPENQUERY INVALID TSQL", "R_invalidOpenqueryCall"); + executeInvalidFmt("INSERT INTO OPENXML INVALID TSQL VALUES (?,?,?)", "R_invalidOpenqueryCall"); + executeInvalidFmt("WITH INVALID_CTE AS SELECT * FROM FOO", "R_invalidCTEFormat"); + } + + @Test + public void tempTableTest() throws SQLException { + String tempTableName = "[#jdbc_temp" + UUID.randomUUID() + "]"; + try (Connection c = PrepUtil.getConnection(AbstractTest.connectionString + ";useFmtOnly=true;"); + Statement s = c.createStatement()) { + TestUtils.dropTableIfExists(tempTableName, s); + s.execute("CREATE TABLE " + tempTableName + " (c1 int)"); + try (PreparedStatement p = c.prepareStatement("SELECT * FROM " + tempTableName + " WHERE c1 = ?")) { + ParameterMetaData pmd = p.getParameterMetaData(); + assertTrue(pmd.getParameterCount() == 1); + } + } finally { + try (Statement s = connection.createStatement()) { + TestUtils.dropTableIfExists(tempTableName, s); + } + } + } + + @Test + public void viewTest() throws SQLException { + String tempViewName = "[jdbc_view" + UUID.randomUUID() + "]"; + try (Connection c = PrepUtil.getConnection(AbstractTest.connectionString + ";useFmtOnly=true;"); + Statement s = c.createStatement()) { + TestUtils.dropViewIfExists(tempViewName, s); + s.execute("CREATE VIEW " + tempViewName + " AS SELECT cBigInt FROM" + tableName); + try (PreparedStatement p = c.prepareStatement("SELECT * FROM " + tempViewName + " WHERE cBigInt = ?")) { + ParameterMetaData pmd = p.getParameterMetaData(); + assertTrue(pmd.getParameterCount() == 1); + } + } finally { + try (Statement s = connection.createStatement()) { + TestUtils.dropViewIfExists(tempViewName, s); + } + } + } + + private void executeFmt(String userSQL) { + try (Connection c = PrepUtil.getConnection(AbstractTest.connectionString + ";useFmtOnly=true;"); + PreparedStatement pstmt = c.prepareStatement(userSQL)) { + ParameterMetaData pmd = pstmt.getParameterMetaData(); + for (int i = 1; i <= pmd.getParameterCount(); i++) { + pmd.getParameterClassName(i); + pmd.getParameterMode(i); + pmd.getParameterType(i); + pmd.getParameterTypeName(i); + pmd.getPrecision(i); + pmd.getScale(i); + } + } catch (SQLException e) { + fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); + } + } + + private void executeInvalidFmt(String userSQL, String expectedError) { + + try (Connection c = PrepUtil.getConnection(AbstractTest.connectionString + ";useFmtOnly=true;"); + PreparedStatement pstmt = c.prepareStatement(userSQL)) { + pstmt.getParameterMetaData(); + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (SQLException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg(expectedError))); + } + } + + private void compareFmtAndSp(String userSQL) { + try (Connection c = getConnection(); + Connection c2 = PrepUtil.getConnection(AbstractTest.connectionString + ";useFmtOnly=true;"); + PreparedStatement stmt1 = c.prepareStatement(userSQL); + PreparedStatement stmt2 = c2.prepareStatement(userSQL)) { + ParameterMetaData pmd1 = stmt1.getParameterMetaData(); + ParameterMetaData pmd2 = stmt2.getParameterMetaData(); + assertEquals(pmd1.getParameterCount(), pmd2.getParameterCount()); + for (int i = 1; i <= pmd1.getParameterCount(); i++) { + assertEquals(pmd1.getParameterClassName(i), pmd2.getParameterClassName(i)); + assertEquals(pmd1.getParameterMode(i), pmd2.getParameterMode(i)); + assertEquals(pmd1.getParameterType(i), pmd2.getParameterType(i)); + assertEquals(pmd1.getParameterTypeName(i), pmd2.getParameterTypeName(i)); + assertEquals(pmd1.getPrecision(i), pmd2.getPrecision(i)); + assertEquals(pmd1.getScale(i), pmd2.getScale(i)); + } + } catch (SQLException e) { + fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); + } + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/SelectTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/SelectTest.java new file mode 100644 index 000000000..eef90ab6e --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/SelectTest.java @@ -0,0 +1,233 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ParserUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class SelectTest extends AbstractTest { + + @Test + public void basicSelectTest() { + // minor case sensitivity checking + ParserUtils.compareTableName("select * from jdbctest", "jdbctest"); + ParserUtils.compareTableName("SelECt * fRom jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT c1, c2 FROM jdbctest, jdbctest2;", "jdbctest , jdbctest2"); + + // escape sequence + ParserUtils.compareTableName("select * from [jdbctest]]]", "[jdbctest]]]"); + ParserUtils.compareTableName("select * from [jdb]]ctest]", "[jdb]]ctest]"); + ParserUtils.compareTableName("select * from [j[db]]ctest]", "[j[db]]ctest]"); + + // basic cases + ParserUtils.compareTableName("SELECT * FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM jdbctest;", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM /*hello this is a comment*/jdbctest;", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM jdbctest ORDER BY blah...", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM jdbctest WHERE blah...", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM jdbctest HAVING blah...", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM jdbctest OPTION blah...", "jdbctest"); + ParserUtils.compareTableName("SELECT * FROM jdbctest GROUP BY blah...", "jdbctest"); + + // double quote literal + ParserUtils.compareTableName("SELECT * FROM \"jdbc test\"", "\"jdbc test\""); + ParserUtils.compareTableName("SELECT * FROM \"jdbc /*test*/\"", "\"jdbc /*test*/\""); + ParserUtils.compareTableName("SELECT * FROM \"jdbc //test\"", "\"jdbc //test\""); + ParserUtils.compareTableName("SELECT * FROM \"dbo\".\"jdbcDB\".\"jdbctest\"", + "\"dbo\" . \"jdbcDB\" . \"jdbctest\""); + ParserUtils.compareTableName("SELECT * FROM \"jdbctest\"", "\"jdbctest\""); + + // square bracket literal + ParserUtils.compareTableName("SELECT * FROM [jdbctest]", "[jdbctest]"); + ParserUtils.compareTableName("SELECT * FROM [dbo].[jdbcDB].[jdbctest]", "[dbo] . [jdbcDB] . [jdbctest]"); + ParserUtils.compareTableName("SELECT * FROM [dbo].\"jdbcDB\".\"jdbctest\"", + "[dbo] . \"jdbcDB\" . \"jdbctest\""); + ParserUtils.compareTableName("SELECT * FROM [jdbc test]", "[jdbc test]"); + ParserUtils.compareTableName("SELECT * FROM [jdbc /*test*/]", "[jdbc /*test*/]"); + ParserUtils.compareTableName("SELECT * FROM [jdbc //test]", "[jdbc //test]"); + + // with parameters + ParserUtils.compareTableName("SELECT c1,c2,c3 FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT (c1,c2,c3) FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT ?,?,? FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT (c1,?,c3) FROM jdbctest", "jdbctest"); + + // with special parameters + ParserUtils.compareTableName("SELECT [c1],\"c2\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT [c1]]],\"c2\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT [c]]1],\"c2\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT [c1],\"[c2]\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT [\"c1],\"FROM\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT [\"c\"1\"],\"c2\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT ['FROM'1)],\"c2\" FROM jdbctest", "jdbctest"); + ParserUtils.compareTableName("SELECT [((c)1{}],\"{{c}2)(\" FROM jdbctest", "jdbctest"); + + // with sub queries + ParserUtils.compareTableName( + "SELECT t.*, a+b AS total_sum FROM (SELECT SUM(col1) as a, SUM(col2) AS b FROM table ORDER BY a ASC) t ORDER BY total_sum DSC", + "(SELECT SUM (col1 )as a , SUM (col2 )AS b FROM table ORDER BY a ASC ) t"); + ParserUtils.compareTableName("SELECT col1 FROM myTestInts UNION " + + "SELECT top 1 (select top 1 CONVERT(char(10), max(col1),121) a FROM myTestInts Order by a) FROM myTestInts Order by col1", + "myTestInts UNION SELECT top 1 (select top 1 CONVERT (char (10 ), max (col1 ), 121 )a FROM myTestInts Order by a ) FROM myTestInts"); + + // Multiple Selects + ParserUtils.compareTableName("SELECT * FROM table1;SELECT * FROM table2", "table1,table2"); + ParserUtils.compareTableName("SELECT * FROM table1;SELECT * FROM table1", "table1"); + } + + /* + * A collection of SELECT T-SQL statements from the Microsoft docs. + * @link https://docs.microsoft.com/en-us/sql/t-sql/queries/select-examples-transact-sql?view=sql-server-2017 + */ + @Test + public void selectExamplesTest() { + // A. Using SELECT to retrieve rows and columns + ParserUtils.compareTableName("SELECT * FROM Production.Product ORDER BY Name ASC;", "Production . Product"); + ParserUtils.compareTableName("SELECT p.* FROM Production.Product AS p ORDER BY Name ASC;", + "Production . Product AS p"); + ParserUtils.compareTableName( + "SELECT Name, ProductNumber, ListPrice AS Price FROM Production.Product ORDER BY Name ASC;", + "Production . Product"); + ParserUtils.compareTableName( + "SELECT Name, ProductNumber, ListPrice AS Price FROM Production.Product WHERE ProductLine = 'R' AND DaysToManufacture < 4 ORDER BY Name ASC;", + "Production . Product"); + // B. Using SELECT with column headings and calculations + ParserUtils.compareTableName( + "SELECT p.Name AS ProductName, NonDiscountSales = (OrderQty * UnitPrice), Discounts = ((OrderQty * UnitPrice) * UnitPriceDiscount) FROM Production.Product AS p INNER JOIN Sales.SalesOrderDetail AS sod ON p.ProductID = sod.ProductID ORDER BY ProductName DESC;", + "Production . Product AS p INNER JOIN Sales . SalesOrderDetail AS sod ON p . ProductID = sod . ProductID"); + ParserUtils.compareTableName( + "SELECT 'Total income is', ((OrderQty * UnitPrice) * (1.0 - UnitPriceDiscount)), ' for ', p.Name AS ProductName FROM Production.Product AS p INNER JOIN Sales.SalesOrderDetail AS sod ON p.ProductID = sod.ProductID ORDER BY ProductName ASC;", + "Production . Product AS p INNER JOIN Sales . SalesOrderDetail AS sod ON p . ProductID = sod . ProductID"); + // C. Using DISTINCT with SELECT + ParserUtils.compareTableName("SELECT DISTINCT JobTitle FROM HumanResources.Employee ORDER BY JobTitle;", + "HumanResources . Employee"); + // D. Creating tables with SELECT INTO + ParserUtils.compareTableName("SELECT * INTO #Bicycles FROM AdventureWorks2012.Production.Product " + + "WHERE ProductNumber LIKE 'BK%';", "AdventureWorks2012 . Production . Product"); + ParserUtils.compareTableName("SELECT * INTO dbo.NewProducts FROM Production.Product " + + "WHERE ListPrice > $25 AND ListPrice < $100;", "Production . Product"); + // E. Using correlated subqueries + ParserUtils.compareTableName( + "SELECT DISTINCT Name\r\n" + "FROM Production.Product AS p WHERE EXISTS " + + " (SELECT * FROM Production.ProductModel AS pm " + + " WHERE p.ProductModelID = pm.ProductModelID " + + " AND pm.Name LIKE 'Long-Sleeve Logo Jersey%');", + "Production . Product AS p,Production . ProductModel AS pm"); + ParserUtils.compareTableName( + "SELECT DISTINCT Name FROM Production.Product WHERE ProductModelID IN " + + " (SELECT ProductModelID FROM Production.ProductModel " + + " WHERE Name LIKE 'Long-Sleeve Logo Jersey%');", + "Production . Product,Production . ProductModel"); + ParserUtils.compareTableName( + "SELECT DISTINCT p.LastName, p.FirstName FROM Person.Person AS p " + + "JOIN HumanResources.Employee AS e " + + "ON e.BusinessEntityID = p.BusinessEntityID WHERE 5000.00 IN (SELECT Bonus " + + "FROM Sales.SalesPerson AS sp WHERE e.BusinessEntityID = sp.BusinessEntityID);", + "Person . Person AS p JOIN HumanResources . Employee AS e ON e . BusinessEntityID = p . BusinessEntityID,Sales . SalesPerson AS sp"); + // F. Using GROUP BY + ParserUtils.compareTableName("SELECT SalesOrderID, SUM(LineTotal) AS SubTotal FROM Sales.SalesOrderDetail " + + "GROUP BY SalesOrderID ORDER BY SalesOrderID;", "Sales . SalesOrderDetail"); + // G. Using GROUP BY with multiple groups + ParserUtils.compareTableName("SELECT ProductID, SpecialOfferID, AVG(UnitPrice) AS [Average Price], " + + "SUM(LineTotal) AS SubTotal FROM Sales.SalesOrderDetail " + "GROUP BY ProductID, SpecialOfferID" + + "ORDER BY ProductID;", "Sales . SalesOrderDetail"); + // H. Using GROUP BY and WHERE + ParserUtils.compareTableName( + "SELECT ProductModelID, AVG(ListPrice) AS [Average List Price] FROM Production.Product " + + "WHERE ListPrice > $1000 GROUP BY ProductModelID ORDER BY ProductModelID;", + "Production . Product"); + // I. Using GROUP BY with an expression + ParserUtils.compareTableName( + "SELECT AVG(OrderQty) AS [Average Quantity], " + + "NonDiscountSales = (OrderQty * UnitPrice) FROM Sales.SalesOrderDetail " + + "GROUP BY (OrderQty * UnitPrice) ORDER BY (OrderQty * UnitPrice) DESC;", + "Sales . SalesOrderDetail"); + // J. Using GROUP BY with ORDER BY + ParserUtils.compareTableName( + "SELECT ProductID, AVG(UnitPrice) AS [Average Price] FROM Sales.SalesOrderDetail " + + "WHERE OrderQty > 10 GROUP BY ProductID ORDER BY AVG(UnitPrice);", + "Sales . SalesOrderDetail"); + // K. Using the HAVING clause + ParserUtils.compareTableName( + "SELECT ProductID FROM Sales.SalesOrderDetail GROUP BY ProductID HAVING AVG(OrderQty) > 5 ORDER BY ProductID", + "Sales . SalesOrderDetail"); + ParserUtils.compareTableName( + "SELECT SalesOrderID, CarrierTrackingNumber FROM Sales.SalesOrderDetail " + + "GROUP BY SalesOrderID, CarrierTrackingNumber " + + "HAVING CarrierTrackingNumber LIKE '4BD%' ORDER BY SalesOrderID ; ", + "Sales . SalesOrderDetail"); + // L. Using HAVING and GROUP BY + ParserUtils.compareTableName( + "SELECT ProductID FROM Sales.SalesOrderDetail WHERE UnitPrice < 25.00 " + + "GROUP BY ProductID HAVING AVG(OrderQty) > 5 ORDER BY ProductID;", + "Sales . SalesOrderDetail"); + // M. Using HAVING with SUM and AVG + ParserUtils.compareTableName( + "SELECT ProductID, AVG(OrderQty) AS AverageQuantity, SUM(LineTotal) AS Total\r\n" + + "FROM Sales.SalesOrderDetail\r\n" + "GROUP BY ProductID\r\n" + + "HAVING SUM(LineTotal) > $1000000.00\r\n" + "AND AVG(OrderQty) < 3;", + "Sales . SalesOrderDetail"); + ParserUtils.compareTableName( + "SELECT ProductID, Total = SUM(LineTotal)\r\n" + "FROM Sales.SalesOrderDetail\r\n" + + "GROUP BY ProductID\r\n" + "HAVING SUM(LineTotal) > $2000000.00;", + "Sales . SalesOrderDetail"); + ParserUtils.compareTableName("SELECT ProductID, SUM(LineTotal) AS Total\r\n" + "FROM Sales.SalesOrderDetail\r\n" + + "GROUP BY ProductID\r\n" + "HAVING COUNT(*) > 1500;", "Sales . SalesOrderDetail"); + // N. Using the INDEX optimizer hint + ParserUtils.compareTableName( + "SELECT pp.FirstName, pp.LastName, e.NationalIDNumber " + + "FROM HumanResources.Employee AS e WITH (INDEX(AK_Employee_NationalIDNumber)) " + + "JOIN Person.Person AS pp on e.BusinessEntityID = pp.BusinessEntityID " + + "WHERE LastName = 'Johnson';", + "HumanResources . Employee AS e WITH (INDEX (AK_Employee_NationalIDNumber )) JOIN Person . Person AS pp on e . BusinessEntityID = pp . BusinessEntityID"); + ParserUtils.compareTableName( + "SELECT pp.LastName, pp.FirstName, e.JobTitle " + + "FROM HumanResources.Employee AS e WITH (INDEX = 0) JOIN Person.Person AS pp " + + "ON e.BusinessEntityID = pp.BusinessEntityID WHERE LastName = 'Johnson';", + "HumanResources . Employee AS e WITH (INDEX = 0 ) JOIN Person . Person AS pp ON e . BusinessEntityID = pp . BusinessEntityID"); + // M. Using OPTION and the GROUP hints + ParserUtils.compareTableName("SELECT ProductID, OrderQty, SUM(LineTotal) AS Total FROM Sales.SalesOrderDetail " + + "WHERE UnitPrice < $5.00 GROUP BY ProductID, OrderQty " + + "ORDER BY ProductID, OrderQty OPTION (HASH GROUP, FAST 10);", "Sales . SalesOrderDetail"); + // O. Using the UNION query hint + ParserUtils.compareTableName( + "SELECT BusinessEntityID, JobTitle, HireDate, VacationHours, SickLeaveHours " + + "FROM HumanResources.Employee AS e1 UNION " + + "SELECT BusinessEntityID, JobTitle, HireDate, VacationHours, SickLeaveHours " + + "FROM HumanResources.Employee AS e2 OPTION (MERGE UNION);", + "HumanResources . Employee AS e1 UNION SELECT BusinessEntityID , JobTitle , HireDate , VacationHours , SickLeaveHours FROM HumanResources . Employee AS e2"); + // P. Using a simple UNION + ParserUtils.compareTableName("SELECT ProductModelID, Name FROM Production.ProductModel " + + "WHERE ProductModelID NOT IN (3, 4) UNION SELECT ProductModelID, Name " + + "FROM dbo.Gloves ORDER BY Name;", "Production . ProductModel,dbo . Gloves"); + + // Q. Using SELECT INTO with UNION + ParserUtils.compareTableName("SELECT ProductModelID, Name INTO dbo.ProductResults " + + "FROM Production.ProductModel WHERE ProductModelID NOT IN (3, 4) UNION " + + "SELECT ProductModelID, Name FROM dbo.Gloves;", "Production . ProductModel,dbo . Gloves"); + // R. Using UNION of two SELECT statements with ORDER BY* + ParserUtils.compareTableName("SELECT ProductModelID, Name FROM Production.ProductModel " + + "WHERE ProductModelID NOT IN (3, 4) UNION SELECT ProductModelID, Name " + + "FROM dbo.Gloves ORDER BY Name;", "Production . ProductModel,dbo . Gloves"); + // S. Using UNION of three SELECT statements to show the effects of ALL and parentheses + ParserUtils.compareTableName( + "SELECT LastName, FirstName, JobTitle FROM dbo.EmployeeOne UNION ALL " + + "SELECT LastName, FirstName ,JobTitle FROM dbo.EmployeeTwo UNION ALL " + + "SELECT LastName, FirstName,JobTitle FROM dbo.EmployeeThree;", + "dbo . EmployeeOne UNION ALL SELECT LastName , FirstName , JobTitle FROM dbo . EmployeeTwo UNION ALL SELECT LastName , FirstName , JobTitle FROM dbo . EmployeeThree"); + ParserUtils.compareTableName( + "SELECT LastName, FirstName,JobTitle FROM dbo.EmployeeOne UNION " + + "SELECT LastName, FirstName, JobTitle FROM dbo.EmployeeTwo UNION " + + "SELECT LastName, FirstName, JobTitle FROM dbo.EmployeeThree;", + "dbo . EmployeeOne UNION SELECT LastName , FirstName , JobTitle FROM dbo . EmployeeTwo UNION SELECT LastName , FirstName , JobTitle FROM dbo . EmployeeThree"); + ParserUtils.compareTableName( + "SELECT LastName, FirstName,JobTitle FROM [dbo].[EmployeeOne] UNION ALL (" + + "SELECT LastName, FirstName, JobTitle FROM dbo.EmployeeTwo UNION " + + "SELECT LastName, FirstName, JobTitle FROM dbo.EmployeeThree);", + "[dbo] . [EmployeeOne] UNION ALL (SELECT LastName , FirstName , JobTitle FROM dbo . EmployeeTwo UNION SELECT LastName , FirstName , JobTitle FROM dbo . EmployeeThree )"); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/UpdateTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/UpdateTest.java new file mode 100644 index 000000000..d99c90405 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fmtOnly/UpdateTest.java @@ -0,0 +1,237 @@ +package com.microsoft.sqlserver.jdbc.fmtOnly; + +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; + +import com.microsoft.sqlserver.jdbc.ParserUtils; +import com.microsoft.sqlserver.testframework.AbstractTest; + + +@RunWith(JUnitPlatform.class) +public class UpdateTest extends AbstractTest { + + /* + * A collection of INSERT T-SQL statements from the Microsoft docs. + * @link https://docs.microsoft.com/en-us/sql/t-sql/queries/update-transact-sql?view=sql-server-2017#UpdateExamples + */ + @Test + public void updateExamplesTest() { + // A. Using DELETE with no WHERE clause + ParserUtils.compareTableName("UPDATE Person.Address \r\n" + "SET ModifiedDate = GETDATE(); ", + "Person . Address"); + + // B. Updating multiple columns + ParserUtils.compareTableName( + "UPDATE Sales.SalesPerson \r\n" + "SET Bonus = 6000, CommissionPct = .10, SalesQuota = NULL;", + "Sales . SalesPerson"); + + // C. Using the WHERE clause + ParserUtils.compareTableName("UPDATE Production.Product \r\n" + "SET Color = N'Metallic Red' \r\n" + + "WHERE Name LIKE N'Road-250%' AND Color = N'Red'; ", "Production . Product"); + + // D. Using the TOP clause + ParserUtils.compareTableName( + "UPDATE TOP (10) HumanResources.Employee\r\n" + "SET VacationHours = VacationHours * 1.25 ;", + "HumanResources . Employee"); + ParserUtils.compareTableName( + "UPDATE HumanResources.Employee \r\n" + "SET VacationHours = VacationHours + 8 \r\n" + + "FROM (SELECT TOP 10 BusinessEntityID FROM HumanResources.Employee \r\n" + + " ORDER BY HireDate ASC) AS th \r\n" + + "WHERE HumanResources.Employee.BusinessEntityID = th.BusinessEntityID; ", + "HumanResources . Employee,(SELECT TOP 10 BusinessEntityID FROM HumanResources . Employee ORDER BY HireDate ASC ) AS th"); + + // E. Using the WITH common_table_expression clause + ParserUtils.compareTableName( + "WITH Parts(AssemblyID, ComponentID, PerAssemblyQty, EndDate, ComponentLevel) AS \r\n" + "( \r\n" + + " SELECT b.ProductAssemblyID, b.ComponentID, b.PerAssemblyQty, \r\n" + + " b.EndDate, 0 AS ComponentLevel \r\n" + " FROM Production.BillOfMaterials AS b \r\n" + + " WHERE b.ProductAssemblyID = 800 \r\n" + " AND b.EndDate IS NULL \r\n" + " UNION ALL \r\n" + + " SELECT bom.ProductAssemblyID, bom.ComponentID, p.PerAssemblyQty, \r\n" + + " bom.EndDate, ComponentLevel + 1 \r\n" + " FROM Production.BillOfMaterials AS bom \r\n" + + " INNER JOIN Parts AS p \r\n" + " ON bom.ProductAssemblyID = p.ComponentID \r\n" + + " AND bom.EndDate IS NULL \r\n" + ") \r\n" + "UPDATE Production.BillOfMaterials \r\n" + + "SET PerAssemblyQty = c.PerAssemblyQty * 2 \r\n" + "FROM Production.BillOfMaterials AS c \r\n" + + "JOIN Parts AS d ON c.ProductAssemblyID = d.AssemblyID \r\n" + "WHERE d.ComponentLevel = 0;", + "Production . BillOfMaterials,Production . BillOfMaterials AS c JOIN Parts AS d ON c . ProductAssemblyID = d . AssemblyID"); + + // F. Using the WHERE CURRENT OF clause + ParserUtils.compareTableName( + "UPDATE HumanResources.EmployeePayHistory SET PayFrequency = 2 " + "WHERE CURRENT OF complex_cursor;", + "HumanResources . EmployeePayHistory"); + + // G. Specifying a computed value + ParserUtils.compareTableName("UPDATE Production.Product SET ListPrice = ListPrice * 2; ", + "Production . Product"); + + // H. Specifying a compound operator + ParserUtils.compareTableName("UPDATE Production.Product SET ListPrice += @NewPrice WHERE Color = N'Red'; ", + "Production . Product"); + + ParserUtils.compareTableName( + "UPDATE Production.ScrapReason SET Name += ' - tool malfunction' WHERE ScrapReasonID BETWEEN 10 and 12; ", + "Production . ScrapReason"); + + // I. Specifying a subquery in the SET clause + ParserUtils.compareTableName("UPDATE Sales.SalesPerson \r\n" + "SET SalesYTD = SalesYTD + \r\n" + + " (SELECT SUM(so.SubTotal) \r\n" + " FROM Sales.SalesOrderHeader AS so \r\n" + + " WHERE so.OrderDate = (SELECT MAX(OrderDate) \r\n" + " FROM Sales.SalesOrderHeader AS so2 \r\n" + + " WHERE so2.SalesPersonID = so.SalesPersonID) \r\n" + + " AND Sales.SalesPerson.BusinessEntityID = so.SalesPersonID \r\n" + " GROUP BY so.SalesPersonID);", + "Sales . SalesPerson,Sales . SalesOrderHeader AS so,Sales . SalesOrderHeader AS so2"); + + // J. Updating rows using DEFAULT values + ParserUtils.compareTableName( + "UPDATE Production.Location \r\n" + "SET CostRate = DEFAULT \r\n" + "WHERE CostRate > 20.00; ", + "Production . Location"); + + // K. Specifying a view as the target object + ParserUtils.compareTableName("UPDATE Person.vStateProvinceCountryRegion \r\n" + + "SET CountryRegionName = 'United States of America' \r\n" + + "WHERE CountryRegionName = 'United States'; ", "Person . vStateProvinceCountryRegion"); + + // L. Specifying a table alias as the target object + ParserUtils.compareTableName( + "UPDATE sr \r\n" + "SET sr.Name += ' - tool malfunction' \r\n" + + "FROM Production.ScrapReason AS sr \r\n" + "JOIN Production.WorkOrder AS wo \r\n" + + " ON sr.ScrapReasonID = wo.ScrapReasonID \r\n" + " AND wo.ScrappedQty > 300; ", + "Production . ScrapReason AS sr JOIN Production . WorkOrder AS wo ON sr . ScrapReasonID = wo . ScrapReasonID"); + + // M. Specifying a table variable as the target object + ParserUtils.compareTableName( + "INSERT INTO @MyTableVar (EmpID) \r\n" + " SELECT BusinessEntityID FROM HumanResources.Employee;", + "@MyTableVar,HumanResources . Employee"); + + // N. Using the UPDATE statement with information from another table + ParserUtils.compareTableName( + "UPDATE Sales.SalesPerson SET SalesYTD = SalesYTD + SubTotal " + "FROM Sales.SalesPerson AS sp \r\n" + + "JOIN Sales.SalesOrderHeader AS so ON sp.BusinessEntityID = so.SalesPersonID " + + " AND so.OrderDate = (SELECT MAX(OrderDate) \r\n" + + " FROM Sales.SalesOrderHeader \r\n" + + " WHERE SalesPersonID = sp.BusinessEntityID); ", + "Sales . SalesPerson,Sales . SalesPerson AS sp JOIN Sales . SalesOrderHeader AS so ON sp . BusinessEntityID = so . SalesPersonID,Sales . SalesOrderHeader"); + ParserUtils.compareTableName( + "UPDATE Sales.SalesPerson \r\n" + "SET SalesYTD = SalesYTD + \r\n" + + " (SELECT SUM(so.SubTotal) \r\n" + " FROM Sales.SalesOrderHeader AS so \r\n" + + " WHERE so.OrderDate = (SELECT MAX(OrderDate) \r\n" + + " FROM Sales.SalesOrderHeader AS so2 \r\n" + + " WHERE so2.SalesPersonID = so.SalesPersonID) \r\n" + + " AND Sales.SalesPerson.BusinessEntityID = so.SalesPersonID \r\n" + + " GROUP BY so.SalesPersonID);", + "Sales . SalesPerson,Sales . SalesOrderHeader AS so,Sales . SalesOrderHeader AS so2"); + + // O. Updating data in a remote table by using a linked server + ParserUtils.compareTableName( + "UPDATE MyLinkedServer.AdventureWorks2012.HumanResources.Department \r\n" + + "SET GroupName = N'Public Relations' \r\n" + "WHERE DepartmentID = 4; ", + "MyLinkedServer . AdventureWorks2012 . HumanResources . Department"); + + // P. Updating data in a remote table by using the OPENQUERY function + ParserUtils.compareTableName( + "UPDATE OPENQUERY (MyLinkedServer, 'SELECT GroupName FROM HumanResources.Department WHERE DepartmentID = 4') \r\n" + + "SET GroupName = 'Sales and Marketing'; ", + "OPENQUERY(MyLinkedServer , 'SELECT GroupName FROM HumanResources.Department WHERE DepartmentID = 4' )"); + + // Q. Updating data in a remote table by using the OPENDATASOURCE function + ParserUtils.compareTableName( + "UPDATE OPENDATASOURCE('SQLNCLI', 'Data Source=;Integrated Security=SSPI').AdventureWorks2012.HumanResources.Department\r\n" + + "SET GroupName = 'Sales and Marketing' WHERE DepartmentID = 4; ", + "OPENDATASOURCE('SQLNCLI' , 'Data Source=;Integrated Security=SSPI' ) . AdventureWorks2012 . HumanResources . Department"); + + // R. Using UPDATE with .WRITE to modify data in an nvarchar(max) column + ParserUtils.compareTableName( + "UPDATE Production.Document \r\n" + "SET DocumentSummary .WRITE (N'features',28,10) \r\n" + + "OUTPUT deleted.DocumentSummary, \r\n" + " inserted.DocumentSummary \r\n" + + " INTO @MyTableVar \r\n" + "WHERE Title = N'Front Reflector Bracket Installation';", + "Production . Document"); + + // S. Using UPDATE with .WRITE to add and remove data in an nvarchar(max) column + ParserUtils.compareTableName("UPDATE Production.Document \r\n" + + "SET DocumentSummary .WRITE (N' Appending data to the end of the column.', NULL, 0) \r\n" + + "WHERE Title = N'Crank Arm and Tire Maintenance'; ", "Production . Document"); + + // T. Using UPDATE with OPENROWSET to modify a varbinary(max) column + // ParserUtils.compareTableName( + // "UPDATE Production.ProductPhoto \r\n" + "SET ThumbNailPhoto = ( \r\n" + " SELECT * \r\n" + // + " FROM OPENROWSET(BULK 'c:Tires.jpg', SINGLE_BLOB) AS x ) \r\n" + // + "WHERE ProductPhotoID = 1;", + // "Production . ProductPhoto,OPENROWSET (BULK 'c:Tires.jpg' , SINGLE_BLOB ) AS x")); + + // U. Using UPDATE to modify FILESTREAM data + ParserUtils.compareTableName("UPDATE Archive.dbo.Records SET [Chart] = CAST('Xray 1' as varbinary(max)) " + + "WHERE [SerialNumber] = 2; ", "Archive . dbo . Records"); + + // V. Using a system data type + ParserUtils.compareTableName("UPDATE dbo.Cities \r\n" + "SET Location = CONVERT(Point, '12.3:46.2') \r\n" + + "WHERE Name = 'Anchorage';", "dbo . Cities"); + + // W. Invoking a method + ParserUtils.compareTableName( + "UPDATE dbo.Cities \r\n" + "SET Location.SetXY(23.5, 23.5) \r\n" + "WHERE Name = 'Anchorage'; ", + "dbo . Cities"); + + // X. Modifying the value of a property or data member + ParserUtils.compareTableName( + "UPDATE dbo.Cities \r\n" + "SET Location.X = 23.5 \r\n" + "WHERE Name = 'Anchorage'; ", + "dbo . Cities"); + + // Y. Specifying a table hint + ParserUtils.compareTableName("UPDATE Production.Product \r\n" + "WITH (TABLOCK) " + + "SET ListPrice = ListPrice * 1.10 \r\n" + "WHERE ProductNumber LIKE 'BK-%';", + "Production . Product WITH (TABLOCK )"); + + // Z. Specifying a query hint + ParserUtils.compareTableName( + "UPDATE Production.Product \r\n" + "SET ListPrice = ListPrice * 1.10 \r\n" + + "WHERE ProductNumber LIKE @Product \r\n" + "OPTION (OPTIMIZE FOR (@Product = 'BK-%') ); ", + "Production . Product"); + + // AA. Using UPDATE with the OUTPUT clause + ParserUtils.compareTableName("UPDATE TOP (10) HumanResources.Employee \r\n" + + "SET VacationHours = VacationHours * 1.25, \r\n" + " ModifiedDate = GETDATE() \r\n" + + "OUTPUT inserted.BusinessEntityID, \r\n" + " deleted.VacationHours, \r\n" + + " inserted.VacationHours, \r\n" + " inserted.ModifiedDate \r\n" + "INTO @MyTableVar; ", + "HumanResources . Employee"); + + // AB. Using UPDATE in a stored procedure + ParserUtils.compareTableName("UPDATE HumanResources.Employee \r\n" + "SET VacationHours = \r\n" + + " ( CASE \r\n" + " WHEN SalariedFlag = 0 THEN VacationHours + @NewHours \r\n" + + " ELSE @NewHours \r\n" + " END \r\n" + " ) \r\n" + "WHERE CurrentFlag = 1;", + "HumanResources . Employee"); + + // AC. Using UPDATE in a TRY...CATCH Block + ParserUtils + .compareTableName( + "BEGIN TRY \r\n" + " -- Intentionally generate a constraint violation error. \r\n" + + " UPDATE HumanResources.Department \r\n" + " SET Name = N'MyNewName' \r\n" + + " WHERE DepartmentID BETWEEN 1 AND 2; \r\n" + "END TRY", + "HumanResources . Department"); + + // AD. Using a simple UPDATE statement + ParserUtils.compareTableName("UPDATE DimEmployee \r\n" + "SET EndDate = '2010-12-31', CurrentFlag='False';", + "DimEmployee"); + + // AE. Using the UPDATE statement with a WHERE clause + ParserUtils.compareTableName( + "UPDATE DimEmployee \r\n" + "SET FirstName = 'Gail' \r\n" + "WHERE EmployeeKey = 500;", + "DimEmployee"); + + // AF. Using the UPDATE statement with label + ParserUtils.compareTableName("UPDATE DimProduct \r\n" + "SET ProductSubcategoryKey = 2 \r\n" + + "WHERE ProductKey = 313 \r\n" + "OPTION (LABEL = N'label1');", "DimProduct"); + + // AG. Using the UPDATE statement with information from another table + ParserUtils.compareTableName("UPDATE YearlyTotalSales \r\n" + "SET YearlySalesAmount= \r\n" + + "(SELECT SUM(SalesAmount) FROM FactInternetSales WHERE OrderDateKey >=20040000 AND OrderDateKey < 20050000) \r\n" + + "WHERE Year=2004;", "YearlyTotalSales,FactInternetSales"); + + // AH. ANSI join replacement for update statements + ParserUtils.compareTableName("-- Use an implicit join to perform the update\r\n" + + "UPDATE AnnualCategorySales\r\n" + + "SET AnnualCategorySales.TotalSalesAmount = CTAS_ACS.TotalSalesAmount\r\n" + + "FROM CTAS_acs\r\n" + + "WHERE CTAS_acs.[EnglishProductCategoryName] = AnnualCategorySales.[EnglishProductCategoryName]\r\n" + + "AND CTAS_acs.[CalendarYear] = AnnualCategorySales.[CalendarYear]", + "AnnualCategorySales,CTAS_acs"); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java index 6cdabe976..5c0c94f00 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PQImpsTest.java @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import com.microsoft.sqlserver.jdbc.RandomUtil; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerParameterMetaData; import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.jdbc.TestUtils; @@ -761,7 +762,7 @@ private static void populateTablesForCompexQueries() throws SQLException { public void testSubquery() throws SQLException { if (version >= SQL_SERVER_2012_VERSION) { String sql = "SELECT FirstName,LastName" + " FROM " + AbstractSQLGenerator.escapeIdentifier(nameTable) - + " WHERE ID IN " + " (SELECT ID" + " FROM " + + " a WHERE a.ID IN " + " (SELECT ID" + " FROM " + AbstractSQLGenerator.escapeIdentifier(phoneNumberTable) + " WHERE PhoneNumber = ? and ID = ? and PlainID = ?" + ")"; @@ -827,7 +828,8 @@ public void testJoin() throws SQLException { @Test @DisplayName("Merge Queries") public void testMerge() throws SQLException { - if (version >= SQL_SERVER_2012_VERSION) { + // FMTOnly currently only supports SELECT/INSERT/DELETE/UPDATE + if (version >= SQL_SERVER_2012_VERSION && !((SQLServerConnection)connection).getUseFmtOnly()) { String sql = "merge " + AbstractSQLGenerator.escapeIdentifier(mergeNameDesTable) + " as T" + " using " + AbstractSQLGenerator.escapeIdentifier(nameTable) + " as S" + " on T.PlainID=S.PlainID" + " when matched" + " then update set T.firstName = ?, T.lastName = ?;"; @@ -1143,7 +1145,7 @@ public void testAllInOneQuery() throws SQLException { if (version >= SQL_SERVER_2012_VERSION) { String sql = "select lower(FirstName), count(lastName) from " - + AbstractSQLGenerator.escapeIdentifier(nameTable) + "where ID = ? and FirstName in" + "(" + + AbstractSQLGenerator.escapeIdentifier(nameTable) + " a where a.ID = ? and FirstName in" + "(" + " select " + AbstractSQLGenerator.escapeIdentifier(nameTable) + ".FirstName from " + AbstractSQLGenerator.escapeIdentifier(nameTable) + " join " + AbstractSQLGenerator.escapeIdentifier(phoneNumberTable) + " on "