raminqaf commented on code in PR #26638:
URL: https://github.com/apache/flink/pull/26638#discussion_r2131553523


##########
docs/content.zh/docs/dev/table/types.md:
##########
@@ -1281,15 +1293,51 @@ For some classes an annotation is required in order to 
map the class to a data t
 to assign a fixed precision and scale for `java.math.BigDecimal`).
 {{< /tab >}}
 {{< tab "Python" >}}
+```python
+Not supported.
+```
 {{< /tab >}}
 {{< /tabs >}}
 
 **Declaration**
 
 {{< tabs "c5e5527b-b09d-4dc5-9549-8fd2bfc7cc2a" >}}
-{{< tab "Java" >}}
+{{< tab "Java/Scala" >}}
+Structured types are usually declared via their implementation classes:
+
 ```java
-class User {
+// A simple POJO that qualifies as a structured type.
+// Note: Without a fully assigning constructor, the order of fields will be 
alphabetical.
+// The final data type will be:
+// STRUCTURED<'com.myorg.Customer', active BOOLEAN, id INT NOT NULL, name 
STRING, properties MAP<STRING, STRING>>
+class Customer {
+  public int id;
+  public String name;
+  public Map<String, String> properties;
+  public boolean active;
+}
+
+// A POJO with a fully assigning constructor defining the field order.
+// The final data type will be:
+// STRUCTURED<'com.myorg.Customer', id INT NOT NULL, name STRING, properties 
MAP<STRING, STRING>, active BOOLEAN>

Review Comment:
   Question: if the field `id` was defined as `public Integer id` would the 
structured type be
   ```
   STRUCTURED<'com.myorg.Customer', id INT, name STRING, properties MAP<STRING, 
STRING>, active BOOLEAN>
   ```



##########
docs/content.zh/docs/dev/table/types.md:
##########
@@ -1232,34 +1232,46 @@ equivalent to `ROW<myField INT, myOtherField BOOLEAN>`.
 
 ### User-Defined Data Types
 
-{{< tabs "udf" >}}
-{{< tab "Java/Scala" >}}
-<span class="label label-danger">Attention</span> User-defined data types are 
not fully supported yet. They are
-currently (as of Flink 1.11) only exposed as unregistered structured types in 
parameters and return types of functions.
+#### `STRUCTURED`
 
-A structured type is similar to an object in an object-oriented programming 
language. It contains
-zero, one or more attributes. Each attribute consists of a name and a type.
+Data type for a user-defined object.
 
-There are two kinds of structured types:
+Compared to `ROW`, which may also be considered a "struct-like" type, 
structured types are distinguishable even if they
+contain the same set of fields. For example, `Visit(amount DOUBLE)` is 
distinct from `Interaction(amount DOUBLE)` due
+its identifier.
 
-- Types that are stored in a catalog and are identified by a _catalog 
identifier_ (like `cat.db.MyType`). Those
-are equal to the SQL standard definition of structured types.
+Similar to classes in object-oriented programming languages, structured types 
are identified by a class name and contain
+zero, one or more attributes. Each attribute consists of a name and a type. A 
type cannot be defined in such a way that
+one of its attribute types (transitively) refers to itself.
 
-- Anonymously defined, unregistered types (usually reflectively extracted) 
that are identified by
-an _implementation class_ (like `com.myorg.model.MyType`). Those are useful 
when programmatically
-defining a table program. They enable reusing existing JVM classes without 
manually defining the
-schema of a data type again.
+Structured types are internally converted by the system into suitable data 
structures. Serialization and equality checks
+are managed by the system based on the logical type.
 
-#### Registered Structured Types
+{{< tabs "udt" >}}
+{{< tab "SQL" >}}
+```sql
+STRUCTURED<'c', n0 t0, n1 t1, ...>
+STRUCTURED<'c', n0 t0, n1 t1 'd1', ...>
+```
+The type can be declared using `STRUCTURED<'c', n0 t0 'd0', n1 t1 'd1', ...>` 
where `c` is the class name, `n` is the
+unique name of a field, `t` is the logical type of a field, `d` is the 
description of a field.

Review Comment:
   ```suggestion
   unique name of a field, `t` is the logical type of a field, `d` is the 
