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 3596b0b2c5d Add PostgreSQLColumnPropertiesAppenderTest (#37126)
3596b0b2c5d is described below
commit 3596b0b2c5d27ef20d30b4d806599b057bf3fb28
Author: Liang Zhang <[email protected]>
AuthorDate: Mon Nov 17 19:16:36 2025 +0800
Add PostgreSQLColumnPropertiesAppenderTest (#37126)
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
* Add PostgreSQLColumnPropertiesAppenderTest
---
CLAUDE.md | 357 ++++-----
.../PostgreSQLColumnPropertiesAppenderTest.java | 866 +++++++++++++++++++++
2 files changed, 1045 insertions(+), 178 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index a06ce00cccc..d9f638d6e4a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2,6 +2,62 @@
*Professional Guide for AI Programming Assistants - Best Practices for
ShardingSphere Code Development*
+## 🏗️ ShardingSphere Architecture Overview
+
+### Project Overview
+ShardingSphere is an ecosystem of distributed database solutions with JDBC
driver, database proxy, and planned Sidecar modes.
+
+### Core Module Architecture
+```yaml
+module_hierarchy:
+ infrastructure_layer:
+ - shardingsphere-infra: Common utilities, SPI definitions
+ - shardingsphere-parser: SQL parsing (ANTLR4-based)
+ engine_layer:
+ - shardingsphere-mode: Configuration management
+ - shardingsphere-kernel: Core execution engine
+ access_layer:
+ - shardingsphere-jdbc: Java JDBC driver
+ - shardingsphere-proxy: Database proxy
+ feature_layer:
+ - shardingsphere-sharding: Data sharding
+ - shardingsphere-encryption: Data encryption
+ - shardingsphere-readwrite-splitting: Read/write splitting
+```
+
+### Technology Stack Decisions
+- **ANTLR4**: SQL parsing and abstract syntax tree generation
+- **Netty**: High-performance network communication (proxy mode)
+- **Apache Calcite**: Query optimization and execution plans
+- **SPI**: Plugin architecture for hot-pluggable extensions
+
+### JDBC vs Proxy Patterns
+- **JDBC**: Zero invasion, Java-only, highest performance
+- **Proxy**: Language-agnostic, centralized management, advanced features
+
+### Key Concepts
+- **Sharding**: Horizontal data partitioning
+- **DistSQL**: Distributed SQL for dynamic configuration
+- **SPI Extension**: Algorithm, protocol, and execution extensions
+- **Data Pipeline**: Migration and synchronization functionality
+
+### Code Quality Standards
+```yaml
+self_documenting_code:
+ method_naming: "10-15 characters, verb-noun patterns, no comments needed"
+ examples: ["isValidEmailAddress()", "calculateOrderTotal()"]
+ anti_examples: ["proc()", "getData()", "handle()"]
+
+complex_logic:
+ definition: "3+ nested levels or 20+ lines per method"
+ handling: "Extract to meaningful private methods"
+
+mock_boundaries:
+ no_mock: "Simple objects, DTOs, stateless utilities"
+ must_mock: "Database connections, network services, third-party interfaces"
+ judgment: "Mock only with external dependencies or high construction cost"
+```
+
## 🚀 AI Programming Best Practices
### How to Obtain High-Quality Code
@@ -132,26 +188,35 @@ decision_logic:
### Build Commands
```bash
-./mvnw install -T1C # Full build
+./mvnw install -T1C # Full build with parallel
execution
./mvnw install -T1C -DskipTests # Build without tests
+./mvnw clean compile # Compile only
```
-### Validation Commands
+### Code Quality Commands
```bash
./mvnw spotless:apply -Pcheck # Format code
+./mvnw checkstyle:check # Code style checking
+./mvnw pmd:check # Static code analysis
+./mvnw spotbugs:check # Bug detection
+./mvnw dependency-check # Security vulnerability scan
+./mvnw archunit:test # Architecture rule validation
+```
+
+### Testing Commands
+```bash
+./mvnw test # Run all tests
+./mvnw test -Dtest=${TestClassName} # Run specific test class
+./mvnw test -pl ${submodule} # Run tests for specific module
+./mvnw test jacoco:report -Djacoco.skip=false -pl ${submodule} # Generate
coverage report
./mvnw test jacoco:check@jacoco-check -Pcoverage-check -Djacoco.skip=false \
-Djacoco.check.class.pattern=${ClassName} -pl ${submodule} # Coverage check
-# Parameter instructions:
-# ${ClassName} - Specific class name for coverage check (e.g.,
ShardingRuleService)
-# ${submodule} - Specific submodule name (e.g., shardingsphere-jdbc,
shardingsphere-proxy)
+# Performance Testing
+./mvnw jmh:benchmark # Run performance benchmarks
```
-### Troubleshooting Commands
-```bash
-./mvnw clean test jacoco:report -Djacoco.skip=false -pl ${submodule} #
Generate coverage report
-open ${submodule}/target/site/jacoco/index.html # View
coverage details
-```
+# Parameters: ${ClassName}, ${TestClassName}, ${submodule}
## 📝 Code Templates
@@ -209,35 +274,55 @@ void
assert${MethodName}With${Condition}Expects${Result}() {
### Mock Configuration Template
-**Mock Usage Boundary Principles:**
-- ✅ **No Mock Needed**: Simple objects (String, basic types, DTOs, POJOs),
stateless utility classes, configuration objects
-- ✅ **Mock Required**: Complex external dependencies (database connections,
network services, file systems), third-party interfaces, SPI services, stateful
objects
-- ✅ **Judgment Criteria**: If object construction cost is high or has external
dependencies, Mock is needed
+**Mock Usage Boundaries:**
+- **No Mock**: Simple objects, DTOs, stateless utilities, configuration objects
+- **Must Mock**: Database connections, network services, third-party
interfaces, SPI services
+- **Judgment**: Mock only with external dependencies or high construction cost
+
+**Basic Mock Patterns:**
+```java
+// Interface method Mock
+when(dependency.method(any())).thenReturn(result);
+
+// Constructor Mock with MockedConstruction
+try (MockedConstruction<ClassName> mocked = mockConstruction(ClassName.class))
{
+ // Test code involving new ClassName()
+}
+```
+**Advanced Mock Patterns:**
```java
-// Basic Mock configuration - for interface method Mock
-when(dependency.${method}(any())).thenReturn(${result});
-
-// Complex dependency Mock - for constructor objects that need Mock
-try (MockedConstruction<${ClassName}> mocked =
mockConstruction(${ClassName}.class, (mock, context) -> {
- ${DependencyChainSetup}
- when(mock.${getMethod}()).thenReturn(${mockResult});
-})) {
- // Test code - code paths involving new ${ClassName}()
+// Static method Mocking (avoid UnfinishedStubbingException)
+@SneakyThrows(SQLException.class)
+private static Array createMockArray(final Object data) {
+ Array result = mock(Array.class);
+ doReturn(data).when(result).getArray();
+ return result;
}
-// Mock configuration example comparison:
-// ❌ No Mock needed - simple objects
-String result = "testValue"; // Direct creation
-Map<String, Object> config = new HashMap<>(); // Direct creation
+// Deep stubs for complex dependencies
+@Mock(answer = Answers.RETURNS_DEEP_STUBS)
+private ComplexService complexService;
-// ✅ Mock needed - complex dependencies
-when(dataSource.getConnection()).thenReturn(mockConnection); // External
dependency
-try (MockedConstruction<DatabaseMetaData> mocked =
mockConstruction(DatabaseMetaData.class)) {
- // Constructor needs Mock case
+// MockedStatic for static method calls
+try (MockedStatic<UtilityClass> mocked = mockStatic(UtilityClass.class)) {
+ when(UtilityClass.staticMethod(any())).thenReturn(value);
+ // Test code
}
```
+**Example Comparison:**
+```java
+// ❌ Over-mocking simple objects
+String result = mock(String.class); // Unnecessary
+
+// ✅ Direct creation for simple objects
+String result = "testValue";
+
+// ✅ Mock external dependencies
+when(dataSource.getConnection()).thenReturn(mockConnection);
+```
+
### SPI Implementation Template
```java
package org.apache.shardingsphere.${module}.spi;
@@ -344,75 +429,52 @@ public final class ${SPIName}Impl implements
${SPIName}SPI {
## 📋 Project Constraint Rules
-### YAML Format Constraint Configuration
+### Core Design Principles
```yaml
-# Package naming conventions
-package_naming:
+class_design:
+ - final classes with final fields
+ - constructor injection only
+ - @RequiredArgsConstructor for dependencies
+ - self-documenting code (no comments)
+
+package_structure:
service: "org.apache.shardingsphere.{module}.service"
spi: "org.apache.shardingsphere.{module}.spi"
config: "org.apache.shardingsphere.{module}.config"
util: "org.apache.shardingsphere.{module}.util"
-
-# Class design rules
-class_design:
- services:
- - final_class_with_final_fields
- - constructor_injection_only
- - use_requiredArgsConstructor
- - self_documenting_code_only
-
- naming_conventions:
- test_methods: "assert{MethodName}With{Condition}Expects{Result}"
- production_variables: "result"
- test_variables: "actual"
- private_methods: "descriptive_verb_noun"
-
- test_organization:
- - one_test_per_branch
- - branch_first_naming
- - minimal_test_count
- - test_isolation
- - try_with_resources_for_mocks
-
-# Quality requirements
-quality_requirements:
- test_coverage: "100%"
- code_formatting: "spotless_applied"
- documentation: "self_documenting_only"
- mock_strategy: "use_mockedconstruction_for_external_deps"
-
-# Assertion standards
-assertion_standards:
- preferred_style: "hamcrest_matchers"
- usage_pattern: "assertThat(actual, is(expected))"
- variable_naming: "use_actual_for_assertions"
```
-### Code Pattern Examples
+### Code Patterns
```java
-// Self-documenting code pattern (must follow)
-if (userIsAdminWithPermission()) {
- // Complex logic extracted to private method
+// Self-documenting pattern
+if (isValidUserWithPermission()) {
+ processPayment();
}
-private boolean userIsAdminWithPermission() {
- return user.isAdmin() && user.hasPermission();
+private boolean isValidUserWithPermission() {
+ return user.isValid() && user.hasPermission();
}
-// Standard test structure (must follow)
+// Test structure
@Test
-void assertMethodNameWithConditionExpectsResult() {
+void assertMethodWithConditionExpectsResult() {
// Given
- mockDependencyChain();
+ mockDependencies();
// When
- MyResult actual = target.methodUnderTest(input);
+ Result actual = target.method(input);
// Then
- assertThat(actual, is(expectedResult));
+ assertThat(actual, is(expected));
}
```
+### Quality Requirements
+- **Test Coverage**: 100% branch coverage
+- **Code Formatting**: Spotless applied
+- **Mock Strategy**: Mock only external dependencies
+- **Naming**: Test methods use assert*() prefix
+
## 🔍 Quick Search Index
### AI Search Mapping Table
@@ -480,116 +542,55 @@ error_recovery_index:
reference: "ShardingSphere Testing Style Guide.Mock Usage Patterns"
```
-## 🛠️ Troubleshooting Guide
+## 🛠️ Common Issues & Solutions
-### Common Problem Diagnosis
+### Coverage Problems
+- **Issue**: Mock configuration incomplete, branches not executed
+- **Solution**: Use MockedConstruction, create dedicated test methods for each
branch
+- **Command**: `./mvnw clean test jacoco:report -pl ${submodule}`
-#### Coverage Issues
-```yaml
-problem: "Coverage not met"
-cause_check:
- - Mock configuration incomplete, some branches not executed
- - Tests exit early, not covering target code
- - Complex conditional statement branch testing missing
-
-solution:
- 1. Check Mock configuration, use MockedConstruction to control external
dependencies
- 2. View JaCoCo HTML report, locate red diamond marked uncovered branches
- 3. Create dedicated test methods for each conditional branch
-
-reference_commands:
- ./mvnw clean test jacoco:report -Djacoco.skip=false -pl ${submodule}
- open ${submodule}/target/site/jacoco/index.html
-```
+### Mock Configuration Errors
+- **Issue**: UnfinishedStubbingException in static methods
+- **Solution**: Use `doReturn().when()` instead of `when().thenReturn()`
+- **Pattern**: `@SneakyThrows(SQLException.class) private static Array
createMockArray()`
-#### Compilation Errors
-```yaml
-problem: "Compilation failure"
-cause_check:
- - Dependency version conflicts
- - Syntax errors
- - Package import errors
-
-solution:
- 1. Check dependency versions and compatibility
- 2. Verify syntax correctness
- 3. Confirm package paths and import statements
-
-reference_commands:
- ./mvnw clean compile
- ./mvnw dependency:tree
-```
-
-#### Test Failures
-```yaml
-problem: "Test execution failure"
-cause_check:
- - Mock configuration incorrect
- - Assertion logic errors
- - Test data construction problems
-
-solution:
- 1. Check Mock configuration, ensure complete dependency chain
- 2. Verify assertion logic, use Hamcrest matchers
- 3. Confirm test data validity
-
-reference_template: "Code Templates.Test Method Template"
-```
+### Test Failures
+- **Issue**: Mock dependency chain broken
+- **Solution**: Verify complete dependency chain, use RETURNS_DEEP_STUBS
+- **Check**: Mock calls with `verify(mock).method(params)`
-#### Mock Configuration Issues
-```yaml
-problem: "Mock configuration complex and difficult to manage"
-cause_check:
- - Nested dependencies too deep
- - Constructor Mock missing
- - Static method call Mock inappropriate
- - Mock object selection inappropriate (over-Mocking simple objects)
-
-solution:
- 1. Identify Mock boundaries: direct creation for simple objects, Mock only
for complex objects
- 2. Use RETURNS_DEEP_STUBS to handle complex dependencies
- 3. Use MockedConstruction for constructor calls
- 4. Use MockedStatic for static method calls
- 5. Reduce unnecessary Mock, improve test readability
-
-mock_boundary_judgment:
- - No Mock needed: String, basic types, DTOs, POJOs, stateless utility classes
- - Must Mock: database connections, network services, file systems,
third-party interfaces
-
-reference_template: "Code Templates.Mock Configuration Template"
-```
+### Compilation Errors
+- **Issue**: Dependency conflicts, syntax errors
+- **Solution**: Check versions, verify imports, run `./mvnw dependency:tree`
-### Debugging Techniques
+### Quick Reference
+```bash
+# Generate coverage report
+./mvnw clean test jacoco:report -Djacoco.skip=false -pl ${submodule}
-#### Coverage Debugging
-1. **Generate detailed report**: `./mvnw clean test jacoco:report
-Djacoco.skip=false -pl ${submodule}`
-2. **View HTML report**: `open ${submodule}/target/site/jacoco/index.html`
-3. **Locate uncovered lines**: Look for red-marked code lines
-4. **Analyze branch conditions**: Identify unexecuted conditional statement
branches
+# View coverage details
+open ${submodule}/target/site/jacoco/index.html
-#### Mock Debugging
-1. **Verify Mock calls**: `verify(mock).method(params)`
-2. **Check Mock state**: Confirm Mock configuration is correct
-3. **Debug dependency chain**: Verify Mock configuration layer by layer
+# Check dependencies
+./mvnw dependency:tree
+```
---
-## 📋 Quick Checklist
+## 📋 Quality Checklist
-### Pre-Task Check
-- [ ] Clarify task type (source code/test/documentation)
-- [ ] Understand quality requirements (100%
coverage/formatting/self-documenting)
-- [ ] Find relevant templates and constraint rules
+### Before Starting
+- [ ] Task type identified (source/test/docs)
+- [ ] Quality requirements understood
+- [ ] Relevant templates found
-### Pre-Completion Check
-- [ ] Source code task: 100% test coverage
-- [ ] Source code task: Code formatting
-- [ ] Source code task: Self-documenting code
-- [ ] Test task: Complete branch coverage
-- [ ] Documentation task: Link validity
-- [ ] All tasks: Conform to project constraint rules
+### Before Completing
+- [ ] Source: 100% coverage + formatting + self-documenting
+- [ ] Test: Complete branch coverage
+- [ ] Docs: Valid links + consistent format
+- [ ] All: Project constraints satisfied
### Final Verification
-- [ ] Run full build: `./mvnw install -T1C`
-- [ ] Verify coverage: `./mvnw test jacoco:check@jacoco-check -Pcoverage-check`
-- [ ] Check formatting: `./mvnw spotless:apply -Pcheck`
\ No newline at end of file
+- [ ] Build: `./mvnw install -T1C`
+- [ ] Coverage: `./mvnw test jacoco:check@jacoco-check -Pcoverage-check`
+- [ ] Format: `./mvnw spotless:apply -Pcheck`
\ No newline at end of file
diff --git
a/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/column/PostgreSQLColumnPropertiesAppenderTest.java
b/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/column/PostgreSQLColumnPropertiesAppenderTest.java
new file mode 100644
index 00000000000..aee65e94a14
--- /dev/null
+++
b/kernel/data-pipeline/dialect/postgresql/src/test/java/org/apache/shardingsphere/data/pipeline/postgresql/sqlbuilder/ddl/column/PostgreSQLColumnPropertiesAppenderTest.java
@@ -0,0 +1,866 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package
org.apache.shardingsphere.data.pipeline.postgresql.sqlbuilder.ddl.column;
+
+import lombok.SneakyThrows;
+import
org.apache.shardingsphere.data.pipeline.postgresql.sqlbuilder.ddl.PostgreSQLDDLTemplateExecutor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.internal.configuration.plugins.Plugins;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.sql.Array;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+@SuppressWarnings("CollectionWithoutInitialCapacity")
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class PostgreSQLColumnPropertiesAppenderTest {
+
+ private final PostgreSQLColumnPropertiesAppender appender = new
PostgreSQLColumnPropertiesAppender(mock(Connection.class), 0, 0);
+
+ @Mock
+ private PostgreSQLDDLTemplateExecutor templateExecutor;
+
+ @BeforeEach
+ void setUp() throws ReflectiveOperationException {
+ reset(templateExecutor);
+
Plugins.getMemberAccessor().set(PostgreSQLColumnPropertiesAppender.class.getDeclaredField("templateExecutor"),
appender, templateExecutor);
+ }
+
+ @Test
+ void assertGetTypeAndInheritedColumnsWithoutTypeOrInheritsReturnsEmpty() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ appender.append(context);
+ assertThat(context.size(), is(1));
+ assertThat(context.get("columns"), is(Collections.emptyList()));
+ }
+
+ @Test
+ void assertGetTypeAndInheritedColumnsFromInheritsWithNoMatchReturnsEmpty()
{
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("coll_inherits", mockSQLArray(new String[]{"missing"}));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/table/%s/get_inherits.ftl"))).thenReturn(Collections.singleton(createInheritEntry(1L)));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ assertThat(context.get("coll_inherits"),
is(Collections.singletonList("missing")));
+ assertThat(context.get("columns"), is(Collections.emptyList()));
+ }
+
+ @Test
+ void
assertAppendUsesDefaultInheritedFromWithEmptyInheritsAndInheritedColumns() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", null);
+ context.put("coll_inherits", mockSQLArray(new String[0]));
+ Map<String, Object> baseColumn = createTextColumnWithName("test_col");
+ baseColumn.put("inheritedfrom", "parent_table");
+ Map<String, Object> inheritedColumn = new LinkedHashMap<>();
+ inheritedColumn.put("name", "test_col");
+ inheritedColumn.put("inheritedfrom", "parent_table");
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singletonList(baseColumn));
+ when(templateExecutor.executeByTemplate(context,
"table/%s/get_columns_for_table.ftl")).thenReturn(Collections.singletonList(inheritedColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("inheritedfrom"), is("parent_table"));
+ assertFalse(singleColumn.containsKey("inheritedfromtype"));
+ assertFalse(singleColumn.containsKey("inheritedfromtable"));
+ }
+
+ @Test
+ void assertAppendLeavesPrimaryMarkersMissingWhenIndkeyMissing() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("pk_col");
+ column.put("attnum", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ assertFalse(getSingleColumn(context).containsKey("is_pk"));
+ }
+
+ @Test
+ void assertAppendMarksPrimaryColumnFalseWhenIndkeyDoesNotContainAttnum() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("pk_col");
+ column.put("attnum", 1);
+ column.put("indkey", "2");
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("is_pk"), is(false));
+ assertThat(singleColumn.get("is_primary_key"), is(false));
+ }
+
+ @Test
+ void assertAppendKeepsLengthAbsentWhenTypmodMissing() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("numeric_col");
+ column.put("elemoid", 1231L);
+ column.put("typname", "numeric");
+ column.put("atttypmod", -1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ assertFalse(getSingleColumn(context).containsKey("attlen"));
+ }
+
+ @Test
+ void assertAppendSkipsLengthForVarCharWithoutDigits() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("var_char_col");
+ column.put("elemoid", 1043L);
+ column.put("typname", "text");
+ column.put("atttypmod", -1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertFalse(singleColumn.containsKey("attlen"));
+ assertFalse(singleColumn.containsKey("attprecision"));
+ }
+
+ @Test
+ void assertAppendHandlesDateLengthBranch() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> typeColumn = createMapWithNameAndInherited();
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.singleton(typeColumn));
+ Map<String, Object> column = createTextColumnWithName("date_col");
+ column.put("elemoid", 1114L);
+ column.put("typname", "timestamp without time zone");
+ column.put("cltype", "timestamp(3) without time zone");
+ column.put("atttypmod", 4 + (3 << 16));
+ column.put("indkey", "1");
+ column.put("attnum", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(column));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ assertThat(getSingleColumn(context).get("cltype"), is("timestamp
without time zone"));
+ }
+
+ @Test
+ void assertAppendSkipsLengthWhenElemoidUnknown() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 2L);
+ Map<String, Object> typeColumn = createMapWithNameAndInherited();
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.singletonList(typeColumn));
+ Map<String, Object> column = createTextColumnWithName("unknown_col");
+ column.put("elemoid", 9999L);
+ column.put("typname", "unknown");
+ column.put("cltype", "unknown");
+ column.put("indkey", "1");
+ column.put("attnum", 1);
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertFalse(singleColumn.containsKey("attlen"));
+ assertFalse(singleColumn.containsKey("attprecision"));
+ }
+
+ @Test
+ void assertAppendFormatsColumnVariables() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("opt_col");
+ column.put("attoptions", mockSQLArray(new String[]{"foo=bar"}));
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Collection<?> options = (Collection<?>)
getSingleColumn(context).get("attoptions");
+ assertThat(options.size(), is(1));
+ Map<?, ?> option = (Map<?, ?>) options.iterator().next();
+ assertThat(option.get("name"), is("foo"));
+ assertThat(option.get("value"), is("bar"));
+ }
+
+ @Test
+ void assertAppendCopiesInheritedFromTable() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("coll_inherits", mockSQLArray(new String[]{"parent"}));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/table/%s/get_columns_for_table.ftl"))).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/table/%s/get_inherits.ftl"))).thenReturn(Collections.singletonList(createInheritEntry(5L)));
+ Map<String, Object> inheritedColumn = new LinkedHashMap<>();
+ inheritedColumn.put("name", "col");
+ inheritedColumn.put("inheritedfrom", "parent_table");
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("table/%s/get_columns_for_table.ftl"))).thenReturn(Collections.singletonList(inheritedColumn));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singletonList(createTextColumnWithName("col")));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ assertThat(getSingleColumn(context).get("inheritedfromtable"),
is("parent_table"));
+ }
+
+ @Test
+ void assertCheckTypmodInterval() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> intervalColumn =
createTextColumnWithName("interval_col");
+ intervalColumn.put("elemoid", 1186L);
+ intervalColumn.put("typname", "interval");
+ intervalColumn.put("typnspname", "pg_catalog");
+ intervalColumn.put("atttypmod", 3);
+ intervalColumn.put("cltype", "interval");
+ intervalColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(intervalColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("atttypmod"), is(3));
+ assertThat(singleColumn.get("typname"), is("interval"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("interval"));
+ }
+
+ @Test
+ void assertCheckTypmodDate() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> dateColumn = createTextColumnWithName("date_col");
+ dateColumn.put("elemoid", 1083L);
+ dateColumn.put("typname", "date");
+ dateColumn.put("typnspname", "pg_catalog");
+ dateColumn.put("atttypmod", 1);
+ dateColumn.put("cltype", "date");
+ dateColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(dateColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("atttypmod"), is(1));
+ assertThat(singleColumn.get("typname"), is("date"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("date"));
+ }
+
+ @Test
+ void assertCheckTypmodBitType() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> bitColumn = createTextColumnWithName("bit_col");
+ bitColumn.put("elemoid", 1560L);
+ bitColumn.put("typname", "bit");
+ bitColumn.put("typnspname", "pg_catalog");
+ bitColumn.put("atttypmod", 5);
+ bitColumn.put("cltype", "bit");
+ bitColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singleton(bitColumn));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("atttypmod"), is(5));
+ assertThat(singleColumn.get("typname"), is("bit"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("bit"));
+ }
+
+ @Test
+ void assertCheckTypmodDefaultCaseSubtractsFour() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> textColumn = createTextColumnWithName("text_col");
+ textColumn.put("elemoid", 9999L);
+ textColumn.put("typname", "text");
+ textColumn.put("typnspname", "pg_catalog");
+ textColumn.put("atttypmod", 10);
+ textColumn.put("cltype", "text");
+ textColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singleton(textColumn));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("atttypmod"), is(10));
+ assertThat(singleColumn.get("typname"), is("text"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("text"));
+ }
+
+ @Test
+ void assertCheckTypmodIntervalLenGreaterThanSixProducesEmptyPrecision() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> intervalColumn =
createTextColumnWithName("interval_col");
+ intervalColumn.put("elemoid", 1186L);
+ intervalColumn.put("typname", "interval");
+ intervalColumn.put("typnspname", "pg_catalog");
+ intervalColumn.put("atttypmod", 7);
+ intervalColumn.put("cltype", "interval");
+ intervalColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Collections.singleton(intervalColumn));
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("atttypmod"), is(7));
+ assertThat(singleColumn.get("typname"), is("interval"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("interval"));
+ }
+
+ @Test
+ void assertGetFullTypeValueCharCatalog() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> charColumn = createTextColumnWithName("char_col");
+ charColumn.put("elemoid", 18L);
+ charColumn.put("typname", "\"char\"");
+ charColumn.put("typnspname", "pg_catalog");
+ charColumn.put("atttypmod", 2);
+ charColumn.put("cltype", "\"char\"");
+ charColumn.put("attndims", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(charColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("\"char\""));
+ assertThat(singleColumn.get("typnspname"), is("pg_catalog"));
+ assertThat(singleColumn.get("atttypmod"), is(2));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("\"char\""));
+ }
+
+ @Test
+ void assertGetFullTypeValueTimeWithTimeZone() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> timeColumn = createTextColumnWithName("time_col");
+ timeColumn.put("elemoid", 1186L);
+ timeColumn.put("typname", "time with time zone");
+ timeColumn.put("typnspname", "public");
+ timeColumn.put("atttypmod", -1);
+ timeColumn.put("cltype", "time with time zone");
+ timeColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(timeColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("time with time zone"));
+ assertThat(singleColumn.get("typnspname"), is("public"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("time with time zone"));
+ }
+
+ @Test
+ void assertGetFullTypeValueTimeWithoutTimeZone() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> timeColumn = createTextColumnWithName("time_col");
+ timeColumn.put("elemoid", 1183L);
+ timeColumn.put("typname", "time without time zone");
+ timeColumn.put("typnspname", "public");
+ timeColumn.put("atttypmod", 2);
+ timeColumn.put("cltype", "time without time zone");
+ timeColumn.put("attndims", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(timeColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("time without time zone"));
+ assertThat(singleColumn.get("typnspname"), is("public"));
+ assertThat(singleColumn.get("atttypmod"), is(2));
+ assertThat(singleColumn.get("attndims"), is(1));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("time without time zone"));
+ }
+
+ @Test
+ void assertGetFullTypeValueTimestampWithTimeZone() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> timestampColumn =
createTextColumnWithName("timestamp_col");
+ timestampColumn.put("elemoid", 1184L);
+ timestampColumn.put("typname", "timestamp with time zone");
+ timestampColumn.put("typnspname", "public");
+ timestampColumn.put("atttypmod", -1);
+ timestampColumn.put("cltype", "timestamp with time zone");
+ timestampColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(timestampColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("timestamp with time
zone"));
+ assertThat(singleColumn.get("typnspname"), is("public"));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("timestamp with time zone"));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesSchemaWithQuotes() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("char_col");
+ column.put("typname", "public.\"char\"");
+ column.put("typnspname", "public");
+ column.put("atttypmod", -1);
+ column.put("cltype", "\"char\"");
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("cltype"), is("\"char\""));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesArrayAndPrefix() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = createTextColumnWithName("int4_col");
+ arrayColumn.put("elemoid", 23L);
+ arrayColumn.put("typname", "_int4");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "_int4[]");
+ arrayColumn.put("attndims", 0);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("_int4"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ assertThat(singleColumn.get("attndims"), is(0));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("_int4[]"));
+ }
+
+ @Test
+ void
assertGetFullDataTypeSkipsNumdimsAdjustmentWhenAttndimsNonZeroForArray() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = createTextColumnWithName("int8_col");
+ arrayColumn.put("elemoid", 20L);
+ arrayColumn.put("typname", "_int8");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "_int8[]");
+ arrayColumn.put("attndims", 2);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("_int8"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ assertThat(singleColumn.get("attndims"), is(2));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("_int8[]"));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesArrayWithoutPrefixWhenAttndimsMissing() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = new LinkedHashMap<>();
+ arrayColumn.put("name", "int4_col");
+ arrayColumn.put("elemoid", 23L);
+ arrayColumn.put("typname", "int4[]");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "int4[]");
+ arrayColumn.put("atttypid", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("int4[]"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("int4[]"));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesArrayWithoutPrefixWhenAttndimsZero() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = new LinkedHashMap<>();
+ arrayColumn.put("name", "int4_col");
+ arrayColumn.put("elemoid", 23L);
+ arrayColumn.put("typname", "int4[]");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "int4[]");
+ arrayColumn.put("attndims", 0);
+ arrayColumn.put("atttypid", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("int4[]"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ assertThat(singleColumn.get("attndims"), is(0));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("int4[]"));
+ }
+
+ @Test
+ void assertGetFullDataTypeArrayWhenAttndimsMissingLeavesNumdimsNull() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = new LinkedHashMap<>();
+ arrayColumn.put("name", "int8_col");
+ arrayColumn.put("elemoid", 20L);
+ arrayColumn.put("typname", "_int8");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "_int8[]");
+ arrayColumn.put("atttypid", 1);
+ arrayColumn.put("attndims", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("_int8"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("_int8[]"));
+ }
+
+ @Test
+ void assertGetFullDataTypeLeavesNameWithOpeningQuoteOnly() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> quotedColumn = new LinkedHashMap<>();
+ quotedColumn.put("name", "foo_col");
+ quotedColumn.put("elemoid", 9999L);
+ quotedColumn.put("typname", "\"foo");
+ quotedColumn.put("typnspname", null);
+ quotedColumn.put("atttypmod", -1);
+ quotedColumn.put("cltype", "\"foo");
+ quotedColumn.put("attndims", 0);
+ quotedColumn.put("atttypid", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(quotedColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("\"foo"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ assertThat(singleColumn.get("attndims"), is(0));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("\"foo"));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesUnderscorePrefixWithNullNumdims() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = new LinkedHashMap<>();
+ arrayColumn.put("name", "int4_col");
+ arrayColumn.put("elemoid", 23L);
+ arrayColumn.put("typname", "_int4");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "_int4");
+ arrayColumn.put("atttypid", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("_int4"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("_int4"));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesUnderscorePrefixWithZeroNumdims() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = new LinkedHashMap<>();
+ arrayColumn.put("name", "int4_col");
+ arrayColumn.put("elemoid", 23L);
+ arrayColumn.put("typname", "_int4");
+ arrayColumn.put("typnspname", null);
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "_int4");
+ arrayColumn.put("attndims", 0);
+ arrayColumn.put("atttypid", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("_int4"));
+ assertThat(singleColumn.get("typnspname"), nullValue());
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ assertThat(singleColumn.get("attndims"), is(0));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("_int4"));
+ }
+
+ @Test
+ void assertGetFullDataTypeHandlesArraySuffixWithNonZeroNumdims() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> arrayColumn = new LinkedHashMap<>();
+ arrayColumn.put("name", "text_col");
+ arrayColumn.put("elemoid", 25L);
+ arrayColumn.put("typname", "text[]");
+ arrayColumn.put("typnspname", "public");
+ arrayColumn.put("atttypmod", -1);
+ arrayColumn.put("cltype", "text[]");
+ arrayColumn.put("attndims", 2);
+ arrayColumn.put("atttypid", 1);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singleton(arrayColumn));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("typname"), is("text[]"));
+ assertThat(singleColumn.get("typnspname"), is("public"));
+ assertThat(singleColumn.get("atttypmod"), is(-1));
+ assertThat(singleColumn.get("attndims"), is(2));
+ @SuppressWarnings("unchecked")
+ Collection<String> editTypes = (Collection<String>)
singleColumn.get("edit_types");
+ assertThat(editTypes, contains("text[]"));
+ }
+
+ @Test
+ void assertCheckSchemaInNameHandlesQuotedSchemaDot() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("test_col");
+ column.put("typname", "public\".\"foo\"");
+ column.put("typnspname", "public");
+ column.put("atttypmod", -1);
+ column.put("cltype", "public\".\"foo\"");
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("cltype"), is("public\".\"foo\""));
+ }
+
+ @Test
+ void assertParseTypeNameHandlesArraySuffix() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("text_col");
+ column.put("typname", "text[]");
+ column.put("typnspname", "public");
+ column.put("atttypmod", -1);
+ column.put("cltype", "text[]");
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("cltype"), is("text[]"));
+ }
+
+ @Test
+ void assertParseTypeNameHandlesInterval() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("interval_col");
+ column.put("typname", "interval");
+ column.put("typnspname", "public");
+ column.put("atttypmod", -1);
+ column.put("cltype", "interval");
+ column.put("atttypid", 1186);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("cltype"), is("interval"));
+ }
+
+ @Test
+ void assertParseTypeNameIgnoresTimeBranchWhenPrefixMismatch() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 1L);
+ Map<String, Object> column = createTextColumnWithName("foo_time_col");
+ column.put("typname", "foo(time");
+ column.put("typnspname", "public");
+ column.put("atttypmod", -1);
+ column.put("cltype", "foo(time");
+ column.put("atttypid", 25);
+ when(templateExecutor.executeByTemplate(context,
"component/table/%s/get_columns_for_table.ftl")).thenReturn(Collections.emptyList());
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/properties.ftl")).thenReturn(Collections.singletonList(column));
+ when(templateExecutor.executeByTemplate(context,
"component/columns/%s/edit_mode_types_multi.ftl")).thenReturn(Collections.emptyList());
+ appender.append(context);
+ Map<String, Object> singleColumn = getSingleColumn(context);
+ assertThat(singleColumn.get("cltype"), is("foo(time"));
+ }
+
+ @Test
+ void assertAppendPopulatesInheritedAndEditTypes() {
+ Map<String, Object> context = new LinkedHashMap<>();
+ context.put("typoid", 20L);
+ Map<String, Object> typeColumn = new LinkedHashMap<>();
+ typeColumn.put("name", "col");
+ typeColumn.put("inheritedfrom", "parent");
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/table/%s/get_columns_for_table.ftl"))).thenReturn(Collections.singletonList(typeColumn));
+ Map<String, Object> column = new LinkedHashMap<>();
+ column.put("name", "col");
+ column.put("atttypid", 1);
+ column.put("attnum", 1);
+ column.put("indkey", "1");
+ column.put("elemoid", 1231L);
+ column.put("typname", "numeric");
+ column.put("typnspname", "public");
+ column.put("attndims", 0);
+ column.put("atttypmod", 4 + (5 << 16) + 2);
+ column.put("cltype", "numeric(5,2)");
+ Map<String, Object> unmatchedColumn = createUnmatchedColumn();
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/properties.ftl"))).thenReturn(Arrays.asList(column,
unmatchedColumn));
+ Map<String, Object> editModeTypesEntry =
createEditModeTypesEntry("alpha");
+ when(templateExecutor.executeByTemplate(anyMap(),
eq("component/columns/%s/edit_mode_types_multi.ftl"))).thenReturn(Collections.singleton(editModeTypesEntry));
+ appender.append(context);
+ Collection<?> columns = (Collection<?>) context.get("columns");
+ assertThat(columns.size(), is(2));
+ @SuppressWarnings("unchecked")
+ Map<String, Object> actualColumn = columns.stream().map(each ->
(Map<String, Object>) each)
+ .filter(each ->
"col".equals(each.get("name"))).findFirst().orElseThrow(() -> new
AssertionError("missing column 'col'"));
+ assertThat(actualColumn.get("inheritedfromtype"), is("parent"));
+ assertThat(actualColumn.get("attlen"), is("5"));
+ assertThat(actualColumn.get("attprecision"), is("2"));
+ assertThat(actualColumn.get("is_pk"), is(true));
+ assertThat(actualColumn.get("cltype"), is("numeric"));
+ assertThat((Collection<?>) actualColumn.get("edit_types"),
contains("alpha", "numeric(5,2)"));
+ }
+
+ private Map<String, Object> createInheritEntry(final long oid) {
+ Map<String, Object> result = new LinkedHashMap<>(2, 1F);
+ result.put("inherits", "parent");
+ result.put("oid", oid);
+ return result;
+ }
+
+ private Map<String, Object> createTextColumnWithName(final String name) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("name", name);
+ result.put("cltype", "text");
+ result.put("typname", "text");
+ result.put("typnspname", "public");
+ result.put("attndims", 0);
+ result.put("atttypmod", -1);
+ result.put("atttypid", 1);
+ return result;
+ }
+
+ private Map<String, Object> createMapWithNameAndInherited() {
+ Map<String, Object> result = new LinkedHashMap<>(2, 1F);
+ result.put("name", "col");
+ result.put("inheritedfrom", "parent_type");
+ return result;
+ }
+
+ private Map<String, Object> createUnmatchedColumn() {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("name", "other");
+ result.put("atttypid", 2);
+ result.put("cltype", "text");
+ result.put("typname", "text");
+ result.put("typnspname", "public");
+ result.put("attndims", 0);
+ result.put("atttypmod", -1);
+ return result;
+ }
+
+ private Map<String, Object> createEditModeTypesEntry(final String...
editTypes) {
+ Map<String, Object> entry = new LinkedHashMap<>();
+ entry.put("main_oid", "1");
+ entry.put("edit_types", mockSQLArray(editTypes));
+ return entry;
+ }
+
+ @SneakyThrows(SQLException.class)
+ private Array mockSQLArray(final Object data) {
+ Array result = mock(Array.class);
+ doReturn(data).when(result).getArray();
+ return result;
+ }
+
+ private Map<String, Object> getSingleColumn(final Map<String, Object>
context) {
+ @SuppressWarnings("unchecked")
+ Collection<Map<String, Object>> columns = (Collection<Map<String,
Object>>) context.get("columns");
+ assertThat(columns.size(), is(1));
+ return columns.iterator().next();
+ }
+}