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();
+    }
+}

Reply via email to