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

kasakrisz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new d1282396679 HIVE-29786: Support SELECT * EXCLUDE syntax for column 
omission (#6665)
d1282396679 is described below

commit d1282396679bc07d4dc45ca27677155d18b32a0c
Author: Raghav Aggarwal <[email protected]>
AuthorDate: Wed Aug 26 13:08:04 2026 +0530

    HIVE-29786: Support SELECT * EXCLUDE syntax for column omission (#6665)
---
 .../apache/hadoop/hive/ql/parse/FromClauseParser.g |   8 +-
 .../apache/hadoop/hive/ql/parse/HiveLexerParent.g  |   1 +
 .../hadoop/hive/ql/parse/IdentifiersParser.g       |   1 +
 .../hadoop/hive/ql/parse/CalcitePlanner.java       |   7 +-
 .../hadoop/hive/ql/parse/SemanticAnalyzer.java     |  59 ++++-
 .../test/queries/clientpositive/select_exclude.q   |  39 +++
 .../clientpositive/llap/select_exclude.q.out       | 283 +++++++++++++++++++++
 7 files changed, 389 insertions(+), 9 deletions(-)

diff --git a/parser/src/java/org/apache/hadoop/hive/ql/parse/FromClauseParser.g 
b/parser/src/java/org/apache/hadoop/hive/ql/parse/FromClauseParser.g
index ad34eddbc1a..2822c66d1c1 100644
--- a/parser/src/java/org/apache/hadoop/hive/ql/parse/FromClauseParser.g
+++ b/parser/src/java/org/apache/hadoop/hive/ql/parse/FromClauseParser.g
@@ -48,10 +48,10 @@ catch (RecognitionException e) {
 
//-----------------------------------------------------------------------------------
 
 tableAllColumns
-    : STAR
-        -> ^(TOK_ALLCOLREF)
-    | tableName DOT STAR
-        -> ^(TOK_ALLCOLREF tableName)
+    : STAR (KW_EXCLUDE LPAREN columnNameList RPAREN)?
+        -> ^(TOK_ALLCOLREF columnNameList?)
+    | tableName DOT STAR (KW_EXCLUDE LPAREN columnNameList RPAREN)?
+        -> ^(TOK_ALLCOLREF tableName columnNameList?)
     ;
 
 // (table|column)
diff --git a/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g 
b/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g
index a96812d4969..d784056d783 100644
--- a/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g
+++ b/parser/src/java/org/apache/hadoop/hive/ql/parse/HiveLexerParent.g
@@ -399,6 +399,7 @@ KW_SYSTEM_TIME: 'SYSTEM_TIME';
 KW_SYSTEM_VERSION: 'SYSTEM_VERSION';
 KW_EXPIRE_SNAPSHOTS: 'EXPIRE_SNAPSHOTS';
 KW_REWRITE_MANIFESTS: 'REWRITE_MANIFESTS';
+KW_EXCLUDE: 'EXCLUDE';
 KW_SET_CURRENT_SNAPSHOT: 'SET_CURRENT_SNAPSHOT';
 KW_BRANCH: 'BRANCH';
 KW_SNAPSHOTS: 'SNAPSHOTS';
diff --git 
a/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g 
b/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g
index 37fd6187d16..c7e63cd286a 100644
--- a/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g
+++ b/parser/src/java/org/apache/hadoop/hive/ql/parse/IdentifiersParser.g
@@ -1026,6 +1026,7 @@ nonReserved
     | KW_TRIM
     | KW_SPEC
     | KW_SYSTEM_TIME | KW_SYSTEM_VERSION
+    | KW_EXCLUDE
     | KW_EXPIRE_SNAPSHOTS
     | KW_REWRITE_MANIFESTS
     | KW_SET_CURRENT_SNAPSHOT
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
index 819ff93dbb5..6c9f412a1a5 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/CalcitePlanner.java
@@ -4367,8 +4367,11 @@ private Pair<RelNode, RowResolver> 
internalGenSelectLogicalPlan(QB qb, RelNode s
 
           // 6.4 Build ExprNode corresponding to colums
           if (expr.getType() == HiveParser.TOK_ALLCOLREF) {
-            pos = genRexNodeRegex(".*",
-                expr.getChildCount() == 0 ? null : getUnescapedName((ASTNode) 
expr.getChild(0)).toLowerCase(),
+            // Parse SELECT * EXCLUDE columns and pass them to the Calcite 
engine for exclusion
+            ExcludeResult excludeResult = processAllColRefAndExclude(expr, 
inputRR);
+            String starTabAlias = excludeResult.tableAlias();
+            excludedColumns.addAll(excludeResult.excludedColumns());
+            pos = genRexNodeRegex(".*", starTabAlias,
                 expr, columnList, excludedColumns, inputRR, starRR, pos, 
outputRR, qb.getAliases(), true);
           } else if (expr.getType() == HiveParser.TOK_TABLE_OR_COL
                   && !hasAsClause
diff --git a/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java 
b/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java
index 6bcbd346b23..d74351bd590 100644
--- a/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java
+++ b/ql/src/java/org/apache/hadoop/hive/ql/parse/SemanticAnalyzer.java
@@ -4738,6 +4738,52 @@ static boolean isRegex(String pattern, HiveConf conf) {
     return false;
   }
 
+  /**
+   * Helper method to parse the excluded columns from an EXCLUDE AST node. 
Returns an unmodifiable
+   * set to ensure the caller cannot accidentally mutate the result.
+   */
+  private Set<ColumnInfo> processExcludeColumns(
+      ASTNode excludeNode, String starTabAlias, RowResolver inputRR) throws 
SemanticException {
+    Set<ColumnInfo> localExcluded = new HashSet<>();
+    for (int e = 0; e < excludeNode.getChildCount(); e++) {
+      String excludeColName = 
unescapeIdentifier(excludeNode.getChild(e).getText()).toLowerCase();
+      ColumnInfo colInfo = inputRR.get(starTabAlias, excludeColName);
+      if (colInfo != null) {
+        localExcluded.add(colInfo);
+      }
+    }
+    return Collections.unmodifiableSet(localExcluded);
+  }
+
+  protected record ExcludeResult(String tableAlias, Set<ColumnInfo> 
excludedColumns) {}
+
+  /**
+   * Parses a TOK_ALLCOLREF node (e.g. `*` or `t.* EXCLUDE (a)`) to extract 
the table alias and the
+   * set of columns to be excluded.
+   */
+  protected ExcludeResult processAllColRefAndExclude(ASTNode expr, RowResolver 
inputRR)
+      throws SemanticException {
+
+    String starTabAlias = null;
+
+    // Zero-allocation initialization for queries that don't use EXCLUDE.
+    Set<ColumnInfo> excludedColumns = Set.of();
+
+    if (expr.getChildren() != null) {
+      for (Node childNode : expr.getChildren()) {
+        ASTNode child = (ASTNode) childNode;
+        switch (child.getType()) {
+          case HiveParser.TOK_TABNAME -> starTabAlias = 
getUnescapedName(child).toLowerCase();
+          case HiveParser.TOK_TABCOLNAME ->
+              excludedColumns = processExcludeColumns(child, starTabAlias, 
inputRR);
+          default ->
+              throw new SemanticException(
+                  "Unexpected node type in TOK_ALLCOLREF: " + child.getType());
+        }
+      }
+    }
+    return new ExcludeResult(starTabAlias, excludedColumns);
+  }
 
   private Operator<?> genSelectPlan(String dest, QB qb, Operator<?> input,
                                     Operator<?> inputForSelectStar) throws 
SemanticException {
@@ -4915,9 +4961,16 @@ private Operator<?> genSelectPlan(String dest, ASTNode 
selExprList, QB qb, Opera
       // The real expression
       if (expr.getType() == HiveParser.TOK_ALLCOLREF) {
         int initPos = pos;
-        pos = genExprNodeDescRegex(".*", expr.getChildCount() == 0 ? null
-                : getUnescapedName((ASTNode) expr.getChild(0)).toLowerCase(),
-            expr, colList, null, inputRR, starRR, pos, out_rwsch, 
qb.getAliases(), false);
+
+        ExcludeResult excludeResult = processAllColRefAndExclude(expr, 
inputRR);
+        String starTabAlias = excludeResult.tableAlias();
+        Set<ColumnInfo> excludeCols = excludeResult.excludedColumns();
+        if (excludeCols.isEmpty()) {
+          excludeCols = null;
+        }
+
+        pos = genExprNodeDescRegex(".*", starTabAlias,
+            expr, colList, excludeCols, inputRR, starRR, pos, out_rwsch, 
qb.getAliases(), false);
         if (unparseTranslator.isEnabled()) {
           offset += pos - initPos - 1;
         }
diff --git a/ql/src/test/queries/clientpositive/select_exclude.q 
b/ql/src/test/queries/clientpositive/select_exclude.q
new file mode 100644
index 00000000000..3159ea299fc
--- /dev/null
+++ b/ql/src/test/queries/clientpositive/select_exclude.q
@@ -0,0 +1,39 @@
+set hive.cli.print.header=true;
+
+CREATE TABLE test_exclude (
+  id INT,
+  name STRING,
+  email STRING,
+  address STRING,
+  phone STRING
+);
+
+INSERT INTO test_exclude VALUES
+(1, 'Alice', '[email protected]', '123 Apple St', '555-0100'),
+(2, 'Bob', '[email protected]', '456 Banana Ave', '555-0200');
+
+-- Exclude a single column
+EXPLAIN SELECT * EXCLUDE (email) FROM test_exclude;
+SELECT * EXCLUDE (email) FROM test_exclude;
+
+-- Exclude multiple columns
+EXPLAIN SELECT * EXCLUDE (email, address, phone) FROM test_exclude;
+SELECT * EXCLUDE (email, address, phone) FROM test_exclude;
+
+-- Exclude with table alias
+EXPLAIN SELECT t.* EXCLUDE (id, phone) FROM test_exclude t;
+SELECT t.* EXCLUDE (id, phone) FROM test_exclude t;
+
+-- Exclude with JOIN
+CREATE TABLE test_exclude_join (
+  id INT,
+  department STRING
+);
+INSERT INTO test_exclude_join VALUES (1, 'Engineering'), (2, 'Sales');
+
+EXPLAIN
+SELECT a.* EXCLUDE (address, phone), b.* EXCLUDE (id)
+FROM test_exclude a JOIN test_exclude_join b ON a.id = b.id;
+
+SELECT a.* EXCLUDE (address, phone), b.* EXCLUDE (id)
+FROM test_exclude a JOIN test_exclude_join b ON a.id = b.id;
diff --git a/ql/src/test/results/clientpositive/llap/select_exclude.q.out 
b/ql/src/test/results/clientpositive/llap/select_exclude.q.out
new file mode 100644
index 00000000000..0bb6d242227
--- /dev/null
+++ b/ql/src/test/results/clientpositive/llap/select_exclude.q.out
@@ -0,0 +1,283 @@
+PREHOOK: query: CREATE TABLE test_exclude (
+  id INT,
+  name STRING,
+  email STRING,
+  address STRING,
+  phone STRING
+)
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@test_exclude
+POSTHOOK: query: CREATE TABLE test_exclude (
+  id INT,
+  name STRING,
+  email STRING,
+  address STRING,
+  phone STRING
+)
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@test_exclude
+PREHOOK: query: INSERT INTO test_exclude VALUES
+(1, 'Alice', '[email protected]', '123 Apple St', '555-0100'),
+(2, 'Bob', '[email protected]', '456 Banana Ave', '555-0200')
+PREHOOK: type: QUERY
+PREHOOK: Input: _dummy_database@_dummy_table
+PREHOOK: Output: default@test_exclude
+POSTHOOK: query: INSERT INTO test_exclude VALUES
+(1, 'Alice', '[email protected]', '123 Apple St', '555-0100'),
+(2, 'Bob', '[email protected]', '456 Banana Ave', '555-0200')
+POSTHOOK: type: QUERY
+POSTHOOK: Input: _dummy_database@_dummy_table
+POSTHOOK: Output: default@test_exclude
+POSTHOOK: Lineage: test_exclude.address SCRIPT []
+POSTHOOK: Lineage: test_exclude.email SCRIPT []
+POSTHOOK: Lineage: test_exclude.id SCRIPT []
+POSTHOOK: Lineage: test_exclude.name SCRIPT []
+POSTHOOK: Lineage: test_exclude.phone SCRIPT []
+col1   col2    col3    col4    col5
+PREHOOK: query: EXPLAIN SELECT * EXCLUDE (email) FROM test_exclude
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+POSTHOOK: query: EXPLAIN SELECT * EXCLUDE (email) FROM test_exclude
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+Explain
+STAGE DEPENDENCIES:
+  Stage-0 is a root stage
+
+STAGE PLANS:
+  Stage: Stage-0
+    Fetch Operator
+      limit: -1
+      Processor Tree:
+        TableScan
+          alias: test_exclude
+          Select Operator
+            expressions: id (type: int), name (type: string), address (type: 
string), phone (type: string)
+            outputColumnNames: _col0, _col1, _col2, _col3
+            ListSink
+
+PREHOOK: query: SELECT * EXCLUDE (email) FROM test_exclude
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+POSTHOOK: query: SELECT * EXCLUDE (email) FROM test_exclude
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+test_exclude.id        test_exclude.name       test_exclude.address    
test_exclude.phone
+1      Alice   123 Apple St    555-0100
+2      Bob     456 Banana Ave  555-0200
+PREHOOK: query: EXPLAIN SELECT * EXCLUDE (email, address, phone) FROM 
test_exclude
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+POSTHOOK: query: EXPLAIN SELECT * EXCLUDE (email, address, phone) FROM 
test_exclude
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+Explain
+STAGE DEPENDENCIES:
+  Stage-0 is a root stage
+
+STAGE PLANS:
+  Stage: Stage-0
+    Fetch Operator
+      limit: -1
+      Processor Tree:
+        TableScan
+          alias: test_exclude
+          Select Operator
+            expressions: id (type: int), name (type: string)
+            outputColumnNames: _col0, _col1
+            ListSink
+
+PREHOOK: query: SELECT * EXCLUDE (email, address, phone) FROM test_exclude
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+POSTHOOK: query: SELECT * EXCLUDE (email, address, phone) FROM test_exclude
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+test_exclude.id        test_exclude.name
+1      Alice
+2      Bob
+PREHOOK: query: EXPLAIN SELECT t.* EXCLUDE (id, phone) FROM test_exclude t
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+POSTHOOK: query: EXPLAIN SELECT t.* EXCLUDE (id, phone) FROM test_exclude t
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+Explain
+STAGE DEPENDENCIES:
+  Stage-0 is a root stage
+
+STAGE PLANS:
+  Stage: Stage-0
+    Fetch Operator
+      limit: -1
+      Processor Tree:
+        TableScan
+          alias: t
+          Select Operator
+            expressions: name (type: string), email (type: string), address 
(type: string)
+            outputColumnNames: _col0, _col1, _col2
+            ListSink
+
+PREHOOK: query: SELECT t.* EXCLUDE (id, phone) FROM test_exclude t
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+POSTHOOK: query: SELECT t.* EXCLUDE (id, phone) FROM test_exclude t
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+#### A masked pattern was here ####
+t.name t.email t.address
+Alice  [email protected]  123 Apple St
+Bob    [email protected]   456 Banana Ave
+PREHOOK: query: CREATE TABLE test_exclude_join (
+  id INT,
+  department STRING
+)
+PREHOOK: type: CREATETABLE
+PREHOOK: Output: database:default
+PREHOOK: Output: default@test_exclude_join
+POSTHOOK: query: CREATE TABLE test_exclude_join (
+  id INT,
+  department STRING
+)
+POSTHOOK: type: CREATETABLE
+POSTHOOK: Output: database:default
+POSTHOOK: Output: default@test_exclude_join
+PREHOOK: query: INSERT INTO test_exclude_join VALUES (1, 'Engineering'), (2, 
'Sales')
+PREHOOK: type: QUERY
+PREHOOK: Input: _dummy_database@_dummy_table
+PREHOOK: Output: default@test_exclude_join
+POSTHOOK: query: INSERT INTO test_exclude_join VALUES (1, 'Engineering'), (2, 
'Sales')
+POSTHOOK: type: QUERY
+POSTHOOK: Input: _dummy_database@_dummy_table
+POSTHOOK: Output: default@test_exclude_join
+POSTHOOK: Lineage: test_exclude_join.department SCRIPT []
+POSTHOOK: Lineage: test_exclude_join.id SCRIPT []
+col1   col2
+PREHOOK: query: EXPLAIN
+SELECT a.* EXCLUDE (address, phone), b.* EXCLUDE (id)
+FROM test_exclude a JOIN test_exclude_join b ON a.id = b.id
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+PREHOOK: Input: default@test_exclude_join
+#### A masked pattern was here ####
+POSTHOOK: query: EXPLAIN
+SELECT a.* EXCLUDE (address, phone), b.* EXCLUDE (id)
+FROM test_exclude a JOIN test_exclude_join b ON a.id = b.id
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+POSTHOOK: Input: default@test_exclude_join
+#### A masked pattern was here ####
+Explain
+STAGE DEPENDENCIES:
+  Stage-1 is a root stage
+  Stage-0 depends on stages: Stage-1
+
+STAGE PLANS:
+  Stage: Stage-1
+    Tez
+#### A masked pattern was here ####
+      Edges:
+        Reducer 2 <- Map 1 (SIMPLE_EDGE), Map 3 (SIMPLE_EDGE)
+#### A masked pattern was here ####
+      Vertices:
+        Map 1 
+            Map Operator Tree:
+                TableScan
+                  alias: a
+                  filterExpr: id is not null (type: boolean)
+                  Statistics: Num rows: 2 Data size: 378 Basic stats: COMPLETE 
Column stats: COMPLETE
+                  Filter Operator
+                    predicate: id is not null (type: boolean)
+                    Statistics: Num rows: 2 Data size: 378 Basic stats: 
COMPLETE Column stats: COMPLETE
+                    Select Operator
+                      expressions: id (type: int), name (type: string), email 
(type: string)
+                      outputColumnNames: _col0, _col1, _col2
+                      Statistics: Num rows: 2 Data size: 378 Basic stats: 
COMPLETE Column stats: COMPLETE
+                      Reduce Output Operator
+                        key expressions: _col0 (type: int)
+                        null sort order: z
+                        sort order: +
+                        Map-reduce partition columns: _col0 (type: int)
+                        Statistics: Num rows: 2 Data size: 378 Basic stats: 
COMPLETE Column stats: COMPLETE
+                        value expressions: _col1 (type: string), _col2 (type: 
string)
+            Execution mode: vectorized, llap
+            LLAP IO: all inputs
+        Map 3 
+            Map Operator Tree:
+                TableScan
+                  alias: b
+                  filterExpr: id is not null (type: boolean)
+                  Statistics: Num rows: 2 Data size: 192 Basic stats: COMPLETE 
Column stats: COMPLETE
+                  Filter Operator
+                    predicate: id is not null (type: boolean)
+                    Statistics: Num rows: 2 Data size: 192 Basic stats: 
COMPLETE Column stats: COMPLETE
+                    Select Operator
+                      expressions: id (type: int), department (type: string)
+                      outputColumnNames: _col0, _col1
+                      Statistics: Num rows: 2 Data size: 192 Basic stats: 
COMPLETE Column stats: COMPLETE
+                      Reduce Output Operator
+                        key expressions: _col0 (type: int)
+                        null sort order: z
+                        sort order: +
+                        Map-reduce partition columns: _col0 (type: int)
+                        Statistics: Num rows: 2 Data size: 192 Basic stats: 
COMPLETE Column stats: COMPLETE
+                        value expressions: _col1 (type: string)
+            Execution mode: vectorized, llap
+            LLAP IO: all inputs
+        Reducer 2 
+            Execution mode: llap
+            Reduce Operator Tree:
+              Merge Join Operator
+                condition map:
+                     Inner Join 0 to 1
+                keys:
+                  0 _col0 (type: int)
+                  1 _col0 (type: int)
+                outputColumnNames: _col0, _col1, _col2, _col4
+                Statistics: Num rows: 2 Data size: 562 Basic stats: COMPLETE 
Column stats: COMPLETE
+                Select Operator
+                  expressions: _col0 (type: int), _col1 (type: string), _col2 
(type: string), _col4 (type: string)
+                  outputColumnNames: _col0, _col1, _col2, _col3
+                  Statistics: Num rows: 2 Data size: 562 Basic stats: COMPLETE 
Column stats: COMPLETE
+                  File Output Operator
+                    compressed: false
+                    Statistics: Num rows: 2 Data size: 562 Basic stats: 
COMPLETE Column stats: COMPLETE
+                    table:
+                        input format: 
org.apache.hadoop.mapred.SequenceFileInputFormat
+                        output format: 
org.apache.hadoop.hive.ql.io.HiveSequenceFileOutputFormat
+                        serde: 
org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe
+
+  Stage: Stage-0
+    Fetch Operator
+      limit: -1
+      Processor Tree:
+        ListSink
+
+PREHOOK: query: SELECT a.* EXCLUDE (address, phone), b.* EXCLUDE (id)
+FROM test_exclude a JOIN test_exclude_join b ON a.id = b.id
+PREHOOK: type: QUERY
+PREHOOK: Input: default@test_exclude
+PREHOOK: Input: default@test_exclude_join
+#### A masked pattern was here ####
+POSTHOOK: query: SELECT a.* EXCLUDE (address, phone), b.* EXCLUDE (id)
+FROM test_exclude a JOIN test_exclude_join b ON a.id = b.id
+POSTHOOK: type: QUERY
+POSTHOOK: Input: default@test_exclude
+POSTHOOK: Input: default@test_exclude_join
+#### A masked pattern was here ####
+a.id   a.name  a.email b.department
+1      Alice   [email protected]  Engineering
+2      Bob     [email protected]   Sales

Reply via email to