This is an automated email from the ASF dual-hosted git repository.

nicholasjiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 7a6198474 [spark] Supports parser of Spark call procedure command 
(#1785)
7a6198474 is described below

commit 7a6198474b714a68bcf82ff2bbaa394f0b0cdc1c
Author: Nicholas Jiang <[email protected]>
AuthorDate: Fri Aug 25 13:09:47 2023 +0800

    [spark] Supports parser of Spark call procedure command (#1785)
---
 LICENSE                                            |   9 +-
 paimon-spark/paimon-spark-common/pom.xml           |  18 ++
 .../PaimonSqlExtensions.g4                         | 229 ++++++++++++++++++
 .../java/org/apache/paimon/spark/SparkCatalog.java |  17 +-
 .../org/apache/paimon/spark/SparkProcedures.java   |  47 ++++
 .../spark/analysis/NoSuchProcedureException.java}  |  23 +-
 .../paimon/spark/catalog/ProcedureCatalog.java     |  41 ++++
 .../apache/paimon/spark/procedure/Procedure.java   |  45 ++++
 .../paimon/spark/procedure/ProcedureBuilder.java   |  41 ++++
 .../paimon/spark/procedure/ProcedureParameter.java |  56 +++++
 .../spark/procedure/ProcedureParameterImpl.java    |  80 +++++++
 .../PaimonSparkSessionExtensions.scala}            |  17 +-
 .../sql/catalyst/analysis/CoerceArguments.scala    |  61 +++++
 .../sql/catalyst/analysis}/PaimonAnalysis.scala    |  10 +-
 .../sql/catalyst/analysis/ResolveProcedures.scala  | 213 +++++++++++++++++
 .../PaimonSparkSqlExtensionsParser.scala           | 263 +++++++++++++++++++++
 .../extensions/PaimonSqlExtensionsAstBuilder.scala | 132 +++++++++++
 .../sql/catalyst/plans/logical/CallCommand.scala}  |  20 +-
 .../catalyst/plans/logical/CallStatement.scala}    |  24 +-
 .../PaimonDynamicPartitionOverwriteCommand.scala   |   3 +-
 .../logical}/PaimonTableValuedFunctions.scala      |   4 +-
 .../spark/extensions/CallStatementParserTest.java  | 164 +++++++++++++
 .../apache/paimon/spark/PaimonSparkTestBase.scala  |   3 +-
 .../spark/sql/TableValuedFunctionsTest.scala       |   5 +-
 pom.xml                                            |   1 +
 25 files changed, 1472 insertions(+), 54 deletions(-)

diff --git a/LICENSE b/LICENSE
index ef08963ea..74613fb9d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -232,10 +232,15 @@ 
paimon-common/src/main/java/org/apache/paimon/types/DataType.java
 paimon-common/src/main/java/org/apache/paimon/options/ConfigOption.java
 from http://flink.apache.org/ version 1.17.0
 
-paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkGenericCatalog.java
-paimon-hive/paimon-hive-common/src/test/java/org/apache/paimon/hive/TestHiveMetastore.java
 paimon-core/src/main/java/org/apache/paimon/utils/ZOrderByteUtils.java
 paimon-core/src/test/java/org/apache/paimon/utils/TestZOrderByteUtil.java
+paimon-hive/paimon-hive-common/src/test/java/org/apache/paimon/hive/TestHiveMetastore.java
+paimon-spark/paimon-spark-common/src/main/antlr4/org.apache.spark.sql.catalyst.parser.extensions/PaimonSqlExtensions.g4
+paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkGenericCatalog.java
+paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/CoerceArguments.scala
+paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveProcedures.scala
+paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSparkSqlExtensionsParser.scala
+paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSqlExtensionsAstBuilder.scala
 from http://iceberg.apache.org/ version 1.3.0
 
 paimon-hive/paimon-hive-common/src/test/resources/hive-schema-3.1.0.derby.sql
diff --git a/paimon-spark/paimon-spark-common/pom.xml 
b/paimon-spark/paimon-spark-common/pom.xml
index 9a065808f..2f34e06d8 100644
--- a/paimon-spark/paimon-spark-common/pom.xml
+++ b/paimon-spark/paimon-spark-common/pom.xml
@@ -190,6 +190,7 @@ under the License.
     </dependencies>
 
     <build>
+
         <pluginManagement>
             <plugins>
                 <plugin>
@@ -276,6 +277,23 @@ under the License.
                     </scala>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.antlr</groupId>