optional description of a field.
   ```



##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/StructuredFunctionsITCase.java:
##########


Review Comment:
   Do we want to have IT tests for nested STRUCTURED type?



##########
docs/content.zh/docs/dev/table/types.md:
##########
@@ -1232,34 +1232,46 @@ equivalent to `ROW<myField INT, myOtherField BOOLEAN>`.
 
 ### User-Defined Data Types
 
-{{< tabs "udf" >}}
-{{< tab "Java/Scala" >}}
-<span class="label label-danger">Attention</span> User-defined data types are 
not fully supported yet. They are
-currently (as of Flink 1.11) only exposed as unregistered structured types in 
parameters and return types of functions.
+#### `STRUCTURED`
 
-A structured type is similar to an object in an object-oriented programming 
language. It contains
-zero, one or more attributes. Each attribute consists of a name and a type.
+Data type for a user-defined object.
 
-There are two kinds of structured types:
+Compared to `ROW`, which may also be considered a "struct-like" type, 
structured types are distinguishable even if they
+contain the same set of fields. For example, `Visit(amount DOUBLE)` is 
distinct from `Interaction(amount DOUBLE)` due
+its identifier.
 
-- Types that are stored in a catalog and are identified by a _catalog 
identifier_ (like `cat.db.MyType`). Those
-are equal to the SQL standard definition of structured types.
+Similar to classes in object-oriented programming languages, structured types 
are identified by a class name and contain
+zero, one or more attributes. Each attribute consists of a name and a type. A 
type cannot be defined in such a way that

Review Comment:
   ```suggestion
   zero, one or more attributes. Each attribute has a name, a type, and an 
optional description. A type cannot be defined in such a way that
   ```



##########
docs/content.zh/docs/dev/table/types.md:
##########
@@ -1281,15 +1293,51 @@ For some classes an annotation is required in order to 
map the class to a data t
 to assign a fixed precision and scale for `java.math.BigDecimal`).
 {{< /tab >}}
 {{< tab "Python" >}}
+```python
+Not supported.
+```
 {{< /tab >}}
 {{< /tabs >}}
 
 **Declaration**
 
 {{< tabs "c5e5527b-b09d-4dc5-9549-8fd2bfc7cc2a" >}}
-{{< tab "Java" >}}
+{{< tab "Java/Scala" >}}
+Structured types are usually declared via their implementation classes:
+
 ```java
-class User {
+// A simple POJO that qualifies as a structured type.

Review Comment:
   I don't know how important that would be, but having a tiny example with the 
description doesn't hurt. WDYT?



##########
flink-table/flink-sql-parser/src/main/codegen/includes/parserImpls.ftl:
##########
@@ -2489,6 +2489,34 @@ SqlTypeNameSpec ExtendedSqlRowTypeName() :
     }
 }
 
