You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ru...@apache.org on 2024/11/12 16:29:14 UTC
(iceberg) branch main updated: Spark 3.5: Iceberg parser should passthrough unsupported procedure to delegate (#11480)
This is an automated email from the ASF dual-hosted git repository.
russellspitzer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git
The following commit(s) were added to refs/heads/main by this push:
new 50545781d5 Spark 3.5: Iceberg parser should passthrough unsupported procedure to delegate (#11480)
50545781d5 is described below
commit 50545781d54adedb6e6f1e753323ccf37ba9030f
Author: Cheng Pan <ch...@apache.org>
AuthorDate: Wed Nov 13 00:29:08 2024 +0800
Spark 3.5: Iceberg parser should passthrough unsupported procedure to delegate (#11480)
---
.../IcebergSparkSqlExtensionsParser.scala | 12 +++-
.../spark/extensions/TestCallStatementParser.java | 72 +++++++++++++++-------
.../TestCherrypickSnapshotProcedure.java | 12 +++-
.../extensions/TestExpireSnapshotsProcedure.java | 11 +++-
.../extensions/TestFastForwardBranchProcedure.java | 11 +++-
.../extensions/TestPublishChangesProcedure.java | 12 +++-
.../extensions/TestRemoveOrphanFilesProcedure.java | 10 ++-
.../extensions/TestRewriteDataFilesProcedure.java | 13 ++--
.../extensions/TestRewriteManifestsProcedure.java | 11 +++-
.../TestRollbackToSnapshotProcedure.java | 12 +++-
.../TestRollbackToTimestampProcedure.java | 12 +++-
.../TestSetCurrentSnapshotProcedure.java | 12 +++-
.../iceberg/spark/procedures/SparkProcedures.java | 5 ++
13 files changed, 152 insertions(+), 53 deletions(-)
diff --git a/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSparkSqlExtensionsParser.scala b/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSparkSqlExtensionsParser.scala
index 02bd59366c..30940d9cff 100644
--- a/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSparkSqlExtensionsParser.scala
+++ b/spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSparkSqlExtensionsParser.scala
@@ -29,6 +29,7 @@ import org.apache.iceberg.common.DynConstructors
import org.apache.iceberg.spark.ExtendedParser
import org.apache.iceberg.spark.ExtendedParser.RawOrderField
import org.apache.iceberg.spark.Spark3Util
+import org.apache.iceberg.spark.procedures.SparkProcedures
import org.apache.iceberg.spark.source.SparkTable
import org.apache.spark.sql.AnalysisException
import org.apache.spark.sql.SparkSession
@@ -136,8 +137,11 @@ class IcebergSparkSqlExtensionsParser(delegate: ParserInterface) extends ParserI
// Strip comments of the form /* ... */. This must come after stripping newlines so that
// comments that span multiple lines are caught.
.replaceAll("/\\*.*?\\*/", " ")
+ // Strip backtick then `system`.`ancestors_of` changes to system.ancestors_of
+ .replaceAll("`", "")
.trim()
- normalized.startsWith("call") || (
+
+ isIcebergProcedure(normalized) || (
normalized.startsWith("alter table") && (
normalized.contains("add partition field") ||
normalized.contains("drop partition field") ||
@@ -151,6 +155,12 @@ class IcebergSparkSqlExtensionsParser(delegate: ParserInterface) extends ParserI
isSnapshotRefDdl(normalized)))
}
+ // All builtin Iceberg procedures are under the 'system' namespace
+ private def isIcebergProcedure(normalized: String): Boolean = {
+ normalized.startsWith("call") &&
+ SparkProcedures.names().asScala.map("system." + _).exists(normalized.contains)
+ }
+
private def isSnapshotRefDdl(normalized: String): Boolean = {
normalized.contains("create branch") ||
normalized.contains("replace branch") ||
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCallStatementParser.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCallStatementParser.java
index 65a3690373..ade19de36f 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCallStatementParser.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCallStatementParser.java
@@ -68,11 +68,34 @@ public class TestCallStatementParser {
currentSpark.stop();
}
+ @Test
+ public void testDelegateUnsupportedProcedure() {
+ assertThatThrownBy(() -> parser.parsePlan("CALL cat.d.t()"))
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
+ }
+
+ @Test
+ public void testCallWithBackticks() throws ParseException {
+ CallStatement call =
+ (CallStatement) parser.parsePlan("CALL cat.`system`.`rollback_to_snapshot`()");
+ assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "rollback_to_snapshot");
+
+ assertThat(seqAsJavaList(call.args())).hasSize(0);
+ }
+
@Test
public void testCallWithPositionalArgs() throws ParseException {
CallStatement call =
- (CallStatement) parser.parsePlan("CALL c.n.func(1, '2', 3L, true, 1.0D, 9.0e1, 900e-1BD)");
- assertThat(seqAsJavaList(call.name())).containsExactly("c", "n", "func");
+ (CallStatement)
+ parser.parsePlan(
+ "CALL c.system.rollback_to_snapshot(1, '2', 3L, true, 1.0D, 9.0e1, 900e-1BD)");
+ assertThat(seqAsJavaList(call.name())).containsExactly("c", "system", "rollback_to_snapshot");
assertThat(seqAsJavaList(call.args())).hasSize(7);
@@ -88,8 +111,10 @@ public class TestCallStatementParser {
@Test
public void testCallWithNamedArgs() throws ParseException {
CallStatement call =
- (CallStatement) parser.parsePlan("CALL cat.system.func(c1 => 1, c2 => '2', c3 => true)");
- assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "func");
+ (CallStatement)
+ parser.parsePlan(
+ "CALL cat.system.rollback_to_snapshot(c1 => 1, c2 => '2', c3 => true)");
+ assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "rollback_to_snapshot");
assertThat(seqAsJavaList(call.args())).hasSize(3);
@@ -100,8 +125,9 @@ public class TestCallStatementParser {
@Test
public void testCallWithMixedArgs() throws ParseException {
- CallStatement call = (CallStatement) parser.parsePlan("CALL cat.system.func(c1 => 1, '2')");
- assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "func");
+ CallStatement call =
+ (CallStatement) parser.parsePlan("CALL cat.system.rollback_to_snapshot(c1 => 1, '2')");
+ assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "rollback_to_snapshot");
assertThat(seqAsJavaList(call.args())).hasSize(2);
@@ -113,8 +139,9 @@ public class TestCallStatementParser {
public void testCallWithTimestampArg() throws ParseException {
CallStatement call =
(CallStatement)
- parser.parsePlan("CALL cat.system.func(TIMESTAMP '2017-02-03T10:37:30.00Z')");
- assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "func");
+ parser.parsePlan(
+ "CALL cat.system.rollback_to_snapshot(TIMESTAMP '2017-02-03T10:37:30.00Z')");
+ assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "rollback_to_snapshot");
assertThat(seqAsJavaList(call.args())).hasSize(1);
@@ -125,8 +152,9 @@ public class TestCallStatementParser {
@Test
public void testCallWithVarSubstitution() throws ParseException {
CallStatement call =
- (CallStatement) parser.parsePlan("CALL cat.system.func('${spark.extra.prop}')");
- assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "func");
+ (CallStatement)
+ parser.parsePlan("CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')");
+ assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "rollback_to_snapshot");
assertThat(seqAsJavaList(call.args())).hasSize(1);
@@ -135,28 +163,30 @@ public class TestCallStatementParser {
@Test
public void testCallParseError() {
- assertThatThrownBy(() -> parser.parsePlan("CALL cat.system radish kebab"))
+ assertThatThrownBy(() -> parser.parsePlan("CALL cat.system.rollback_to_snapshot kebab"))
.isInstanceOf(IcebergParseException.class)
- .hasMessageContaining("missing '(' at 'radish'");
+ .hasMessageContaining("missing '(' at 'kebab'");
}
@Test
public void testCallStripsComments() throws ParseException {
List<String> callStatementsWithComments =
Lists.newArrayList(
- "/* bracketed comment */ CALL cat.system.func('${spark.extra.prop}')",
- "/**/ CALL cat.system.func('${spark.extra.prop}')",
- "-- single line comment \n CALL cat.system.func('${spark.extra.prop}')",
- "-- multiple \n-- single line \n-- comments \n CALL cat.system.func('${spark.extra.prop}')",
- "/* select * from multiline_comment \n where x like '%sql%'; */ CALL cat.system.func('${spark.extra.prop}')",
+ "/* bracketed comment */ CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')",
+ "/**/ CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')",
+ "-- single line comment \n CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')",
+ "-- multiple \n-- single line \n-- comments \n CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')",
+ "/* select * from multiline_comment \n where x like '%sql%'; */ CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')",
"/* {\"app\": \"dbt\", \"dbt_version\": \"1.0.1\", \"profile_name\": \"profile1\", \"target_name\": \"dev\", "
- + "\"node_id\": \"model.profile1.stg_users\"} \n*/ CALL cat.system.func('${spark.extra.prop}')",
+ + "\"node_id\": \"model.profile1.stg_users\"} \n*/ CALL cat.system.rollback_to_snapshot('${spark.extra.prop}')",
"/* Some multi-line comment \n"
- + "*/ CALL /* inline comment */ cat.system.func('${spark.extra.prop}') -- ending comment",
- "CALL -- a line ending comment\n" + "cat.system.func('${spark.extra.prop}')");
+ + "*/ CALL /* inline comment */ cat.system.rollback_to_snapshot('${spark.extra.prop}') -- ending comment",
+ "CALL -- a line ending comment\n"
+ + "cat.system.rollback_to_snapshot('${spark.extra.prop}')");
for (String sqlText : callStatementsWithComments) {
CallStatement call = (CallStatement) parser.parsePlan(sqlText);
- assertThat(seqAsJavaList(call.name())).containsExactly("cat", "system", "func");
+ assertThat(seqAsJavaList(call.name()))
+ .containsExactly("cat", "system", "rollback_to_snapshot");
assertThat(seqAsJavaList(call.args())).hasSize(1);
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCherrypickSnapshotProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCherrypickSnapshotProcedure.java
index d61456fa73..08b0754df4 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCherrypickSnapshotProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestCherrypickSnapshotProcedure.java
@@ -20,6 +20,7 @@ package org.apache.iceberg.spark.extensions;
import static org.apache.iceberg.TableProperties.WRITE_AUDIT_PUBLISH_ENABLED;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import java.util.List;
import org.apache.iceberg.Snapshot;
@@ -30,7 +31,7 @@ import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
@@ -169,8 +170,13 @@ public class TestCherrypickSnapshotProcedure extends ExtensionsTestBase {
.hasMessage("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.cherrypick_snapshot('n', 't', 1L)", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.cherrypick_snapshot not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.cherrypick_snapshot('t')", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestExpireSnapshotsProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestExpireSnapshotsProcedure.java
index 970fcc47b8..0f24c5613f 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestExpireSnapshotsProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestExpireSnapshotsProcedure.java
@@ -51,7 +51,7 @@ import org.apache.iceberg.spark.data.TestHelpers;
import org.apache.iceberg.spark.source.SimpleRecord;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Encoders;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
@@ -168,8 +168,13 @@ public class TestExpireSnapshotsProcedure extends ExtensionsTestBase {
.hasMessage("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.expire_snapshots('n', 't')", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.expire_snapshots not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.expire_snapshots()", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestFastForwardBranchProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestFastForwardBranchProcedure.java
index fd8ee7d91b..7eb334f70a 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestFastForwardBranchProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestFastForwardBranchProcedure.java
@@ -29,7 +29,7 @@ import org.apache.iceberg.SnapshotRef;
import org.apache.iceberg.Table;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.spark.sql.AnalysisException;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
@@ -171,8 +171,13 @@ public class TestFastForwardBranchProcedure extends ExtensionsTestBase {
assertThatThrownBy(
() ->
sql("CALL %s.custom.fast_forward('test_table', 'main', 'newBranch')", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.fast_forward not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.fast_forward('test_table', 'main')", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestPublishChangesProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestPublishChangesProcedure.java
index cfca0c068c..6284d88a15 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestPublishChangesProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestPublishChangesProcedure.java
@@ -20,6 +20,7 @@ package org.apache.iceberg.spark.extensions;
import static org.apache.iceberg.TableProperties.WRITE_AUDIT_PUBLISH_ENABLED;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import java.util.List;
import org.apache.iceberg.Snapshot;
@@ -30,7 +31,7 @@ import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
@@ -167,8 +168,13 @@ public class TestPublishChangesProcedure extends ExtensionsTestBase {
assertThatThrownBy(
() -> sql("CALL %s.custom.publish_changes('n', 't', 'not_valid')", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.publish_changes not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.publish_changes('t')", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRemoveOrphanFilesProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRemoveOrphanFilesProcedure.java
index 042b846cb1..d8feaa7707 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRemoveOrphanFilesProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRemoveOrphanFilesProcedure.java
@@ -62,7 +62,6 @@ import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Encoders;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
@@ -252,8 +251,13 @@ public class TestRemoveOrphanFilesProcedure extends ExtensionsTestBase {
.hasMessage("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.remove_orphan_files('n', 't')", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.remove_orphan_files not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.remove_orphan_files()", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteDataFilesProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteDataFilesProcedure.java
index 4a20521c48..93198825e3 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteDataFilesProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteDataFilesProcedure.java
@@ -45,7 +45,7 @@ import org.apache.iceberg.spark.source.ThreeColumnRecord;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.apache.spark.sql.internal.SQLConf;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
@@ -694,11 +694,16 @@ public class TestRewriteDataFilesProcedure extends ExtensionsTestBase {
assertThatThrownBy(
() -> sql("CALL %s.system.rewrite_data_files('n', table => 't')", catalogName))
.isInstanceOf(AnalysisException.class)
- .hasMessage("Named and positional arguments cannot be mixed");
+ .hasMessageContaining("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.rewrite_data_files('n', 't')", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.rewrite_data_files not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.rewrite_data_files()", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteManifestsProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteManifestsProcedure.java
index 83ec1ef84d..5eebd9aeb7 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteManifestsProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRewriteManifestsProcedure.java
@@ -31,8 +31,8 @@ import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.RowFactory;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
import org.apache.spark.sql.catalyst.analysis.NoSuchTableException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -293,8 +293,13 @@ public class TestRewriteManifestsProcedure extends ExtensionsTestBase {
.hasMessage("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.rewrite_manifests('n', 't')", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.rewrite_manifests not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.rewrite_manifests()", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToSnapshotProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToSnapshotProcedure.java
index 315f6c4c1b..43df78bf76 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToSnapshotProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToSnapshotProcedure.java
@@ -19,6 +19,7 @@
package org.apache.iceberg.spark.extensions;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import java.util.List;
@@ -30,7 +31,7 @@ import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -254,8 +255,13 @@ public class TestRollbackToSnapshotProcedure extends ExtensionsTestBase {
.hasMessage("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.rollback_to_snapshot('n', 't', 1L)", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.rollback_to_snapshot not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.rollback_to_snapshot('t')", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToTimestampProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToTimestampProcedure.java
index 6b74391898..ae35b9f181 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToTimestampProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestRollbackToTimestampProcedure.java
@@ -19,6 +19,7 @@
package org.apache.iceberg.spark.extensions;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import java.sql.Timestamp;
@@ -31,7 +32,7 @@ import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.spark.sql.AnalysisException;
import org.apache.spark.sql.Dataset;
import org.apache.spark.sql.Row;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
@@ -295,8 +296,13 @@ public class TestRollbackToTimestampProcedure extends ExtensionsTestBase {
assertThatThrownBy(
() -> sql("CALL %s.custom.rollback_to_timestamp('n', 't', %s)", catalogName, timestamp))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.rollback_to_timestamp not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.rollback_to_timestamp('t')", catalogName))
.isInstanceOf(AnalysisException.class)
diff --git a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSetCurrentSnapshotProcedure.java b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSetCurrentSnapshotProcedure.java
index 1133d74e66..4c34edef5d 100644
--- a/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSetCurrentSnapshotProcedure.java
+++ b/spark/v3.5/spark-extensions/src/test/java/org/apache/iceberg/spark/extensions/TestSetCurrentSnapshotProcedure.java
@@ -20,6 +20,7 @@ package org.apache.iceberg.spark.extensions;
import static org.apache.iceberg.TableProperties.WRITE_AUDIT_PUBLISH_ENABLED;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import java.util.List;
@@ -30,7 +31,7 @@ import org.apache.iceberg.exceptions.ValidationException;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList;
import org.apache.iceberg.relocated.com.google.common.collect.Iterables;
import org.apache.spark.sql.AnalysisException;
-import org.apache.spark.sql.catalyst.analysis.NoSuchProcedureException;
+import org.apache.spark.sql.catalyst.parser.ParseException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -203,8 +204,13 @@ public class TestSetCurrentSnapshotProcedure extends ExtensionsTestBase {
.hasMessage("Named and positional arguments cannot be mixed");
assertThatThrownBy(() -> sql("CALL %s.custom.set_current_snapshot('n', 't', 1L)", catalogName))
- .isInstanceOf(NoSuchProcedureException.class)
- .hasMessage("Procedure custom.set_current_snapshot not found");
+ .isInstanceOf(ParseException.class)
+ .satisfies(
+ exception -> {
+ ParseException parseException = (ParseException) exception;
+ assertThat(parseException.getErrorClass()).isEqualTo("PARSE_SYNTAX_ERROR");
+ assertThat(parseException.getMessageParameters().get("error")).isEqualTo("'CALL'");
+ });
assertThatThrownBy(() -> sql("CALL %s.system.set_current_snapshot('t')", catalogName))
.isInstanceOf(IllegalArgumentException.class)
diff --git a/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/procedures/SparkProcedures.java b/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/procedures/SparkProcedures.java
index b324cd4422..42003b24e9 100644
--- a/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/procedures/SparkProcedures.java
+++ b/spark/v3.5/spark/src/main/java/org/apache/iceberg/spark/procedures/SparkProcedures.java
@@ -20,6 +20,7 @@ package org.apache.iceberg.spark.procedures;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.function.Supplier;
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
import org.apache.spark.sql.connector.catalog.TableCatalog;
@@ -37,6 +38,10 @@ public class SparkProcedures {
return builderSupplier != null ? builderSupplier.get() : null;
}
+ public static Set<String> names() {
+ return BUILDERS.keySet();
+ }
+
private static Map<String, Supplier<ProcedureBuilder>> initProcedureBuilders() {
ImmutableMap.Builder<String, Supplier<ProcedureBuilder>> mapBuilder = ImmutableMap.builder();
mapBuilder.put("rollback_to_snapshot", RollbackToSnapshotProcedure::builder);