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;
}
}