LuciferYang commented on code in PR #14643:
URL: https://github.com/apache/gravitino/pull/14643#discussion_r4495446775


##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTable.java:
##########
@@ -31,14 +34,26 @@
 import org.apache.spark.sql.connector.catalog.Identifier;
 import org.apache.spark.sql.connector.write.LogicalWriteInfo;
 import org.apache.spark.sql.connector.write.WriteBuilder;
+import org.apache.spark.sql.execution.datasources.jdbc.JDBCOptions;
+import org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCScanBuilder;
 import org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTable;
 import org.apache.spark.sql.execution.datasources.v2.jdbc.JDBCTableCatalog;
+import org.apache.spark.sql.jdbc.JdbcDialects;
+import org.apache.spark.sql.types.DataTypes;
+import org.apache.spark.sql.types.StructField;
 import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+import scala.collection.JavaConverters;
 
 public class SparkJdbcTable extends JDBCTable {
 
+  private static final String[] UNSAFE_OPTIONS = {
+    "pushDownPredicate", "pushDownAggregate", "pushDownLimit", "pushDownOffset"

Review Comment:
   This turns off predicate, aggregate, limit and offset pushdown for the whole 
table as soon as it has one string, binary, UUID or external column, and on 
Spark 3.5 as soon as a PostgreSQL table has a `timestamp` column. Collation, 
binary literals and NTZ literals only matter for the expressions that contain 
them.
   
   I ran it through `GravitinoPostgreSqlCatalogSpark35/40` and 
`GravitinoJdbcCatalogSpark35/40` on 3.5.3 and 4.0.3. On base, a 1M-row `(id 
int, s text, n int)` table gets `WHERE id = 1`, `count(*) WHERE n > 500` and 
`LIMIT 10` pushed; with this PR none are. PostgreSQL then receives `SELECT 
"id","n" FROM "public"."big"` without the LIMIT, and at the default `fetchsize` 
of 0 pgjdbc fetches the whole result before returning. A MySQL table with a 
`varchar` column loses `id = 1` and `LIMIT 10` pushdown the same way. On 3.5, a 
PostgreSQL table with only `int` and `timestamp` columns loses `WHERE ts > 
TIMESTAMP_NTZ '2024-01-10 00:00:00'` and `max(ts)` pushdown, both of which base 
pushes. Since the PR is labeled for branch-1.3, this would ship in a 1.3.x 
patch release.
   
   diqiu50 raised this earlier, and I think it belongs in this PR. I tried 
returning a `JDBCScanBuilder` subclass from `newScanBuilder`. Its 
`pushPredicates`, `pushAggregation` and `pushTopN` reject expressions that 
involve a string or binary value (column, cast or literal, including array 
elements) or an NTZ literal the dialect can't compile. LIMIT and OFFSET stay 
on. On both versions `id = 1`, `LIMIT 10`, `count(*)` and `ORDER BY id LIMIT 3` 
were pushed again, and string filters, string Top-N, `min/max(s)`, `cast(n AS 
string) > '9'` and array ordering stayed in Spark. When a string predicate 
stayed in Spark, LIMIT stayed in Spark with it. Aggregates need one more rule 
for columns whose physical and Spark-visible types differ; see my comment on 
line 161.



##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTypeConverter.java:
##########
@@ -44,7 +64,7 @@ public DataType toSparkType(Type gravitinoType) {
           "Reading type {} as a string, Gravitino cannot represent it",
           ((Types.ExternalType) gravitinoType).catalogString());
       return DataTypes.StringType;
-    } else if (gravitinoType instanceof Types.TimestampType) {
+    } else if (gravitinoType instanceof Types.TimestampType && 
!useTimestampNtz) {

Review Comment:
   `timestamp[]` columns can no longer be read on Spark 3.5. The parent 
converter recurses into this method for `ListType`, so the element becomes NTZ 
too. On 3.5.3, though, PostgresDialect maps an array element `timestamp` to 
`TimestampType` and the array getter has no NTZ case, so the elements stay 
`java.sql.Timestamp`. Base reads the column; with this PR 3.5.3 throws 
`ClassCastException: class java.sql.Timestamp cannot be cast to class 
java.lang.Long`, and 4.0.3 works.
   
   Taking the element type from `jdbcTable.schema()` fixes it. I tried it: the 
column reads on both versions, the 4.0 value is the same in UTC and 
America/Los_Angeles sessions, and the 3.5 value shifts with the session time 
zone, as on base. Keeping array elements as `TimestampType` everywhere also 
reads, but then the 4.0 value shifts with the session time zone while top-level 
columns don't. Please cover `timestamp[]` in `TestSparkJdbcReadCorrectness`.



##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTypeConverter.java:
##########
@@ -44,7 +64,7 @@ public DataType toSparkType(Type gravitinoType) {
           "Reading type {} as a string, Gravitino cannot represent it",
           ((Types.ExternalType) gravitinoType).catalogString());
       return DataTypes.StringType;
-    } else if (gravitinoType instanceof Types.TimestampType) {
+    } else if (gravitinoType instanceof Types.TimestampType && 
!useTimestampNtz) {

Review Comment:
   The NTZ read path loses the clamp for PostgreSQL `infinity`. The base 
`TimestampType` getter goes through 
`PostgresDialect.convertJavaTimestampToTimestamp`, which maps 
`infinity`/`-infinity` to 9999-12-31 and 0001-01-01. The NTZ getter calls 
`convertJavaTimestampToTimestampNTZ`, which is just `t.toLocalDateTime` in 
3.5.3, 4.0.3 and Spark master. Through the PostgreSQL catalog, a `timestamp` 
column holding `'infinity'` reads as 9999-12-31 on base, and with this PR the 
query fails with `ArithmeticException: long overflow` on 3.5.3 and 4.0.3.
   
   NTZ is on by default for every PostgreSQL catalog and the PR is labeled for 
branch-1.3, so this needs handling before merge. Registering an extra dialect 
from Gravitino doesn't fix it. I registered a second PostgreSQL dialect 
overriding both conversions: `JdbcDialects.get` returned an 
`AggregatedDialect`, which forwards neither conversion, so both my overrides 
and the built-in clamp were skipped and the read overflowed with the NTZ 
mapping and with the old `TimestampType` mapping. On 4.0 the 
`AggregatedDialect` also compiled a `byte[]` literal as `X'01'` instead of 
`'\x01'::bytea`. A Java subclass of the 4.0 `PostgresDialect` does replace the 
built-in one, but the registry is JVM-wide, so the subclass handles every 
PostgreSQL JDBC URL in the application.
   
   I'd make the NTZ mapping a catalog option that keeps the old mapping by 
default, and document `infinity` as a known limitation when it's on. The root 
cause is Spark's `convertJavaTimestampToTimestampNTZ`, which should get the 
same clamp upstream.



##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTable.java:
##########
@@ -99,4 +145,51 @@ public StructType schema() {
   public Map<String, String> properties() {
     return gravitinoTableInfoHelper.properties();
   }
+
+  private static boolean hasUnsafePushDown(JDBCTable table, Table source) {
+    for (Column column : source.columns()) {
+      Type type = column.dataType();
+      if (type instanceof Types.StringType
+          || type instanceof Types.VarCharType
+          || type instanceof Types.FixedCharType
+          || type instanceof Types.BinaryType
+          || type instanceof Types.ExternalType
+          || type instanceof Types.UUIDType) {
+        return true;
+      }
+    }
+    for (StructField field : table.schema().fields()) {

Review Comment:
   This check reads the physical schema (`table.schema()`), but pushed literals 
are typed by the schema Spark sees. On Spark 3.5 the two differ for 
`timestamptz`: the 3.5 PostgresDialect doesn't tell it apart from `timestamp`, 
so with `preferTimestampNTZ=true` it is physically NTZ while Spark sees 
`TimestampType`. On 3.5.3, a table with only `int` and `timestamptz` columns 
loses filter and aggregate pushdown, even `WHERE id = 1`, which base pushes. 
`testUnsafeReadPushdownIsDisabledOnlyForAffectedTables` builds the physical 
schema from the converter's output, so it never hits this case.
   
   Checking the Spark-visible type instead isn't enough, because pushed 
aggregates are typed by the reader. With that check and everything else pushed, 
`max(tz)` failed planning with an `UnresolvedException` on 3.5.3, and on 3.5.6 
(after SPARK-50870) it returned `2024-01-02 08:00:00` instead of `2024-01-02 
00:00:00`. Not pushing aggregates over columns whose physical and Spark-visible 
types differ fixed both, and `WHERE id = 1` was pushed again. Please add a 3.5 
test for a `timestamptz` table.



##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/SparkJdbcTable.java:
##########
@@ -99,4 +145,51 @@ public StructType schema() {
   public Map<String, String> properties() {
     return gravitinoTableInfoHelper.properties();
   }
+
+  private static boolean hasUnsafePushDown(JDBCTable table, Table source) {
+    for (Column column : source.columns()) {
+      Type type = column.dataType();
+      if (type instanceof Types.StringType

Review Comment:
   The classification only looks at top-level types, so array columns such as 
`text[]` don't count and the table keeps all four pushdowns. PostgreSQL 
compares arrays with the element collation, so the problem this PR fixes is 
still there for array columns. On 3.5.3 and 4.0.3, with a `text[] COLLATE 
"en-US-x-icu"` column holding `{a}` (id 1) and `{B}` (id 2), the pushed `ORDER 
BY tags LIMIT 1` returns id 1 and the pushed `max(tags)` returns `{B}`; 
evaluated in Spark they return id 2 and `{a}`. Base behaves the same, so this 
is a gap in the new check rather than a regression.
   
   Whether the check stays per table or moves per expression, it needs to 
recurse into the `ListType` element type. Please add a `text[]` case to 
`testUnsafeReadPushdownIsDisabledOnlyForAffectedTables`, or to the 
per-expression test if the check moves.



##########
docs/spark-connector/spark-connector.md:
##########
@@ -152,3 +152,24 @@ these columns are rejected before JDBC execution, 
including writes of NULL value
 INSERT OVERWRITE. The error identifies the unsupported column. Spark binds 
these columns
 as VARCHAR, which does not preserve the PostgreSQL target type. Use a native 
PostgreSQL
 writer for these tables. Other PostgreSQL column types retain their existing 
write behavior.
+
+### JDBC query consistency
+
+Some JDBC tables are read without pushdown. For these tables, Spark evaluates 
predicates,
+aggregates, Top-N, limits, and offsets itself. This avoids result differences 
caused by the
+column types below. A table is affected when it has a column of one of these 
types:
+
+- String, char, varchar, binary, UUID, or an external type. The source 
database can compare
+  strings with a different collation, and Spark cannot always write binary 
literals in SQL.
+- A column that maps to Spark `TIMESTAMP_NTZ` (today, only a PostgreSQL 
timestamp without time
+  zone), when the JDBC dialect cannot write `TIMESTAMP_NTZ` literals in SQL. 
This is the case
+  in Spark 3.5, not in Spark 4.0.
+
+The rule applies to the whole table. Filters on numeric columns are not pushed 
down either, so
+Spark can read more data from the source. You cannot turn pushdown back on 
with the
+`pushDownPredicate`, `pushDownAggregate`, `pushDownLimit`, or `pushDownOffset` 
options. Other
+tables keep their normal pushdown settings.
+
+For PostgreSQL catalogs, a timestamp without time zone maps to Spark 
`TIMESTAMP_NTZ`. The

Review Comment:
   Besides the read change this section describes, the NTZ mapping changes two 
existing behaviors it doesn't mention. On 3.5.3 and 4.0.3, `partitionColumn` on 
a PostgreSQL `timestamp` column now fails analysis with `Partition column type 
should be numeric, date, or timestamp, but timestamp_ntz found`, while base 
reads the table. Writes change too: with the JVM in Asia/Shanghai, inserting 
`TIMESTAMP '2024-01-01 00:00:00'` from a UTC session stores 08:00 on base and 
00:00 with this PR. From an Asia/Shanghai session both store 00:00.
   
   Both should go into the docs and the release notes for the next 1.3.x 
release. The write change only affects jobs whose session time zone differs 
from the JVM time zone, but tables those jobs write to will mix rows stored in 
the JVM time zone (before the upgrade) with rows stored in the session time 
zone (after it). Users also have no working opt-out today: with 
`preferTimestampNTZ=false` on the catalog the column is still `timestamp_ntz` 
(see my comment on `GravitinoJdbcCatalog` line 57).



##########
spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/jdbc/GravitinoJdbcCatalog.java:
##########
@@ -53,6 +53,9 @@ protected TableCatalog createAndInitSparkCatalog(
     Map<String, String> all =
         getPropertiesConverter().toSparkCatalogProperties(options, properties);
     applyJdbcCredential(gravitinoCatalogClient, all);
+    if (useTimestampNtz()) {
+      all.put("preferTimestampNTZ", "true");

Review Comment:
   A user's `preferTimestampNTZ=false` can win over this `put`, while the 
converter still maps `timestamp` to NTZ. User options arrive through 
`CaseInsensitiveStringMap` with lowercased keys, so the reader gets both 
`prefertimestampntz` and `preferTimestampNTZ`. I set `preferTimestampNTZ=false` 
on the PostgreSQL catalog. On 3.5.3 (Scala 2.12) this `put` won; on 4.0.3 and 
on 3.5.3 (Scala 2.13) the user's `false` won while the schema still showed 
`timestamp_ntz`.
   
   With the reader at `false`, the physical schema has no NTZ, so the NTZ check 
in `SparkJdbcTable` doesn't fire and the table keeps pushdown on 4.0.3 and on 
3.5.3 (Scala 2.13). Some pushed queries then return wrong results or fail. With 
the JVM in Asia/Shanghai and the session in UTC, a pushed `max(ts)` on 4.0.3 
returned `2024-01-19 16:00:00` instead of `2024-01-20 00:00:00`, the shift 
#14642 is about. On 3.5.3 (Scala 2.13), `max(ts)` failed planning, and `WHERE 
ts > TIMESTAMP_NTZ '2024-01-10 00:00:00'` was pushed unquoted and rejected by 
PostgreSQL.
   
   If you make the NTZ mapping a catalog option, as I suggested in my 
`infinity` comment on `SparkJdbcTypeConverter` line 67, set the reader flag and 
the converter from that one option. If it stays always on, remove any case 
variant of the key before the `put`, as `safeOptions` does.



-- 
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]

Reply via email to