github-actions[bot] commented on code in PR #66677:
URL: https://github.com/apache/doris/pull/66677#discussion_r3763468504


##########
fe/be-java-extensions/jdbc-scanner/src/main/java/org/apache/doris/jdbc/JdbcJniScanner.java:
##########
@@ -214,6 +217,51 @@ public void open() throws IOException {
         }
     }
 
+    /**
+     * Refuse the scan when the driver reports a wider column than the plan 
was built with.
+     *
+     * <p>The plan's types come from the catalog's cached view of the remote 
schema. If the remote
+     * DDL widened a column since then, the planner never inserted a narrowing 
cast, and the extra
+     * digits would land in a column whose declared type cannot hold them.
+     *
+     * <p>Checked once per scan, so the error can name the column and both 
precisions. Only the
+     * widening direction, and only when the driver answers: they vary in how 
faithfully they fill
+     * in ResultSetMetaData, and one with nothing to say must not fail every 
scan.
+     */
+    private void checkSourcePrecisionAgainstPlan() throws SQLException {
+        for (int i = 0; i < types.length; i++) {
+            ColumnType planned = types[i];
+            int rsIndex = columnIndexMapping[i];
+
+            if (planned.isDateTimeV2() || planned.isTimestampTz()) {
+                // Doris parses the sub-second digits into precision; the 
driver reports them as scale.
+                int plannedPrecision = planned.getPrecision();
+                int sourceScale = resultSetMetaData.getScale(rsIndex);
+                if (sourceScale > plannedPrecision) {

Review Comment:
   [P1] Do not treat every native scale above the planned precision as stale 
schema. Several supported mappings intentionally cap the source at Doris's 
six-digit limit: for example `JdbcOracleConnectorClient` maps a fresh 
`TIMESTAMP(9)` to `DATETIMEV2(6)`. The existing Oracle fixture already has such 
a column and selects it successfully; this check will now reject that whole 
scan before reading a row. SQL Server `datetime2(7)`, DB2 high-precision 
timestamps, and ClickHouse `DateTime64(7..9)` have the same shape. The 
comparison needs the connector's canonical source-to-Doris mapping rather than 
raw driver scale, with a fresh high-precision regression.



##########
fe/be-java-extensions/jdbc-scanner/src/main/java/org/apache/doris/jdbc/JdbcJniScanner.java:
##########
@@ -197,6 +198,8 @@ public void open() throws IOException {
                 }
             }
 
+            checkSourcePrecisionAgainstPlan();

Review Comment:
   [P1] Make this new failure path release the JDBC resources acquired earlier 
in `open()`. At this point `conn`, `stmt`, and `resultSet` are live, but the 
catch below only wraps the exception. Native `JniReader::open` sets 
`_scanner_opened` only after Java `open()` returns successfully, and 
`JniReader::close()` skips Java close while that flag is false, so nothing 
later returns this lease to Hikari. Repeating a stale-schema scan eventually 
consumes all 30 default pool slots and turns subsequent scans into 
connection-timeout failures. Please make the partial open exception-safe and 
cover repeated failures with a small pool.



##########
fe/be-java-extensions/jdbc-scanner/src/main/java/org/apache/doris/jdbc/JdbcJniScanner.java:
##########
@@ -214,6 +217,51 @@ public void open() throws IOException {
         }
     }
 
+    /**
+     * Refuse the scan when the driver reports a wider column than the plan 
was built with.
+     *
+     * <p>The plan's types come from the catalog's cached view of the remote 
schema. If the remote
+     * DDL widened a column since then, the planner never inserted a narrowing 
cast, and the extra
+     * digits would land in a column whose declared type cannot hold them.
+     *
+     * <p>Checked once per scan, so the error can name the column and both 
precisions. Only the
+     * widening direction, and only when the driver answers: they vary in how 
faithfully they fill
+     * in ResultSetMetaData, and one with nothing to say must not fail every 
scan.
+     */
+    private void checkSourcePrecisionAgainstPlan() throws SQLException {
+        for (int i = 0; i < types.length; i++) {
+            ColumnType planned = types[i];
+            int rsIndex = columnIndexMapping[i];
+
+            if (planned.isDateTimeV2() || planned.isTimestampTz()) {

Review Comment:
   [P1] The type-specific branches leave two supported corruption paths 
unchecked. First, a planned `ARRAY` never reaches these branches; PostgreSQL 
numeric/timestamp arrays are later flattened into cached child `VectorColumn`s, 
so stale child precision still reaches the unchecked serializer. Second, scalar 
integer widening is skipped: a cached MySQL `TINYINT` changed to `SMALLINT` can 
return `200`, and the planned-TINYINT converter calls `byteValue()` and yields 
`-56`. Please validate canonical live/planned types recursively and compare 
scalar integer ranges (or make narrowing converters checked), with regressions 
for both paths.



##########
regression-test/suites/external_table_p0/jdbc/test_jdbc_stale_schema_precision.groovy:
##########
@@ -0,0 +1,231 @@
+// 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.
+
+import java.sql.Connection
+import java.sql.DriverManager
+
+// A JDBC catalog caches the remote column definition. When the remote DDL 
widens a column's
+// precision, that cache keeps reporting the narrower one until it is 
refreshed, so the planner
+// sees source and target as the same type and emits no narrowing cast. The 
value scanned out of
+// the remote database still carries its full precision, and nothing 
downstream re-normalizes it.
+//
+// For a DATETIMEV2 key column that is not cosmetic: sub-second digits live in 
the same 64-bit word
+// the storage layer encodes as a key, so two rows the column claims are equal 
become two distinct
+// keys and a unique table stops deduplicating them.
+//
+// These cases drive the remote DDL directly (the catalog is read-only) to put 
the cache and the
+// remote schema out of step on purpose, then assert that what lands in Doris 
still respects the
+// target column's declared type.
+suite("test_jdbc_stale_schema_precision", "p0,external") {
+    String enabled = context.config.otherConfigs.get("enableJdbcTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        return
+    }
+
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String pg_port = context.config.otherConfigs.get("pg_14_port")
+    String s3_endpoint = getS3Endpoint()
+    String bucket = getS3BucketName()
+    String driver_url = 
"https://${bucket}.${s3_endpoint}/regression/jdbc_driver/postgresql-42.5.0.jar";
+
+    String catalog_name = "test_jdbc_stale_schema_precision_catalog"
+    String internal_db = "regression_test_jdbc_stale_schema_precision"
+    String pg_schema = "stale_precision"
+    String pg_url = 
"jdbc:postgresql://${externalEnvIp}:${pg_port}/postgres?useSSL=false"
+
+    Class.forName("org.postgresql.Driver")
+
+    // The catalog cannot issue DDL against the source, so drive PostgreSQL 
directly.
+    def onPostgres = { List<String> statements ->
+        Connection conn = DriverManager.getConnection(pg_url, "postgres", 
"123456")
+        try {
+            def stmt = conn.createStatement()
+            try {
+                statements.each { stmt.execute(it) }
+            } finally {
+                stmt.close()
+            }
+        } finally {
+            conn.close()
+        }
+    }
+
+    sql """drop catalog if exists ${catalog_name}"""
+    sql """drop database if exists internal.${internal_db}"""
+    sql """create database internal.${internal_db}"""
+
+    onPostgres([
+        "DROP SCHEMA IF EXISTS ${pg_schema} CASCADE",
+        "CREATE SCHEMA ${pg_schema}",
+        // Declared narrow to begin with: this is what the catalog will cache.
+        """CREATE TABLE ${pg_schema}.orders (
+               mchid varchar, orderid varchar, createtime timestamp(0), status 
int)""",
+        "INSERT INTO ${pg_schema}.orders VALUES ('M001','ORD1','2026-06-19 
12:00:00',0)",
+        "CREATE TABLE ${pg_schema}.amounts (id int, amount numeric(10,2))",
+        "INSERT INTO ${pg_schema}.amounts VALUES (1, 1.00)",
+    ])
+
+    sql """create catalog ${catalog_name} properties(
+        "type"="jdbc",
+        "user"="postgres",
+        "password"="123456",
+        "jdbc_url" = "${pg_url}&currentSchema=${pg_schema}",
+        "driver_url" = "${driver_url}",
+        "driver_class" = "org.postgresql.Driver"
+    );"""
+
+    try {
+        // Populate the catalog's schema cache while the remote columns are 
still narrow.
+        qt_cached_datetime_type """desc ${catalog_name}.${pg_schema}.orders"""
+        qt_cached_decimal_type """desc ${catalog_name}.${pg_schema}.amounts"""
+
+        // Widen the remote columns and write values that only the wider type 
can hold. The catalog
+        // is deliberately not refreshed, so from here on its cache disagrees 
with the source.
+        onPostgres([
+            "ALTER TABLE ${pg_schema}.orders ALTER COLUMN createtime TYPE 
timestamp(6)",
+            "DELETE FROM ${pg_schema}.orders",
+            """INSERT INTO ${pg_schema}.orders VALUES
+                   ('M001','ORD1','2026-06-19 12:23:23.486067',1),
+                   ('M001','ORD1','2026-06-19 12:23:23.000000',2)""",
+            "ALTER TABLE ${pg_schema}.amounts ALTER COLUMN amount TYPE 
numeric(30,2)",
+            "DELETE FROM ${pg_schema}.amounts",
+            "INSERT INTO ${pg_schema}.amounts VALUES (1, 
12345678901234567890.12)",
+        ])
+
+        // Still the narrow types -- the cache has not caught up.
+        qt_stale_datetime_type """desc ${catalog_name}.${pg_schema}.orders"""
+
+        sql """create table internal.${internal_db}.orders_target (
+                   `mchid`      varchar(65533) NOT NULL,
+                   `createtime` datetime       NOT NULL,
+                   `orderid`    varchar(65533) NOT NULL,
+                   `status`     int            NULL
+               ) ENGINE=OLAP
+               UNIQUE KEY(`mchid`, `createtime`, `orderid`)
+               DISTRIBUTED BY HASH(`mchid`) BUCKETS 1
+               PROPERTIES ("replication_num" = "1", 
"enable_unique_key_merge_on_write" = "true");"""
+
+        // The plan was built from the cached, narrower type, so no narrowing 
cast exists and
+        // values carrying the extra digits would land in a column that cannot 
hold them --
+        // splitting one logical key into two and breaking dedup on the unique 
table. The scanner
+        // compares the plan against the driver's live metadata when it opens 
the result set, so
+        // this is refused before a single row is read, naming the column and 
both precisions.
+        test {
+            sql """insert into internal.${internal_db}.orders_target
+                   select mchid, createtime, orderid, status
+                   from ${catalog_name}.${pg_schema}.orders;"""
+            exception "is datetime precision 0 in the plan but the source 
reports 6"
+        }
+
+        test {
+            sql """select createtime from 
${catalog_name}.${pg_schema}.orders"""
+            exception "is datetime precision 0 in the plan but the source 
reports 6"
+        }
+
+        // Same for a decimal the cached type is too narrow for.
+        test {
+            sql """select amount from ${catalog_name}.${pg_schema}.amounts"""
+            exception "in the plan but the source reports"
+        }
+
+        // A widened scale counts too: the value would fit the cached 
precision, but rounding it
+        // down to the cached scale returns a different number than the source 
holds -- the
+        // quietest way to be wrong, since nothing about the result looks 
unusual.
+        onPostgres([
+            "CREATE TABLE ${pg_schema}.wider_scale (id int, amount 
numeric(20,2))",
+            "INSERT INTO ${pg_schema}.wider_scale VALUES (1, 1.00)",
+        ])
+        qt_cached_wider_scale_type """desc 
${catalog_name}.${pg_schema}.wider_scale"""
+        onPostgres([
+            "ALTER TABLE ${pg_schema}.wider_scale ALTER COLUMN amount TYPE 
numeric(20,6)",
+            "UPDATE ${pg_schema}.wider_scale SET amount = 123.456789",
+        ])
+        test {
+            sql """select amount from 
${catalog_name}.${pg_schema}.wider_scale"""
+            exception "in the plan but the source reports"
+        }
+
+        // The drift is what is refused, not the data: a column whose values 
all still fit the
+        // cached type is rejected just the same, because the next row need 
not.
+        onPostgres([
+            "CREATE TABLE ${pg_schema}.whole_seconds (id int, ts 
timestamp(0))",
+            "INSERT INTO ${pg_schema}.whole_seconds VALUES (1, '2026-06-19 
12:23:23')",
+        ])
+        qt_cached_whole_seconds_type """desc 
${catalog_name}.${pg_schema}.whole_seconds"""
+        onPostgres(["ALTER TABLE ${pg_schema}.whole_seconds ALTER COLUMN ts 
TYPE timestamp(6)"])
+        test {
+            sql """select ts from ${catalog_name}.${pg_schema}.whole_seconds"""
+            exception "is datetime precision 0 in the plan but the source 
reports 6"
+        }
+
+        // Refreshing reconciles the cache. Every read that failed above has 
to succeed now, and
+        // return the value the source actually holds -- the rejection is 
about the stale
+        // declaration, not about the data.
+        sql """refresh catalog ${catalog_name}"""
+
+        qt_refreshed_datetime_type """desc 
${catalog_name}.${pg_schema}.orders"""
+        qt_refreshed_datetime_value """select createtime from 
${catalog_name}.${pg_schema}.orders
+                                      where status = 1"""
+
+        qt_refreshed_decimal_type """desc 
${catalog_name}.${pg_schema}.amounts"""
+        qt_refreshed_decimal_value """select amount from 
${catalog_name}.${pg_schema}.amounts"""
+
+        qt_refreshed_whole_seconds """select ts from 
${catalog_name}.${pg_schema}.whole_seconds"""
+        qt_refreshed_wider_scale """select amount from 
${catalog_name}.${pg_schema}.wider_scale"""
+
+        // A source narrower than the plan cannot produce a value the column 
will not hold, so it
+        // is left alone -- only the widening direction is evidence of a 
problem.
+        onPostgres([
+            "CREATE TABLE ${pg_schema}.narrowed (id int, ts timestamp(6))",
+            "INSERT INTO ${pg_schema}.narrowed VALUES (1, '2026-06-19 
12:23:23.486067')",
+        ])
+        sql """select ts from ${catalog_name}.${pg_schema}.narrowed"""
+        onPostgres(["ALTER TABLE ${pg_schema}.narrowed ALTER COLUMN ts TYPE 
timestamp(0)"])
+        qt_narrowed_source """select ts from 
${catalog_name}.${pg_schema}.narrowed"""
+
+        // With the cache reconciled the planner can see the source is wider 
and emits the
+        // narrowing cast, so the same insert now succeeds: both source rows 
round onto one
+        // second-granularity key and the unique table keeps the later one.
+        sql """insert into internal.${internal_db}.orders_target

Review Comment:
   [P2] Make this duplicate winner deterministic, or assert only the one-row 
invariant. Both PostgreSQL rows become the same unique key after the cast, but 
this source query has no ordering and the target has no sequence column, while 
the expected output pins `status = 2`. A valid remote/execution order can 
preserve status 1 and fail this regression even though deduplication is correct.



##########
fe/be-java-extensions/jdbc-scanner/src/main/java/org/apache/doris/jdbc/JdbcJniScanner.java:
##########
@@ -214,6 +217,51 @@ public void open() throws IOException {
         }
     }
 
+    /**
+     * Refuse the scan when the driver reports a wider column than the plan 
was built with.
+     *
+     * <p>The plan's types come from the catalog's cached view of the remote 
schema. If the remote
+     * DDL widened a column since then, the planner never inserted a narrowing 
cast, and the extra
+     * digits would land in a column whose declared type cannot hold them.
+     *
+     * <p>Checked once per scan, so the error can name the column and both 
precisions. Only the
+     * widening direction, and only when the driver answers: they vary in how 
faithfully they fill
+     * in ResultSetMetaData, and one with nothing to say must not fail every 
scan.
+     */
+    private void checkSourcePrecisionAgainstPlan() throws SQLException {
+        for (int i = 0; i < types.length; i++) {
+            ColumnType planned = types[i];
+            int rsIndex = columnIndexMapping[i];
+
+            if (planned.isDateTimeV2() || planned.isTimestampTz()) {
+                // Doris parses the sub-second digits into precision; the 
driver reports them as scale.
+                int plannedPrecision = planned.getPrecision();
+                int sourceScale = resultSetMetaData.getScale(rsIndex);
+                if (sourceScale > plannedPrecision) {
+                    throw new SQLException(String.format(
+                            "Column '%s' is datetime precision %d in the plan 
but the source reports %d."
+                                    + " The cached external schema is stale -- 
refresh the catalog.",
+                            fields[i], plannedPrecision, sourceScale));
+                }
+            } else if (planned.isDecimal()) {
+                // Extra significant digits cannot be represented; extra 
fractional digits would be
+                // rounded away, quietly returning a different number than the 
source holds.
+                int plannedPrecision = planned.getPrecision();
+                int plannedScale = planned.getScale();
+                int sourcePrecision = resultSetMetaData.getPrecision(rsIndex);
+                int sourceScale = resultSetMetaData.getScale(rsIndex);
+                if (sourcePrecision > plannedPrecision || sourceScale > 
plannedScale) {

Review Comment:
   [P1] This predicate still admits decimal sources that do not fit the planned 
type. For a bounded case, `decimal(9,5) -> decimal(9,2)` makes both comparisons 
false but expands integer capacity from four to seven digits; containment must 
also compare `precision - scale`. There is also an unbounded case: after 
PostgreSQL `numeric(10,2) -> numeric`, PgJDBC reports unspecified precision as 
zero, so these `>` checks again pass even though the source can return 
arbitrarily large values (a refreshed catalog maps that type to STRING). Both 
paths send a live BigDecimal to `TypeNativeBytes.getDecimalBytes`, which copies 
it into the cached fixed width without a range check and can silently change 
the value. Please handle both integer-capacity growth and unbounded/unknown 
live numeric types, with regressions for each.



##########
regression-test/suites/external_table_p0/jdbc/test_jdbc_stale_schema_precision.groovy:
##########
@@ -0,0 +1,231 @@
+// 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.
+
+import java.sql.Connection
+import java.sql.DriverManager
+
+// A JDBC catalog caches the remote column definition. When the remote DDL 
widens a column's
+// precision, that cache keeps reporting the narrower one until it is 
refreshed, so the planner
+// sees source and target as the same type and emits no narrowing cast. The 
value scanned out of
+// the remote database still carries its full precision, and nothing 
downstream re-normalizes it.
+//
+// For a DATETIMEV2 key column that is not cosmetic: sub-second digits live in 
the same 64-bit word
+// the storage layer encodes as a key, so two rows the column claims are equal 
become two distinct
+// keys and a unique table stops deduplicating them.
+//
+// These cases drive the remote DDL directly (the catalog is read-only) to put 
the cache and the
+// remote schema out of step on purpose, then assert that what lands in Doris 
still respects the
+// target column's declared type.
+suite("test_jdbc_stale_schema_precision", "p0,external") {
+    String enabled = context.config.otherConfigs.get("enableJdbcTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        return
+    }
+
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String pg_port = context.config.otherConfigs.get("pg_14_port")
+    String s3_endpoint = getS3Endpoint()
+    String bucket = getS3BucketName()
+    String driver_url = 
"https://${bucket}.${s3_endpoint}/regression/jdbc_driver/postgresql-42.5.0.jar";
+
+    String catalog_name = "test_jdbc_stale_schema_precision_catalog"
+    String internal_db = "regression_test_jdbc_stale_schema_precision"
+    String pg_schema = "stale_precision"
+    String pg_url = 
"jdbc:postgresql://${externalEnvIp}:${pg_port}/postgres?useSSL=false"
+
+    Class.forName("org.postgresql.Driver")
+
+    // The catalog cannot issue DDL against the source, so drive PostgreSQL 
directly.
+    def onPostgres = { List<String> statements ->
+        Connection conn = DriverManager.getConnection(pg_url, "postgres", 
"123456")
+        try {
+            def stmt = conn.createStatement()
+            try {
+                statements.each { stmt.execute(it) }
+            } finally {
+                stmt.close()
+            }
+        } finally {
+            conn.close()
+        }
+    }
+
+    sql """drop catalog if exists ${catalog_name}"""
+    sql """drop database if exists internal.${internal_db}"""
+    sql """create database internal.${internal_db}"""
+
+    onPostgres([
+        "DROP SCHEMA IF EXISTS ${pg_schema} CASCADE",
+        "CREATE SCHEMA ${pg_schema}",
+        // Declared narrow to begin with: this is what the catalog will cache.
+        """CREATE TABLE ${pg_schema}.orders (
+               mchid varchar, orderid varchar, createtime timestamp(0), status 
int)""",
+        "INSERT INTO ${pg_schema}.orders VALUES ('M001','ORD1','2026-06-19 
12:00:00',0)",
+        "CREATE TABLE ${pg_schema}.amounts (id int, amount numeric(10,2))",
+        "INSERT INTO ${pg_schema}.amounts VALUES (1, 1.00)",
+    ])
+
+    sql """create catalog ${catalog_name} properties(
+        "type"="jdbc",
+        "user"="postgres",
+        "password"="123456",
+        "jdbc_url" = "${pg_url}&currentSchema=${pg_schema}",
+        "driver_url" = "${driver_url}",
+        "driver_class" = "org.postgresql.Driver"
+    );"""
+
+    try {
+        // Populate the catalog's schema cache while the remote columns are 
still narrow.
+        qt_cached_datetime_type """desc ${catalog_name}.${pg_schema}.orders"""
+        qt_cached_decimal_type """desc ${catalog_name}.${pg_schema}.amounts"""
+
+        // Widen the remote columns and write values that only the wider type 
can hold. The catalog
+        // is deliberately not refreshed, so from here on its cache disagrees 
with the source.
+        onPostgres([
+            "ALTER TABLE ${pg_schema}.orders ALTER COLUMN createtime TYPE 
timestamp(6)",
+            "DELETE FROM ${pg_schema}.orders",
+            """INSERT INTO ${pg_schema}.orders VALUES
+                   ('M001','ORD1','2026-06-19 12:23:23.486067',1),
+                   ('M001','ORD1','2026-06-19 12:23:23.000000',2)""",
+            "ALTER TABLE ${pg_schema}.amounts ALTER COLUMN amount TYPE 
numeric(30,2)",
+            "DELETE FROM ${pg_schema}.amounts",
+            "INSERT INTO ${pg_schema}.amounts VALUES (1, 
12345678901234567890.12)",
+        ])
+
+        // Still the narrow types -- the cache has not caught up.
+        qt_stale_datetime_type """desc ${catalog_name}.${pg_schema}.orders"""
+
+        sql """create table internal.${internal_db}.orders_target (
+                   `mchid`      varchar(65533) NOT NULL,
+                   `createtime` datetime       NOT NULL,
+                   `orderid`    varchar(65533) NOT NULL,
+                   `status`     int            NULL
+               ) ENGINE=OLAP
+               UNIQUE KEY(`mchid`, `createtime`, `orderid`)
+               DISTRIBUTED BY HASH(`mchid`) BUCKETS 1
+               PROPERTIES ("replication_num" = "1", 
"enable_unique_key_merge_on_write" = "true");"""
+
+        // The plan was built from the cached, narrower type, so no narrowing 
cast exists and
+        // values carrying the extra digits would land in a column that cannot 
hold them --
+        // splitting one logical key into two and breaking dedup on the unique 
table. The scanner
+        // compares the plan against the driver's live metadata when it opens 
the result set, so
+        // this is refused before a single row is read, naming the column and 
both precisions.
+        test {
+            sql """insert into internal.${internal_db}.orders_target
+                   select mchid, createtime, orderid, status
+                   from ${catalog_name}.${pg_schema}.orders;"""
+            exception "is datetime precision 0 in the plan but the source 
reports 6"
+        }
+
+        test {
+            sql """select createtime from 
${catalog_name}.${pg_schema}.orders"""
+            exception "is datetime precision 0 in the plan but the source 
reports 6"
+        }
+
+        // Same for a decimal the cached type is too narrow for.
+        test {
+            sql """select amount from ${catalog_name}.${pg_schema}.amounts"""
+            exception "in the plan but the source reports"
+        }
+
+        // A widened scale counts too: the value would fit the cached 
precision, but rounding it
+        // down to the cached scale returns a different number than the source 
holds -- the
+        // quietest way to be wrong, since nothing about the result looks 
unusual.
+        onPostgres([
+            "CREATE TABLE ${pg_schema}.wider_scale (id int, amount 
numeric(20,2))",
+            "INSERT INTO ${pg_schema}.wider_scale VALUES (1, 1.00)",
+        ])
+        qt_cached_wider_scale_type """desc 
${catalog_name}.${pg_schema}.wider_scale"""
+        onPostgres([
+            "ALTER TABLE ${pg_schema}.wider_scale ALTER COLUMN amount TYPE 
numeric(20,6)",
+            "UPDATE ${pg_schema}.wider_scale SET amount = 123.456789",
+        ])
+        test {
+            sql """select amount from 
${catalog_name}.${pg_schema}.wider_scale"""
+            exception "in the plan but the source reports"
+        }
+
+        // The drift is what is refused, not the data: a column whose values 
all still fit the
+        // cached type is rejected just the same, because the next row need 
not.
+        onPostgres([
+            "CREATE TABLE ${pg_schema}.whole_seconds (id int, ts 
timestamp(0))",
+            "INSERT INTO ${pg_schema}.whole_seconds VALUES (1, '2026-06-19 
12:23:23')",
+        ])
+        qt_cached_whole_seconds_type """desc 
${catalog_name}.${pg_schema}.whole_seconds"""
+        onPostgres(["ALTER TABLE ${pg_schema}.whole_seconds ALTER COLUMN ts 
TYPE timestamp(6)"])
+        test {
+            sql """select ts from ${catalog_name}.${pg_schema}.whole_seconds"""
+            exception "is datetime precision 0 in the plan but the source 
reports 6"
+        }
+
+        // Refreshing reconciles the cache. Every read that failed above has 
to succeed now, and
+        // return the value the source actually holds -- the rejection is 
about the stale
+        // declaration, not about the data.
+        sql """refresh catalog ${catalog_name}"""
+
+        qt_refreshed_datetime_type """desc 
${catalog_name}.${pg_schema}.orders"""
+        qt_refreshed_datetime_value """select createtime from 
${catalog_name}.${pg_schema}.orders
+                                      where status = 1"""
+
+        qt_refreshed_decimal_type """desc 
${catalog_name}.${pg_schema}.amounts"""
+        qt_refreshed_decimal_value """select amount from 
${catalog_name}.${pg_schema}.amounts"""
+
+        qt_refreshed_whole_seconds """select ts from 
${catalog_name}.${pg_schema}.whole_seconds"""
+        qt_refreshed_wider_scale """select amount from 
${catalog_name}.${pg_schema}.wider_scale"""
+
+        // A source narrower than the plan cannot produce a value the column 
will not hold, so it
+        // is left alone -- only the widening direction is evidence of a 
problem.
+        onPostgres([
+            "CREATE TABLE ${pg_schema}.narrowed (id int, ts timestamp(6))",
+            "INSERT INTO ${pg_schema}.narrowed VALUES (1, '2026-06-19 
12:23:23.486067')",
+        ])
+        sql """select ts from ${catalog_name}.${pg_schema}.narrowed"""
+        onPostgres(["ALTER TABLE ${pg_schema}.narrowed ALTER COLUMN ts TYPE 
timestamp(0)"])
+        qt_narrowed_source """select ts from 
${catalog_name}.${pg_schema}.narrowed"""
+
+        // With the cache reconciled the planner can see the source is wider 
and emits the
+        // narrowing cast, so the same insert now succeeds: both source rows 
round onto one
+        // second-granularity key and the unique table keeps the later one.
+        sql """insert into internal.${internal_db}.orders_target
+               select mchid, createtime, orderid, status
+               from ${catalog_name}.${pg_schema}.orders;"""
+        qt_after_refresh_dedup """select mchid, orderid, createtime,
+                                         microsecond(cast(createtime as 
datetime(6))) as hidden_us, status
+                                  from internal.${internal_db}.orders_target 
order by status"""
+
+        def rows = sql """select count(*) from 
internal.${internal_db}.orders_target"""
+        assertEquals(1, (rows[0][0] as Number).intValue())
+
+        // And a wider destination keeps the microseconds the source actually 
has -- the reason
+        // this layer rejects rather than rounds.
+        sql """create table internal.${internal_db}.orders_wide (
+                   `id` int, `createtime` datetime(6)
+               ) DISTRIBUTED BY HASH(`id`) BUCKETS 1 PROPERTIES 
("replication_num" = "1");"""
+        sql """insert into internal.${internal_db}.orders_wide
+               select status, createtime from 
${catalog_name}.${pg_schema}.orders where status = 1;"""
+        qt_wide_destination """select createtime, microsecond(createtime) as us
+                               from internal.${internal_db}.orders_wide"""
+    } finally {

Review Comment:
   [P2] Please keep cleanup at the start of the suite and preserve this state 
on failure. The repository test standard explicitly says not to drop tables 
after a regression so the environment remains available for debugging; this 
`finally` removes the catalog, Doris tables, and remote schema even when an 
assertion fails.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to