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 extends Token> 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 extends Token>) 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 extends Token> iter;
+
+ SQLServerTokenIterator(ArrayList extends Token> 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 extends Token> tokenList = (ArrayList extends Token>) 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 "