This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/dev/pr-12882-7f87688505cfc915de0ed4e87a4eda6fcee9f2cf in repository https://gitbox.apache.org/repos/asf/seatunnel.git
commit 4f70bc55d1fe78b7cb2a32a7cddac95d4560bedc Author: zhengxiang378928908-code <[email protected]> AuthorDate: Mon Oct 26 15:25:09 2026 +0000 [Fix][Connector-V2][JDBC] Report the declared length of Dameng NVARCHAR and NVARCHAR2 (#12882) Co-authored-by: Claude Opus 5.5 <[email protected]> --- .../introduction/concepts/incompatible-changes.md | 7 + .../introduction/concepts/incompatible-changes.md | 7 + .../seatunnel/jdbc/catalog/dm/DamengCatalog.java | 47 +++++- .../jdbc/catalog/dm/DamengCatalogTest.java | 148 +++++++++++++++++++ .../connectors/seatunnel/jdbc/JdbcDmIT.java | 161 +++++++++++++++++++++ 5 files changed, 369 insertions(+), 1 deletion(-) diff --git a/docs/en/introduction/concepts/incompatible-changes.md b/docs/en/introduction/concepts/incompatible-changes.md index f8e3f2f95c..50d813457f 100644 --- a/docs/en/introduction/concepts/incompatible-changes.md +++ b/docs/en/introduction/concepts/incompatible-changes.md @@ -20,6 +20,13 @@ Such a value now causes a parse error that identifies the field and the expected The same strictness applies wherever the shared JSON-to-row conversion is used without ignoring parse errors: the `JsonPath` transform (column `dest_type`), the `Python` transform (declared output columns and their declared types) and the Assert sink's `equals_to` check for `ARRAY` and `MAP` values. These callers do not consult the JSON format's `ignore-parse-errors` option, so that switch does not apply to them: a value that does not match the declared `ARRAY` or `MAP` type still fails [...] +### JDBC Dameng: NVARCHAR and NVARCHAR2 report their declared length + +- **Schema change: the Dameng catalog reports `NVARCHAR(n)` and `NVARCHAR2(n)` with length n instead of the byte length** + - **Description**: `NVARCHAR(n)` and `NVARCHAR2(n)` hold n characters, but the catalog took the length from `ALL_TAB_COLUMNS.DATA_LENGTH`, which counts bytes: 2n on a GB18030 database and 4n on a UTF-8 database, whatever `LENGTH_IN_CHAR` is set to. `NVARCHAR(10)` was therefore reported as `NVARCHAR(20)` or `NVARCHAR(40)`. The length now comes from `SYS.SYSCOLUMNS.LENGTH$`, the declared length. + - **Impact**: The source type of these columns changes, for example from `NVARCHAR(20)` to `NVARCHAR(10)`, and the column length used for sink auto-create is halved (GB18030) or quartered (UTF-8), for example a MySQL sink gets `VARCHAR(40)` instead of `VARCHAR(80)` for `NVARCHAR(10)`. Each new column still holds every value of the source column, because the length is counted in characters. Tables that already exist downstream are not altered. `CHAR`, `VARCHAR`, `VARCHAR2` and `NCHAR` c [...] + - **Migration Guide**: No change is needed. Jobs that rely on the wider auto-created columns, for example to write longer values from another source into the same table, should create the sink table themselves. + ### File sink: XML applies `date_format`, `datetime_format` and `time_format` - **Behavior change: the XML writer of the file sinks formats DATE, TIME and TIMESTAMP values with the configured formats** diff --git a/docs/zh/introduction/concepts/incompatible-changes.md b/docs/zh/introduction/concepts/incompatible-changes.md index 9b9a4c3ec8..2c444d5b85 100644 --- a/docs/zh/introduction/concepts/incompatible-changes.md +++ b/docs/zh/introduction/concepts/incompatible-changes.md @@ -19,6 +19,13 @@ 同样的严格校验也适用于未忽略解析错误而使用共享 JSON 转行转换的地方:`JsonPath` 转换(列的 `dest_type`)、`Python` 转换(声明的输出列及其声明类型),以及 Assert 的 `equals_to` 对 `ARRAY`、`MAP` 值的比较。这些调用方不会读取 JSON 格式的 `ignore-parse-errors` 选项,因此该开关对它们不生效:与声明的 `ARRAY` 或 `MAP` 类型不匹配的值在那里仍会失败,而这些转换插件自身的错误处理行为保持不变。 +### JDBC 达梦:NVARCHAR 和 NVARCHAR2 按声明长度上报 + +- **模式变更:达梦 Catalog 现在按声明长度 n 上报 `NVARCHAR(n)` 和 `NVARCHAR2(n)`,不再使用字节长度** + - **描述**:`NVARCHAR(n)` 和 `NVARCHAR2(n)` 可容纳 n 个字符,但 Catalog 此前从 `ALL_TAB_COLUMNS.DATA_LENGTH` 读取长度,该字段是字节数:GB18030 库上是 2n,UTF-8 库上是 4n,与 `LENGTH_IN_CHAR` 的取值无关。因此 `NVARCHAR(10)` 会被上报为 `NVARCHAR(20)` 或 `NVARCHAR(40)`。现在长度取自 `SYS.SYSCOLUMNS.LENGTH$`,即声明长度。 + - **影响**:这类列的源类型会变化,例如从 `NVARCHAR(20)` 变为 `NVARCHAR(10)`;Sink 自动建表使用的列长度变为原来的 1/2(GB18030)或 1/4(UTF-8),例如 `NVARCHAR(10)` 写入 MySQL Sink 时建为 `VARCHAR(40)`,而不是 `VARCHAR(80)`。由于长度按字符计算,新列仍能容纳源列的所有值。下游已存在的表不会被修改。`CHAR`、`VARCHAR`、`VARCHAR2` 和 `NCHAR` 列不受影响。 + - **迁移指南**:无需修改。如果作业依赖原来较宽的自动建表列(例如还要从其他数据源向同一张表写入更长的值),请自行创建 Sink 表。 + ### File Sink:XML 应用 `date_format`、`datetime_format` 和 `time_format` - **行为变更:文件类 Sink 的 XML 写入器按配置的格式输出 DATE、TIME 和 TIMESTAMP** diff --git a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalog.java b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalog.java index 69dc9f154f..d4791bcfec 100644 --- a/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalog.java +++ b/seatunnel-connectors-v2/connector-jdbc/src/main/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalog.java @@ -54,6 +54,7 @@ public class DamengCatalog extends AbstractJdbcCatalog { private static final String SELECT_COLUMNS_SQL = "SELECT COLUMNS.COLUMN_NAME, COLUMNS.DATA_TYPE, COLUMNS.DATA_LENGTH, COLUMNS.DATA_PRECISION, COLUMNS.DATA_SCALE " + ", COLUMNS.NULLABLE, COLUMNS.DATA_DEFAULT, COMMENTS.COMMENTS ," + + "COLUMNS.CHAR_LENGTH, SYSCOL.LENGTH$ AS DECLARED_LENGTH, " + "CASE \n" + " WHEN COLUMNS.DATA_TYPE IN ('CHAR', 'CHARACTER', 'VARCHAR', 'VARCHAR2', 'VARBINARY', 'BINARY') THEN COLUMNS.DATA_TYPE || '(' || COLUMNS.DATA_LENGTH || ')'\n" + " WHEN COLUMNS.DATA_TYPE IN ('NUMERIC', 'DECIMAL', 'NUMBER') AND COLUMNS.DATA_PRECISION IS NOT NULL AND COLUMNS.DATA_SCALE IS NOT NULL AND COLUMNS.DATA_PRECISION != 0 AND COLUMNS.DATA_SCALE != 0 THEN COLUMNS.DATA_TYPE || '(' || COLUMNS.DATA_PRECISION || ', ' || COLUMNS.DATA_SCALE || ')'\n" @@ -64,6 +65,15 @@ public class DamengCatalog extends AbstractJdbcCatalog { + "ON COLUMNS.OWNER = COMMENTS.SCHEMA_NAME " + "AND COLUMNS.TABLE_NAME = COMMENTS.TABLE_NAME " + "AND COLUMNS.COLUMN_NAME = COMMENTS.COLUMN_NAME " + // SYSCOLUMNS.LENGTH$ is the declared length. ALL_TAB_COLUMNS derives + // DATA_LENGTH from it as a byte count and leaves CHAR_LENGTH at 0 for + // NVARCHAR2, see nationalCharLength. + + "LEFT JOIN SYS.SYSOBJECTS SCH " + + "ON SCH.NAME = COLUMNS.OWNER AND SCH.TYPE$ = 'SCH' " + + "LEFT JOIN SYS.SYSOBJECTS TAB " + + "ON TAB.SCHID = SCH.ID AND TAB.NAME = COLUMNS.TABLE_NAME AND TAB.TYPE$ = 'SCHOBJ' " + + "LEFT JOIN SYS.SYSCOLUMNS SYSCOL " + + "ON SYSCOL.ID = TAB.ID AND SYSCOL.NAME = COLUMNS.COLUMN_NAME " + "WHERE COLUMNS.OWNER = '%s' " + "AND COLUMNS.TABLE_NAME = '%s' " + "ORDER BY COLUMNS.COLUMN_ID ASC"; @@ -163,7 +173,12 @@ public class DamengCatalog extends AbstractJdbcCatalog { protected Column buildColumn(ResultSet resultSet) throws SQLException { String columnName = resultSet.getString("COLUMN_NAME"); String typeName = resultSet.getString("DATA_TYPE"); - long columnLength = resultSet.getLong("DATA_LENGTH"); + long columnLength = + nationalCharLength( + typeName, + resultSet.getLong("DATA_LENGTH"), + resultSet.getLong("CHAR_LENGTH"), + resultSet.getLong("DECLARED_LENGTH")); long columnPrecision = resultSet.getLong("DATA_PRECISION"); int columnScale = resultSet.getInt("DATA_SCALE"); String columnComment = resultSet.getString("COMMENTS"); @@ -185,6 +200,36 @@ public class DamengCatalog extends AbstractJdbcCatalog { return DmdbTypeConverter.INSTANCE.convert(typeDefine); } + /** + * Length of a column in the unit the type converter expects: characters for {@code NVARCHAR} + * and {@code NVARCHAR2}, {@code DATA_LENGTH} for every other type. + * + * <p>{@code NVARCHAR(n)} and {@code NVARCHAR2(n)} always hold n characters. {@code + * ALL_TAB_COLUMNS.DATA_LENGTH} reports them in bytes: n * 2 on a GB18030 database and n * 4 on + * a UTF-8 database, whatever {@code LENGTH_IN_CHAR} is. {@code CHAR_LENGTH} is n for {@code + * NVARCHAR} but 0 for {@code NVARCHAR2}, which the view does not list. {@code + * SYS.SYSCOLUMNS.LENGTH$} is n for both, so it is used first. + * + * @param dataLength {@code ALL_TAB_COLUMNS.DATA_LENGTH} + * @param charLength {@code ALL_TAB_COLUMNS.CHAR_LENGTH}, 0 if unknown + * @param declaredLength {@code SYS.SYSCOLUMNS.LENGTH$}, 0 if unknown + */ + static long nationalCharLength( + String typeName, long dataLength, long charLength, long declaredLength) { + if (!DmdbTypeConverter.DM_NVARCHAR.equalsIgnoreCase(typeName) + && !DmdbTypeConverter.DM_NVARCHAR2.equalsIgnoreCase(typeName)) { + return dataLength; + } + if (declaredLength > 0) { + return declaredLength; + } + if (charLength > 0) { + return charLength; + } + // Unknown character length: keep the byte count, which oversizes but never truncates. + return dataLength; + } + @Override protected String getUrlFromDatabaseName(String databaseName) { return defaultUrl; diff --git a/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogTest.java b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogTest.java new file mode 100644 index 0000000000..808015bbdf --- /dev/null +++ b/seatunnel-connectors-v2/connector-jdbc/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/catalog/dm/DamengCatalogTest.java @@ -0,0 +1,148 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.seatunnel.connectors.seatunnel.jdbc.catalog.dm; + +import org.apache.seatunnel.api.table.catalog.Column; +import org.apache.seatunnel.common.utils.JdbcUrlUtil; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.SQLException; + +class DamengCatalogTest { + + /** + * Metadata measured on DM V8 (0x7000c) for columns declared with length 10 or 100, on databases + * initialized with each LENGTH_IN_CHAR / CHARSET combination. Every one of these columns + * accepts exactly the declared number of characters, single-byte or Chinese. + */ + @ParameterizedTest(name = "{0} {1} on LENGTH_IN_CHAR={2}, {3}") + @CsvSource({ + // type, declared, LENGTH_IN_CHAR, charset, DATA_LENGTH, CHAR_LENGTH, SYSCOLUMNS.LENGTH$ + "NVARCHAR, 10, 0, GB18030, 20, 10, 10", + "NVARCHAR2, 10, 0, GB18030, 20, 0, 10", + "NVARCHAR, 100, 0, GB18030, 200, 100, 100", + "NVARCHAR2, 100, 0, GB18030, 200, 0, 100", + "NVARCHAR, 10, 1, GB18030, 20, 10, 10", + "NVARCHAR2, 10, 1, GB18030, 20, 0, 10", + "NVARCHAR, 10, 0, UTF-8, 40, 10, 10", + "NVARCHAR2, 10, 0, UTF-8, 40, 0, 10", + "NVARCHAR, 100, 0, UTF-8, 400, 100, 100", + "NVARCHAR2, 100, 0, UTF-8, 400, 0, 100", + "NVARCHAR, 10, 1, UTF-8, 40, 10, 10", + "NVARCHAR2, 10, 1, UTF-8, 40, 0, 10", + }) + void nationalCharLengthIsTheDeclaredLength( + String type, + long declared, + int lengthInChar, + String charset, + long dataLength, + long charLength, + long sysColumnsLength) { + Assertions.assertEquals( + declared, + DamengCatalog.nationalCharLength(type, dataLength, charLength, sysColumnsLength)); + } + + @Test + void nationalCharLengthFallsBackWhenTheDeclaredLengthIsUnknown() { + // NVARCHAR still has CHAR_LENGTH. + Assertions.assertEquals(10, DamengCatalog.nationalCharLength("NVARCHAR", 20, 10, 0)); + // NVARCHAR2 has CHAR_LENGTH = 0: keep the byte count, which oversizes but never + // truncates, instead of guessing a bytes-per-character ratio. + Assertions.assertEquals(40, DamengCatalog.nationalCharLength("NVARCHAR2", 40, 0, 0)); + Assertions.assertEquals(10, DamengCatalog.nationalCharLength("nvarchar2", 20, 0, 10)); + } + + @ParameterizedTest(name = "{0}") + @CsvSource({ + // type, DATA_LENGTH, CHAR_LENGTH, SYSCOLUMNS.LENGTH$ + "CHAR, 10, 10, 10", + "CHARACTER, 10, 10, 10", + "VARCHAR, 10, 10, 10", + "VARCHAR2, 10, 10, 10", + "NCHAR, 10, 10, 10", + // VARCHAR(10 CHAR) on GB18030: kept as DATA_LENGTH, as before this change. + "VARCHAR, 20, 10, 10", + "VARBINARY, 8, 0, 8", + }) + void otherTypesKeepDataLength( + String type, long dataLength, long charLength, long sysColumnsLength) { + Assertions.assertEquals( + dataLength, + DamengCatalog.nationalCharLength(type, dataLength, charLength, sysColumnsLength)); + } + + @ParameterizedTest(name = "{0}({1}) with DATA_LENGTH {2}") + @CsvSource({ + "NVARCHAR, 10, 20, 10", + "NVARCHAR2, 10, 20, 0", + "NVARCHAR, 10, 40, 10", + "NVARCHAR2, 10, 40, 0", + }) + void buildColumnReportsTheDeclaredNationalCharLength( + String type, long declared, long dataLength, long charLength) throws SQLException { + Column column = catalog().buildColumn(resultSet(type, dataLength, charLength, declared)); + Assertions.assertEquals(type + "(" + declared + ")", column.getSourceType()); + Assertions.assertEquals(declared * 4, column.getColumnLength()); + } + + @Test + void buildColumnKeepsCharAndVarcharUnchanged() throws SQLException { + Column varchar = catalog().buildColumn(resultSet("VARCHAR", 10, 10, 10)); + Assertions.assertEquals("VARCHAR2(10)", varchar.getSourceType()); + Assertions.assertEquals(40, varchar.getColumnLength()); + + Column nchar = catalog().buildColumn(resultSet("NCHAR", 10, 10, 10)); + Assertions.assertEquals("NCHAR(10)", nchar.getSourceType()); + Assertions.assertEquals(40, nchar.getColumnLength()); + + Column charColumn = catalog().buildColumn(resultSet("CHAR", 10, 10, 10)); + Assertions.assertEquals("CHAR(10)", charColumn.getSourceType()); + Assertions.assertEquals(40, charColumn.getColumnLength()); + } + + private static DamengCatalog catalog() { + return new DamengCatalog( + "dameng", + "SYSDBA", + "SYSDBA", + JdbcUrlUtil.getUrlInfo("jdbc:dm://localhost:5236"), + "SYSDBA", + "dm.jdbc.driver.DmDriver"); + } + + private static ResultSet resultSet( + String type, long dataLength, long charLength, long declaredLength) + throws SQLException { + ResultSet resultSet = Mockito.mock(ResultSet.class); + Mockito.when(resultSet.getString("COLUMN_NAME")).thenReturn("C"); + Mockito.when(resultSet.getString("DATA_TYPE")).thenReturn(type); + Mockito.when(resultSet.getLong("DATA_LENGTH")).thenReturn(dataLength); + Mockito.when(resultSet.getLong("CHAR_LENGTH")).thenReturn(charLength); + Mockito.when(resultSet.getLong("DECLARED_LENGTH")).thenReturn(declaredLength); + Mockito.when(resultSet.getString("NULLABLE")).thenReturn("Y"); + return resultSet; + } +} diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-5/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDmIT.java b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-5/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDmIT.java index e38a59268c..1b6568a1aa 100644 --- a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-5/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDmIT.java +++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-jdbc-e2e/connector-jdbc-e2e-part-5/src/test/java/org/apache/seatunnel/connectors/seatunnel/jdbc/JdbcDmIT.java @@ -22,6 +22,7 @@ import org.apache.seatunnel.shade.com.google.common.collect.Lists; import org.apache.seatunnel.shade.org.apache.commons.lang3.tuple.Pair; import org.apache.seatunnel.api.table.catalog.CatalogTable; +import org.apache.seatunnel.api.table.catalog.Column; import org.apache.seatunnel.api.table.catalog.TablePath; import org.apache.seatunnel.api.table.type.BasicType; import org.apache.seatunnel.api.table.type.LocalTimeType; @@ -38,8 +39,11 @@ import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerLoggerFactory; import java.math.BigDecimal; +import java.sql.Connection; import java.sql.Date; +import java.sql.Driver; import java.sql.ResultSet; +import java.sql.SQLException; import java.sql.Statement; import java.sql.Timestamp; import java.time.Duration; @@ -50,6 +54,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Properties; public class JdbcDmIT extends AbstractJdbcIT { @@ -314,6 +319,162 @@ public class JdbcDmIT extends AbstractJdbcIT { } } + /** + * Starts the image with a fresh database initialized with LENGTH_IN_CHAR=1 and CHARSET=1 + * (UTF-8), the opposite of the image's default database on both settings. + */ + private static final String INIT_LENGTH_IN_CHAR_UTF8 = + "cd /opt/dmdbms/bin" + + " && su dmdba -c \"./dminit PATH=/opt/dmdbms/st DB_NAME=ST INSTANCE_NAME=ST" + + " PORT_NUM=5236 LENGTH_IN_CHAR=1 CHARSET=1 PAGE_SIZE=8 CASE_SENSITIVE=0" + + " SYSDBA_PWD=SYSDBA001\"" + + " && exec su dmdba -c \"./dmserver path=/opt/dmdbms/st/ST/dm.ini -noconsole\""; + + @Test + public void testNationalCharLengthFromCatalog() throws Exception { + // The shared container runs the image's default database: LENGTH_IN_CHAR=0, GB18030. + String jdbcUrl = jdbcCase.getJdbcUrl().replace(HOST, dbServer.getHost()); + assertNationalCharLengths(jdbcUrl, DM_PASSWORD, 0, 0, true); + + try (GenericContainer<?> lengthInCharUtf8 = + new GenericContainer<>(DM_IMAGE) + .withNetwork(NETWORK) + .withCommand("bash", "-c", INIT_LENGTH_IN_CHAR_UTF8) + .withExposedPorts(DM_CONTAINER_PORT) + .waitingFor( + Wait.forLogMessage(".*SYSTEM IS READY.*", 1) + .withStartupTimeout(Duration.ofMinutes(5))) + .withLogConsumer( + new Slf4jLogConsumer(DockerLoggerFactory.getLogger(DM_IMAGE)))) { + lengthInCharUtf8.start(); + String url = + String.format( + "jdbc:dm://%s:%d", + lengthInCharUtf8.getHost(), + lengthInCharUtf8.getMappedPort(DM_CONTAINER_PORT)); + assertNationalCharLengths(url, "SYSDBA001", 1, 1, false); + } + } + + /** + * Checks that the catalog reports the declared length of NVARCHAR and NVARCHAR2, which is also + * the number of characters they hold, and leaves CHAR, VARCHAR and NCHAR as before. + */ + private void assertNationalCharLengths( + String jdbcUrl, + String password, + int lengthInChar, + int unicodeFlag, + boolean checkReadOnlyUser) + throws Exception { + String table = "E2E_DM_NATIONAL_CHAR"; + String tableInfo = DM_USERNAME + "." + table; + TablePath tablePath = TablePath.of(DM_DATABASE, DM_USERNAME, table); + String reader = "E2E_DM_READER"; + try (Connection conn = connect(jdbcUrl, DM_USERNAME, password); + Statement statement = conn.createStatement()) { + try (ResultSet rs = + statement.executeQuery( + "SELECT SF_GET_LENGTH_IN_CHAR(), SF_GET_UNICODE_FLAG() FROM DUAL")) { + Assertions.assertTrue(rs.next()); + Assertions.assertEquals(lengthInChar, rs.getInt(1)); + Assertions.assertEquals(unicodeFlag, rs.getInt(2)); + } + statement.execute( + "CREATE TABLE " + + tableInfo + + " (V_NVARCHAR NVARCHAR(10), V_NVARCHAR2 NVARCHAR2(10)," + + " V_NVARCHAR_100 NVARCHAR(100), V_NVARCHAR2_100 NVARCHAR2(100)," + + " V_NCHAR NCHAR(10), V_CHAR CHAR(10), V_VARCHAR VARCHAR(10)," + + " V_VARCHAR2 VARCHAR2(10))"); + try { + // NVARCHAR(10) and NVARCHAR2(10) hold 10 characters, multi-byte or not. + String tenChinese = repeat("\u8fbe", 10); + statement.execute( + "INSERT INTO " + + tableInfo + + " (V_NVARCHAR, V_NVARCHAR2) VALUES ('" + + tenChinese + + "', '" + + tenChinese + + "')"); + Assertions.assertThrows( + SQLException.class, + () -> + statement.execute( + "INSERT INTO " + + tableInfo + + " (V_NVARCHAR2) VALUES ('" + + repeat("a", 11) + + "')")); + + assertCatalogLengths(jdbcUrl, DM_USERNAME, password, tablePath); + if (checkReadOnlyUser) { + // A user that can only read the table must get the same lengths. + statement.execute("CREATE USER " + reader + " IDENTIFIED BY \"Reader12345\""); + statement.execute("GRANT SELECT ON " + tableInfo + " TO " + reader); + assertCatalogLengths(jdbcUrl, reader, "Reader12345", tablePath); + } + } finally { + statement.execute("DROP TABLE " + tableInfo); + if (checkReadOnlyUser) { + statement.execute("DROP USER IF EXISTS " + reader + " CASCADE"); + } + } + } + } + + private void assertCatalogLengths( + String jdbcUrl, String user, String password, TablePath tablePath) { + DamengCatalog catalog = + new DamengCatalog( + "dameng", + user, + password, + JdbcUrlUtil.getUrlInfo(jdbcUrl), + DM_USERNAME, + DRIVER_CLASS); + catalog.open(); + try { + CatalogTable catalogTable = catalog.getTable(tablePath); + Map<String, Pair<String, Long>> expected = new HashMap<>(); + expected.put("V_NVARCHAR", Pair.of("NVARCHAR(10)", 40L)); + expected.put("V_NVARCHAR2", Pair.of("NVARCHAR2(10)", 40L)); + expected.put("V_NVARCHAR_100", Pair.of("NVARCHAR(100)", 400L)); + expected.put("V_NVARCHAR2_100", Pair.of("NVARCHAR2(100)", 400L)); + expected.put("V_NCHAR", Pair.of("NCHAR(10)", 40L)); + expected.put("V_CHAR", Pair.of("CHAR(10)", 40L)); + expected.put("V_VARCHAR", Pair.of("VARCHAR2(10)", 40L)); + expected.put("V_VARCHAR2", Pair.of("VARCHAR2(10)", 40L)); + expected.forEach( + (name, type) -> { + Column column = catalogTable.getTableSchema().getColumn(name); + Assertions.assertEquals(type.getLeft(), column.getSourceType(), name); + Assertions.assertEquals(type.getRight(), column.getColumnLength(), name); + }); + } finally { + catalog.close(); + } + } + + private Connection connect(String jdbcUrl, String user, String password) throws Exception { + Driver driver = (Driver) loadDriverClass().newInstance(); + Properties props = new Properties(); + props.put("user", user); + props.put("password", password); + Connection conn = driver.connect(jdbcUrl, props); + conn.setAutoCommit(true); + return conn; + } + + private static String repeat(String value, int count) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < count; i++) { + builder.append(value); + } + return builder.toString(); + } + protected String buildTableInfoWithSchema(String database, String schema, String table) { return buildTableInfoWithSchema(schema, table); }
