This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 67978a6b92 [format] Validate format table options are not empty at
table creation time (#8328)
67978a6b92 is described below
commit 67978a6b92298cb946c6411be609e2e73367aadc
Author: Zouxxyy <[email protected]>
AuthorDate: Tue Jun 23 19:25:40 2026 +0800
[format] Validate format table options are not empty at table creation time
(#8328)
Validate that CSV/JSON/Text format table options (such as
`csv.quote-character`, `csv.field-delimiter`, etc.) are not empty at
table creation time.
Previously, setting `csv.quote-character` to an empty string would
succeed at table creation but fail with
`StringIndexOutOfBoundsException` during INSERT or SELECT. Now the error
is caught early with a clear message.
---
.../org/apache/paimon/catalog/CatalogUtils.java | 61 +++++++++++++++++-----
.../paimon/spark/sql/FormatTableTestBase.scala | 11 ++++
2 files changed, 60 insertions(+), 12 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
b/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
index a33dc2c0c1..9f287a07d2 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
@@ -20,6 +20,9 @@ package org.apache.paimon.catalog;
import org.apache.paimon.CoreOptions;
import org.apache.paimon.TableType;
+import org.apache.paimon.format.csv.CsvOptions;
+import org.apache.paimon.format.json.JsonOptions;
+import org.apache.paimon.format.text.TextOptions;
import org.apache.paimon.fs.FileIO;
import org.apache.paimon.fs.Path;
import org.apache.paimon.manifest.PartitionEntry;
@@ -157,24 +160,58 @@ public class CatalogUtils {
TableType tableType = options.get(CoreOptions.TYPE);
if (tableType.equals(TableType.FORMAT_TABLE)) {
- checkArgument(
- options.get(PRIMARY_KEY) == null,
- "Cannot define %s for format table.",
- PRIMARY_KEY.key());
- if (dataTokenEnabled && options.get(PATH) == null) {
- checkArgument(
- options.get(FORMAT_TABLE_IMPLEMENTATION)
- !=
CoreOptions.FormatTableImplementation.ENGINE,
- "Cannot define %s is engine for format table when data
token is enabled and not define %s.",
- FORMAT_TABLE_IMPLEMENTATION.key(),
- PATH.key());
- }
+ validateFormatTableOptions(options, dataTokenEnabled);
}
for (DataField field : schema.fields()) {
validateDefaultValue(field.type(), field.defaultValue());
}
}
+ private static void validateFormatTableOptions(Options options, boolean
dataTokenEnabled) {
+ checkArgument(
+ options.get(PRIMARY_KEY) == null,
+ "Cannot define %s for format table.",
+ PRIMARY_KEY.key());
+ if (dataTokenEnabled && options.get(PATH) == null) {
+ checkArgument(
+ options.get(FORMAT_TABLE_IMPLEMENTATION)
+ != CoreOptions.FormatTableImplementation.ENGINE,
+ "Cannot define %s is engine for format table when data
token is enabled and not define %s.",
+ FORMAT_TABLE_IMPLEMENTATION.key(),
+ PATH.key());
+ }
+
+ String format = options.get(CoreOptions.FILE_FORMAT);
+ if ("csv".equalsIgnoreCase(format)) {
+ checkArgument(
+ !options.get(CsvOptions.FIELD_DELIMITER).isEmpty(),
+ "%s must not be empty.",
+ CsvOptions.FIELD_DELIMITER.key());
+ checkArgument(
+ !options.get(CsvOptions.LINE_DELIMITER).isEmpty(),
+ "%s must not be empty.",
+ CsvOptions.LINE_DELIMITER.key());
+ checkArgument(
+ !options.get(CsvOptions.QUOTE_CHARACTER).isEmpty(),
+ "%s must not be empty.",
+ CsvOptions.QUOTE_CHARACTER.key());
+ checkArgument(
+ !options.get(CsvOptions.ESCAPE_CHARACTER).isEmpty(),
+ "%s must not be empty.",
+ CsvOptions.ESCAPE_CHARACTER.key());
+ } else if ("json".equalsIgnoreCase(format)) {
+ checkArgument(
+ !options.get(JsonOptions.LINE_DELIMITER).isEmpty(),
+ "%s must not be empty.",
+ JsonOptions.LINE_DELIMITER.key());
+ } else if ("text".equalsIgnoreCase(format)) {
+ checkArgument(
+ !options.get(TextOptions.LINE_DELIMITER).isEmpty(),
+ "%s must not be empty.",
+ TextOptions.LINE_DELIMITER.key());
+ }
+ }
+
public static void validateNamePattern(Catalog catalog, String
namePattern) {
if (Objects.nonNull(namePattern) && !catalog.supportsListByPattern()) {
throw new UnsupportedOperationException(
diff --git
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
index 503005f247..f32447cf43 100644
---
a/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
+++
b/paimon-spark/paimon-spark-ut/src/test/scala/org/apache/paimon/spark/sql/FormatTableTestBase.scala
@@ -245,6 +245,17 @@ abstract class FormatTableTestBase extends
PaimonHiveTestBase with AdaptiveSpark
}
}
+ test("Format table: csv with empty quote-character should fail") {
+ withTable("t") {
+ withSparkSQLConf("spark.paimon.format-table.implementation" -> "paimon")
{
+ val error = intercept[IllegalArgumentException] {
+ sql("CREATE TABLE t (f0 INT, f1 STRING) USING CSV OPTIONS
('csv.quote-character' '')")
+ }
+ assert(error.getMessage.contains("csv.quote-character must not be
empty"))
+ }
+ }
+ }
+
test("Format table: format table and spark table props recognize") {
val paimonFormatTblProps =
"""