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

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


The following commit(s) were added to refs/heads/master by this push:
     new 05f8c893b8c Add PostgreSQLIndexSQLGeneratorTest (#37452)
05f8c893b8c is described below

commit 05f8c893b8c23e96f3096546e47ff92cf000113f
Author: Liang Zhang <[email protected]>
AuthorDate: Sun Dec 21 14:37:47 2025 +0800

    Add PostgreSQLIndexSQLGeneratorTest (#37452)
    
    * Add PostgreSQLIndexSQLGeneratorTest
    
    * Add PostgreSQLIndexSQLGeneratorTest
    
    * Add PostgreSQLIndexSQLGeneratorTest
---
 AGENTS.md                                          |   2 +-
 .../ddl/index/PostgreSQLIndexSQLGenerator.java     |   5 +-
 .../ddl/index/PostgreSQLIndexSQLGeneratorTest.java | 259 ++++++++++++++-------
 3 files changed, 181 insertions(+), 85 deletions(-)

diff --git a/AGENTS.md b/AGENTS.md
index 5102e2087e6..736f10fa496 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -34,7 +34,7 @@ This guide is written **for AI coding agents only**. Follow 
it literally; improv
 - **Continuous Verification**: rely on automated tests and integration 
validation.
 - **Public-Only Tests**: unit tests must exercise behavior via public APIs 
only; never use reflection to access private members.
 - **Coverage Pledge**: when 100% coverage is required, enumerate every 
branch/path and its planned test before coding, then implement once to reach 
100% without post-hoc fixes.
-- **Mock/Spy Specification**: Use mock by default; consider spy only when the 
scenario cannot be adequately represented using a mock.
+- **Mock/Spy Specification**: Use mock by default; consider spy only when the 
scenario cannot be adequately represented using a mock. Avoid spy entirely when 
standard `mock + when` can express behavior, and do not introduce inner classes 
for testing purposes—prefer plain test classes with mocks.
 - **Strictness and Stub Control**: Enable @MockitoSettings(strictness = 
Strictness.LENIENT) in the Mockito scenario or apply lenient() to specific 
stubs to ensure there are no unmatched or redundant stubs; clean up any unused 
stubs, imports, or local variables before committing.
 
 
diff --git 
a/kernel/data-pipeline/dialect/postgresql/src/main/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGenerator.java
 
b/kernel/data-pipeline/dialect/postgresql/src/main/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGenerator.java
index f6d36fff2ee..7031ba9b044 100644
--- 
a/kernel/data-pipeline/dialect/postgresql/src/main/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGenerator.java
+++ 
b/kernel/data-pipeline/dialect/postgresql/src/main/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGenerator.java
@@ -53,10 +53,9 @@ public final class PostgreSQLIndexSQLGenerator {
     public String generate(final Map<String, Object> context) throws 
SQLException {
         StringBuilder result = new StringBuilder();
         for (Map<String, Object> each : getIndexNodes(context)) {
-            if (each.containsKey("is_inherited") && (Boolean) 
each.get("is_inherited")) {
-                continue;
+            if (!each.containsKey("is_inherited") || !((Boolean) 
each.get("is_inherited"))) {
+                result.append(getIndexSQL(context, each));
             }
-            result.append(getIndexSQL(context, each));
         }
         return result.toString().trim();
     }
diff --git 
a/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGeneratorTest.java
 
b/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGeneratorTest.java
index 5afdc2efa93..2580e10143c 100644
--- 
a/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGeneratorTest.java
+++ 
b/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/index/PostgreSQLIndexSQLGeneratorTest.java
@@ -17,107 +17,204 @@
 
 package 
org.apache.shardingsphere.data.pipeline.postgresql.sqlbuilder.ddl.index;
 
+import 
org.apache.shardingsphere.data.pipeline.postgresql.sqlbuilder.ddl.PostgreSQLDDLTemplateExecutor;
+import 
org.apache.shardingsphere.data.pipeline.postgresql.sqlbuilder.template.PostgreSQLPipelineFreemarkerManager;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.internal.configuration.plugins.Plugins;
+import org.postgresql.jdbc.PgArray;
 
 import java.sql.Connection;
-import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.mockito.ArgumentMatchers.contains;
-import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.when;
 
 class PostgreSQLIndexSQLGeneratorTest {
     
     @Test
-    void assertGenerate() throws SQLException {
-        Connection connection = mock(Connection.class, RETURNS_DEEP_STUBS);
-        ResultSet getNodesResultSet = mockGetNodesResultSet();
-        when(connection.createStatement().executeQuery(
-                contains("SELECT DISTINCT ON(cls.relname) cls.oid, cls.relname 
as name," + "\n" + "(SELECT (CASE WHEN count(i.inhrelid) > 0 THEN true ELSE 
false END)")))
-                .thenReturn(getNodesResultSet);
-        ResultSet getPropertiesResultSet = mockGetPropertiesResultSet();
-        when(connection.createStatement().executeQuery(contains("SELECT 
DISTINCT ON(cls.relname) cls.oid, cls.relname as name, indrelid, indkey, 
indisclustered"))).thenReturn(getPropertiesResultSet);
-        Map<String, Object> context = new HashMap<>(5, 1F);
-        context.put("did", 1);
-        context.put("datlastsysoid", 10);
-        context.put("tid", 20);
-        context.put("schema", "foo_schema");
-        context.put("name", "foo_tbl");
-        String actual = new PostgreSQLIndexSQLGenerator(connection, 10, 
0).generate(context);
-        String expected = "CREATE INDEX IF NOT EXISTS foo_tbl" + "\n" + "ON 
foo_schema.foo_tbl USING foo_am_name"
-                + "\n" + "()" + "\n" + "WITH (FILLFACTOR=90)" + "\n" + 
"TABLESPACE default" + "\n" + "WHERE NULL;";
-        assertThat(actual, is(expected));
+    void assertSkipInheritedIndex() throws SQLException, 
ReflectiveOperationException {
+        PostgreSQLDDLTemplateExecutor templateExecutor = 
mock(PostgreSQLDDLTemplateExecutor.class);
+        when(templateExecutor.executeByTemplate(anyMap(), 
eq("component/indexes/%s/nodes.ftl"))).thenReturn(Collections.singletonList(createIndexNode(1L,
 true)));
+        PostgreSQLIndexSQLGenerator generator = new 
PostgreSQLIndexSQLGenerator(mock(Connection.class), 10, 0);
+        
Plugins.getMemberAccessor().set(PostgreSQLIndexSQLGenerator.class.getDeclaredField("templateExecutor"),
 generator, templateExecutor);
+        assertThat(generator.generate(createContext("skip_schema", 
"skip_tbl")), is(""));
     }
     
-    private ResultSet mockGetNodesResultSet() throws SQLException {
-        ResultSet result = mock(ResultSet.class, RETURNS_DEEP_STUBS);
-        when(result.getMetaData().getColumnCount()).thenReturn(3);
-        when(result.next()).thenReturn(true, false);
-        when(result.getMetaData().getColumnName(1)).thenReturn("oid");
-        when(result.getObject(1)).thenReturn(1L);
-        when(result.getMetaData().getColumnName(2)).thenReturn("name");
-        when(result.getObject(2)).thenReturn("foo_tbl");
-        when(result.getMetaData().getColumnName(3)).thenReturn("is_inherited");
-        when(result.getObject(3)).thenReturn(false);
+    @SuppressWarnings("unchecked")
+    @Test
+    void assertGenerateWithoutIncludeForNonBtree() throws SQLException, 
ReflectiveOperationException {
+        PostgreSQLDDLTemplateExecutor templateExecutor = 
mock(PostgreSQLDDLTemplateExecutor.class);
+        when(templateExecutor.getMajorVersion()).thenReturn(10);
+        when(templateExecutor.executeByTemplate(anyMap(), 
eq("component/indexes/%s/nodes.ftl"))).thenReturn(Collections.singletonList(createIndexNode(2L,
 false)));
+        when(templateExecutor.executeByTemplate(anyMap(), 
eq("component/indexes/%s/properties.ftl"))).thenReturn(Collections.singletonList(createNonBtreeIndexProps()));
+        
when(templateExecutor.executeByTemplate(eq(Collections.singletonMap("idx", 
2L)), eq("component/indexes/%s/column_details.ftl")))
+                
.thenReturn(Collections.singletonList(createColumnDetail("foo_col", 
"foo_collation", "foo_op_class", null)));
+        PostgreSQLIndexSQLGenerator generator = new 
PostgreSQLIndexSQLGenerator(mock(Connection.class), 10, 0);
+        
Plugins.getMemberAccessor().set(PostgreSQLIndexSQLGenerator.class.getDeclaredField("templateExecutor"),
 generator, templateExecutor);
+        AtomicReference<Map<String, Object>> capturedCreateData = new 
AtomicReference<>();
+        try (MockedStatic<PostgreSQLPipelineFreemarkerManager> mockedStatic = 
mockStatic(PostgreSQLPipelineFreemarkerManager.class)) {
+            mockedStatic.when(() -> 
PostgreSQLPipelineFreemarkerManager.getSQLByVersion(anyMap(), anyString(), 
anyInt(), anyInt()))
+                    .thenAnswer(invocation -> {
+                        Map<String, Object> dataModel = 
invocation.getArgument(0);
+                        String path = invocation.getArgument(1);
+                        if (path.contains("create")) {
+                            capturedCreateData.set(new 
LinkedHashMap<>(dataModel));
+                            return "create-non-btree";
+                        }
+                        return "alter-non-btree";
+                    });
+            String actual = generator.generate(createContext("foo_schema", 
"foo_tbl"));
+            assertThat(actual, is(String.join(System.lineSeparator(), 
"create-non-btree", "alter-non-btree")));
+        }
+        Map<String, Object> createData = capturedCreateData.get();
+        Collection<Map<String, Object>> columns = (Collection<Map<String, 
Object>>) createData.get("columns");
+        assertThat(columns.size(), is(1));
+        Map<String, Object> column = columns.iterator().next();
+        assertThat(column.get("colname"), is("foo_col"));
+        assertThat(column.get("collspcname"), is("foo_collation"));
+        assertThat(column.get("op_class"), is("foo_op_class"));
+        String columnsCsv = (String) createData.get("columns_csv");
+        assertThat(columnsCsv, is("foo_col COLLATE foo_collation 
foo_op_class"));
+    }
+    
+    private Map<String, Object> createIndexNode(final long oid, final boolean 
isInherited) {
+        Map<String, Object> result = new HashMap<>(2, 1F);
+        result.put("oid", oid);
+        result.put("is_inherited", isInherited);
+        return result;
+    }
+    
+    private Map<String, Object> createNonBtreeIndexProps() {
+        Map<String, Object> result = new HashMap<>(6, 1F);
+        result.put("name", "foo_idx");
+        result.put("amname", "hash");
+        result.put("indisclustered", true);
+        result.put("description", "non btree description");
+        result.put("spcname", "default_spc");
+        result.put("fillfactor", 80);
+        return result;
+    }
+    
+    @SuppressWarnings("unchecked")
+    @Test
+    void assertGenerateWithIncludeAndBtreeOptions() throws SQLException, 
ReflectiveOperationException {
+        PostgreSQLDDLTemplateExecutor templateExecutor = 
mock(PostgreSQLDDLTemplateExecutor.class);
+        when(templateExecutor.getMajorVersion()).thenReturn(11);
+        when(templateExecutor.executeByTemplate(anyMap(), 
eq("component/indexes/%s/nodes.ftl"))).thenReturn(Collections.singletonList(Collections.singletonMap("oid",
 3L)));
+        when(templateExecutor.executeByTemplate(anyMap(), 
eq("component/indexes/%s/properties.ftl"))).thenReturn(Collections.singletonList(createBtreeIndexProps()));
+        PgArray emptyOptions = mock(PgArray.class);
+        when(emptyOptions.getArray()).thenReturn(new String[0]);
+        PgArray descOptions = mock(PgArray.class);
+        when(descOptions.getArray()).thenReturn(new String[]{"DESC"});
+        PgArray nullsFirstOptions = mock(PgArray.class);
+        when(nullsFirstOptions.getArray()).thenReturn(new String[]{"ASC", 
"NULLS FIRST"});
+        PgArray nullsLastOptions = mock(PgArray.class);
+        when(nullsLastOptions.getArray()).thenReturn(new String[]{"ASC", 
"NULLS LAST"});
+        PgArray nullsWithoutSpaceOptions = mock(PgArray.class);
+        when(nullsWithoutSpaceOptions.getArray()).thenReturn(new 
String[]{"ASC", "NULLS"});
+        PgArray emptyArrayOptions = mock(PgArray.class);
+        when(emptyArrayOptions.getArray()).thenReturn(new String[0]);
+        Collection<Map<String, Object>> columnDetails = 
Arrays.asList(createColumnDetailWithNullOptions(emptyOptions),
+                createColumnDetail("desc_col", null, null, descOptions), 
createColumnDetail("nulls_first_col", null, null, nullsFirstOptions),
+                createColumnDetail("nulls_last_col", null, null, 
nullsLastOptions), createColumnDetail("nulls_no_space_col", null, null, 
nullsWithoutSpaceOptions),
+                createColumnDetail("empty_array_col", null, null, 
emptyArrayOptions));
+        
when(templateExecutor.executeByTemplate(Collections.singletonMap("idx", 3L), 
"component/indexes/%s/column_details.ftl")).thenReturn(columnDetails);
+        
when(templateExecutor.executeByTemplate(Collections.singletonMap("idx", 3L), 
"component/indexes/%s/include_details.ftl"))
+                .thenReturn(Arrays.asList(Collections.singletonMap("colname", 
"include_col_one"), Collections.singletonMap("colname", "include_col_two")));
+        PostgreSQLIndexSQLGenerator generator = new 
PostgreSQLIndexSQLGenerator(mock(Connection.class), 11, 0);
+        
Plugins.getMemberAccessor().set(PostgreSQLIndexSQLGenerator.class.getDeclaredField("templateExecutor"),
 generator, templateExecutor);
+        AtomicReference<Map<String, Object>> capturedCreateData = new 
AtomicReference<>();
+        try (MockedStatic<PostgreSQLPipelineFreemarkerManager> mockedStatic = 
mockStatic(PostgreSQLPipelineFreemarkerManager.class)) {
+            mockedStatic.when(() -> 
PostgreSQLPipelineFreemarkerManager.getSQLByVersion(anyMap(), anyString(), 
anyInt(), anyInt()))
+                    .thenAnswer(invocation -> {
+                        Map<String, Object> dataModel = 
invocation.getArgument(0);
+                        String path = invocation.getArgument(1);
+                        if (path.contains("create")) {
+                            capturedCreateData.set(new 
LinkedHashMap<>(dataModel));
+                            return "create-btree";
+                        }
+                        return "alter-btree";
+                    });
+            String actual = generator.generate(createContext("bar_schema", 
"bar_tbl"));
+            assertThat(actual, is(String.join(System.lineSeparator(), 
"create-btree", "alter-btree")));
+        }
+        Map<String, Object> createData = capturedCreateData.get();
+        assertThat(createData.get("include"), 
is(Arrays.asList("include_col_one", "include_col_two")));
+        Collection<Map<String, Object>> columns = (Collection<Map<String, 
Object>>) createData.get("columns");
+        assertThat(columns.size(), is(6));
+        Map<String, Object> columnWithNullOptions = columns.iterator().next();
+        assertThat(columnWithNullOptions.get("sort_order"), is(false));
+        assertThat(columnWithNullOptions.get("nulls"), is(false));
+        Map<String, Object> descColumn = columns.stream().filter(each -> 
"desc_col".equals(each.get("colname"))).findFirst().orElseGet(HashMap::new);
+        assertThat(descColumn.get("sort_order"), is(true));
+        assertThat(descColumn.get("nulls"), is(false));
+        Map<String, Object> nullsFirstColumn = columns.stream().filter(each -> 
"nulls_first_col".equals(each.get("colname"))).findFirst().orElseGet(HashMap::new);
+        assertThat(nullsFirstColumn.get("sort_order"), is(false));
+        assertThat(nullsFirstColumn.get("nulls"), is(true));
+        Map<String, Object> nullsLastColumn = columns.stream().filter(each -> 
"nulls_last_col".equals(each.get("colname"))).findFirst().orElseGet(HashMap::new);
+        assertThat(nullsLastColumn.get("sort_order"), is(false));
+        assertThat(nullsLastColumn.get("nulls"), is(false));
+        Map<String, Object> nullsNoSpaceColumn = columns.stream().filter(each 
-> 
"nulls_no_space_col".equals(each.get("colname"))).findFirst().orElseGet(HashMap::new);
+        assertThat(nullsNoSpaceColumn.get("sort_order"), is(false));
+        assertThat(nullsNoSpaceColumn.get("nulls"), is(false));
+        Map<String, Object> emptyArrayColumn = columns.stream().filter(each -> 
"empty_array_col".equals(each.get("colname"))).findFirst().orElseGet(HashMap::new);
+        assertThat(emptyArrayColumn.get("sort_order"), is(false));
+        assertThat(emptyArrayColumn.get("nulls"), is(false));
+        String columnsCsv = (String) createData.get("columns_csv");
+        assertThat(columnsCsv, is("col_with_null_options, desc_col DESC, 
nulls_first_col ASC NULLS FIRST, nulls_last_col ASC NULLS LAST, 
nulls_no_space_col ASC NULLS, empty_array_col"));
+    }
+    
+    private Map<String, Object> createBtreeIndexProps() {
+        Map<String, Object> result = new HashMap<>(4, 1F);
+        result.put("name", "bar_idx");
+        result.put("amname", "btree");
+        result.put("indisclustered", false);
+        result.put("description", "");
+        return result;
+    }
+    
+    private Map<String, Object> createColumnDetailWithNullOptions(final 
PgArray fallbackOptions) {
+        Map<String, Object> result = mock(Map.class);
+        when(result.get("attdef")).thenReturn("col_with_null_options");
+        when(result.get("collnspname")).thenReturn(null);
+        when(result.get("opcname")).thenReturn(null);
+        when(result.get("options")).thenReturn(null, null, fallbackOptions);
+        return result;
+    }
+    
+    private Map<String, Object> createContext(final String schema, final 
String table) {
+        Map<String, Object> result = new HashMap<>(5, 1F);
+        result.put("did", 1);
+        result.put("datlastsysoid", 10);
+        result.put("tid", 20);
+        result.put("schema", schema);
+        result.put("name", table);
         return result;
     }
     
-    private ResultSet mockGetPropertiesResultSet() throws SQLException {
-        ResultSet result = mock(ResultSet.class, RETURNS_DEEP_STUBS);
-        when(result.getMetaData().getColumnCount()).thenReturn(23);
-        when(result.next()).thenReturn(true, false);
-        when(result.getMetaData().getColumnName(1)).thenReturn("oid");
-        when(result.getObject(1)).thenReturn(1L);
-        when(result.getMetaData().getColumnName(2)).thenReturn("name");
-        when(result.getObject(2)).thenReturn("foo_tbl");
-        when(result.getMetaData().getColumnName(3)).thenReturn("indrelid");
-        when(result.getObject(3)).thenReturn(20);
-        when(result.getMetaData().getColumnName(4)).thenReturn("indkey");
-        when(result.getObject(4)).thenReturn("{1,2}");
-        
when(result.getMetaData().getColumnName(5)).thenReturn("indisclustered");
-        when(result.getObject(5)).thenReturn(false);
-        when(result.getMetaData().getColumnName(6)).thenReturn("indisvalid");
-        when(result.getObject(6)).thenReturn(true);
-        when(result.getMetaData().getColumnName(7)).thenReturn("indisunique");
-        when(result.getObject(7)).thenReturn(false);
-        when(result.getMetaData().getColumnName(8)).thenReturn("indisprimary");
-        when(result.getObject(8)).thenReturn(false);
-        when(result.getMetaData().getColumnName(9)).thenReturn("nspname");
-        when(result.getObject(9)).thenReturn("foo_schema");
-        when(result.getMetaData().getColumnName(10)).thenReturn("indnatts");
-        when(result.getObject(10)).thenReturn(3);
-        when(result.getMetaData().getColumnName(11)).thenReturn("spcoid");
-        when(result.getObject(11)).thenReturn("10101");
-        when(result.getMetaData().getColumnName(12)).thenReturn("spcname");
-        when(result.getObject(12)).thenReturn("default");
-        when(result.getMetaData().getColumnName(13)).thenReturn("tabname");
-        when(result.getObject(13)).thenReturn("foo_tbl");
-        when(result.getMetaData().getColumnName(14)).thenReturn("indclass");
-        when(result.getObject(14)).thenReturn("{pg_am_oid}");
-        when(result.getMetaData().getColumnName(15)).thenReturn("conoid");
-        when(result.getObject(15)).thenReturn(23456);
-        when(result.getMetaData().getColumnName(16)).thenReturn("description");
-        when(result.getObject(16)).thenReturn("");
-        
when(result.getMetaData().getColumnName(17)).thenReturn("indconstraint");
-        when(result.getObject(17)).thenReturn("NULL");
-        when(result.getMetaData().getColumnName(18)).thenReturn("contype");
-        when(result.getObject(18)).thenReturn("p");
-        
when(result.getMetaData().getColumnName(19)).thenReturn("condeferrable");
-        when(result.getObject(19)).thenReturn(true);
-        when(result.getMetaData().getColumnName(20)).thenReturn("condeferred");
-        when(result.getObject(20)).thenReturn(false);
-        when(result.getMetaData().getColumnName(21)).thenReturn("amname");
-        when(result.getObject(21)).thenReturn("foo_am_name");
-        when(result.getMetaData().getColumnName(22)).thenReturn("fillfactor");
-        when(result.getObject(22)).thenReturn(90);
-        when(result.getMetaData().getColumnName(23)).thenReturn("is_sys_idx");
-        when(result.getObject(23)).thenReturn(true);
+    private Map<String, Object> createColumnDetail(final String attdef, final 
String collation, final String opClass, final PgArray options) {
+        Map<String, Object> result = new HashMap<>(4, 1F);
+        result.put("attdef", attdef);
+        result.put("collnspname", collation);
+        result.put("opcname", opClass);
+        if (null != options) {
+            result.put("options", options);
+        }
         return result;
     }
 }

Reply via email to