This is an automated email from the ASF dual-hosted git repository. tuhaihe pushed a commit to branch REL_2_STABLE in repository https://gitbox.apache.org/repos/asf/cloudberry-pxf.git
commit 02384465d11a1f7a47548f0be953969520428947 Author: Nikolay Antonov <[email protected]> AuthorDate: Thu Apr 16 12:38:01 2026 +0500 Testcontainers: Add MS SQL JDBC tests (#106) ### Add Microsoft SQL JDBC tests Add new tests to cover Cloudberry -> PXF -> JDBC path. In this test we are verifying that main data Cloudberry and MS SQL types can be converted back and forth. Test covers MS SQL 2019 and 2022 versions. --- automation/pom.xml | 12 + .../jdbc/mssql/read_types/expected/query01.ans | 46 ++++ .../features/jdbc/mssql/read_types/sql/query01.sql | 37 +++ .../jdbc/mssql/write_types/expected/query01.ans | 77 +++++++ .../jdbc/mssql/write_types/sql/query01.sql | 67 ++++++ .../testcontainers/MssqlServerContainer.java | 76 ++++++ .../automation/features/jdbc/JdbcMssqlTest.java | 254 +++++++++++++++++++++ server/pxf-jdbc-drivers/build.gradle | 3 + 8 files changed, 572 insertions(+) diff --git a/automation/pom.xml b/automation/pom.xml index d4db4177..825633df 100644 --- a/automation/pom.xml +++ b/automation/pom.xml @@ -270,6 +270,18 @@ <version>2.0.4</version> </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers-mssqlserver</artifactId> + <version>2.0.4</version> + </dependency> + + <dependency> + <groupId>com.microsoft.sqlserver</groupId> + <artifactId>mssql-jdbc</artifactId> + <version>8.2.2.jre11</version> + </dependency> + <dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> diff --git a/automation/sqlrepo/features/jdbc/mssql/read_types/expected/query01.ans b/automation/sqlrepo/features/jdbc/mssql/read_types/expected/query01.ans new file mode 100644 index 00000000..b5cae2e7 --- /dev/null +++ b/automation/sqlrepo/features/jdbc/mssql/read_types/expected/query01.ans @@ -0,0 +1,46 @@ +-- start_ignore +-- end_ignore +-- 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. + +-- @description MSSQL JDBC read: expect one row of primitive types from PXF +SET timezone='utc'; +SET + +SET bytea_output='hex'; +SET + +SELECT + i_int, + s_small, + b_big, + f_float32, + d_float64, + b_bool, + dec, + t_text, + bin, + d_date, + d_ts, + d_tstz, + d_uuid +FROM pxf_mssql_read_types +LIMIT 1; + i_int | s_small | b_big | f_float32 | d_float64 | b_bool | dec | t_text | bin | d_date | d_ts | d_tstz | d_uuid +-------+---------+-------+-----------+-----------+--------+-------------------+--------+------------+------------+-------------------------+---------------------------+-------------------------------------- + 1 | 2 | 3 | 1.25 | 3.1415926 | t | 12345.6789012345 | hello | \x41424344 | 2020-01-02 | 2020-01-02 03:04:05.006 | 2020-01-02 03:04:05.006+00 | 550e8400-e29b-41d4-a716-446655440000 +(1 row) diff --git a/automation/sqlrepo/features/jdbc/mssql/read_types/sql/query01.sql b/automation/sqlrepo/features/jdbc/mssql/read_types/sql/query01.sql new file mode 100644 index 00000000..91aef308 --- /dev/null +++ b/automation/sqlrepo/features/jdbc/mssql/read_types/sql/query01.sql @@ -0,0 +1,37 @@ +-- 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. + +-- @description MSSQL JDBC read: expect one row of primitive types from PXF +SET timezone='utc'; +SET bytea_output='hex'; + +SELECT + i_int, + s_small, + b_big, + f_float32, + d_float64, + b_bool, + dec, + t_text, + bin, + d_date, + d_ts, + d_tstz, + d_uuid +FROM pxf_mssql_read_types + LIMIT 1; diff --git a/automation/sqlrepo/features/jdbc/mssql/write_types/expected/query01.ans b/automation/sqlrepo/features/jdbc/mssql/write_types/expected/query01.ans new file mode 100644 index 00000000..570d9475 --- /dev/null +++ b/automation/sqlrepo/features/jdbc/mssql/write_types/expected/query01.ans @@ -0,0 +1,77 @@ +-- start_ignore +-- end_ignore +-- 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. + +-- @description MSSQL JDBC write: insert full row then verify via readable external table/fdw +SET timezone='utc'; +SET + +SET bytea_output='hex'; +SET + +INSERT INTO pxf_mssql_write_types ( + i_int, + s_small, + b_big, + f_float32, + d_float64, + b_bool, + dec, + t_text, + bin, + d_date, + d_ts, + d_tstz, + d_uuid +) VALUES ( + 1, + 2, + 3, + 1.25, + 3.1415926, + true, + CAST('12345.6789012345' AS numeric), + 'hello', + decode('41424344', 'hex'), + DATE '2020-01-02', + TIMESTAMP '2020-01-02 03:04:05.006', + TIMESTAMPTZ '2020-01-02 03:04:05.006+00', + '550e8400-e29b-41d4-a716-446655440000'::uuid + ); +INSERT 0 1 + +SELECT + i_int, + s_small, + b_big, + f_float32, + d_float64, + b_bool, + dec, + t_text, + bin, + d_date, + d_ts, + d_tstz, + d_uuid +FROM pxf_mssql_write_verify +LIMIT 1; + i_int | s_small | b_big | f_float32 | d_float64 | b_bool | dec | t_text | bin | d_date | d_ts | d_tstz | d_uuid +-------+---------+-------+-----------+-----------+--------+-------------------+--------+------------+------------+-------------------------+---------------------------+-------------------------------------- + 1 | 2 | 3 | 1.25 | 3.1415926 | t | 12345.6789012345 | hello | \x41424344 | 2020-01-02 | 2020-01-02 03:04:05.006 | 2020-01-02 03:04:05.006+00 | 550e8400-e29b-41d4-a716-446655440000 +(1 row) diff --git a/automation/sqlrepo/features/jdbc/mssql/write_types/sql/query01.sql b/automation/sqlrepo/features/jdbc/mssql/write_types/sql/query01.sql new file mode 100644 index 00000000..1eef84cc --- /dev/null +++ b/automation/sqlrepo/features/jdbc/mssql/write_types/sql/query01.sql @@ -0,0 +1,67 @@ +-- 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. + +-- @description MSSQL JDBC write: insert full row then verify via readable external table/fdw +SET timezone='utc'; +SET bytea_output='hex'; + +INSERT INTO pxf_mssql_write_types ( + i_int, + s_small, + b_big, + f_float32, + d_float64, + b_bool, + dec, + t_text, + bin, + d_date, + d_ts, + d_tstz, + d_uuid +) VALUES ( + 1, + 2, + 3, + 1.25, + 3.1415926, + true, + CAST('12345.6789012345' AS numeric), + 'hello', + decode('41424344', 'hex'), + DATE '2020-01-02', + TIMESTAMP '2020-01-02 03:04:05.006', + TIMESTAMPTZ '2020-01-02 03:04:05.006+00', + '550e8400-e29b-41d4-a716-446655440000'::uuid + ); + +SELECT + i_int, + s_small, + b_big, + f_float32, + d_float64, + b_bool, + dec, + t_text, + bin, + d_date, + d_ts, + d_tstz, + d_uuid +FROM pxf_mssql_write_verify + LIMIT 1; diff --git a/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/MssqlServerContainer.java b/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/MssqlServerContainer.java new file mode 100644 index 00000000..c862fa7c --- /dev/null +++ b/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/MssqlServerContainer.java @@ -0,0 +1,76 @@ +package org.apache.cloudberry.pxf.automation.testcontainers; + +/* + * 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. + */ + +import org.testcontainers.containers.Network; +import org.testcontainers.utility.DockerImageName; + +/** + * Testcontainers wrapper around Microsoft SQL Server. + * + * The container joins a shared Docker network with a version-derived alias so that PXF + * inside the Cloudberry container can reach it at `mssql-<tag>:1433`. + */ +public class MssqlServerContainer + extends org.testcontainers.containers.MSSQLServerContainer<MssqlServerContainer> { + + private static final String DEFAULT_IMAGE = "mcr.microsoft.com/mssql/server"; + private static final String NETWORK_ALIAS_PREFIX = "mssql-"; + + public static final String MSSQL_USER = "SA"; + public static final String MSSQL_PASSWORD = "Pxf-Test1!"; + + private final String networkAlias; + + public MssqlServerContainer(String tag, Network network) { + super(DockerImageName.parse(DEFAULT_IMAGE + ":" + tag) + .asCompatibleSubstituteFor("mcr.microsoft.com/mssql/server")); + + this.networkAlias = NETWORK_ALIAS_PREFIX + tag.replaceAll("[-.]", ""); + + acceptLicense(); + withNetwork(network) + .withNetworkAliases(this.networkAlias) + .withPassword(MSSQL_PASSWORD); + } + + /** + * JDBC URL for PXF / other containers on the same Docker network. + * Uses `trustServerCertificate=true` because the container uses a self-signed certificate. + */ + public String getInternalJdbcUrl() { + return "jdbc:sqlserver://" + networkAlias + ":" + MS_SQL_SERVER_PORT + ";" + + "databaseName=master;" + + "encrypt=true;" + + "trustServerCertificate=true"; + } + + /** + * JDBC URL reachable from the host (mapped port), used to seed data from the test JVM. + * Uses `trustServerCertificate=true` because the container uses a self-signed certificate. + */ + @Override + public String getJdbcUrl() { + return "jdbc:sqlserver://localhost:" + getMappedPort(MS_SQL_SERVER_PORT) + ";" + + "databaseName=master;" + + "encrypt=true;" + + "trustServerCertificate=true"; + } +} diff --git a/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcMssqlTest.java b/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcMssqlTest.java new file mode 100644 index 00000000..ff19e3af --- /dev/null +++ b/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcMssqlTest.java @@ -0,0 +1,254 @@ +package org.apache.cloudberry.pxf.automation.features.jdbc; + +/* + * 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. + */ + +import org.apache.cloudberry.pxf.automation.AbstractTestcontainersTest; +import org.apache.cloudberry.pxf.automation.structures.tables.pxf.ExternalTable; +import org.apache.cloudberry.pxf.automation.structures.tables.utils.TableFactory; +import org.apache.cloudberry.pxf.automation.testcontainers.MssqlServerContainer; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Factory; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.sql.Connection; +import java.sql.Date; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Properties; +import java.util.TimeZone; +import java.util.UUID; + +public class JdbcMssqlTest extends AbstractTestcontainersTest { + + private static final String MSSQL_DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; + + private static final String MSSQL_TABLE_READ = "pxf_types_read"; + private static final String MSSQL_TABLE_WRITE = "pxf_types_write"; + + /** PXF external/foreign table column definitions — same for read and write tests. */ + private static final String[] MSSQL_PXF_FIELDS = new String[] { + "i_int int", + "s_small smallint", + "b_big bigint", + "f_float32 real", + "d_float64 double precision", + "b_bool boolean", + "dec numeric", + "t_text text", + "bin bytea", + "d_date date", + "d_ts timestamp", + "d_tstz timestamp with time zone", + "d_uuid uuid" + }; + + private static final int V_I_INT = 1; + private static final short V_S_SMALL = 2; + private static final long V_B_BIG = 3L; + private static final float V_F_FLOAT32 = 1.25f; + private static final double V_D_FLOAT64 = 3.1415926d; + private static final boolean V_B_BOOL = true; + private static final String V_DEC_TEXT = "12345.6789012345"; + private static final String V_T_TEXT = "hello"; + private static final String V_D_DATE = "2020-01-02"; + private static final String V_D_TS = "2020-01-02 03:04:05.006"; + private static final String V_D_UUID = "550e8400-e29b-41d4-a716-446655440000"; + + private final String dockerImageTag; + private MssqlServerContainer mssqlContainer; + + /** + * TestNG Factory: one test class instance per `mssqlVersions` row. + */ + @Factory(dataProvider = "mssqlVersions") + public static Object[] createInstances(String imageTag) { + return new Object[] { new JdbcMssqlTest(imageTag) }; + } + + /** Docker image tags for MS SQL. */ + @DataProvider(name = "mssqlVersions") + public static Object[][] mssqlVersions() { + return new Object[][] { + { "2019-latest" }, + { "2022-latest" }, + }; + } + + private JdbcMssqlTest(String dockerImageTag) { + this.dockerImageTag = dockerImageTag; + } + + @Override + public void beforeClass() throws Exception { + mssqlContainer = new MssqlServerContainer(dockerImageTag, container.getSharedNetwork()); + mssqlContainer.start(); + + Assert.assertTrue(container.isRunning(), "PXFCloudberry container should be running"); + Assert.assertTrue(mssqlContainer.isRunning(), "MSSQL container should be running"); + } + + @Override + public void afterClass() throws Exception { + if (mssqlContainer != null) { + mssqlContainer.stop(); + } + } + + @Test(groups = {"testcontainers", "pxf-jdbc"}) + public void readSupportedTypes() throws Exception { + runReadSupportedTypes(mssqlContainer.getInternalJdbcUrl(), mssqlContainer.getJdbcUrl()); + } + + @Test(groups = {"testcontainers", "pxf-jdbc"}) + public void writeSupportedTypes() throws Exception { + runWriteSupportedTypes(mssqlContainer.getInternalJdbcUrl(), mssqlContainer.getJdbcUrl()); + } + + private void runReadSupportedTypes(String internalJdbcUrl, String externalJdbcUrl) throws Exception { + createAndSeedMssqlReadTable(externalJdbcUrl); + + ExternalTable pxfRead = TableFactory.getPxfJdbcReadableTable( + "pxf_mssql_read_types", + MSSQL_PXF_FIELDS, + MSSQL_TABLE_READ, + MSSQL_DRIVER, + internalJdbcUrl, + MssqlServerContainer.MSSQL_USER, + "PASS=" + MssqlServerContainer.MSSQL_PASSWORD); + pxfRead.setHost(pxfHost); + pxfRead.setPort(pxfPort); + cloudberry.createTableAndVerify(pxfRead); + + try { + regress.runSqlTest("features/jdbc/mssql/read_types"); + } finally { + cloudberry.dropTable(pxfRead, true); + } + } + + private void runWriteSupportedTypes(String internalJdbcUrl, String externalJdbcUrl) throws Exception { + createMssqlWriteTable(externalJdbcUrl); + + ExternalTable pxfWrite = TableFactory.getPxfJdbcWritableTable( + "pxf_mssql_write_types", + MSSQL_PXF_FIELDS, + MSSQL_TABLE_WRITE, + MSSQL_DRIVER, + internalJdbcUrl, + MssqlServerContainer.MSSQL_USER, + "PASS=" + MssqlServerContainer.MSSQL_PASSWORD); + pxfWrite.setHost(pxfHost); + pxfWrite.setPort(pxfPort); + cloudberry.createTableAndVerify(pxfWrite); + + ExternalTable pxfVerify = TableFactory.getPxfJdbcReadableTable( + "pxf_mssql_write_verify", + MSSQL_PXF_FIELDS, + MSSQL_TABLE_WRITE, + MSSQL_DRIVER, + internalJdbcUrl, + MssqlServerContainer.MSSQL_USER, + "PASS=" + MssqlServerContainer.MSSQL_PASSWORD); + pxfVerify.setHost(pxfHost); + pxfVerify.setPort(pxfPort); + cloudberry.createTableAndVerify(pxfVerify); + + try { + regress.runSqlTest("features/jdbc/mssql/write_types"); + } finally { + cloudberry.dropTable(pxfVerify, true); + cloudberry.dropTable(pxfWrite, true); + } + } + + private void createAndSeedMssqlReadTable(String jdbcUrl) throws SQLException { + try (Connection conn = openMssqlConnection(jdbcUrl)) { + createMssqlServerTable(conn, MSSQL_TABLE_READ); + insertMssqlReadFixture(conn); + } + } + + private void createMssqlWriteTable(String jdbcUrl) throws SQLException { + try (Connection conn = openMssqlConnection(jdbcUrl)) { + createMssqlServerTable(conn, MSSQL_TABLE_WRITE); + } + } + + /** Creates MSSQL table (DROP IF EXISTS + CREATE). */ + private void createMssqlServerTable(Connection conn, String tableName) throws SQLException { + try (Statement st = conn.createStatement()) { + st.execute("IF OBJECT_ID('dbo." + tableName + "', 'U') IS NOT NULL " + + "DROP TABLE dbo." + tableName); + st.execute("CREATE TABLE dbo." + tableName + " (" + + "i_int INT, " + + "s_small SMALLINT, " + + "b_big BIGINT, " + + "f_float32 REAL, " + + "d_float64 FLOAT, " + + "b_bool BIT, " + + "dec NUMERIC(38,10), " + + "t_text NVARCHAR(1000), " + + "bin VARBINARY(MAX), " + + "d_date DATE, " + + "d_ts DATETIME2(3), " + + "d_tstz DATETIMEOFFSET(3), " + + "d_uuid UNIQUEIDENTIFIER" + + ")"); + } + } + + /** Inserts fixture row into `MSSQL_TABLE_READ` for the read test. */ + private void insertMssqlReadFixture(Connection conn) throws SQLException { + String insertSql = "INSERT INTO dbo." + MSSQL_TABLE_READ + " (" + + "i_int, s_small, b_big, f_float32, d_float64, b_bool, dec, t_text, bin, d_date, d_ts, d_tstz, d_uuid" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + try (PreparedStatement ps = conn.prepareStatement(insertSql)) { + Calendar utcCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + ps.setInt(1, V_I_INT); + ps.setShort(2, V_S_SMALL); + ps.setLong(3, V_B_BIG); + ps.setFloat(4, V_F_FLOAT32); + ps.setDouble(5, V_D_FLOAT64); + ps.setBoolean(6, V_B_BOOL); + ps.setBigDecimal(7, new BigDecimal(V_DEC_TEXT)); + ps.setString(8, V_T_TEXT); + ps.setBytes(9, new byte[]{0x41, 0x42, 0x43, 0x44}); + ps.setDate(10, Date.valueOf(V_D_DATE), utcCalendar); + ps.setTimestamp(11, Timestamp.valueOf(V_D_TS), utcCalendar); + ps.setTimestamp(12, Timestamp.valueOf(V_D_TS), utcCalendar); + ps.setObject(13, UUID.fromString(V_D_UUID)); + ps.executeUpdate(); + } + } + + private Connection openMssqlConnection(String jdbcUrl) throws SQLException { + Properties props = new Properties(); + props.setProperty("user", MssqlServerContainer.MSSQL_USER); + props.setProperty("password", MssqlServerContainer.MSSQL_PASSWORD); + return DriverManager.getConnection(jdbcUrl, props); + } +} diff --git a/server/pxf-jdbc-drivers/build.gradle b/server/pxf-jdbc-drivers/build.gradle index 19e2f721..69acb7ca 100644 --- a/server/pxf-jdbc-drivers/build.gradle +++ b/server/pxf-jdbc-drivers/build.gradle @@ -29,4 +29,7 @@ dependencies { implementation('com.oracle.database.jdbc:ucp11:23.3.0.23.09') implementation('com.oracle.database.nls:orai18n:23.3.0.23.09') + // MSSQL + implementation('com.microsoft.sqlserver:mssql-jdbc:8.2.2.jre11') + } \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
