|
| 1 | +package com.databricks.jdbc.integration.fakeservice.tests; |
| 2 | + |
| 3 | +import static com.databricks.jdbc.integration.IntegrationTestUtil.*; |
| 4 | +import static org.junit.jupiter.api.Assertions.*; |
| 5 | + |
| 6 | +import com.databricks.jdbc.api.impl.DatabricksConnection; |
| 7 | +import com.databricks.jdbc.common.DatabricksClientType; |
| 8 | +import com.databricks.jdbc.integration.fakeservice.AbstractFakeServiceIntegrationTests; |
| 9 | +import com.databricks.jdbc.integration.fakeservice.FakeServiceExtension; |
| 10 | +import java.sql.*; |
| 11 | +import java.util.Properties; |
| 12 | +import java.util.stream.Stream; |
| 13 | +import org.junit.jupiter.api.AfterEach; |
| 14 | +import org.junit.jupiter.api.BeforeEach; |
| 15 | +import org.junit.jupiter.api.Test; |
| 16 | +import org.junit.jupiter.params.ParameterizedTest; |
| 17 | +import org.junit.jupiter.params.provider.Arguments; |
| 18 | +import org.junit.jupiter.params.provider.MethodSource; |
| 19 | + |
| 20 | +/** Integration tests for string edge cases and nested complex types. */ |
| 21 | +public class DataTypesIntegrationTests extends AbstractFakeServiceIntegrationTests { |
| 22 | + |
| 23 | + private Connection connection; |
| 24 | + private Connection inlineConnection; |
| 25 | + |
| 26 | + private static final String LEADING_TRAILING_SPACES = " leading and trailing spaces "; |
| 27 | + private static final String UNICODE_TEXT = "こんにちは"; |
| 28 | + private static final String SPECIAL_CHARS = "special chars: !@#$%^&*()"; |
| 29 | + private static final String DOUBLE_QUOTES = "string with \"double quotes\" inside"; |
| 30 | + |
| 31 | + @BeforeEach |
| 32 | + void setUp() throws SQLException { |
| 33 | + connection = getValidJDBCConnection(); |
| 34 | + Properties properties = new Properties(); |
| 35 | + properties.setProperty("enableArrow", "0"); |
| 36 | + inlineConnection = getValidJDBCConnection(properties); |
| 37 | + } |
| 38 | + |
| 39 | + @AfterEach |
| 40 | + void cleanUp() throws SQLException { |
| 41 | + closeConnection(connection); |
| 42 | + closeConnection(inlineConnection); |
| 43 | + } |
| 44 | + |
| 45 | + @Test |
| 46 | + void testStringEdgeCases() throws SQLException { |
| 47 | + String query = |
| 48 | + "SELECT * FROM (VALUES " |
| 49 | + + "(1, '" |
| 50 | + + LEADING_TRAILING_SPACES |
| 51 | + + "')," |
| 52 | + + "(2, '" |
| 53 | + + UNICODE_TEXT |
| 54 | + + "')," |
| 55 | + + "(3, '" |
| 56 | + + SPECIAL_CHARS |
| 57 | + + "')," |
| 58 | + + "(4, '" |
| 59 | + + DOUBLE_QUOTES |
| 60 | + + "')," |
| 61 | + + "(5, NULL)" |
| 62 | + + ") AS string_edge_cases(id, test_string) " |
| 63 | + + "ORDER BY id"; |
| 64 | + ResultSet resultSet = executeQuery(connection, query); |
| 65 | + ResultSet inlineResultSet = executeQuery(inlineConnection, query); |
| 66 | + |
| 67 | + validateStringResults(resultSet); |
| 68 | + validateStringResults(inlineResultSet); |
| 69 | + |
| 70 | + resultSet.close(); |
| 71 | + inlineResultSet.close(); |
| 72 | + } |
| 73 | + |
| 74 | + @ParameterizedTest |
| 75 | + @MethodSource("nullHandlingProvider") |
| 76 | + void testNullHandling(String query, int expectedType) throws SQLException { |
| 77 | + ResultSet resultSet = executeQuery(connection, query); |
| 78 | + ResultSet inlineResultSet = executeQuery(inlineConnection, query); |
| 79 | + assertTrue(resultSet.next()); |
| 80 | + assertTrue(inlineResultSet.next()); |
| 81 | + assertNull(resultSet.getObject(1)); |
| 82 | + assertNull(inlineResultSet.getObject(1)); |
| 83 | + assertEquals(expectedType, resultSet.getMetaData().getColumnType(1)); |
| 84 | + assertEquals(expectedType, inlineResultSet.getMetaData().getColumnType(1)); |
| 85 | + resultSet.close(); |
| 86 | + inlineResultSet.close(); |
| 87 | + } |
| 88 | + |
| 89 | + private static Stream<Arguments> nullHandlingProvider() { |
| 90 | + return Stream.of( |
| 91 | + Arguments.of("SELECT NULL", Types.VARCHAR), |
| 92 | + Arguments.of("SELECT CAST(NULL AS DOUBLE)", Types.DOUBLE), |
| 93 | + Arguments.of("SELECT NULL UNION (SELECT 1) order by 1", Types.INTEGER)); |
| 94 | + } |
| 95 | + |
| 96 | + private void validateStringResults(ResultSet resultSet) throws SQLException { |
| 97 | + int rowCount = 0; |
| 98 | + while (resultSet.next()) { |
| 99 | + rowCount++; |
| 100 | + int id = resultSet.getInt("id"); |
| 101 | + String value = resultSet.getString("test_string"); |
| 102 | + switch (id) { |
| 103 | + case 1: |
| 104 | + assertEquals(LEADING_TRAILING_SPACES, value); |
| 105 | + break; |
| 106 | + case 2: |
| 107 | + assertEquals(UNICODE_TEXT, value); |
| 108 | + break; |
| 109 | + case 3: |
| 110 | + assertEquals(SPECIAL_CHARS, value); |
| 111 | + break; |
| 112 | + case 4: |
| 113 | + assertEquals(DOUBLE_QUOTES, value); |
| 114 | + break; |
| 115 | + case 5: |
| 116 | + assertEquals(null, value); |
| 117 | + break; |
| 118 | + default: |
| 119 | + fail("Unexpected row id: " + id); |
| 120 | + } |
| 121 | + } |
| 122 | + assertEquals(5, rowCount); |
| 123 | + } |
| 124 | + |
| 125 | + @Test |
| 126 | + void testVariantTypes() throws SQLException { |
| 127 | + String tableName = "variant_types_table"; |
| 128 | + String createTableSQL = |
| 129 | + "CREATE TABLE IF NOT EXISTS " |
| 130 | + + getFullyQualifiedTableName(tableName) |
| 131 | + + " (id INT PRIMARY KEY, variant_col VARIANT)"; |
| 132 | + setupDatabaseTable(connection, tableName, createTableSQL); |
| 133 | + |
| 134 | + // Insert rows with JSON data via PARSE_JSON: |
| 135 | + // - A simple JSON object |
| 136 | + // - A nested JSON object with an array and boolean value |
| 137 | + // - A null variant |
| 138 | + String insertSQL = |
| 139 | + "INSERT INTO " |
| 140 | + + getFullyQualifiedTableName(tableName) |
| 141 | + + " (id, variant_col) VALUES " |
| 142 | + + "(1, PARSE_JSON('{\"key\": \"value\", \"number\": 123}')), " |
| 143 | + + "(2, PARSE_JSON('{\"nested\": {\"a\": \"b\", \"c\": [1, 2, 3]}, \"flag\": true}')), " |
| 144 | + + "(3, NULL)"; |
| 145 | + executeSQL(connection, insertSQL); |
| 146 | + |
| 147 | + String query = |
| 148 | + "SELECT id, variant_col FROM " + getFullyQualifiedTableName(tableName) + " ORDER BY id"; |
| 149 | + ResultSet rs = executeQuery(connection, query); |
| 150 | + ResultSetMetaData rsmd = rs.getMetaData(); |
| 151 | + assertEquals(Types.OTHER, rsmd.getColumnType(2)); |
| 152 | + assertEquals("VARIANT", rsmd.getColumnTypeName(2)); |
| 153 | + int rowCount = 0; |
| 154 | + while (rs.next()) { |
| 155 | + rowCount++; |
| 156 | + int id = rs.getInt("id"); |
| 157 | + Object variant = rs.getObject("variant_col"); |
| 158 | + switch (id) { |
| 159 | + case 1: |
| 160 | + String variantStr1 = variant.toString(); |
| 161 | + assertTrue(variantStr1.contains("\"key\":\"value\"")); |
| 162 | + assertTrue(variantStr1.contains("\"number\":123")); |
| 163 | + break; |
| 164 | + case 2: |
| 165 | + String variantStr2 = variant.toString(); |
| 166 | + assertTrue(variantStr2.contains("\"nested\"")); |
| 167 | + assertTrue(variantStr2.contains("\"a\":\"b\"")); |
| 168 | + assertTrue(variantStr2.contains("\"c\":[1,2,3]")); |
| 169 | + assertTrue(variantStr2.contains("\"flag\":true")); |
| 170 | + break; |
| 171 | + case 3: |
| 172 | + assertNull(variant); |
| 173 | + break; |
| 174 | + default: |
| 175 | + fail("Unexpected row id in variant test: " + id); |
| 176 | + } |
| 177 | + } |
| 178 | + assertEquals(3, rowCount); |
| 179 | + deleteTable(connection, tableName); |
| 180 | + rs.close(); |
| 181 | + } |
| 182 | + |
| 183 | + @Test |
| 184 | + void testTimestampWithTimezoneConversion() throws SQLException { |
| 185 | + String tableName = "timestamp_test_timezone"; |
| 186 | + String createTableSQL = |
| 187 | + "CREATE TABLE IF NOT EXISTS " |
| 188 | + + getFullyQualifiedTableName(tableName) |
| 189 | + + " (id INT PRIMARY KEY, ts TIMESTAMP)"; |
| 190 | + setupDatabaseTable(connection, tableName, createTableSQL); |
| 191 | + |
| 192 | + /* |
| 193 | + * Use from_utc_timestamp to simulate converting a UTC timestamp into a specific timezone. |
| 194 | + * converting '2021-06-15 12:34:56.789' from UTC to America/Los_Angeles. |
| 195 | + * expected local time should be: |
| 196 | + * 2021-06-15 12:34:56.789 UTC --> 2021-06-15 05:34:56.789 PDT |
| 197 | + */ |
| 198 | + String insertSQL = |
| 199 | + "INSERT INTO " |
| 200 | + + getFullyQualifiedTableName(tableName) |
| 201 | + + " (id, ts) VALUES " |
| 202 | + + "(1, from_utc_timestamp('2021-06-15 12:34:56.789', 'America/Los_Angeles'))"; |
| 203 | + executeSQL(connection, insertSQL); |
| 204 | + |
| 205 | + // Query and validate the timezone conversion result. |
| 206 | + String query = "SELECT id, ts FROM " + getFullyQualifiedTableName(tableName) + " ORDER BY id"; |
| 207 | + ResultSet rs = executeQuery(connection, query); |
| 208 | + assertTrue(rs.next()); |
| 209 | + int id = rs.getInt("id"); |
| 210 | + Timestamp ts = rs.getTimestamp("ts"); |
| 211 | + assertEquals(1, id); |
| 212 | + Timestamp expected = Timestamp.valueOf("2021-06-15 05:34:56.789"); // Expected PDT value. |
| 213 | + assertEquals(expected, ts); |
| 214 | + deleteTable(connection, tableName); |
| 215 | + rs.close(); |
| 216 | + } |
| 217 | + |
| 218 | + @Test |
| 219 | + void testTimestamp() throws SQLException { |
| 220 | + String tableName = "timestamp_test_table"; |
| 221 | + String createTableSQL = |
| 222 | + "CREATE TABLE IF NOT EXISTS " |
| 223 | + + getFullyQualifiedTableName(tableName) |
| 224 | + + " (id INT PRIMARY KEY, ts TIMESTAMP)"; |
| 225 | + setupDatabaseTable(connection, tableName, createTableSQL); |
| 226 | + String insertSQL = |
| 227 | + "INSERT INTO " |
| 228 | + + getFullyQualifiedTableName(tableName) |
| 229 | + + " (id, ts) VALUES " |
| 230 | + + "(1, '2021-01-01 10:00:00'), " |
| 231 | + + "(2, '2021-06-15 12:34:56.789'), " |
| 232 | + + "(3, NULL)"; |
| 233 | + executeSQL(connection, insertSQL); |
| 234 | + |
| 235 | + String query = "SELECT id, ts FROM " + getFullyQualifiedTableName(tableName) + " ORDER BY id"; |
| 236 | + ResultSet resultSet = executeQuery(connection, query); |
| 237 | + ResultSet inlineResultSet = executeQuery(inlineConnection, query); |
| 238 | + validateTimestampResults(resultSet); |
| 239 | + validateTimestampResults(inlineResultSet); |
| 240 | + |
| 241 | + deleteTable(connection, tableName); |
| 242 | + resultSet.close(); |
| 243 | + } |
| 244 | + |
| 245 | + private void validateTimestampResults(ResultSet resultSet) throws SQLException { |
| 246 | + int rowCount = 0; |
| 247 | + while (resultSet.next()) { |
| 248 | + rowCount++; |
| 249 | + int id = resultSet.getInt("id"); |
| 250 | + Timestamp ts = resultSet.getTimestamp("ts"); |
| 251 | + switch (id) { |
| 252 | + case 1: |
| 253 | + Timestamp expected1 = Timestamp.valueOf("2021-01-01 10:00:00"); |
| 254 | + assertEquals(expected1, ts); |
| 255 | + break; |
| 256 | + case 2: |
| 257 | + Timestamp expected2 = Timestamp.valueOf("2021-06-15 12:34:56.789"); |
| 258 | + assertEquals(expected2, ts); |
| 259 | + break; |
| 260 | + case 3: |
| 261 | + assertNull(ts); |
| 262 | + break; |
| 263 | + default: |
| 264 | + fail("Unexpected row id in timestamp test: " + id); |
| 265 | + } |
| 266 | + } |
| 267 | + assertEquals(3, rowCount); |
| 268 | + } |
| 269 | + |
| 270 | + private void closeConnection(Connection connection) throws SQLException { |
| 271 | + if (connection != null) { |
| 272 | + if (((DatabricksConnection) connection).getConnectionContext().getClientType() |
| 273 | + == DatabricksClientType.THRIFT |
| 274 | + && getFakeServiceMode() == FakeServiceExtension.FakeServiceMode.REPLAY) { |
| 275 | + // Hacky fix for THRIFT + REPLAY mode |
| 276 | + } else { |
| 277 | + connection.close(); |
| 278 | + } |
| 279 | + } |
| 280 | + } |
| 281 | +} |
0 commit comments