You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by mp...@apache.org on 2025/05/14 09:32:33 UTC

(ignite-3) branch main updated: IGNITE-24391 Introduce the new QUORUM_SIZE parameter into CREATE ZONE and ALTER ZONE commands (#5795)

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

mpochatkin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 43cb97d50b2 IGNITE-24391 Introduce the new QUORUM_SIZE parameter into CREATE ZONE and ALTER ZONE commands (#5795)
43cb97d50b2 is described below

commit 43cb97d50b24da68a523c074ec266f58b527eed7
Author: Vadim Pakhnushev <86...@users.noreply.github.com>
AuthorDate: Wed May 14 12:32:28 2025 +0300

    IGNITE-24391 Introduce the new QUORUM_SIZE parameter into CREATE ZONE and ALTER ZONE commands (#5795)
---
 .../systemviews/ZonesSystemViewProvider.java       |   1 +
 .../commands/AbstractCommandValidationTest.java    |   6 +
 .../commands/CreateZoneCommandValidationTest.java  |  25 ++
 .../ignite/internal/table/ItReplicasTest.java      |  41 +--
 .../engine/systemviews/ItZonesSystemViewTest.java  |  61 ++++-
 modules/sql-engine/src/main/codegen/config.fmpp    |   2 +
 .../src/main/codegen/includes/parserImpls.ftl      |   6 +
 .../prepare/ddl/DdlSqlToCommandConverter.java      |   5 +
 .../sql/engine/prepare/ddl/ZoneOptionEnum.java     |   3 +
 .../DistributionZoneSqlToCommandConverterTest.java | 280 +++++++++++----------
 .../sql/DistributionZoneSqlDdlParserTest.java      |   6 +
 11 files changed, 260 insertions(+), 176 deletions(-)

diff --git a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/systemviews/ZonesSystemViewProvider.java b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/systemviews/ZonesSystemViewProvider.java
index 00899cdcd28..7c352d0e030 100644
--- a/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/systemviews/ZonesSystemViewProvider.java
+++ b/modules/catalog/src/main/java/org/apache/ignite/internal/catalog/systemviews/ZonesSystemViewProvider.java
@@ -67,6 +67,7 @@ public final class ZonesSystemViewProvider implements CatalogSystemViewProvider
                 .addColumn("REPLICAS", INT32, wrapper -> wrapper.zone.replicas())
                 .addColumn("CONSISTENCY_MODE", STRING, wrapper -> wrapper.zone.consistencyMode().name())
                 // End of legacy columns list. New columns must be added below this line.
+                .addColumn("ZONE_QUORUM_SIZE", INT32, wrapper -> wrapper.zone.quorumSize())
                 .dataProvider(SubscriptionUtils.fromIterable(() -> {
                             Catalog catalog = catalogSupplier.get();
                             CatalogZoneDescriptor defaultZone = catalog.defaultZone();
diff --git a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/AbstractCommandValidationTest.java b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/AbstractCommandValidationTest.java
index eb7dc9b46b2..dd159143e7b 100644
--- a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/AbstractCommandValidationTest.java
+++ b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/AbstractCommandValidationTest.java
@@ -95,6 +95,12 @@ abstract class AbstractCommandValidationTest extends BaseIgniteAbstractTest {
         return Stream.of(null, Set.of()).map(Arguments::of);
     }
 
+    /**
+     * Test cases for the create/alter zone parameter validation. The order is: number of replicas, default quorum size, default consensus
+     * group size, minimum quorum size, maximum quorum size.
+     *
+     * @return Stream of arguments for parameterized test.
+     */
     static Stream<Arguments> quorumTable() {
         return Stream.of(
                 Arguments.of(null, 1, 1, 1, 1), // default replicas count is 1
diff --git a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/CreateZoneCommandValidationTest.java b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/CreateZoneCommandValidationTest.java
index e434826cdef..545aa17604d 100644
--- a/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/CreateZoneCommandValidationTest.java
+++ b/modules/catalog/src/test/java/org/apache/ignite/internal/catalog/commands/CreateZoneCommandValidationTest.java
@@ -17,12 +17,14 @@
 
 package org.apache.ignite.internal.catalog.commands;
 
+import static java.lang.Math.round;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_FILTER;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_PARTITION_COUNT;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_REPLICA_COUNT;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.IMMEDIATE_TIMER_VALUE;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.INFINITE_TIMER_VALUE;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.MAX_PARTITION_COUNT;
+import static org.apache.ignite.internal.catalog.commands.CatalogUtils.defaultQuorumSize;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.is;
@@ -152,6 +154,29 @@ public class CreateZoneCommandValidationTest extends AbstractCommandValidationTe
         }
     }
 
+    @Test
+    void extremeQuorumSize() {
+        int replicas = Integer.MAX_VALUE; // REPLICAS = ALL
+        int maxQuorumSize = (int) (round(replicas / 2.0));
+        int defaultQuorumSize = defaultQuorumSize(replicas);
+        int defaultConsensusGroupSize = defaultQuorumSize * 2 - 1;
+
+        String errorMessageFragmentMaxQuorum = "Specified quorum size doesn't fit into the specified replicas count";
+
+        assertThrows(
+                CatalogValidationException.class,
+                () -> getZoneDescriptor(createZoneBuilder().replicas(replicas).quorumSize(maxQuorumSize + 1)),
+                errorMessageFragmentMaxQuorum
+        );
+
+        CatalogZoneDescriptor zoneDescriptor = getZoneDescriptor(createZoneBuilder().replicas(replicas));
+        assertThat(zoneDescriptor.quorumSize(), is(defaultQuorumSize));
+        assertThat(zoneDescriptor.consensusGroupSize(), is(defaultConsensusGroupSize));
+
+        zoneDescriptor = getZoneDescriptor(createZoneBuilder().replicas(replicas).quorumSize(maxQuorumSize - 1));
+        assertThat(zoneDescriptor.quorumSize(), is(maxQuorumSize - 1));
+    }
+
     @Test
     void zoneAutoAdjust() {
         assertThrows(
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItReplicasTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItReplicasTest.java
index b9d8ddfa7de..11ac2e86ebd 100644
--- a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItReplicasTest.java
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/table/ItReplicasTest.java
@@ -22,7 +22,6 @@ import static java.util.Objects.requireNonNull;
 import static java.util.Optional.ofNullable;
 import static org.apache.ignite.internal.TestWrappers.unwrapIgniteImpl;
 import static org.apache.ignite.internal.TestWrappers.unwrapTableManager;
-import static org.apache.ignite.internal.distributionzones.rebalance.RebalanceUtil.pendingPartAssignmentsQueueKey;
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static org.apache.ignite.internal.lang.IgniteSystemProperties.enabledColocation;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.bypassingThreadAssertions;
@@ -38,10 +37,7 @@ import org.apache.ignite.Ignite;
 import org.apache.ignite.internal.ClusterPerTestIntegrationTest;
 import org.apache.ignite.internal.app.IgniteImpl;
 import org.apache.ignite.internal.distributionzones.rebalance.RebalanceUtil;
-import org.apache.ignite.internal.lang.ByteArray;
 import org.apache.ignite.internal.partitiondistribution.Assignment;
-import org.apache.ignite.internal.partitiondistribution.Assignments;
-import org.apache.ignite.internal.partitiondistribution.AssignmentsQueue;
 import org.apache.ignite.internal.raft.PeersAndLearners;
 import org.apache.ignite.internal.raft.service.RaftGroupService;
 import org.apache.ignite.internal.replicator.ReplicaManager;
@@ -57,27 +53,26 @@ import org.junit.jupiter.api.Test;
 
 class ItReplicasTest extends ClusterPerTestIntegrationTest {
 
-    @Disabled("https://issues.apache.org/jira/browse/IGNITE-24391")
+    @Disabled("https://issues.apache.org/jira/browse/IGNITE-24072")
     @Test
     void testStartNewNodeAsLearner() {
-        cluster.doInSession(0, session -> {
-            session.execute(null, "CREATE ZONE TEST_ZONE WITH PARTITIONS=1, REPLICAS=ALL, STORAGE_PROFILES='default'");
-            session.execute(null, "CREATE TABLE TEST (id INT PRIMARY KEY, name INT) ZONE TEST_ZONE");
-            session.execute(null, "INSERT INTO TEST VALUES (0, 0)");
-        });
+        executeSql("CREATE ZONE TEST_ZONE (PARTITIONS 1, REPLICAS ALL, QUORUM SIZE 2) STORAGE PROFILES ['default']");
+        executeSql("CREATE TABLE TEST (id INT PRIMARY KEY, name INT) ZONE TEST_ZONE");
+        executeSql("INSERT INTO TEST VALUES (0, 0)");
         await().untilAsserted(() -> {
             assertTrue(cluster.runningNodes().map(resolvePartition("TEST")).allMatch(notNull()),
                     "all nodes should contain table partition replica");
         });
         Set<Assignment> assignmentsBefore = getStableAssignments(cluster.node(0), "TEST");
 
-        Ignite newNode = cluster.startNode((int) cluster.runningNodes().count());
+        cluster.startNode((int) cluster.runningNodes().count());
 
-        // TODO change learners using alter zone https://issues.apache.org/jira/browse/IGNITE-24391
-        Set<Assignment> newAssignment = Set.of(Assignment.forLearner(newNode.name()));
-        Set<Assignment> assignmentsAfterExpected = RebalanceUtil.union(assignmentsBefore, newAssignment);
+        // Node 1 is downgraded from peer to learner
+        String learnerName = node(1).name();
 
-        putPendingAssignments(cluster.aliveNode(), "TEST", assignmentsAfterExpected);
+        Set<Assignment> assignmentsAfterExpected = new HashSet<>(assignmentsBefore);
+        assignmentsAfterExpected.removeIf(assignment -> assignment.consistentId().equals(learnerName));
+        assignmentsAfterExpected.add(Assignment.forLearner(learnerName));
 
         await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> {
             assertTrue(cluster.runningNodes()
@@ -109,18 +104,6 @@ class ItReplicasTest extends ClusterPerTestIntegrationTest {
                 .orElse(Set.of());
     }
 
-    private static void putPendingAssignments(Ignite node, String tableName, Set<Assignment> assignments) {
-        IgniteImpl ignite = unwrapIgniteImpl(node);
-
-        TablePartitionId partitionId = new TablePartitionId(requireNonNull(getTableId(node, tableName)), 0);
-        ByteArray pendingKey = pendingPartAssignmentsQueueKey(partitionId);
-
-        long timestamp = ignite.catalogManager().catalog(ignite.catalogManager().latestCatalogVersion()).time();
-        byte[] pendingVal = AssignmentsQueue.toBytes(Assignments.of(assignments, timestamp));
-
-        ignite.metaStorageManager().put(pendingKey, pendingVal).join();
-    }
-
     private static int getTableOrZoneId(Ignite node, String tableName) {
         // TODO https://issues.apache.org/jira/browse/IGNITE-22522 tableOrZoneId -> zoneId, remove.
         IgniteImpl ignite = unwrapIgniteImpl(node);
@@ -167,8 +150,8 @@ class ItReplicasTest extends ClusterPerTestIntegrationTest {
     private static Predicate<RaftGroupService> equalsPeersAndLearners(Set<Assignment> assignments) {
         return client -> {
             PeersAndLearners expected = PeersAndLearners.fromAssignments(assignments);
-            return expected.peers().equals(new HashSet<>(requireNonNull(client.peers())))
-                    && expected.learners().equals(new HashSet<>(requireNonNull(client.learners())));
+            PeersAndLearners actual = PeersAndLearners.fromPeers(requireNonNull(client.peers()), requireNonNull(client.learners()));
+            return expected.equals(actual);
         };
     }
 
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/systemviews/ItZonesSystemViewTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/systemviews/ItZonesSystemViewTest.java
index 1beae275910..ba2512ed9e7 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/systemviews/ItZonesSystemViewTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/systemviews/ItZonesSystemViewTest.java
@@ -24,6 +24,7 @@ import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_F
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_PARTITION_COUNT;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_REPLICA_COUNT;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_VARLEN_LENGTH;
+import static org.apache.ignite.internal.catalog.commands.CatalogUtils.DEFAULT_ZONE_QUORUM_SIZE;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.INFINITE_TIMER_VALUE;
 import static org.apache.ignite.internal.catalog.commands.CatalogUtils.defaultZoneDefaultAutoAdjustScaleUpTimeoutSeconds;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.assertThrows;
@@ -54,11 +55,13 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
                 catalogManager.catalog(catalogManager.activeCatalogVersion(node.clock().nowLong()))
         );
 
-        assertQuery("SELECT ZONE_NAME, ZONE_PARTITIONS, ZONE_REPLICAS, DATA_NODES_AUTO_ADJUST_SCALE_UP, DATA_NODES_AUTO_ADJUST_SCALE_DOWN, "
-                + "DATA_NODES_FILTER, IS_DEFAULT_ZONE, ZONE_CONSISTENCY_MODE FROM SYSTEM.ZONES").returns(
+        assertQuery("SELECT ZONE_NAME, ZONE_PARTITIONS, ZONE_REPLICAS, ZONE_QUORUM_SIZE, DATA_NODES_AUTO_ADJUST_SCALE_UP,"
+                + " DATA_NODES_AUTO_ADJUST_SCALE_DOWN, DATA_NODES_FILTER, IS_DEFAULT_ZONE, ZONE_CONSISTENCY_MODE FROM SYSTEM.ZONES")
+                .returns(
                 catalog.defaultZone().name(),
                 DEFAULT_PARTITION_COUNT,
                 DEFAULT_REPLICA_COUNT,
+                DEFAULT_ZONE_QUORUM_SIZE,
                 defaultZoneDefaultAutoAdjustScaleUpTimeoutSeconds(),
                 INFINITE_TIMER_VALUE,
                 DEFAULT_FILTER,
@@ -69,11 +72,12 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
 
     @Test
     public void systemViewCustomZone() {
-        sql(createZoneSql(ZONE_NAME, 1, 2, 3, 4, DEFAULT_FILTER));
+        sql(createZoneSql(ZONE_NAME, 1, 5, 2, 3, 4, DEFAULT_FILTER));
 
         assertQuery(selectFromZonesSystemView(ZONE_NAME)).returns(
                 ZONE_NAME,
                 1,
+                5,
                 2,
                 3,
                 4,
@@ -87,11 +91,12 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
 
     @Test
     public void systemViewAlterCustomZone() {
-        sql(createZoneSql(ZONE_NAME, 1, 2, 3, 4, DEFAULT_FILTER));
+        sql(createZoneSql(ZONE_NAME, 1, 5, 2, 3, 4, DEFAULT_FILTER));
 
         assertQuery(selectFromZonesSystemView(ZONE_NAME)).returns(
                 ZONE_NAME,
                 1,
+                5,
                 2,
                 3,
                 4,
@@ -106,6 +111,21 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
                 ZONE_NAME,
                 1,
                 100,
+                2,
+                3,
+                4,
+                DEFAULT_FILTER,
+                false,
+                DEFAULT_CONSISTENCY_MODE.name()
+        ).check();
+
+        sql("ALTER ZONE " + ZONE_NAME + " SET (QUORUM SIZE 20)");
+
+        assertQuery(selectFromZonesSystemView(ZONE_NAME)).returns(
+                ZONE_NAME,
+                1,
+                100,
+                20,
                 3,
                 4,
                 DEFAULT_FILTER,
@@ -118,11 +138,12 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
 
     @Test
     public void systemViewRenameCustomZone() {
-        sql(createZoneSql(ZONE_NAME, 1, 2, 3, 4, DEFAULT_FILTER));
+        sql(createZoneSql(ZONE_NAME, 1, 5, 2, 3, 4, DEFAULT_FILTER));
 
         assertQuery(selectFromZonesSystemView(ZONE_NAME)).returns(
                 ZONE_NAME,
                 1,
+                5,
                 2,
                 3,
                 4,
@@ -136,6 +157,7 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
         assertQuery(selectFromZonesSystemView(ALTER_ZONE_NAME)).returns(
                 ALTER_ZONE_NAME,
                 1,
+                5,
                 2,
                 3,
                 4,
@@ -153,7 +175,7 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
     public void systemViewDropCustomZone() {
         assertQuery("SELECT COUNT(*) FROM SYSTEM.ZONES").returns(1L).check();
 
-        sql(createZoneSql(ZONE_NAME, 1, 2, 3, 4, DEFAULT_FILTER));
+        sql(createZoneSql(ZONE_NAME, 1, 5, 2, 3, 4, DEFAULT_FILTER));
 
         assertQuery("SELECT COUNT(*) FROM SYSTEM.ZONES").returns(2L).check();
 
@@ -170,6 +192,7 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
                 ZONE_NAME,
                 1,
                 2,
+                2,
                 3,
                 4,
                 DEFAULT_FILTER,
@@ -183,6 +206,7 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
                 ZONE_NAME,
                 1,
                 100,
+                2,
                 3,
                 4,
                 DEFAULT_FILTER,
@@ -195,11 +219,12 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
 
     @Test
     public void systemViewAlterConsistencyMode() {
-        sql(createZoneSql(ZONE_NAME, 1, 2, 3, 4, DEFAULT_FILTER));
+        sql(createZoneSql(ZONE_NAME, 1, 5, 2, 3, 4, DEFAULT_FILTER));
 
         assertQuery(selectFromZonesSystemView(ZONE_NAME)).returns(
                 ZONE_NAME,
                 1,
+                5,
                 2,
                 3,
                 4,
@@ -216,6 +241,7 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
         assertQuery(selectFromZonesSystemView(ZONE_NAME)).returns(
                 ZONE_NAME,
                 1,
+                5,
                 2,
                 3,
                 4,
@@ -247,7 +273,8 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
                         new MetadataMatcher().name("PARTITIONS").type(ColumnType.INT32).nullable(true),
                         new MetadataMatcher().name("REPLICAS").type(ColumnType.INT32).nullable(true),
                         new MetadataMatcher().name("CONSISTENCY_MODE").type(ColumnType.STRING).precision(DEFAULT_VARLEN_LENGTH)
-                                .nullable(true)
+                                .nullable(true),
+                        new MetadataMatcher().name("ZONE_QUORUM_SIZE").type(ColumnType.INT32).nullable(true)
                 )
                 .check();
     }
@@ -265,16 +292,25 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
                 .check();
     }
 
-    private static String createZoneSql(String zoneName, int partitions, int replicas, int scaleUp, int scaleDown, String filter) {
+    private static String createZoneSql(
+            String zoneName,
+            int partitions,
+            int replicas,
+            int quorumSize,
+            int scaleUp,
+            int scaleDown,
+            String filter
+    ) {
         String sqlFormat = "CREATE ZONE %s ("
                 + "PARTITIONS %d, "
                 + "REPLICAS %d, "
+                + "QUORUM SIZE %d, "
                 + "AUTO SCALE UP %d, "
                 + "AUTO SCALE DOWN %d,"
                 + "NODES FILTER '%s') "
                 + "STORAGE PROFILES ['%s']";
 
-        return String.format(sqlFormat, zoneName, partitions, replicas, scaleUp, scaleDown, filter, DEFAULT_STORAGE_PROFILE);
+        return String.format(sqlFormat, zoneName, partitions, replicas, quorumSize, scaleUp, scaleDown, filter, DEFAULT_STORAGE_PROFILE);
     }
 
     private static String createZoneSql(
@@ -309,8 +345,9 @@ public class ItZonesSystemViewTest extends AbstractSystemViewTest {
     }
 
     private static String selectFromZonesSystemView(String zoneName) {
-        String sqlFormat = "SELECT ZONE_NAME, PARTITIONS, REPLICAS, DATA_NODES_AUTO_ADJUST_SCALE_UP, DATA_NODES_AUTO_ADJUST_SCALE_DOWN, "
-                + "DATA_NODES_FILTER, IS_DEFAULT_ZONE, CONSISTENCY_MODE FROM SYSTEM.ZONES WHERE ZONE_NAME = '%s'";
+        String sqlFormat = "SELECT ZONE_NAME, ZONE_PARTITIONS, ZONE_REPLICAS, ZONE_QUORUM_SIZE, "
+                + "DATA_NODES_AUTO_ADJUST_SCALE_UP, DATA_NODES_AUTO_ADJUST_SCALE_DOWN, DATA_NODES_FILTER, IS_DEFAULT_ZONE, "
+                + "CONSISTENCY_MODE FROM SYSTEM.ZONES WHERE ZONE_NAME = '%s'";
 
         return String.format(sqlFormat, zoneName);
     }
diff --git a/modules/sql-engine/src/main/codegen/config.fmpp b/modules/sql-engine/src/main/codegen/config.fmpp
index 42ba9ccb02c..f48c99d46f1 100644
--- a/modules/sql-engine/src/main/codegen/config.fmpp
+++ b/modules/sql-engine/src/main/codegen/config.fmpp
@@ -72,6 +72,7 @@ data: {
       "DOWN"
       "PARTITIONS"
       "REPLICAS"
+      "QUORUM"
       "DISTRIBUTION"
       "ALGORITHM"
       "NODES"
@@ -103,6 +104,7 @@ data: {
       "DOWN"
       "PARTITIONS"
       "REPLICAS"
+      "QUORUM"
       "DISTRIBUTION"
       "ALGORITHM"
       "NODES"
diff --git a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
index 61cf553345a..b89bb20ecb1 100644
--- a/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
+++ b/modules/sql-engine/src/main/codegen/includes/parserImpls.ftl
@@ -658,6 +658,12 @@ void ZoneElement(List<SqlNode> zoneOptions) :
           }
       )
       |
+      <QUORUM> { pos = getPos(); } <SIZE> option = UnsignedIntegerLiteral()
+      {
+          key = new SqlIdentifier(ZoneOptionEnum.QUORUM_SIZE.name(), pos);
+          zoneOptions.add(new IgniteSqlZoneOption(key, option, s.end(this)));
+      }
+      |
       <DISTRIBUTION> { pos = getPos(); } <ALGORITHM> option = NonEmptyCharacterStringLiteral()
       {
           key = new SqlIdentifier(ZoneOptionEnum.DISTRIBUTION_ALGORITHM.name(), pos);
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java
index cce60463dce..eb8de7660c4 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DdlSqlToCommandConverter.java
@@ -29,6 +29,7 @@ import static org.apache.ignite.internal.sql.engine.prepare.ddl.ZoneOptionEnum.D
 import static org.apache.ignite.internal.sql.engine.prepare.ddl.ZoneOptionEnum.DATA_NODES_FILTER;
 import static org.apache.ignite.internal.sql.engine.prepare.ddl.ZoneOptionEnum.DISTRIBUTION_ALGORITHM;
 import static org.apache.ignite.internal.sql.engine.prepare.ddl.ZoneOptionEnum.PARTITIONS;
+import static org.apache.ignite.internal.sql.engine.prepare.ddl.ZoneOptionEnum.QUORUM_SIZE;
 import static org.apache.ignite.internal.sql.engine.prepare.ddl.ZoneOptionEnum.REPLICAS;
 import static org.apache.ignite.internal.sql.engine.util.IgniteMath.convertToByteExact;
 import static org.apache.ignite.internal.sql.engine.util.IgniteMath.convertToIntExact;
@@ -174,6 +175,8 @@ public class DdlSqlToCommandConverter {
         // CREATE ZONE options.
         zoneOptionInfos = new EnumMap<>(Map.of(
                 PARTITIONS, new DdlOptionInfo<>(Integer.class, this::checkPositiveNumber, CreateZoneCommandBuilder::partitions),
+                // We can't properly validate quorum size since it depends on the replicas number.
+                QUORUM_SIZE, new DdlOptionInfo<>(Integer.class, this::checkPositiveNumber, CreateZoneCommandBuilder::quorumSize),
                 // TODO https://issues.apache.org/jira/browse/IGNITE-22162 appropriate setter method should be used.
                 DISTRIBUTION_ALGORITHM, new DdlOptionInfo<>(String.class, null, (builder, params) -> {}),
                 DATA_NODES_FILTER, new DdlOptionInfo<>(String.class, null, CreateZoneCommandBuilder::filter),
@@ -192,6 +195,8 @@ public class DdlSqlToCommandConverter {
         // ALTER ZONE options.
         alterZoneOptionInfos = new EnumMap<>(Map.of(
                 PARTITIONS, new DdlOptionInfo<>(Integer.class, this::checkPositiveNumber, AlterZoneCommandBuilder::partitions),
+                // We can't properly validate quorum size since it depends on the replicas number.
+                QUORUM_SIZE, new DdlOptionInfo<>(Integer.class, this::checkPositiveNumber, AlterZoneCommandBuilder::quorumSize),
                 DATA_NODES_FILTER, new DdlOptionInfo<>(String.class, null, AlterZoneCommandBuilder::filter),
                 DATA_NODES_AUTO_ADJUST,
                 new DdlOptionInfo<>(Integer.class, this::checkPositiveNumber, AlterZoneCommandBuilder::dataNodesAutoAdjust),
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ZoneOptionEnum.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ZoneOptionEnum.java
index 9326ca4105b..d79de9391ae 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ZoneOptionEnum.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ddl/ZoneOptionEnum.java
@@ -28,6 +28,9 @@ public enum ZoneOptionEnum {
     /** Number of replicas. */
     REPLICAS("REPLICAS"),
 
+    /** Quorum size. */
+    QUORUM_SIZE("QUORUM SIZE"),
+
     /** Partition distribution algorithm name. */
     DISTRIBUTION_ALGORITHM("DISTRIBUTION ALGORITHM"),
 
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java
index fde1083b0f3..5fb83f9a700 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/ddl/DistributionZoneSqlToCommandConverterTest.java
@@ -34,6 +34,7 @@ import org.apache.calcite.sql.SqlDdl;
 import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.parser.SqlParseException;
 import org.apache.ignite.internal.catalog.CatalogCommand;
+import org.apache.ignite.internal.catalog.CatalogValidationException;
 import org.apache.ignite.internal.catalog.commands.AlterZoneCommand;
 import org.apache.ignite.internal.catalog.commands.AlterZoneSetDefaultCommand;
 import org.apache.ignite.internal.catalog.commands.DropZoneCommand;
@@ -65,9 +66,11 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
     private static final List<ZoneOptionEnum> NUMERIC_OPTIONS = List.of(
             ZoneOptionEnum.PARTITIONS,
             ZoneOptionEnum.REPLICAS,
+            ZoneOptionEnum.QUORUM_SIZE,
             ZoneOptionEnum.DATA_NODES_AUTO_ADJUST,
             ZoneOptionEnum.DATA_NODES_AUTO_ADJUST_SCALE_UP,
-            ZoneOptionEnum.DATA_NODES_AUTO_ADJUST_SCALE_DOWN);
+            ZoneOptionEnum.DATA_NODES_AUTO_ADJUST_SCALE_DOWN
+    );
 
     private static final List<ZoneOptionEnum> STRING_OPTIONS = List.of(
             ZoneOptionEnum.DISTRIBUTION_ALGORITHM,
@@ -83,14 +86,10 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
     @ParameterizedTest(name = "with syntax = {0}")
     @ValueSource(booleans = {true, false})
     public void testCreateZone(boolean withPresent) throws SqlParseException {
-        SqlNode node = parse(withPresent
+        CatalogCommand cmd = convert(withPresent
                 ? "CREATE ZONE test WITH STORAGE_PROFILES='" + DEFAULT_STORAGE_PROFILE + "'"
                 : "CREATE ZONE test STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "']");
 
-        assertThat(node, instanceOf(SqlDdl.class));
-
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
-
         CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
         assertThat(desc.name(), equalTo("TEST"));
@@ -107,11 +106,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
                 : "CREATE ZONE test (CONSISTENCY MODE '" + ConsistencyMode.STRONG_CONSISTENCY.name() + "') "
                         + "STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "'] ";
 
-        SqlNode node = parse(sql);
-
-        assertThat(node, instanceOf(SqlDdl.class));
-
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+        CatalogCommand cmd = convert(sql);
 
         CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
@@ -127,11 +122,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
                 : "CREATE ZONE test (CONSISTENCY MODE '" + ConsistencyMode.HIGH_AVAILABILITY + "') "
                         + "STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "'] ";
 
-        SqlNode node = parse(sql);
-
-        assertThat(node, instanceOf(SqlDdl.class));
-
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+        CatalogCommand cmd = convert(sql);
 
         CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
@@ -140,20 +131,16 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @ParameterizedTest(name = "with syntax = {0}")
     @ValueSource(booleans = {true, false})
-    public void testCreateZoneWithConsistencyModeInvalid(boolean withPresent) throws SqlParseException {
+    public void testCreateZoneWithConsistencyModeInvalid(boolean withPresent) {
         String sql = withPresent
                 ? "CREATE ZONE test WITH STORAGE_PROFILES='" + DEFAULT_STORAGE_PROFILE + "',"
                         + " CONSISTENCY_MODE='MY_CUSTOM_MODE'"
                 : "CREATE ZONE test (CONSISTENCY MODE 'MY_CUSTOM_MODE') "
                         + "STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "'] ";
 
-        SqlNode node = parse(sql);
-
-        assertThat(node, instanceOf(SqlDdl.class));
-
         assertThrows(
                 SqlException.class,
-                () -> converter.convert((SqlDdl) node, createContext()),
+                () -> convert(sql),
                 "Failed to parse consistency mode: MY_CUSTOM_MODE. Valid values are: [STRONG_CONSISTENCY, HIGH_AVAILABILITY]"
         );
     }
@@ -177,28 +164,27 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
             String sql = withPresent
                     ? "CREATE ZONE test with "
                             + "partitions=2, "
-                            + "replicas=3, "
+                            + "replicas=5, "
+                            + "quorum_size=2, " // non-default value
                             + "distribution_algorithm='rendezvous', "
                             + "data_nodes_filter='$[?(@.region == \"US\")]', "
                             + "data_nodes_auto_adjust=300, "
                             + "storage_profiles='lru_rocks , segmented_aipersist ' "
                     : "CREATE ZONE test "
                             + "(partitions 2, "
-                            + "replicas 3, "
+                            + "replicas 5, "
+                            + "quorum size 2, " // non-default value
                             + "distribution algorithm 'rendezvous', "
                             + "nodes filter '$[?(@.region == \"US\")]', "
                             + "auto adjust 300) "
                             + "storage profiles ['lru_rocks', 'segmented_aipersist '] ";
 
-            SqlNode node = parse(sql);
-
-            assertThat(node, instanceOf(SqlDdl.class));
-
-            CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+            CatalogCommand cmd = convert(sql);
             CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
             assertThat(desc.partitions(), equalTo(2));
-            assertThat(desc.replicas(), equalTo(3));
+            assertThat(desc.replicas(), equalTo(5));
+            assertThat(desc.quorumSize(), equalTo(2));
             // TODO https://issues.apache.org/jira/browse/IGNITE-22162
             // assertThat(desc.distributionAlgorithm(), equalTo("rendezvous"));
             assertThat(desc.filter(), equalTo("$[?(@.region == \"US\")]"));
@@ -222,11 +208,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
                             + "auto scale down 200) "
                             + "storage profiles ['lru_rocks']";
 
-            SqlNode node = parse(sql);
-
-            assertThat(node, instanceOf(SqlDdl.class));
-
-            CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+            CatalogCommand cmd = convert(sql);
             CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
             assertThat(desc.dataNodesAutoAdjustScaleUp(), equalTo(100));
@@ -262,20 +244,82 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
                 ? "CREATE ZONE test WITH STORAGE_PROFILES='" + DEFAULT_STORAGE_PROFILE + "', REPLICAS=ALL"
                 : "CREATE ZONE test (REPLICAS ALL) STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "']";
 
-        SqlNode node = parse(sql);
+        CatalogCommand cmd = convert(sql);
 
-        assertThat(node, instanceOf(SqlDdl.class));
+        CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+        assertThat(desc.replicas(), equalTo(DistributionAlgorithm.ALL_REPLICAS));
+    }
+
+    private static List<Arguments> defaultQuorum() {
+        return List.of(
+                Arguments.of(1, 1),
+                Arguments.of(2, 2),
+                Arguments.of(5, 3),
+                Arguments.of(10, 3)
+        );
+    }
+
+    @ParameterizedTest(name = "replicas = {0}, expectedQuorum = {1}")
+    @MethodSource("defaultQuorum")
+    public void testCreateZoneWithQuorumDefault(int replicas, int expectedQuorum) throws SqlParseException {
+        String sql = "CREATE ZONE test WITH REPLICAS=" + replicas + ", STORAGE_PROFILES='" + DEFAULT_STORAGE_PROFILE + "'";
+
+        CatalogCommand cmd = convert(sql);
 
         CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
 
-        assertThat(desc.replicas(), equalTo(DistributionAlgorithm.ALL_REPLICAS));
+        assertThat(desc.quorumSize(), equalTo(expectedQuorum));
+    }
+
+    private static List<Arguments> correctQuorumSize() {
+        return List.of(
+                Arguments.of(1, 1),
+                Arguments.of(2, 2),
+                Arguments.of(5, 2),
+                Arguments.of(10, 5)
+        );
+    }
+
+    @ParameterizedTest(name = "(replicas = {0}, quorum size = {1})")
+    @MethodSource("correctQuorumSize")
+    public void testCreateZoneWithQuorum(int replicas, int quorum) throws SqlParseException {
+        String sql = "CREATE ZONE test (REPLICAS " + replicas + ", QUORUM SIZE " + quorum
+                + ") STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "']";
+
+        CatalogCommand cmd = convert(sql);
+
+        CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, NewZoneEntry.class).descriptor();
+
+        assertThat(desc.quorumSize(), equalTo(quorum));
+    }
+
+    private static List<Arguments> invalidQuorumSize() {
+        return List.of(
+                Arguments.of(1, 2),
+                Arguments.of(2, 3),
+                Arguments.of(5, 4),
+                Arguments.of(10, 1)
+        );
+    }
+
+    @ParameterizedTest(name = "(replicas {0}, quorum size {1})")
+    @MethodSource("invalidQuorumSize")
+    public void testCreateZoneWithInvalidQuorum(int replicas, int quorum) {
+        String sql = "CREATE ZONE test (REPLICAS " + replicas + ", QUORUM SIZE " + quorum
+                + ") STORAGE PROFILES ['" + DEFAULT_STORAGE_PROFILE + "']";
+
+        // We can't properly validate quorum size in the parser, so the exception is thrown from the catalog command validation instead.
+        assertThrows(
+                CatalogValidationException.class,
+                () -> convert(sql),
+                "Specified quorum size doesn't fit into the specified replicas count"
+        );
     }
 
     @ParameterizedTest(name = "with syntax = {0}")
     @ValueSource(booleans = {true, false})
-    public void testCreateZoneWithoutStorageProfileOptionShouldThrowError(boolean withPresent) throws SqlParseException {
+    public void testCreateZoneWithoutStorageProfileOptionShouldThrowError(boolean withPresent) {
         assertThrowsWithPos("CREATE ZONE test", "<EOF>", 16);
 
         if (withPresent) {
@@ -287,23 +331,17 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @ParameterizedTest(name = "with syntax = {0}")
     @ValueSource(booleans = {true, false})
-    public void testCreateZoneWithDuplicateOptions(boolean withPresent) throws SqlParseException {
+    public void testCreateZoneWithDuplicateOptions(boolean withPresent) {
         String sql = withPresent
                 ? "CREATE ZONE test with partitions=2, replicas=0, PARTITIONS=1, STORAGE_PROFILES='profile'"
                 : "CREATE ZONE test (partitions 2, replicas 0, PARTITIONS 1) STORAGE PROFILES ['profile']";
 
-        SqlNode node = parse(sql);
-
-        assertThat(node, instanceOf(SqlDdl.class));
-
-        expectDuplicateOptionError((SqlDdl) node, "PARTITIONS");
+        expectDuplicateOptionError(sql, "PARTITIONS");
     }
 
     @Test
     public void testRenameZoneCommand() throws SqlParseException {
-        SqlNode node = parse("ALTER ZONE test RENAME TO test2");
-
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+        CatalogCommand cmd = convert("ALTER ZONE test RENAME TO test2");
 
         assertThat(cmd, instanceOf(RenameZoneCommand.class));
 
@@ -317,9 +355,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @Test
     public void testRenameZoneIfExistCommand() throws SqlParseException {
-        SqlNode node = parse("ALTER ZONE IF EXISTS test RENAME TO test2");
+        CatalogCommand cmd = convert("ALTER ZONE IF EXISTS test RENAME TO test2");
 
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
         assertThat(cmd, instanceOf(RenameZoneCommand.class));
 
         RenameZoneCommand zoneCmd = (RenameZoneCommand) cmd;
@@ -335,9 +372,11 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
     @ParameterizedTest(name = "obsolete = {0}")
     @ValueSource(booleans = {true, false})
     public void testAlterZoneCommand(boolean obsolete) throws SqlParseException {
-        SqlNode node = parse(obsolete ? "ALTER ZONE test SET replicas=3" : "ALTER ZONE test SET (replicas 3)");
+        CatalogCommand cmd = convert(obsolete
+                ? "ALTER ZONE test SET replicas=5, quorum_size=3"
+                : "ALTER ZONE test SET (replicas 5, quorum size 3)"
+        );
 
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
         assertThat(cmd, instanceOf(AlterZoneCommand.class));
 
         CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class);
@@ -350,16 +389,15 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
         CatalogZoneDescriptor desc = invokeAndGetFirstEntry(cmd, AlterZoneEntry.class).descriptor();
 
         assertThat(desc.name(), equalTo("TEST"));
-        assertThat(desc.replicas(), is(3));
+        assertThat(desc.replicas(), is(5));
+        assertThat(desc.quorumSize(), is(3));
         assertThat(((AlterZoneCommand) cmd).ifExists(), is(false));
     }
 
     @ParameterizedTest(name = "obsolete = {0}")
     @ValueSource(booleans = {true, false})
     public void testAlterZoneIfExistsCommand(boolean obsolete) throws SqlParseException {
-        SqlNode node = parse(obsolete ? "ALTER ZONE IF EXISTS test SET replicas=3" : "ALTER ZONE IF EXISTS test SET (replicas 3)");
-
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+        CatalogCommand cmd = convert(obsolete ? "ALTER ZONE IF EXISTS test SET replicas=3" : "ALTER ZONE IF EXISTS test SET (replicas 3)");
 
         assertThat(cmd, instanceOf(AlterZoneCommand.class));
         assertThat(((AlterZoneCommand) cmd).ifExists(), is(true));
@@ -372,17 +410,18 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
         {
             String sql = obsolete
                     ? "ALTER ZONE test SET "
-                            + "replicas=3, "
+                            + "replicas=5, "
+                            + "quorum_size=3, "
                             + "data_nodes_filter='$[?(@.region == \"US\")]', "
                             + "data_nodes_auto_adjust=300"
                     : "ALTER ZONE test SET "
-                            + "(replicas 3, "
+                            + "(replicas 5, "
+                            + "quorum size 3, "
                             + "nodes filter '$[?(@.region == \"US\")]', "
                             + "auto adjust 300)";
 
-            SqlNode node = parse(sql);
+            CatalogCommand cmd = convert(sql);
 
-            CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
             assertThat(cmd, instanceOf(AlterZoneCommand.class));
 
             CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class);
@@ -395,7 +434,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
             assertThat(desc.name(), equalTo("TEST"));
 
-            assertThat(desc.replicas(), equalTo(3));
+            assertThat(desc.replicas(), equalTo(5));
+            assertThat(desc.quorumSize(), equalTo(3));
             assertThat(desc.filter(), equalTo("$[?(@.region == \"US\")]"));
             assertThat(desc.dataNodesAutoAdjust(), equalTo(300));
         }
@@ -410,9 +450,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
                             + "(auto scale up 100, "
                             + "auto scale down 200)";
 
-            SqlNode node = parse(sql);
+            CatalogCommand cmd = convert(sql);
 
-            CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
             assertThat(cmd, instanceOf(AlterZoneCommand.class));
 
             CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class);
@@ -433,9 +472,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
     @ParameterizedTest(name = "obsolete = {0}")
     @ValueSource(booleans = {true, false})
     public void testAlterZoneReplicasAll(boolean obsolete) throws SqlParseException {
-        SqlNode node = parse(obsolete ? "ALTER ZONE test SET replicas=ALL" : "ALTER ZONE test SET (replicas ALL)");
+        CatalogCommand cmd = convert(obsolete ? "ALTER ZONE test SET replicas=ALL" : "ALTER ZONE test SET (replicas ALL)");
 
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
         assertThat(cmd, instanceOf(AlterZoneCommand.class));
 
         CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class);
@@ -451,9 +489,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @Test
     public void testAlterZoneSetDefault() throws SqlParseException {
-        SqlNode node = parse("ALTER ZONE test SET DEFAULT");
+        CatalogCommand cmd = convert("ALTER ZONE test SET DEFAULT");
 
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
         assertThat(cmd, instanceOf(AlterZoneSetDefaultCommand.class));
 
         CatalogZoneDescriptor zoneMock = mock(CatalogZoneDescriptor.class);
@@ -468,9 +505,8 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @Test
     public void testAlterZoneSetDefaultIfExists() throws SqlParseException {
-        SqlNode node = parse("ALTER ZONE IF EXISTS test SET DEFAULT");
+        CatalogCommand cmd = convert("ALTER ZONE IF EXISTS test SET DEFAULT");
 
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
         assertThat(cmd, instanceOf(AlterZoneSetDefaultCommand.class));
 
         assertThat(((AlterZoneSetDefaultCommand) cmd).ifExists(), is(true));
@@ -478,7 +514,7 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @ParameterizedTest(name = "obsolete = {0}, option = {1}")
     @MethodSource("numericOptions")
-    public void testAlterZoneCommandWithInvalidOptions(boolean obsolete, ZoneOptionEnum option) throws SqlParseException {
+    public void testAlterZoneCommandWithInvalidOptions(boolean obsolete, ZoneOptionEnum option) {
         String sql = obsolete
                 ? "ALTER ZONE test SET replicas=2, data_nodes_auto_adjust=-100"
                 : "ALTER ZONE test SET ({} -100)";
@@ -499,20 +535,13 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
         String sql = obsolete
                 ? "ALTER ZONE test SET replicas=2, data_nodes_auto_adjust=300, DATA_NODES_AUTO_ADJUST=400"
                 : "ALTER ZONE test SET (replicas 2, auto adjust 300, AUTO ADJUST 400)";
-        SqlNode node = parse(sql);
-
-        assertThat(node, instanceOf(SqlDdl.class));
 
-        expectDuplicateOptionError((SqlDdl) node, ZoneOptionEnum.DATA_NODES_AUTO_ADJUST.sqlName);
+        expectDuplicateOptionError(sql, ZoneOptionEnum.DATA_NODES_AUTO_ADJUST.sqlName);
     }
 
     @Test
     public void testDropZone() throws SqlParseException {
-        SqlNode node = parse("DROP ZONE test");
-
-        assertThat(node, instanceOf(SqlDdl.class));
-
-        CatalogCommand cmd = converter.convert((SqlDdl) node, createContext());
+        CatalogCommand cmd = convert("DROP ZONE test");
 
         assertThat(cmd, instanceOf(DropZoneCommand.class));
 
@@ -526,31 +555,28 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @ParameterizedTest(name = "with syntax = {0}, option = {1}")
     @MethodSource("numericOptions")
-    public void createZoneWithInvalidNumericOptionValue(boolean withPresent, ZoneOptionEnum option) throws Exception {
+    public void createZoneWithInvalidNumericOptionValue(boolean withPresent, ZoneOptionEnum option) {
         String sql = withPresent ? "create zone test_zone with {}={}, storage_profiles='p'" : "create zone test_zone ({} {})";
 
         if (withPresent) {
-            SqlDdl node = (SqlDdl) parse(format(sql, option, "'bar'"));
-            expectInvalidOptionType(node, option.name());
+            expectInvalidOptionType(format(sql, option, "'bar'"), option.name());
         } else {
             String sqlName = option.sqlName;
             String prefix = "create zone test_zone (";
-            assertThrowsWithPos(format(sql, sqlName, "'bar'"), "\\'bar\\'", prefix.length() + sqlName.length() + 1 /* start pos*/
-                    + 1 /* first symbol after bracket*/);
+            int errorPos = prefix.length() + sqlName.length() + 1 /* start pos*/ + 1 /* first symbol after bracket*/;
 
-            assertThrowsWithPos(format(sql, sqlName, "-1"), "-", prefix.length() + sqlName.length() + 1 /* start pos*/
-                    + 1 /* first symbol after bracket*/);
+            assertThrowsWithPos(format(sql, sqlName, "'bar'"), "\\'bar\\'", errorPos);
+            assertThrowsWithPos(format(sql, sqlName, "-1"), "-", errorPos);
         }
     }
 
     @ParameterizedTest(name = "with syntax = {0}")
     @ValueSource(booleans = {true, false})
-    public void createZoneWithUnexpectedOption(boolean withPresent) throws SqlParseException {
+    public void createZoneWithUnexpectedOption(boolean withPresent) {
         String sql = withPresent ? "create zone test_zone with ABC=1, storage_profiles='p'" : "create zone test_zone (ABC 1)";
 
         if (withPresent) {
-            SqlDdl node = (SqlDdl) parse(sql);
-            expectUnexpectedOption(node, "ABC");
+            expectUnexpectedOption(sql, "ABC");
         } else {
             assertThrowsWithPos(sql, "ABC", 24);
         }
@@ -558,10 +584,10 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @ParameterizedTest(name = "with syntax = {0}, option = {1}")
     @MethodSource("stringOptions")
-    public void createZoneWithInvalidStringOptionValue(boolean withPresent, ZoneOptionEnum option) throws Exception {
+    public void createZoneWithInvalidStringOptionValue(boolean withPresent, ZoneOptionEnum option) {
         if (withPresent) {
-            SqlDdl node = (SqlDdl) parse(format("create zone test_zone with {}={}, storage_profiles='p'", option.name(), "1"));
-            expectInvalidOptionType(node, option.name());
+            String sql = format("create zone test_zone with {}={}, storage_profiles='p'", option.name(), "1");
+            expectInvalidOptionType(sql, option.name());
         } else {
             String sql = "create zone test_zone ({} {})";
 
@@ -585,20 +611,22 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
 
     @ParameterizedTest
     @MethodSource("pureNumericOptions")
-    public void alterZoneWithInvalidNumericOptionValue(ZoneOptionEnum optionParam) throws Exception {
-        SqlDdl node = (SqlDdl) parse(format("alter zone test_zone set {}={}", optionParam.name(), "'bar'"));
-        expectInvalidOptionType(node, optionParam.name());
+    public void alterZoneWithInvalidNumericOptionValue(ZoneOptionEnum optionParam) {
+        String sql = format("alter zone test_zone set {}={}", optionParam.name(), "'bar'");
+        expectInvalidOptionType(sql, optionParam.name());
     }
 
     @Test
-    public void alterZoneWithUnexpectedOption() throws SqlParseException {
-        SqlDdl node = (SqlDdl) parse("alter zone test_zone set ABC=1");
-        expectUnexpectedOption(node, "ABC");
+    public void alterZoneWithUnexpectedOption() {
+        expectUnexpectedOption("alter zone test_zone set ABC=1", "ABC");
     }
 
-    private void convert(String query) throws SqlParseException {
-        SqlDdl node = (SqlDdl) parse(query);
-        converter.convert(node, createContext());
+    private CatalogCommand convert(String query) throws SqlParseException {
+        SqlNode node = parse(query);
+
+        assertThat(node, instanceOf(SqlDdl.class));
+
+        return converter.convert((SqlDdl) node, createContext());
     }
 
     private void assertThrowsWithPos(String query, String encountered, int pos) {
@@ -625,50 +653,32 @@ public class DistributionZoneSqlToCommandConverterTest extends AbstractDdlSqlToC
         );
     }
 
-    private void expectOptionValidationError(String sql, String invalidOption) throws SqlParseException {
-        SqlDdl node = (SqlDdl) parse(sql);
-        assertThrowsWithCode(
-                SqlException.class,
-                Sql.STMT_VALIDATION_ERR,
-                () -> converter.convert(node, createContext()),
-                "Zone option validation failed [option=" + invalidOption
-        );
+    private void expectOptionValidationError(String sql, String invalidOption) {
+        expectStatementValidationError(sql, "Zone option validation failed [option=" + invalidOption);
     }
 
-    private void emptyProfilesValidationError(String sql) throws SqlParseException {
-        SqlDdl node = (SqlDdl) parse(sql);
-        assertThrowsWithCode(
-                SqlException.class,
-                Sql.STMT_VALIDATION_ERR,
-                () -> converter.convert(node, createContext()),
-                "STORAGE PROFILES can not be empty"
-        );
+    private void emptyProfilesValidationError(String sql) {
+        expectStatementValidationError(sql, "STORAGE PROFILES can not be empty");
     }
 
-    private void expectInvalidOptionType(SqlDdl node, String invalidOption) {
-        assertThrowsWithCode(
-                SqlException.class,
-                Sql.STMT_VALIDATION_ERR,
-                () -> converter.convert(node, createContext()),
-                "Invalid zone option type [option=" + invalidOption
-        );
+    private void expectInvalidOptionType(String sql, String invalidOption) {
+        expectStatementValidationError(sql, "Invalid zone option type [option=" + invalidOption);
     }
 
-    private void expectUnexpectedOption(SqlDdl node, String invalidOption) {
-        assertThrowsWithCode(
-                SqlException.class,
-                Sql.STMT_VALIDATION_ERR,
-                () -> converter.convert(node, createContext()),
-                "Unexpected zone option [option=" + invalidOption
-        );
+    private void expectUnexpectedOption(String sql, String invalidOption) {
+        expectStatementValidationError(sql, "Unexpected zone option [option=" + invalidOption);
+    }
+
+    private void expectDuplicateOptionError(String sql, String option) {
+        expectStatementValidationError(sql, "Duplicate zone option has been specified [option=" + option);
     }
 
-    private void expectDuplicateOptionError(SqlDdl node, String option) {
+    private void expectStatementValidationError(String sql, String errorMessageFragment) {
         assertThrowsWithCode(
                 SqlException.class,
                 Sql.STMT_VALIDATION_ERR,
-                () -> converter.convert(node, createContext()),
-                "Duplicate zone option has been specified [option=" + option
+                () -> convert(sql),
+                errorMessageFragment
         );
     }
 }
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/DistributionZoneSqlDdlParserTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/DistributionZoneSqlDdlParserTest.java
index 630f9d0903e..f18b08e8823 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/DistributionZoneSqlDdlParserTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/sql/DistributionZoneSqlDdlParserTest.java
@@ -92,6 +92,7 @@ public class DistributionZoneSqlDdlParserTest extends AbstractParserTest {
         IgniteSqlCreateZone createZone = parseCreateZone(
                 "create zone test_zone "
                         + "(replicas 2, "
+                        + "quorum size 2, "
                         + "partitions 3, "
                         + "nodes filter '(\"US\" || \"EU\") && \"SSD\"', "
                         + "distribution algorithm 'test_Distribution', "
@@ -107,6 +108,7 @@ public class DistributionZoneSqlDdlParserTest extends AbstractParserTest {
         List<SqlNode> optList = createZone.createOptionList().getList();
 
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.REPLICAS, 2);
+        assertThatZoneOptionPresent(optList, ZoneOptionEnum.QUORUM_SIZE, 2);
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.PARTITIONS, 3);
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.DISTRIBUTION_ALGORITHM, "test_Distribution");
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.DATA_NODES_FILTER, "(\"US\" || \"EU\") && \"SSD\"");
@@ -117,6 +119,7 @@ public class DistributionZoneSqlDdlParserTest extends AbstractParserTest {
 
         expectUnparsed(createZone, "CREATE ZONE \"TEST_ZONE\" ("
                 + "REPLICAS 2, "
+                + "QUORUM SIZE 2, "
                 + "PARTITIONS 3, "
                 + "NODES FILTER '(\"US\" || \"EU\") && \"SSD\"', "
                 + "DISTRIBUTION ALGORITHM 'test_Distribution', "
@@ -332,6 +335,7 @@ public class DistributionZoneSqlDdlParserTest extends AbstractParserTest {
         IgniteSqlAlterZoneSet alterZoneSet = parseAlterZone(
                 "alter zone a.test_zone set ("
                         + "REPLICAS 2, "
+                        + "QUORUM SIZE 2, "
                         + "NODES FILTER '(\"US\" || \"EU\") && \"SSD\"', "
                         + "AUTO ADJUST 1, "
                         + "AUTO SCALE UP 2, "
@@ -345,6 +349,7 @@ public class DistributionZoneSqlDdlParserTest extends AbstractParserTest {
         List<SqlNode> optList = alterZoneSet.alterOptionsList().getList();
 
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.REPLICAS, 2);
+        assertThatZoneOptionPresent(optList, ZoneOptionEnum.QUORUM_SIZE, 2);
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.DATA_NODES_FILTER, "(\"US\" || \"EU\") && \"SSD\"");
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.DATA_NODES_AUTO_ADJUST, 1);
         assertThatZoneOptionPresent(optList, ZoneOptionEnum.DATA_NODES_AUTO_ADJUST_SCALE_UP, 2);
@@ -352,6 +357,7 @@ public class DistributionZoneSqlDdlParserTest extends AbstractParserTest {
 
         String expectedStmt = "ALTER ZONE \"A\".\"TEST_ZONE\" SET "
                 + "(REPLICAS 2, "
+                + "QUORUM SIZE 2, "
                 + "NODES FILTER '(\"US\" || \"EU\") && \"SSD\"', "
                 + "AUTO ADJUST 1, "
                 + "AUTO SCALE UP 2, "