+                <artifactId>antlr4-maven-plugin</artifactId>
+                <version>${antlr4-maven-plugin.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>antlr4</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <visitor>true</visitor>
+                    <listener>true</listener>
+                    <sourceDirectory>src/main/antlr4</sourceDirectory>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git 
a/paimon-spark/paimon-spark-common/src/main/antlr4/org.apache.spark.sql.catalyst.parser.extensions/PaimonSqlExtensions.g4
 
b/paimon-spark/paimon-spark-common/src/main/antlr4/org.apache.spark.sql.catalyst.parser.extensions/PaimonSqlExtensions.g4
new file mode 100644
index 000000000..54e71b362
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/antlr4/org.apache.spark.sql.catalyst.parser.extensions/PaimonSqlExtensions.g4
@@ -0,0 +1,229 @@
+/*
+ * 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.
+ *
+ * This file is an adaptation of Spark's grammar files.
+*/
+
+/* This file is based on source code from the Iceberg Project 
(http://iceberg.apache.org/), licensed by the Apache
+ * Software Foundation (ASF) under the Apache License, Version 2.0. See the 
NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. */
+
+grammar PaimonSqlExtensions;
+
+@lexer::members {
+  /**
+   * Verify whether current token is a valid decimal token (which contains 
dot).
+   * Returns true if the character that follows the token is not a digit or 
letter or underscore.
+   *
+   * For example:
+   * For char stream "2.3", "2." is not a valid decimal token, because it is 
followed by digit '3'.
+   * For char stream "2.3_", "2.3" is not a valid decimal token, because it is 
followed by '_'.
+   * For char stream "2.3W", "2.3" is not a valid decimal token, because it is 
followed by 'W'.
+   * For char stream "12.0D 34.E2+0.12 "  12.0D is a valid decimal token 
because it is followed
+   * by a space. 34.E2 is a valid decimal token because it is followed by 
symbol '+'
+   * which is not a digit or letter or underscore.
+   */
+  public boolean isValidDecimal() {
+    int nextChar = _input.LA(1);
+    if (nextChar >= 'A' && nextChar <= 'Z' || nextChar >= '0' && nextChar <= 
'9' ||
+      nextChar == '_') {
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  /**
+   * This method will be called when we see '/*' and try to match it as a 
bracketed comment.
+   * If the next character is '+', it should be parsed as hint later, and we 
cannot match
+   * it as a bracketed comment.
+   *
+   * Returns true if the next character is '+'.
+   */
+  public boolean isHint() {
+    int nextChar = _input.LA(1);
+    if (nextChar == '+') {
+      return true;
+    } else {
+      return false;
+    }
+  }
+}
+
+singleStatement
+    : statement ';'* EOF
+    ;
+
+statement
+    : CALL multipartIdentifier '(' (callArgument (',' callArgument)*)? ')'     
             #call
+    ;
+
+callArgument
+    : expression                    #positionalArgument
+    | identifier '=>' expression    #namedArgument
+    ;
+
+expression
+    : constant
+    | stringMap
+    ;
+
+constant
+    : number                          #numericLiteral
+    | booleanValue                    #booleanLiteral
+    | STRING+                         #stringLiteral
+    | identifier STRING               #typeConstructor
+    ;
+
+stringMap
+    : MAP '(' constant (',' constant)* ')'
+    ;
+
+booleanValue
+    : TRUE | FALSE
+    ;
+
+number
+    : MINUS? EXPONENT_VALUE           #exponentLiteral
+    | MINUS? DECIMAL_VALUE            #decimalLiteral
+    | MINUS? INTEGER_VALUE            #integerLiteral
+    | MINUS? BIGINT_LITERAL           #bigIntLiteral
+    | MINUS? SMALLINT_LITERAL         #smallIntLiteral
+    | MINUS? TINYINT_LITERAL          #tinyIntLiteral
+    | MINUS? DOUBLE_LITERAL           #doubleLiteral
+    | MINUS? FLOAT_LITERAL            #floatLiteral
+    | MINUS? BIGDECIMAL_LITERAL       #bigDecimalLiteral
+    ;
+
+multipartIdentifier
+    : parts+=identifier ('.' parts+=identifier)*
+    ;
+
+identifier
+    : IDENTIFIER              #unquotedIdentifier
+    | quotedIdentifier        #quotedIdentifierAlternative
+    | nonReserved             #unquotedIdentifier
+    ;
+
+quotedIdentifier
+    : BACKQUOTED_IDENTIFIER
+    ;
+
+nonReserved
+    : CALL
+    | TRUE | FALSE
+    | MAP
+    ;
+
+CALL: 'CALL';
+
+TRUE: 'TRUE';
+FALSE: 'FALSE';
+
+MAP: 'MAP';
+
+PLUS: '+';
+MINUS: '-';
+
+STRING
+    : '\'' ( ~('\''|'\\') | ('\\' .) )* '\''
+    | '"' ( ~('"'|'\\') | ('\\' .) )* '"'
+    ;
+
+BIGINT_LITERAL
+    : DIGIT+ 'L'
+    ;
+
+SMALLINT_LITERAL
+    : DIGIT+ 'S'
+    ;
+
+TINYINT_LITERAL
+    : DIGIT+ 'Y'
+    ;
+
+INTEGER_VALUE
+    : DIGIT+
+    ;
+
+EXPONENT_VALUE
+    : DIGIT+ EXPONENT
+    | DECIMAL_DIGITS EXPONENT {isValidDecimal()}?
+    ;
+
+DECIMAL_VALUE
+    : DECIMAL_DIGITS {isValidDecimal()}?
+    ;
+
+FLOAT_LITERAL
+    : DIGIT+ EXPONENT? 'F'
+    | DECIMAL_DIGITS EXPONENT? 'F' {isValidDecimal()}?
+    ;
+
+DOUBLE_LITERAL
+    : DIGIT+ EXPONENT? 'D'
+    | DECIMAL_DIGITS EXPONENT? 'D' {isValidDecimal()}?
+    ;
+
+BIGDECIMAL_LITERAL
+    : DIGIT+ EXPONENT? 'BD'
+    | DECIMAL_DIGITS EXPONENT? 'BD' {isValidDecimal()}?
+    ;
+
+IDENTIFIER
+    : (LETTER | DIGIT | '_')+
+    ;
+
+BACKQUOTED_IDENTIFIER
+    : '`' ( ~'`' | '``' )* '`'
+    ;
+
+fragment DECIMAL_DIGITS
+    : DIGIT+ '.' DIGIT*
+    | '.' DIGIT+
+    ;
+
+fragment EXPONENT
+    : 'E' [+-]? DIGIT+
+    ;
+
+fragment DIGIT
+    : [0-9]
+    ;
+
+fragment LETTER
+    : [A-Z]
+    ;
+
+SIMPLE_COMMENT
+    : '--' ('\\\n' | ~[\r\n])* '\r'? '\n'? -> channel(HIDDEN)
+    ;
+
+BRACKETED_COMMENT
+    : '/*' {!isHint()}? (BRACKETED_COMMENT|.)*? '*/' -> channel(HIDDEN)
+    ;
+
+WS
+    : [ \r\n\t]+ -> channel(HIDDEN)
+    ;
+
+// Catch-all for anything we can't recognize.
+// We use this to be able to ignore and recover all the text
+// when splitting statements with DelimiterLexer
+UNRECOGNIZED
+    : .
+    ;
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
index 879d14c57..81dc8c27e 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkCatalog.java
@@ -25,6 +25,10 @@ import org.apache.paimon.catalog.CatalogFactory;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.schema.Schema;
 import org.apache.paimon.schema.SchemaChange;
+import org.apache.paimon.spark.analysis.NoSuchProcedureException;
+import org.apache.paimon.spark.catalog.ProcedureCatalog;
+import org.apache.paimon.spark.procedure.Procedure;
+import org.apache.paimon.spark.procedure.ProcedureBuilder;
 import org.apache.paimon.table.DataTable;
 import org.apache.paimon.table.Table;
 import org.apache.paimon.utils.Preconditions;
@@ -58,7 +62,7 @@ import java.util.stream.Collectors;
 import static org.apache.paimon.spark.SparkTypeUtils.toPaimonType;
 
 /** Spark {@link TableCatalog} for paimon. */
-public class SparkCatalog implements TableCatalog, SupportsNamespaces {
+public class SparkCatalog implements TableCatalog, ProcedureCatalog, 
SupportsNamespaces {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(SparkCatalog.class);
 
@@ -315,6 +319,17 @@ public class SparkCatalog implements TableCatalog, 
SupportsNamespaces {
         }
     }
 
+    @Override
+    public Procedure loadProcedure(Identifier identifier) throws 
NoSuchProcedureException {
+        if (isValidateNamespace(identifier.namespace())) {
+            ProcedureBuilder builder = SparkProcedures.newBuilder(name);
+            if (builder != null) {
+                return builder.withTableCatalog(this).build();
+            }
+        }
+        throw new NoSuchProcedureException(identifier);
+    }
+
     private SchemaChange toSchemaChange(TableChange change) {
         if (change instanceof TableChange.SetProperty) {
             TableChange.SetProperty set = (TableChange.SetProperty) change;
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
new file mode 100644
index 000000000..8eb08b659
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/SparkProcedures.java
@@ -0,0 +1,47 @@
+/*
+ * 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.paimon.spark;
+
+import org.apache.paimon.spark.procedure.Procedure;
+import org.apache.paimon.spark.procedure.ProcedureBuilder;
+
+import org.apache.hadoop.shaded.com.google.common.collect.ImmutableMap;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/** The {@link Procedure}s including all the stored procedures. */
+public class SparkProcedures {
+
+    private static final Map<String, Supplier<ProcedureBuilder>> BUILDERS = 
initProcedureBuilders();
+
+    private SparkProcedures() {}
+
+    public static ProcedureBuilder newBuilder(String name) {
+        Supplier<ProcedureBuilder> builderSupplier = 
BUILDERS.get(name.toLowerCase(Locale.ROOT));
+        return builderSupplier != null ? builderSupplier.get() : null;
+    }
+
+    private static Map<String, Supplier<ProcedureBuilder>> 
initProcedureBuilders() {
+        ImmutableMap.Builder<String, Supplier<ProcedureBuilder>> 
procedureBuilders =
+                ImmutableMap.builder();
+        return procedureBuilders.build();
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/analysis/NoSuchProcedureException.java
similarity index 57%
copy from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
copy to 
paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/analysis/NoSuchProcedureException.java
index 9d6c8bfd5..f83c5f16c 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/analysis/NoSuchProcedureException.java
@@ -7,7 +7,7 @@
  * "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
+ * 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,
@@ -15,21 +15,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.paimon.spark
 
-import org.apache.spark.sql.SparkSessionExtensions
+package org.apache.paimon.spark.analysis;
 
-class PaimonSparkSessionExtension extends (SparkSessionExtensions => Unit) {
+import org.apache.spark.SparkThrowableHelper;
+import org.apache.spark.sql.connector.catalog.Identifier;
 
-  override def apply(extensions: SparkSessionExtensions): Unit = {
+import java.util.Collections;
 
-    extensions.injectResolutionRule(sparkSession => new 
PaimonAnalysis(sparkSession))
+/** Thrown by a catalog when a stored procedure cannot be found. */
+public class NoSuchProcedureException extends RuntimeException {
 
-    PaimonTableValuedFunctions.supportedFnNames.foreach {
-      fnName =>
-        extensions.injectTableFunction(
-          PaimonTableValuedFunctions.getTableValueFunctionInjection(fnName))
+    public NoSuchProcedureException(Identifier identifier) {
+        super(
+                SparkThrowableHelper.getMessage(
+                        "Procedure " + identifier + " is not found", 
Collections.emptyMap()));
     }
-  }
-
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/catalog/ProcedureCatalog.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/catalog/ProcedureCatalog.java
new file mode 100644
index 000000000..b982c1896
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/catalog/ProcedureCatalog.java
@@ -0,0 +1,41 @@
+/*
+ * 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.paimon.spark.catalog;
+
+import org.apache.paimon.spark.analysis.NoSuchProcedureException;
+import org.apache.paimon.spark.procedure.Procedure;
+
+import org.apache.spark.sql.connector.catalog.CatalogPlugin;
+import org.apache.spark.sql.connector.catalog.Identifier;
+
+/**
+ * A {@link CatalogPlugin catalog} interface that loads stored procedures 
called via CALL
+ * statements.
+ */
+public interface ProcedureCatalog extends CatalogPlugin {
+
+    /**
+     * Loads a {@link Procedure stored procedure} by {@link Identifier 
identifier}.
+     *
+     * @param identifier A stored procedure identifier.
+     * @return The procedure's metadata of given identifier.
+     * @throws NoSuchProcedureException Thrown, if there is no matching 
procedure stored.
+     */
+    Procedure loadProcedure(Identifier identifier) throws 
NoSuchProcedureException;
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/Procedure.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/Procedure.java
new file mode 100644
index 000000000..127a46647
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/Procedure.java
@@ -0,0 +1,45 @@
+/*
+ * 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.paimon.spark.procedure;
+
+import org.apache.spark.sql.catalyst.InternalRow;
+import org.apache.spark.sql.types.StructType;
+
+/** An interface that defines an executable stored procedure. */
+public interface Procedure {
+
+    /** Returns the input parameters of stored procedure. */
+    ProcedureParameter[] parameters();
+
+    /** Returns the type of rows produced by stored procedure. */
+    StructType outputType();
+
+    /**
+     * Executes the given stored procedure.
+     *
+     * @param args Input arguments.
+     * @return The result of executing stored procedure with the given 
arguments.
+     */
+    InternalRow[] call(InternalRow args);
+
+    /** Returns the description of stored procedure. */
+    default String description() {
+        return this.getClass().toString();
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureBuilder.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureBuilder.java
new file mode 100644
index 000000000..8b81112af
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureBuilder.java
@@ -0,0 +1,41 @@
+/*
+ * 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.paimon.spark.procedure;
+
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
+
+/** A builder of {@link Procedure} that builds a stored procedure. */
+public interface ProcedureBuilder {
+
+    /**
+     * Returns a {@link ProcedureBuilder procedure builder} via given table 
catalog.
+     *
+     * @param tableCatalog The table catalog.
+     * @return The procedure builder with given catalog.
+     */
+    ProcedureBuilder withTableCatalog(TableCatalog tableCatalog);
+
+    /**
+     * Builds a {@link Procedure stored procedure} via given {@link Identifier 
identifier}.
+     *
+     * @return The stored procedure of given identifier.
+     */
+    Procedure build();
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureParameter.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureParameter.java
new file mode 100644
index 000000000..94306424a
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureParameter.java
@@ -0,0 +1,56 @@
+/*
+ * 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.paimon.spark.procedure;
+
+import org.apache.spark.sql.types.DataType;
+
+/** An input parameter of a {@link Procedure stored procedure}. */
+public interface ProcedureParameter {
+
+    /**
+     * Creates a required input parameter.
+     *
+     * @param name The name of input parameter.
+     * @param dataType The data type of input parameter.
+     * @return The constructed stored procedure parameter.
+     */
+    static ProcedureParameter required(String name, DataType dataType) {
+        return new ProcedureParameterImpl(name, dataType, true);
+    }
+
+    /**
+     * Creates an optional input parameter.
+     *
+     * @param name The name of input parameter.
+     * @param dataType The data type of input parameter.
+     * @return The constructed optional stored procedure parameter.
+     */
+    static ProcedureParameter optional(String name, DataType dataType) {
+        return new ProcedureParameterImpl(name, dataType, false);
+    }
+
+    /** Returns the name of input parameter. */
+    String name();
+
+    /** Returns the type of input parameter. */
+    DataType dataType();
+
+    /** Returns whether input parameter is required. */
+    boolean required();
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureParameterImpl.java
 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureParameterImpl.java
new file mode 100644
index 000000000..4fe239f46
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/java/org/apache/paimon/spark/procedure/ProcedureParameterImpl.java
@@ -0,0 +1,80 @@
+/*
+ * 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.paimon.spark.procedure;
+
+import org.apache.spark.sql.types.DataType;
+
+import java.util.Objects;
+
+/** An implementation of {@link ProcedureParameter}. */
+public class ProcedureParameterImpl implements ProcedureParameter {
+
+    /** The name of input parameter. */
+    private final String name;
+    /** The data type of input parameter. */
+    private final DataType dataType;
+    /** Whether input parameter is required. */
+    private final boolean required;
+
+    public ProcedureParameterImpl(String name, DataType dataType, boolean 
required) {
+        this.name = name;
+        this.dataType = dataType;
+        this.required = required;
+    }
+
+    @Override
+    public String name() {
+        return name;
+    }
+
+    @Override
+    public DataType dataType() {
+        return dataType;
+    }
+
+    @Override
+    public boolean required() {
+        return required;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (this == other) {
+            return true;
+        } else if (other == null || getClass() != other.getClass()) {
+            return false;
+        }
+
+        ProcedureParameterImpl that = (ProcedureParameterImpl) other;
+        return required == that.required
+                && Objects.equals(name, that.name)
+                && Objects.equals(dataType, that.dataType);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, dataType, required);
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "ProcedureParameter(name='%s', type=%s, required=%b)", name, 
dataType, required);
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
similarity index 60%
copy from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
copy to 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
index 9d6c8bfd5..4e1117b26 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/extensions/PaimonSparkSessionExtensions.scala
@@ -15,21 +15,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.paimon.spark
+package org.apache.paimon.spark.extensions
 
 import org.apache.spark.sql.SparkSessionExtensions
+import org.apache.spark.sql.catalyst.analysis.{CoerceArguments, 
PaimonAnalysis, ResolveProcedures}
+import 
org.apache.spark.sql.catalyst.parser.extensions.PaimonSparkSqlExtensionsParser
+import org.apache.spark.sql.catalyst.plans.logical.PaimonTableValuedFunctions
 
-class PaimonSparkSessionExtension extends (SparkSessionExtensions => Unit) {
+/** Spark session extension to extends the syntax and adds the rules. */
+class PaimonSparkSessionExtensions extends (SparkSessionExtensions => Unit) {
 
   override def apply(extensions: SparkSessionExtensions): Unit = {
+    // parser extensions
+    extensions.injectParser { case (_, parser) => new 
PaimonSparkSqlExtensionsParser(parser) }
 
+    // analyzer extensions
+    // resolution rule extensions
     extensions.injectResolutionRule(sparkSession => new 
PaimonAnalysis(sparkSession))
-
+    extensions.injectResolutionRule(spark => ResolveProcedures(spark))
+    extensions.injectResolutionRule(_ => CoerceArguments)
+    // table function extensions
     PaimonTableValuedFunctions.supportedFnNames.foreach {
       fnName =>
         extensions.injectTableFunction(
           PaimonTableValuedFunctions.getTableValueFunctionInjection(fnName))
     }
   }
-
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/CoerceArguments.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/CoerceArguments.scala
new file mode 100644
index 000000000..389c79bbf
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/CoerceArguments.scala
@@ -0,0 +1,61 @@
+/*
+ * 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.spark.sql.catalyst.analysis
+
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.expressions.Cast
+import org.apache.spark.sql.catalyst.plans.logical.{CallCommand, LogicalPlan}
+import org.apache.spark.sql.catalyst.rules.Rule
+
+/* This file is based on source code from the Iceberg Project 
(http://iceberg.apache.org/), licensed by the Apache
+ * Software Foundation (ASF) under the Apache License, Version 2.0. See the 
NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. */
+
+/**
+ * Coercion of procedure arguments rule.
+ *
+ * <p>Most of the content of this class is referenced from Iceberg's 
ProcedureArgumentCoercion.
+ */
+object CoerceArguments extends Rule[LogicalPlan] {
+
+  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {
+    case call @ CallCommand(procedure, arguments) if call.resolved =>
+      val parameters = procedure.parameters
+      val newArguments = arguments.zipWithIndex.map {
+        case (argument, index) =>
+          val parameter = parameters(index)
+          val parameterType = parameter.dataType
+          val argumentType = argument.dataType
+          if (parameterType != argumentType && !Cast.canUpCast(argumentType, 
parameterType)) {
+            throw new AnalysisException(
+              s"Cannot cast $argumentType to $parameterType of 
${parameter.name}.")
+          }
+          if (parameterType != argumentType) {
+            Cast(argument, parameterType)
+          } else {
+            argument
+          }
+      }
+
+      if (newArguments != arguments) {
+        call.copy(args = newArguments)
+      } else {
+        call
+      }
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonAnalysis.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/PaimonAnalysis.scala
similarity index 85%
rename from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonAnalysis.scala
rename to 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/PaimonAnalysis.scala
index 9b1587fc4..04cf36fd1 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonAnalysis.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/PaimonAnalysis.scala
@@ -15,15 +15,15 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.paimon.spark
+package org.apache.spark.sql.catalyst.analysis
 
+import org.apache.paimon.spark.SparkTable
 import org.apache.paimon.table.FileStoreTable
 
 import org.apache.spark.sql.SparkSession
-import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, 
OverwritePartitionsDynamic}
+import org.apache.spark.sql.catalyst.plans.logical.{LogicalPlan, 
OverwritePartitionsDynamic, PaimonDynamicPartitionOverwriteCommand, 
PaimonTableValuedFunctions, PaimonTableValueFunction}
 import org.apache.spark.sql.catalyst.rules.Rule
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
-import 
org.apache.spark.sql.paimon.commands.PaimonDynamicPartitionOverwriteCommand
 
 class PaimonAnalysis(session: SparkSession) extends Rule[LogicalPlan] {
 
@@ -32,12 +32,12 @@ class PaimonAnalysis(session: SparkSession) extends 
Rule[LogicalPlan] {
     case func: PaimonTableValueFunction if func.args.forall(_.resolved) =>
       PaimonTableValuedFunctions.resolvePaimonTableValuedFunction(session, 
func)
 
-    case o @ OverwritePartitionsDynamicPaimon(r, d) if o.resolved =>
+    case o @ PaimonDynamicPartitionOverwrite(r, d) if o.resolved =>
       PaimonDynamicPartitionOverwriteCommand(r, d, o.query, o.writeOptions, 
o.isByName)
   }
 }
 
-object OverwritePartitionsDynamicPaimon {
+object PaimonDynamicPartitionOverwrite {
   def unapply(o: OverwritePartitionsDynamic): Option[(DataSourceV2Relation, 
FileStoreTable)] = {
     if (o.query.resolved) {
       o.table match {
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveProcedures.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveProcedures.scala
new file mode 100644
index 000000000..27d4777ce
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/analysis/ResolveProcedures.scala
@@ -0,0 +1,213 @@
+/*
+ * 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.spark.sql.catalyst.analysis
+
+import org.apache.paimon.spark.catalog.ProcedureCatalog
+import org.apache.paimon.spark.procedure.ProcedureParameter
+
+import org.apache.spark.sql.{AnalysisException, SparkSession}
+import org.apache.spark.sql.catalyst.expressions.{Expression, Literal}
+import org.apache.spark.sql.catalyst.plans.logical._
+import org.apache.spark.sql.catalyst.rules.Rule
+import org.apache.spark.sql.connector.catalog.{CatalogManager, CatalogPlugin, 
LookupCatalog}
+
+import java.util.Locale
+
+/* This file is based on source code from the Iceberg Project 
(http://iceberg.apache.org/), licensed by the Apache
+ * Software Foundation (ASF) under the Apache License, Version 2.0. See the 
NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. */
+
+/**
+ * Resolution of stored procedures rule.
+ *
+ * <p>Most of the content of this class is referenced from Iceberg's 
ResolveProcedures.
+ *
+ * @param sparkSession
+ *   The Spark session.
+ */
+case class ResolveProcedures(sparkSession: SparkSession)
+  extends Rule[LogicalPlan]
+  with LookupCatalog {
+
+  protected lazy val catalogManager: CatalogManager = 
sparkSession.sessionState.catalogManager
+
+  override def apply(plan: LogicalPlan): LogicalPlan = plan.resolveOperators {
+    case CallStatement(CatalogAndIdentifier(catalog, identifier), arguments) =>
+      val procedure = catalog.asProcedureCatalog.loadProcedure(identifier)
+      val parameters = procedure.parameters
+      val normalizedParameters = normalizeParameters(parameters)
+      validateParameters(normalizedParameters)
+      val normalizedArguments = normalizeArguments(arguments)
+      CallCommand(
+        procedure,
+        args = buildArgumentExpressions(normalizedParameters, 
normalizedArguments))
+  }
+
+  /**
+   * Normalizes input parameters of stored procedure.
+   *
+   * @param parameters
+   *   Input parameters of stored procedure.
+   * @return
+   *   Normalized input parameters.
+   */
+  private def normalizeParameters(parameters: Seq[ProcedureParameter]): 
Seq[ProcedureParameter] = {
+    parameters.map {
+      case parameter if parameter.required =>
+        val normalizedName = parameter.name.toLowerCase(Locale.ROOT)
+        ProcedureParameter.required(normalizedName, parameter.dataType)
+      case param =>
+        val normalizedName = param.name.toLowerCase(Locale.ROOT)
+        ProcedureParameter.optional(normalizedName, param.dataType)
+    }
+  }
+
+  /**
+   * Validates input parameters of stored procedure.
+   *
+   * @param parameters
+   *   Input parameters of stored procedure.
+   */
+  private def validateParameters(parameters: Seq[ProcedureParameter]): Unit = {
+    val duplicateParamNames = parameters.groupBy(_.name).collect {
+      case (name, matchingParams) if matchingParams.length > 1 => name
+    }
+    if (duplicateParamNames.nonEmpty) {
+      throw new AnalysisException(
+        s"Parameter names ${duplicateParamNames.mkString("[", ",", "]")} are 
duplicated.")
+    }
+    parameters.sliding(2).foreach {
+      case Seq(previousParam, currentParam) if !previousParam.required && 
currentParam.required =>
+        throw new AnalysisException(
+          s"Optional parameters should be after required ones but 
$currentParam is after $previousParam.")
+      case _ =>
+    }
+  }
+
+  /**
+   * Normalizes arguments of stored procedure.
+   *
+   * @param arguments
+   *   Arguments of stored procedure.
+   * @return
+   *   Normalized arguments.
+   */
+  private def normalizeArguments(arguments: Seq[CallArgument]): 
Seq[CallArgument] = {
+    arguments.map {
+      case a @ NamedArgument(name, _) => a.copy(name = 
name.toLowerCase(Locale.ROOT))
+      case other => other
+    }
+  }
+
+  /**
+   * Builds argument expressions.
+   *
+   * @param parameters
+   *   Normalized input parameters.
+   * @param arguments
+   *   Normalized arguments.
+   * @return
+   *   Argument expressions.
+   */
+  private def buildArgumentExpressions(
+      parameters: Seq[ProcedureParameter],
+      arguments: Seq[CallArgument]): Seq[Expression] = {
+    val nameToPositionMap = parameters.map(_.name).zipWithIndex.toMap
+    val nameToArgumentMap = buildNameToArgumentMap(parameters, arguments, 
nameToPositionMap)
+    val missingParamNames = parameters.filter(_.required).collect {
+      case parameter if !nameToArgumentMap.contains(parameter.name) => 
parameter.name
+    }
+    if (missingParamNames.nonEmpty) {
+      throw new AnalysisException(
+        s"Required parameters ${missingParamNames.mkString("[", ",", "]")} is 
missed.")
+    }
+    val argumentExpressions = new Array[Expression](parameters.size)
+    nameToArgumentMap.foreach {
+      case (name, argument) => argumentExpressions(nameToPositionMap(name)) = 
argument.expr
+    }
+    parameters.foreach {
+      case parameter if !parameter.required && 
!nameToArgumentMap.contains(parameter.name) =>
+        argumentExpressions(nameToPositionMap(parameter.name)) =
+          Literal.create(null, parameter.dataType)
+      case _ =>
+    }
+    argumentExpressions
+  }
+
+  /**
+   * Builds mapping between name and position.
+   *
+   * @param parameters
+   *   Normalized input parameters.
+   * @param arguments
+   *   Normalized arguments.
+   * @param nameToPositionMap
+   *   Mapping between name and position.
+   * @return
+   *   Mapping between name and position.
+   */
+  private def buildNameToArgumentMap(
+      parameters: Seq[ProcedureParameter],
+      arguments: Seq[CallArgument],
+      nameToPositionMap: Map[String, Int]): Map[String, CallArgument] = {
+    val isNamedArgument = arguments.exists(_.isInstanceOf[NamedArgument])
+    val isPositionalArgument = 
arguments.exists(_.isInstanceOf[PositionalArgument])
+
+    if (isNamedArgument && isPositionalArgument) {
+      throw new AnalysisException("Cannot mix named and positional arguments.")
+    }
+
+    if (isNamedArgument) {
+      val namedArguments = arguments.asInstanceOf[Seq[NamedArgument]]
+      val validationErrors = namedArguments.groupBy(_.name).collect {
+        case (name, procedureArguments) if procedureArguments.size > 1 =>
+          s"Procedure argument $name is duplicated."
+        case (name, _) if !nameToPositionMap.contains(name) => s"Argument 
$name is unknown."
+      }
+      if (validationErrors.nonEmpty) {
+        throw new AnalysisException(
+          s"Builds name to argument map ${validationErrors.mkString(", ")} 
error.")
+      }
+      namedArguments.map(arg => arg.name -> arg).toMap
+    } else {
+      if (arguments.size > parameters.size) {
+        throw new AnalysisException("Too many arguments for procedure")
+      }
+      arguments.zipWithIndex.map {
+        case (argument, position) =>
+          val param = parameters(position)
+          param.name -> argument
+      }.toMap
+    }
+  }
+
+  /**
+   * Procedure catalog validator whether catalog plugin is procedure catalog.
+   *
+   * @param catalogPlugin
+   *   Spark catalog plugin.
+   */
+  implicit class CatalogValidator(catalogPlugin: CatalogPlugin) {
+    def asProcedureCatalog: ProcedureCatalog = catalogPlugin match {
+      case procedureCatalog: ProcedureCatalog =>
+        procedureCatalog
+      case _ =>
+        throw new AnalysisException(s"${catalogPlugin.name} is not a 
ProcedureCatalog.")
+    }
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSparkSqlExtensionsParser.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSparkSqlExtensionsParser.scala
new file mode 100644
index 000000000..f42cbab55
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSparkSqlExtensionsParser.scala
@@ -0,0 +1,263 @@
+/*
+ * 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.spark.sql.catalyst.parser.extensions
+
+import org.antlr.v4.runtime._
+import org.antlr.v4.runtime.atn.PredictionMode
+import org.antlr.v4.runtime.misc.{Interval, ParseCancellationException}
+import org.antlr.v4.runtime.tree.TerminalNodeImpl
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.AnalysisException
+import org.apache.spark.sql.catalyst.{FunctionIdentifier, TableIdentifier}
+import org.apache.spark.sql.catalyst.expressions.Expression
+import org.apache.spark.sql.catalyst.parser.{ParseException, ParserInterface}
+import 
org.apache.spark.sql.catalyst.parser.extensions.PaimonSqlExtensionsParser.{NonReservedContext,
 QuotedIdentifierContext}
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan
+import org.apache.spark.sql.catalyst.trees.Origin
+import org.apache.spark.sql.internal.VariableSubstitution
+import org.apache.spark.sql.types.{DataType, StructType}
+
+import java.util.Locale
+
+/* This file is based on source code from the Iceberg Project 
(http://iceberg.apache.org/), licensed by the Apache
+ * Software Foundation (ASF) under the Apache License, Version 2.0. See the 
NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. */
+
+/**
+ * The implementation of [[ParserInterface]] that parsers the sql extension.
+ *
+ * <p>Most of the content of this class is referenced from Iceberg's
+ * IcebergSparkSqlExtensionsParser.
+ *
+ * @param delegate
+ *   The extension parser.
+ */
+class PaimonSparkSqlExtensionsParser(delegate: ParserInterface)
+  extends ParserInterface
+  with Logging {
+
+  private lazy val substitutor = new VariableSubstitution()
+  private lazy val astBuilder = new PaimonSqlExtensionsAstBuilder(delegate)
+
+  /** Parses a string to a LogicalPlan. */
+  override def parsePlan(sqlText: String): LogicalPlan = {
+    val sqlTextAfterSubstitution = substitutor.substitute(sqlText)
+    if (isCommand(sqlTextAfterSubstitution)) {
+      parse(sqlTextAfterSubstitution)(parser => 
astBuilder.visit(parser.singleStatement()))
+        .asInstanceOf[LogicalPlan]
+    } else {
+      delegate.parsePlan(sqlText)
+    }
+  }
+
+  /** Parses a string to an Expression. */
+  override def parseExpression(sqlText: String): Expression =
+    delegate.parseExpression(sqlText)
+
+  /** Parses a string to a TableIdentifier. */
+  override def parseTableIdentifier(sqlText: String): TableIdentifier =
+    delegate.parseTableIdentifier(sqlText)
+
+  /** Parses a string to a FunctionIdentifier. */
+  override def parseFunctionIdentifier(sqlText: String): FunctionIdentifier =
+    delegate.parseFunctionIdentifier(sqlText)
+
+  /**
+   * Creates StructType for a given SQL string, which is a comma separated 
list of field definitions
+   * which will preserve the correct Hive metadata.
+   */
+  override def parseTableSchema(sqlText: String): StructType =
+    delegate.parseTableSchema(sqlText)
+
+  /** Parses a string to a DataType. */
+  override def parseDataType(sqlText: String): DataType =
+    delegate.parseDataType(sqlText)
+
+  /** Parses a string to a multi-part identifier. */
+  override def parseMultipartIdentifier(sqlText: String): Seq[String] =
+    delegate.parseMultipartIdentifier(sqlText)
+
+  /** Returns whether SQL text is command. */
+  private def isCommand(sqlText: String): Boolean = {
+    val normalized = sqlText
+      .toLowerCase(Locale.ROOT)
+      .trim()
+      .replaceAll("--.*?\\n", " ")
+      .replaceAll("\\s+", " ")
+      .replaceAll("/\\*.*?\\*/", " ")
+      .trim()
+    normalized.startsWith("call")
+  }
+
+  protected def parse[T](command: String)(toResult: PaimonSqlExtensionsParser 
=> T): T = {
+    val lexer = new PaimonSqlExtensionsLexer(
+      new UpperCaseCharStream(CharStreams.fromString(command)))
+    lexer.removeErrorListeners()
+    lexer.addErrorListener(PaimonParseErrorListener)
+
+    val tokenStream = new CommonTokenStream(lexer)
+    val parser = new PaimonSqlExtensionsParser(tokenStream)
+    parser.addParseListener(PaimonSqlExtensionsPostProcessor)
+    parser.removeErrorListeners()
+    parser.addErrorListener(PaimonParseErrorListener)
+
+    try {
+      try {
+        parser.getInterpreter.setPredictionMode(PredictionMode.SLL)
+        toResult(parser)
+      } catch {
+        case _: ParseCancellationException =>
+          tokenStream.seek(0)
+          parser.reset()
+          parser.getInterpreter.setPredictionMode(PredictionMode.LL)
+          toResult(parser)
+      }
+    } catch {
+      case e: PaimonParseException if e.command.isDefined =>
+        throw e
+      case e: PaimonParseException =>
+        throw e.withCommand(command)
+      case e: AnalysisException =>
+        val position = Origin(e.line, e.startPosition)
+        throw new PaimonParseException(Option(command), e.message, position, 
position)
+    }
+  }
+
+  def parseQuery(sqlText: String): LogicalPlan =
+    parsePlan(sqlText)
+}
+
+/* Copied from Apache Spark's to avoid dependency on Spark Internals */
+class UpperCaseCharStream(wrapped: CodePointCharStream) extends CharStream {
+  override def consume(): Unit = wrapped.consume()
+  override def getSourceName: String = wrapped.getSourceName
+  override def index(): Int = wrapped.index
+  override def mark(): Int = wrapped.mark
+  override def release(marker: Int): Unit = wrapped.release(marker)
+  override def seek(where: Int): Unit = wrapped.seek(where)
+  override def size(): Int = wrapped.size
+
+  override def getText(interval: Interval): String = wrapped.getText(interval)
+
+  // scalastyle:off
+  override def LA(i: Int): Int = {
+    val la = wrapped.LA(i)
+    if (la == 0 || la == IntStream.EOF) la
+    else Character.toUpperCase(la)
+  }
+  // scalastyle:on
+}
+
+/** The post-processor validates & cleans-up the parse tree during the parse 
process. */
+case object PaimonSqlExtensionsPostProcessor extends 
PaimonSqlExtensionsBaseListener {
+
+  /** Removes the back ticks from an Identifier. */
+  override def exitQuotedIdentifier(ctx: QuotedIdentifierContext): Unit = {
+    replaceTokenByIdentifier(ctx, 1) {
+      token =>
+        // Remove the double back ticks in the string.
+        token.setText(token.getText.replace("``", "`"))
+        token
+    }
+  }
+
+  /** Treats non-reserved keywords as Identifiers. */
+  override def exitNonReserved(ctx: NonReservedContext): Unit = {
+    replaceTokenByIdentifier(ctx, 0)(identity)
+  }
+
+  private def replaceTokenByIdentifier(ctx: ParserRuleContext, stripMargins: 
Int)(
+      f: CommonToken => CommonToken = identity): Unit = {
+    val parent = ctx.getParent
+    parent.removeLastChild()
+    val token = ctx.getChild(0).getPayload.asInstanceOf[Token]
+    val newToken = new CommonToken(
+      new org.antlr.v4.runtime.misc.Pair(token.getTokenSource, 
token.getInputStream),
+      PaimonSqlExtensionsParser.IDENTIFIER,
+      token.getChannel,
+      token.getStartIndex + stripMargins,
+      token.getStopIndex - stripMargins)
+    parent.addChild(new TerminalNodeImpl(f(newToken)))
+  }
+}
+
+/* Partially copied from Apache Spark's Parser to avoid dependency on Spark 
Internals */
+case object PaimonParseErrorListener extends BaseErrorListener {
+  override def syntaxError(
+      recognizer: Recognizer[_, _],
+      offendingSymbol: scala.Any,
+      line: Int,
+      charPositionInLine: Int,
+      msg: String,
+      e: RecognitionException): Unit = {
+    val (start, stop) = offendingSymbol match {
+      case token: CommonToken =>
+        val start = Origin(Some(line), Some(token.getCharPositionInLine))
+        val length = token.getStopIndex - token.getStartIndex + 1
+        val stop = Origin(Some(line), Some(token.getCharPositionInLine + 
length))
+        (start, stop)
+      case _ =>
+        val start = Origin(Some(line), Some(charPositionInLine))
+        (start, start)
+    }
+    throw new PaimonParseException(None, msg, start, stop)
+  }
+}
+
+/**
+ * Copied from Apache Spark A [[ParseException]] is an [[AnalysisException]] 
that is thrown during
+ * the parse process. It contains fields and an extended error message that 
make reporting and
+ * diagnosing errors easier.
+ */
+class PaimonParseException(
+    val command: Option[String],
+    message: String,
+    val start: Origin,
+    val stop: Origin)
+  extends AnalysisException(message, start.line, start.startPosition) {
+
+  def this(message: String, ctx: ParserRuleContext) =
+    this(
+      Option(PaimonParserUtils.command(ctx)),
+      message,
+      PaimonParserUtils.position(ctx.getStart),
+      PaimonParserUtils.position(ctx.getStop))
+
+  override def getMessage: String = {
+    val builder = new StringBuilder
+    builder ++= "\n" ++= message
+    start match {
+      case Origin(Some(l), Some(p), Some(_), Some(_), Some(_), Some(_), 
Some(_)) =>
+        builder ++= s"(line $l, pos $p)\n"
+        command.foreach {
+          cmd =>
+            val (above, below) = cmd.split("\n").splitAt(l)
+            builder ++= "\n== SQL ==\n"
+            above.foreach(builder ++= _ += '\n')
+            builder ++= (0 until p).map(_ => "-").mkString("") ++= "^^^\n"
+            below.foreach(builder ++= _ += '\n')
+        }
+      case _ =>
+        command.foreach(cmd => builder ++= "\n== SQL ==\n" ++= cmd)
+    }
+    builder.toString
+  }
+
+  def withCommand(cmd: String): PaimonParseException =
+    new PaimonParseException(Option(cmd), message, start, stop)
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSqlExtensionsAstBuilder.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSqlExtensionsAstBuilder.scala
new file mode 100644
index 000000000..1b8d9d8dd
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/PaimonSqlExtensionsAstBuilder.scala
@@ -0,0 +1,132 @@
+/*
+ * 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.spark.sql.catalyst.parser.extensions
+
+import org.antlr.v4.runtime._
+import org.antlr.v4.runtime.misc.Interval
+import org.antlr.v4.runtime.tree.{ParseTree, TerminalNode}
+import org.apache.spark.internal.Logging
+import org.apache.spark.sql.catalyst.expressions.Expression
+import org.apache.spark.sql.catalyst.parser.ParserInterface
+import 
org.apache.spark.sql.catalyst.parser.extensions.PaimonParserUtils.withOrigin
+import 
org.apache.spark.sql.catalyst.parser.extensions.PaimonSqlExtensionsParser._
+import org.apache.spark.sql.catalyst.plans.logical._
+import org.apache.spark.sql.catalyst.trees.{CurrentOrigin, Origin}
+
+import scala.collection.JavaConverters._
+
+/* This file is based on source code from the Iceberg Project 
(http://iceberg.apache.org/), licensed by the Apache
+ * Software Foundation (ASF) under the Apache License, Version 2.0. See the 
NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. */
+
+/**
+ * The AST Builder provides an implementation of 
[[PaimonSqlExtensionsBaseVisitor]], which can be
+ * extended to create a visitor which only needs to handle a subset of the 
available methods.
+ *
+ * <p>Most of the content of this class is referenced from Iceberg's 
IcebergSqlExtensionsAstBuilder.
+ *
+ * @param delegate
+ *   The extension parser.
+ */
+class PaimonSqlExtensionsAstBuilder(delegate: ParserInterface)
+  extends PaimonSqlExtensionsBaseVisitor[AnyRef]
+  with Logging {
+
+  /** Creates a single statement of extension statements. */
+  override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan 
= withOrigin(ctx) {
+    visit(ctx.statement).asInstanceOf[LogicalPlan]
+  }
+
+  /** Creates a [[CallStatement]] for a stored procedure call. */
+  override def visitCall(ctx: CallContext): CallStatement = withOrigin(ctx) {
+    val name = toSeq(ctx.multipartIdentifier.parts).map(_.getText)
+    val args = toSeq(ctx.callArgument).map(typedVisit[CallArgument])
+    CallStatement(name, args)
+  }
+
+  /** Creates a positional argument in a stored procedure call. */
+  override def visitPositionalArgument(ctx: PositionalArgumentContext): 
CallArgument =
+    withOrigin(ctx) {
+      val expression = typedVisit[Expression](ctx.expression)
+      PositionalArgument(expression)
+    }
+
+  /** Creates a named argument in a stored procedure call. */
+  override def visitNamedArgument(ctx: NamedArgumentContext): CallArgument = 
withOrigin(ctx) {
+    val name = ctx.identifier.getText
+    val expression = typedVisit[Expression](ctx.expression)
+    NamedArgument(name, expression)
+  }
+
+  /** Creates a [[Expression]] in a positional and named argument. */
+  override def visitExpression(ctx: ExpressionContext): Expression = {
+    // reconstruct the SQL string and parse it using the main Spark parser
+    // while we can avoid the logic to build Spark expressions, we still have 
to parse them
+    // we cannot call ctx.getText directly since it will not render spaces 
correctly
+    // that's why we need to recurse down the tree in reconstructSqlString
+    val sqlString = reconstructSqlString(ctx)
+    delegate.parseExpression(sqlString)
+  }
+
+  /** Returns a multi-part identifier as Seq[String]. */
+  override def visitMultipartIdentifier(ctx: MultipartIdentifierContext): 
Seq[String] =
+    withOrigin(ctx) {
+      ctx.parts.asScala.map(_.getText)
+    }
+
+  private def toBuffer[T](list: java.util.List[T]) = list.asScala
+
+  private def toSeq[T](list: java.util.List[T]) = toBuffer(list)
+
+  private def reconstructSqlString(ctx: ParserRuleContext): String = {
+    toBuffer(ctx.children)
+      .map {
+        case c: ParserRuleContext => reconstructSqlString(c)
+        case t: TerminalNode => t.getText
+      }
+      .mkString(" ")
+  }
+
+  private def typedVisit[T](ctx: ParseTree): T =
+    ctx.accept(this).asInstanceOf[T]
+}
+
+/* Partially copied from Apache Spark's Parser to avoid dependency on Spark 
Internals */
+object PaimonParserUtils {
+
+  private[sql] def withOrigin[T](ctx: ParserRuleContext)(f: => T): T = {
+    val current = CurrentOrigin.get
+    CurrentOrigin.set(position(ctx.getStart))
+    try {
+      f
+    } finally {
+      CurrentOrigin.set(current)
+    }
+  }
+
+  private[sql] def position(token: Token): Origin = {
+    val opt = Option(token)
+    Origin(opt.map(_.getLine), opt.map(_.getCharPositionInLine))
+  }
+
+  /** Gets the command which created the token. */
+  private[sql] def command(ctx: ParserRuleContext): String = {
+    val stream = ctx.getStart.getInputStream
+    stream.getText(Interval.of(0, stream.size() - 1))
+  }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CallCommand.scala
similarity index 57%
copy from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
copy to 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CallCommand.scala
index 9d6c8bfd5..1fade4a9f 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CallCommand.scala
@@ -15,21 +15,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.paimon.spark
+package org.apache.spark.sql.catalyst.plans.logical
 
-import org.apache.spark.sql.SparkSessionExtensions
+import org.apache.paimon.spark.procedure.Procedure
 
-class PaimonSparkSessionExtension extends (SparkSessionExtensions => Unit) {
+import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression}
+import org.apache.spark.sql.catalyst.util.truncatedString
 
-  override def apply(extensions: SparkSessionExtensions): Unit = {
+/** A CALL command that resolves stored procedure from SQL. */
+case class CallCommand(procedure: Procedure, args: Seq[Expression]) extends 
LeafCommand {
 
-    extensions.injectResolutionRule(sparkSession => new 
PaimonAnalysis(sparkSession))
+  override lazy val output: Seq[Attribute] = procedure.outputType.toAttributes
 
-    PaimonTableValuedFunctions.supportedFnNames.foreach {
-      fnName =>
-        extensions.injectTableFunction(
-          PaimonTableValuedFunctions.getTableValueFunctionInjection(fnName))
-    }
+  override def simpleString(maxFields: Int): String = {
+    s"Call${truncatedString(output, "[", ", ", "]", maxFields)} 
${procedure.description}"
   }
-
 }
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CallStatement.scala
similarity index 58%
rename from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
rename to 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CallStatement.scala
index 9d6c8bfd5..823743e56 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonSparkSessionExtension.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/CallStatement.scala
@@ -15,21 +15,21 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.paimon.spark
+package org.apache.spark.sql.catalyst.plans.logical
 
-import org.apache.spark.sql.SparkSessionExtensions
+import org.apache.spark.sql.catalyst.expressions.Expression
 
-class PaimonSparkSessionExtension extends (SparkSessionExtensions => Unit) {
+/** A CALL statement parsed from SQL. */
+case class CallStatement(name: Seq[String], args: Seq[CallArgument]) extends 
LeafParsedStatement
 
-  override def apply(extensions: SparkSessionExtensions): Unit = {
+/** An argument of a CALL statement. */
+sealed trait CallArgument {
 
-    extensions.injectResolutionRule(sparkSession => new 
PaimonAnalysis(sparkSession))
+  def expr: Expression
+}
 
-    PaimonTableValuedFunctions.supportedFnNames.foreach {
-      fnName =>
-        extensions.injectTableFunction(
-          PaimonTableValuedFunctions.getTableValueFunctionInjection(fnName))
-    }
-  }
+/** An argument identified by name. */
+case class NamedArgument(name: String, expr: Expression) extends CallArgument
 
-}
+/** An argument identified by position. */
+case class PositionalArgument(expr: Expression) extends CallArgument
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/commands/PaimonDynamicPartitionOverwriteCommand.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/PaimonDynamicPartitionOverwriteCommand.scala
similarity index 95%
rename from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/commands/PaimonDynamicPartitionOverwriteCommand.scala
rename to 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/PaimonDynamicPartitionOverwriteCommand.scala
index 842472a2e..730a136dc 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/paimon/commands/PaimonDynamicPartitionOverwriteCommand.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/PaimonDynamicPartitionOverwriteCommand.scala
@@ -15,7 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.spark.sql.paimon.commands
+package org.apache.spark.sql.catalyst.plans.logical
 
 import org.apache.paimon.options.Options
 import org.apache.paimon.spark.DynamicOverWrite
@@ -24,7 +24,6 @@ import org.apache.paimon.table.FileStoreTable
 
 import org.apache.spark.sql.{Dataset, Row, SparkSession}
 import org.apache.spark.sql.catalyst.analysis.NamedRelation
-import org.apache.spark.sql.catalyst.plans.logical.{Command, LogicalPlan, 
V2WriteCommand}
 import org.apache.spark.sql.execution.command.RunnableCommand
 
 import scala.collection.convert.ImplicitConversions._
diff --git 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonTableValuedFunctions.scala
 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/PaimonTableValuedFunctions.scala
similarity index 97%
rename from 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonTableValuedFunctions.scala
rename to 
paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/PaimonTableValuedFunctions.scala
index 414f3a84b..a751a2d0a 100644
--- 
a/paimon-spark/paimon-spark-common/src/main/scala/org/apache/paimon/spark/PaimonTableValuedFunctions.scala
+++ 
b/paimon-spark/paimon-spark-common/src/main/scala/org/apache/spark/sql/catalyst/plans/logical/PaimonTableValuedFunctions.scala
@@ -15,9 +15,10 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.paimon.spark
+package org.apache.spark.sql.catalyst.plans.logical
 
 import org.apache.paimon.CoreOptions
+import org.apache.paimon.spark.SparkCatalog
 import org.apache.paimon.spark.catalog.Catalogs
 
 import org.apache.spark.sql.SparkSession
@@ -25,7 +26,6 @@ import org.apache.spark.sql.catalyst.FunctionIdentifier
 import org.apache.spark.sql.catalyst.analysis.FunctionRegistryBase
 import 
org.apache.spark.sql.catalyst.analysis.TableFunctionRegistry.TableFunctionBuilder
 import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, 
ExpressionInfo}
-import org.apache.spark.sql.catalyst.plans.logical.{LeafNode, LogicalPlan}
 import org.apache.spark.sql.connector.catalog.Identifier
 import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation
 import org.apache.spark.sql.util.CaseInsensitiveStringMap
diff --git 
a/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/extensions/CallStatementParserTest.java
 
b/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/extensions/CallStatementParserTest.java
new file mode 100644
index 000000000..ae7c0e067
--- /dev/null
+++ 
b/paimon-spark/paimon-spark-common/src/test/java/org/apache/paimon/spark/extensions/CallStatementParserTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.paimon.spark.extensions;
+
+import org.apache.spark.sql.SparkSession;
+import org.apache.spark.sql.catalyst.expressions.Literal$;
+import org.apache.spark.sql.catalyst.parser.ParseException;
+import org.apache.spark.sql.catalyst.parser.ParserInterface;
+import org.apache.spark.sql.catalyst.parser.extensions.PaimonParseException;
+import org.apache.spark.sql.catalyst.plans.logical.CallArgument;
+import org.apache.spark.sql.catalyst.plans.logical.CallStatement;
+import org.apache.spark.sql.catalyst.plans.logical.NamedArgument;
+import org.apache.spark.sql.catalyst.plans.logical.PositionalArgument;
+import org.apache.spark.sql.types.DataType;
+import org.apache.spark.sql.types.DataTypes;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Arrays;
+
+import scala.collection.JavaConverters;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Test for {@link CallStatement} of {@link PaimonSparkSessionExtensions}. */
+public class CallStatementParserTest {
+
+    private SparkSession spark = null;
+    private ParserInterface parser = null;
+
+    @BeforeEach
+    public void startSparkSession() {
+        SparkSession.clearActiveSession();
+        spark =
+                SparkSession.builder()
+                        .master("local[2]")
+                        .config(
+                                "spark.sql.extensions",
+                                PaimonSparkSessionExtensions.class.getName())
+                        .getOrCreate();
+        parser = spark.sessionState().sqlParser();
+    }
+
+    @AfterEach
+    public void stopSparkSession() {
+        if (spark != null) {
+            spark.stop();
+            spark = null;
+            parser = null;
+        }
+    }
+
+    @Test
+    public void testCallWithNamedArguments() throws ParseException {
+        CallStatement callStatement =
+                (CallStatement)
+                        parser.parsePlan(
+                                "CALL catalog.system.named_args_func(arg1 => 
1, arg2 => 'test', arg3 => true)");
+        assertThat(JavaConverters.seqAsJavaList(callStatement.name()))
+                .isEqualTo(Arrays.asList("catalog", "system", 
"named_args_func"));
+        assertThat(callStatement.args().size()).isEqualTo(3);
+        assertArgument(callStatement, 0, "arg1", 1, DataTypes.IntegerType);
+        assertArgument(callStatement, 1, "arg2", "test", DataTypes.StringType);
+        assertArgument(callStatement, 2, "arg3", true, DataTypes.BooleanType);
+    }
+
+    @Test
+    public void testCallWithPositionalArguments() throws ParseException {
+        CallStatement callStatement =
+                (CallStatement)
+                        parser.parsePlan(
+                                "CALL catalog.system.positional_args_func(1, 
'${spark.sql.extensions}', 2L, true, 3.0D, 4"
+                                        + ".0e1,500e-1BD, "
+                                        + "TIMESTAMP 
'2017-02-03T10:37:30.00Z')");
+        assertThat(JavaConverters.seqAsJavaList(callStatement.name()))
+                .isEqualTo(Arrays.asList("catalog", "system", 
"positional_args_func"));
+        assertThat(callStatement.args().size()).isEqualTo(8);
+        assertArgument(callStatement, 0, 1, DataTypes.IntegerType);
+        assertArgument(
+                callStatement,
+                1,
+                PaimonSparkSessionExtensions.class.getName(),
+                DataTypes.StringType);
+        assertArgument(callStatement, 2, 2L, DataTypes.LongType);
+        assertArgument(callStatement, 3, true, DataTypes.BooleanType);
+        assertArgument(callStatement, 4, 3.0D, DataTypes.DoubleType);
+        assertArgument(callStatement, 5, 4.0e1, DataTypes.DoubleType);
+        assertArgument(
+                callStatement, 6, new BigDecimal("500e-1"), 
DataTypes.createDecimalType(3, 1));
+        assertArgument(
+                callStatement,
+                7,
+                Timestamp.from(Instant.parse("2017-02-03T10:37:30.00Z")),
+                DataTypes.TimestampType);
+    }
+
+    @Test
+    public void testCallWithMixedArguments() throws ParseException {
+        CallStatement callStatement =
+                (CallStatement)
+                        parser.parsePlan("CALL 
catalog.system.mixed_function(arg1 => 1, 'test')");
+        assertThat(JavaConverters.seqAsJavaList(callStatement.name()))
+                .isEqualTo(Arrays.asList("catalog", "system", 
"mixed_function"));
+        assertThat(callStatement.args().size()).isEqualTo(2);
+        assertArgument(callStatement, 0, "arg1", 1, DataTypes.IntegerType);
+        assertArgument(callStatement, 1, "test", DataTypes.StringType);
+    }
+
+    @Test
+    public void testCallWithParseException() {
+        assertThatThrownBy(() -> parser.parsePlan("CALL catalog.system func 
abc"))
+                .isInstanceOf(PaimonParseException.class)
+                .hasMessageContaining("missing '(' at 'func'");
+    }
+
+    private void assertArgument(
+            CallStatement call, int index, Object expectedValue, DataType 
expectedType) {
+        assertArgument(call, index, null, expectedValue, expectedType);
+    }
+
+    private void assertArgument(
+            CallStatement callStatement,
+            int index,
+            String expectedName,
+            Object expectedValue,
+            DataType expectedType) {
+        if (expectedName == null) {
+            CallArgument callArgument = callStatement.args().apply(index);
+            assertCast(callArgument, PositionalArgument.class);
+        } else {
+            NamedArgument namedArgument =
+                    assertCast(callStatement.args().apply(index), 
NamedArgument.class);
+            assertThat(namedArgument.name()).isEqualTo(expectedName);
+        }
+        assertThat(callStatement.args().apply(index).expr())
+                .isEqualTo(Literal$.MODULE$.create(expectedValue, 
expectedType));
+    }
+
+    private <T> T assertCast(Object value, Class<T> expectedClass) {
+        assertThat(value).isInstanceOf(expectedClass);
+        return expectedClass.cast(value);
+    }
+}
diff --git 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/PaimonSparkTestBase.scala
 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/PaimonSparkTestBase.scala
index 5152b506d..10977ee7d 100644
--- 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/PaimonSparkTestBase.scala
+++ 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/PaimonSparkTestBase.scala
@@ -20,6 +20,7 @@ package org.apache.paimon.spark
 import org.apache.paimon.catalog.{Catalog, CatalogContext, CatalogFactory, 
Identifier}
 import org.apache.paimon.options.Options
 import org.apache.paimon.spark.catalog.Catalogs
+import org.apache.paimon.spark.extensions.PaimonSparkSessionExtensions
 import org.apache.paimon.spark.sql.WithTableOptions
 import org.apache.paimon.table.AbstractFileStoreTable
 
@@ -45,7 +46,7 @@ class PaimonSparkTestBase extends QueryTest with 
SharedSparkSession with WithTab
     super.sparkConf
       .set("spark.sql.catalog.paimon", classOf[SparkCatalog].getName)
       .set("spark.sql.catalog.paimon.warehouse", tempDBDir.getCanonicalPath)
-      .set("spark.sql.extensions", 
classOf[PaimonSparkSessionExtension].getName)
+      .set("spark.sql.extensions", 
classOf[PaimonSparkSessionExtensions].getName)
   }
 
   override protected def beforeAll(): Unit = {
diff --git 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/TableValuedFunctionsTest.scala
 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/TableValuedFunctionsTest.scala
index 16d0d797a..0697d2f27 100644
--- 
a/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/TableValuedFunctionsTest.scala
+++ 
b/paimon-spark/paimon-spark-common/src/test/scala/org/apache/paimon/spark/sql/TableValuedFunctionsTest.scala
@@ -18,7 +18,8 @@
 package org.apache.paimon.spark.sql
 
 import org.apache.paimon.WriteMode.CHANGE_LOG
-import org.apache.paimon.spark.{PaimonSparkSessionExtension, 
PaimonSparkTestBase}
+import org.apache.paimon.spark.PaimonSparkTestBase
+import org.apache.paimon.spark.extensions.PaimonSparkSessionExtensions
 
 import org.apache.spark.SparkConf
 import org.apache.spark.sql.{DataFrame, Row}
@@ -28,7 +29,7 @@ class TableValuedFunctionsTest extends PaimonSparkTestBase {
   override protected def sparkConf: SparkConf = {
     super.sparkConf
       .set("spark.sql.catalog.spark_catalog", 
"org.apache.paimon.spark.SparkGenericCatalog")
-      .set("spark.sql.extensions", 
classOf[PaimonSparkSessionExtension].getName)
+      .set("spark.sql.extensions", 
classOf[PaimonSparkSessionExtensions].getName)
   }
 
   writeModes.foreach {
diff --git a/pom.xml b/pom.xml
index bf5d808d2..eb2d040ee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -126,6 +126,7 @@ under the License.
         <avro.version>1.11.1</avro.version>
         <kafka.version>3.2.3</kafka.version>
         <scala-maven-plugin.version>3.2.2</scala-maven-plugin.version>
+        <antlr4-maven-plugin.version>4.8</antlr4-maven-plugin.version>
         <jsoup.version>1.15.3</jsoup.version>
     </properties>
 

Reply via email to