+/**
+ * Parse inline structured type such as STRUCTURED&lt;'className', name1 type1 
'comment', name2 type2&gt;.
+ */
+SqlTypeNameSpec SqlStructuredTypeName() :
+{
+    final SqlNode className;
+    final List<SqlIdentifier> fieldNames = new ArrayList<SqlIdentifier>();
+    final List<SqlDataTypeSpec> fieldTypes = new ArrayList<SqlDataTypeSpec>();
+    final List<SqlCharStringLiteral> comments = new 
ArrayList<SqlCharStringLiteral>();

Review Comment:
   Maybe staying consistent here would be good. In the docs (also in some parts 
of the code), this field is named `description`:
   ```suggestion
       final List<SqlCharStringLiteral> descriptions = new 
ArrayList<SqlCharStringLiteral>();
   ```
   This should apply to all the `comment` names in this file



##########
flink-table/flink-sql-parser/src/main/java/org/apache/flink/table/calcite/ExtendedRelTypeFactory.java:
##########
@@ -33,4 +35,10 @@ public interface ExtendedRelTypeFactory extends 
RelDataTypeFactory {
 
     /** Creates a RAW type such as {@code RAW('org.my.Class', 'sW3Djsds...')}. 
*/
     RelDataType createRawType(String className, String serializerString);
+
+    /**
+     * Creates a STRUCTURED type such as {@code STRUCTURED('org.my.Class', 
name STRING, age INT)}.
+     */
+    RelDataType createStructuredType(
+            String className, List<RelDataType> typeList, List<String> 
fieldNameList);

Review Comment:
   Question: Why is the comment/description field not present here? I think 
this because we don't really need them, and they are just for clarity, written 
by the user?
   ```suggestion
       /**
        * Creates a STRUCTURED type such as {@code STRUCTURED('org.my.Class', 
name STRING 'full name of the person', age INT)}.
        */
       RelDataType createStructuredType(
               String className, List<RelDataType> typeList, List<String> 
fieldNameList, List<String> fieldCommentList);
   ```



##########
docs/content.zh/docs/dev/table/types.md:
##########
@@ -1301,35 +1349,27 @@ class User {
     // enrich the extraction with forcing using RAW types
     public @DataTypeHint("RAW") Class<?> modelClass;
 }
-
-DataTypes.of(User.class);
 ```
 
-**Bridging to JVM Types**
-
-| Java Type                            | Input | Output | Remarks              
                   |
-|:-------------------------------------|:-----:|:------:|:----------------------------------------|
-|*class*                               | X     | X      | Originating class or 
subclasses (for input) or <br>superclasses (for output). *Default* |
-|`org.apache.flink.types.Row`          | X     | X      | Represent the 
structured type as a row. |
-|`org.apache.flink.table.data.RowData` | X     | X      | Internal data 
structure.                |
-
-{{< /tab >}}
-{{< tab "Scala" >}}
-```scala
-case class User(
-
-    // extract fields automatically
-    age: Int,
-    name: String,
+Or via explicit declaration:
+```java
+DataTypes.STRUCTURED(Class, DataTypes.FIELD(n0, t0), DataTypes.FIELD(n1, t1), 
...);
+DataTypes.STRUCTURED(String, DataTypes.FIELD(n0, t0), DataTypes.FIELD(n1, t1), 
...);

Review Comment:
   Shouldn't this be?
   ```suggestion
   DataTypes.STRUCTURED(String.class, DataTypes.FIELD(n0, t0), 
DataTypes.FIELD(n1, t1), ...);
   ```
   And what is the difference between this line and the one before it?



##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/StructuredFunctionsITCase.java:
##########
@@ -0,0 +1,120 @@
+/*
+ * 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.flink.table.planner.functions;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
+import org.apache.flink.table.functions.ScalarFunction;
+import org.apache.flink.table.types.logical.StructuredType;
+
+import java.util.stream.Stream;
+
+/** Tests for functions dealing with {@link StructuredType}. */
+public class StructuredFunctionsITCase extends BuiltInFunctionTestBase {
+
+    @Override
+    Stream<TestSetSpec> getTestSetSpecs() {
+        return Stream.of(
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.EQUALS)
+                        .onFieldsWithData(14, "Bob")
+                        .andDataTypes(DataTypes.INT(), DataTypes.STRING())
+                        .withFunction(Type1.Type1Constructor.class)
+                        .withFunction(Type2.Type2Constructor.class)
+                        // Same value from function
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = 
Type1Constructor(14, 'Bob')",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // Same value from CAST
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = CAST((14, 'Bob') 
AS "
+                                        + Type1.TYPE
+                                        + ")",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // Different value from function
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = 
Type1Constructor(15, 'Alice')",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // Different value from CAST
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = CAST((15, 'Alice') 
AS "
+                                        + Type1.TYPE
+                                        + ")",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // Different class name from function
+                        .testSqlValidationError(
+                                "Type1Constructor(f0, f1) = 
Type2Constructor(14, 'Bob')",
+                                "Incompatible structured types")
+                        // Different class name from CAST
+                        .testSqlValidationError(
+                                "Type1Constructor(f0, f1) = CAST((14, 'Bob') 
AS "
+                                        + Type2.TYPE
+                                        + ")",
+                                "Incompatible structured types")
+                        // Same class name but different fields
+                        .testSqlValidationError(
+                                "Type1Constructor(f0, f1) = CAST((14, 'Bob') 
AS STRUCTURED<'"
+                                        + Type1.class.getName()
+                                        + "', a BOOLEAN, b BOOLEAN>)",
+                                "Cannot apply '=' to arguments"));
+    }
+
+    // 
--------------------------------------------------------------------------------------------
+    // Helpers
+    // 
--------------------------------------------------------------------------------------------
+
+    /** Structured type Type1. */
+    public static class Type1 {
+        public static final String TYPE =

Review Comment:
   nit: can be private
   ```suggestion
           private static final String TYPE =
   ```
   Same with Type2



##########
flink-table/flink-table-planner/src/test/java/org/apache/flink/table/planner/functions/StructuredFunctionsITCase.java:
##########
@@ -0,0 +1,120 @@
+/*
+ * 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.flink.table.planner.functions;
+
+import org.apache.flink.table.api.DataTypes;
+import org.apache.flink.table.functions.BuiltInFunctionDefinitions;
+import org.apache.flink.table.functions.ScalarFunction;
+import org.apache.flink.table.types.logical.StructuredType;
+
+import java.util.stream.Stream;
+
+/** Tests for functions dealing with {@link StructuredType}. */
+public class StructuredFunctionsITCase extends BuiltInFunctionTestBase {
+
+    @Override
+    Stream<TestSetSpec> getTestSetSpecs() {
+        return Stream.of(
+                TestSetSpec.forFunction(BuiltInFunctionDefinitions.EQUALS)
+                        .onFieldsWithData(14, "Bob")
+                        .andDataTypes(DataTypes.INT(), DataTypes.STRING())
+                        .withFunction(Type1.Type1Constructor.class)
+                        .withFunction(Type2.Type2Constructor.class)
+                        // Same value from function
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = 
Type1Constructor(14, 'Bob')",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // Same value from CAST
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = CAST((14, 'Bob') 
AS "
+                                        + Type1.TYPE
+                                        + ")",
+                                true,
+                                DataTypes.BOOLEAN())
+                        // Different value from function
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = 
Type1Constructor(15, 'Alice')",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // Different value from CAST
+                        .testSqlResult(
+                                "Type1Constructor(f0, f1) = CAST((15, 'Alice') 
AS "
+                                        + Type1.TYPE
+                                        + ")",
+                                false,
+                                DataTypes.BOOLEAN())
+                        // Different class name from function
+                        .testSqlValidationError(
+                                "Type1Constructor(f0, f1) = 
Type2Constructor(14, 'Bob')",
+                                "Incompatible structured types")
+                        // Different class name from CAST
+                        .testSqlValidationError(
+                                "Type1Constructor(f0, f1) = CAST((14, 'Bob') 
AS "
+                                        + Type2.TYPE
+                                        + ")",
+                                "Incompatible structured types")
+                        // Same class name but different fields
+                        .testSqlValidationError(
+                                "Type1Constructor(f0, f1) = CAST((14, 'Bob') 
AS STRUCTURED<'"
+                                        + Type1.class.getName()
+                                        + "', a BOOLEAN, b BOOLEAN>)",
+                                "Cannot apply '=' to arguments"));
+    }
+
+    // 
--------------------------------------------------------------------------------------------
+    // Helpers
+    // 
--------------------------------------------------------------------------------------------
+
+    /** Structured type Type1. */
+    public static class Type1 {
+        public static final String TYPE =
+                "STRUCTURED<'" + Type1.class.getName() + "', a INT, b STRING>";
+
+        public Integer a;

Review Comment:
   Question: Changing the type to `int` on both classes will cause the tests to 
fail. Why is that?
   ```suggestion
           public int a;
   ```



##########
flink-table/flink-table-common/src/main/java/org/apache/flink/table/api/DataTypes.java:
##########
@@ -928,37 +928,88 @@ public static <T> UnresolvedDataType 
RAW(TypeInformation<T> typeInformation) {
     }
 
     /**
-     * Data type of a user-defined object structured type. Structured types 
contain zero, one or
-     * more attributes. Each attribute consists of a name and a type. A type 
cannot be defined so
-     * that one of its attribute types (transitively) uses itself.
-     *
-     * <p>There are two kinds of structured types. Types that are stored in a 
catalog and are
-     * identified by an {@link ObjectIdentifier} or anonymously defined, 
unregistered types (usually
-     * reflectively extracted) that are identified by an implementation {@link 
Class}.
-     *
-     * <p>This method helps in manually constructing anonymous, unregistered 
types. This is useful
-     * in cases where the reflective extraction using {@link 
DataTypes#of(Class)} is not applicable.
-     * However, {@link DataTypes#of(Class)} is the recommended way of creating 
inline structured
-     * types as it also considers {@link DataTypeHint}s.
-     *
-     * <p>Structured types are converted to internal data structures by the 
runtime. The given
-     * implementation class is only used at the edges of the table ecosystem 
(e.g. when bridging to
-     * a function or connector). Serialization and equality ({@code 
hashCode/equals}) are handled by
-     * the runtime based on the logical type. An implementation class must 
offer a default
-     * constructor with zero arguments or a full constructor that assigns all 
attributes.
-     *
-     * <p>Note: A caller of this method must make sure that the {@link
-     * DataType#getConversionClass()} of the given fields matches with the 
attributes of the given
-     * implementation class, otherwise an exception might be thrown during 
runtime.
+     * Data type of a user-defined object structured type. Structured types 
are identified by a
+     * class name and contain zero, one or more attributes. Each attribute 
consists of a name and a
+     * type. A type cannot be defined in such a way that one of its attribute 
types (transitively)
+     * refers to itself.
+     *
+     * <p>Compared to {@link #ROW(Field...)}, which may also be considered a 
"struct-like" type,
+     * structured types are distinguishable even if they contain the same set 
of fields. For
+     * example, "Visit(amount DOUBLE)" is distinct from "Interaction(amount 
DOUBLE)" due its
+     * identifier.
+     *
+     * <p>This method allows for manually constructing an inline structured 
type. This is useful in
+     * cases where the reflective extraction using {@link DataTypes#of(Class)} 
is not applicable.
+     * However, {@link DataTypes#of(Class)} is the recommended approach for 
creating inline
+     * structured types, as it also considers {@link DataTypeHint}s.
+     *
+     * <p>Structured types are internally converted by the system into 
suitable data structures.
+     * Serialization and equality checks (e.g. {@code hashCode/equals}) are 
managed by the system
+     * based on the logical type.
+     *
+     * <p>If an optional implementation class is provided, the system will 
convert a structured
+     * object to a JVM object at the edges of the table ecosystem (e.g. when 
bridging to a function
+     * or connector). The implementation class must provide either a 
zero-argument constructor or a
+     * full constructor that assigns all attributes. The class name does not 
need to be resolvable
+     * in the classpath; it may be used solely to distinguish between objects 
with identical
+     * attribute sets. However, in Table API and UDF calls, the system will 
attempt to resolve the
+     * class name to an actual implementation class. If resolution fails, 
{@link Row} is used as a
+     * fallback.
+     *
+     * <p>Note: The caller of this method must ensure that the {@link 
DataType#getConversionClass()}
+     * of each field matches the corresponding attribute in the implementation 
class. Otherwise, a
+     * runtime exception may be thrown.
      *
      * @see DataTypes#of(Class)
      * @see StructuredType
      */
     public static <T> DataType STRUCTURED(Class<T> implementationClass, 
Field... fields) {
         // some basic validation of the class to prevent common mistakes
         validateStructuredClass(implementationClass);
+        return 
buildStructuredType(StructuredType.newBuilder(implementationClass), fields);
+    }
 
-        final StructuredType.Builder builder = 
StructuredType.newBuilder(implementationClass);
+    /**
+     * Data type of a user-defined object structured type. Structured types 
are identified by a
+     * class name and contain zero, one or more attributes. Each attribute 
consists of a name and a
+     * type. A type cannot be defined in such a way that one of its attribute 
types (transitively)
+     * refers to itself.
+     *
+     * <p>Compared to {@link #ROW(Field...)}, which may also be considered a 
"struct-like" type,
+     * structured types are distinguishable even if they contain the same set 
of fields. For
+     * example, "Visit(amount DOUBLE)" is distinct from "Interaction(amount 
DOUBLE)" due its
+     * identifier.
+     *
+     * <p>This method allows for manually constructing an inline structured 
type. This is useful in
+     * cases where the reflective extraction using {@link DataTypes#of(Class)} 
is not applicable.
+     * However, {@link DataTypes#of(Class)} is the recommended approach for 
creating inline
+     * structured types, as it also considers {@link DataTypeHint}s.
+     *
+     * <p>Structured types are internally converted by the system into 
suitable data structures.
+     * Serialization and equality checks (e.g. {@code hashCode/equals}) are 
managed by the system
+     * based on the logical type.
+     *
+     * <p>If an optional implementation class is provided, the system will 
convert a structured
+     * object to a JVM object at the edges of the table ecosystem (e.g. when 
bridging to a function
+     * or connector). The implementation class must provide either a 
zero-argument constructor or a
+     * full constructor that assigns all attributes. The class name does not 
need to be resolvable
+     * in the classpath; it may be used solely to distinguish between objects 
with identical
+     * attribute sets. However, in Table API and UDF calls, the system will 
attempt to resolve the
+     * class name to an actual implementation class. If resolution fails, 
{@link Row} is used as a
+     * fallback.
+     *
+     * <p>Note: The caller of this method must ensure that the {@link 
DataType#getConversionClass()}
+     * of each field matches the corresponding attribute in the implementation 
class. Otherwise, a
+     * runtime exception may be thrown.
+     *
+     * @see DataTypes#of(Class)
+     * @see StructuredType
+     */
+    public static <T> DataType STRUCTURED(String className, Field... fields) {
+        return buildStructuredType(StructuredType.newBuilder(className), 
fields);

Review Comment:
   Do you think it would be a good idea to fail fast here with a simple 
validation like `className.isBlank() || className.isEmpty()`?
   
   Or is it a valid statement to have 
   ```sql
   STRUCTURED<``, f0 t0>
   STRUCTURED<` `, f0 t0>
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to