ADR-075: In-Memory Preflight Validation Model

Status: Proposed Date: 2025-12-30 Related: ADR-074 (Workflow Architecture), ADR-004 (Enhanced Table Creation), ADR-005 (Migration Scripts)


Context and Problem Statement

Currently, table and window creation operations execute directly against the database. If validation fails mid-process, partial data may remain in the Application Dictionary. Additionally, migration scripts and 2Pack files are generated without comprehensive validation against iDempiere core class constraints.

Problems:

  1. No pre-execution validation - Errors discovered during DB operations
  2. Partial failures - Incomplete data left in AD on errors
  3. No format validation - 2Pack XML structure not verified before generation
  4. No constraint checking - Column types, lengths, references not validated against iDempiere rules

Inspiration: Nx build system pattern of "plan → validate → confirm → execute"


Decision Drivers


Decision

Implement an In-Memory Preflight Validation Model that:

  1. Builds complete in-memory representation before any DB operations
  2. Validates against iDempiere core class rules
  3. Validates output formats (2Pack XML, Migration SQL)
  4. Only executes if all validations pass

Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                    PREFLIGHT VALIDATION PIPELINE                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  INPUT                    IN-MEMORY MODEL              VALIDATION    │
│  ──────                   ───────────────              ──────────    │
│                                                                      │
│  ┌──────────────┐        ┌──────────────────┐        ┌────────────┐ │
│  │ Column Defs  │───────▶│ TableSpec        │───────▶│ Core Rules │ │
│  │ S#Name       │        │  ├─ name         │        │ ├─ MColumn  │ │
│  │ Q#Qty        │        │  ├─ columns[]    │        │ ├─ MTable   │ │
│  │ D#DueDate    │        │  │   ├─ name     │        │ ├─ Display  │ │
│  └──────────────┘        │  │   ├─ type     │        │ │   Type    │ │
│                          │  │   ├─ length   │        │ └─ Ref      │ │
│                          │  │   └─ ref      │        │     Valid   │ │
│                          │  ├─ window       │        └──────┬─────┘ │
│                          │  └─ entityType   │               │       │
│                          └──────────────────┘               │       │
│                                   │                         │       │
│                                   ▼                         ▼       │
│                          ┌──────────────────┐        ┌────────────┐ │
│                          │ Output Preview   │───────▶│ Format     │ │
│                          │  ├─ 2Pack XML    │        │ Validation │ │
│                          │  ├─ Migration SQL│        │ ├─ XML     │ │
│                          │  └─ Model Java   │        │ ├─ SQL     │ │
│                          └──────────────────┘        │ └─ Java    │ │
│                                                      └──────┬─────┘ │
│                                                             │       │
│                                   ┌─────────────────────────┘       │
│                                   ▼                                 │
│                          ┌──────────────────┐                       │
│                          │ Validation Result│                       │
│                          │  ├─ errors[]     │                       │
│                          │  ├─ warnings[]   │                       │
│                          │  └─ canProceed   │                       │
│                          └────────┬─────────┘                       │
│                                   │                                 │
│              ┌────────────────────┼────────────────────┐            │
│              ▼                    ▼                    ▼            │
│        ┌──────────┐        ┌──────────┐        ┌──────────┐         │
│        │ BLOCKED  │        │ WARNINGS │        │   OK     │         │
│        │ (errors) │        │ (proceed │        │ (execute)│         │
│        │          │        │  w/ack)  │        │          │         │
│        └──────────┘        └──────────┘        └──────────┘         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

In-Memory Model Classes

TableSpec (Central Model)

package org.idempiere.cli.model;

/**
 * In-memory specification for a table before creation.
 * Validated against iDempiere core classes before execution.
 */
public class TableSpec {
    private String tableName;
    private String description;
    private String entityType = "U";
    private String accessLevel = "3";
    private List<ColumnSpec> columns = new ArrayList<>();
    private WindowSpec window;  // Optional linked window

    // Computed fields (populated during validation)
    private Integer estimatedTableId;
    private String keyColumnName;
    private boolean hasStandardColumns = true;

    // Validation state
    private ValidationResult validationResult;

    public static TableSpec parse(String tableName, String columnDefs) {
        // Parse "S#Name,Q#Qty,D#DueDate" into structured model
    }

    public ValidationResult validate() {
        // Run all validations
    }

    public String toMigrationSql() {
        // Generate SQL without executing
    }

    public String to2PackXml() {
        // Generate 2Pack XML without executing
    }
}

ColumnSpec

/**
 * In-memory specification for a column.
 */
public class ColumnSpec {
    private String columnName;
    private int displayType;        // From DisplayType constants
    private String referenceTable;  // For TableDir references
    private int fieldLength;
    private boolean mandatory;
    private String defaultValue;
    private String description;

    // Validation
    private List<String> validationErrors = new ArrayList<>();

    public static ColumnSpec fromPrefix(String prefixDef) {
        // Parse "S#Name" → ColumnSpec with displayType=10, length=60
        // Parse "ID#C_BPartner_ID" → ColumnSpec with displayType=19, ref=C_BPartner
    }
}

WindowSpec

/**
 * In-memory specification for a window.
 */
public class WindowSpec {
    private String windowName;
    private String windowType = "M";  // Maintain
    private String entityType = "U";
    private List<TabSpec> tabs = new ArrayList<>();
    private boolean includeMenu;
}

Validation Rules

1. Core Class Validations (iDempiere Rules)

Rule Source Class Validation
Table name format MTable Must match ^[A-Z][A-Za-z0-9_]*$
Table name length MTable Max 40 characters
Column name format MColumn Must match ^[A-Z][A-Za-z0-9_]*$
Column name length MColumn Max 40 characters
DisplayType valid DisplayType Must be known type (10, 11, 12, etc.)
Reference exists MColumn TableDir reference must exist in AD
Field length MColumn Must match DisplayType constraints
Key column MTable Must have {TableName}_ID column
UUID column MTable Must have {TableName}_UU column (v11+)

2. Naming Convention Validations

Rule Validation
Custom prefix Custom tables should use XX_ or entity-specific prefix
No reserved names Cannot use AD_, C_, M_ for custom tables
Unique name Table/column name not already in AD

3. Format Validations

Format Validations
2Pack XML Well-formed XML, required elements, valid UUIDs
Migration SQL Valid PostgreSQL syntax, proper escaping
Java Model Valid class name, imports, compilation

Validation Service

@ApplicationScoped
public class PreflightValidationService {

    @Inject
    ADRegistryService registryService;  // For checking existing elements

    /**
     * Validate a TableSpec against all rules.
     */
    public ValidationResult validateTable(TableSpec spec) {
        ValidationResult result = new ValidationResult();

        // 1. Core class validations
        validateTableName(spec, result);
        validateColumns(spec, result);
        validateKeyColumn(spec, result);

        // 2. Naming conventions
        validateNamingConventions(spec, result);

        // 3. Uniqueness (requires DB check)
        validateUniqueness(spec, result);

        // 4. Preview outputs
        if (result.canProceed()) {
            result.setPreviewSql(spec.toMigrationSql());
            result.setPreview2Pack(spec.to2PackXml());
        }

        return result;
    }

    /**
     * Validate column against MColumn rules.
     */
    private void validateColumn(ColumnSpec col, ValidationResult result) {
        // DisplayType validation
        if (!DisplayType.isValid(col.getDisplayType())) {
            result.addError("Column " + col.getName() +
                ": Invalid DisplayType " + col.getDisplayType());
        }

        // Length validation
        int maxLength = DisplayType.getMaxLength(col.getDisplayType());
        if (col.getFieldLength() > maxLength) {
            result.addWarning("Column " + col.getName() +
                ": Length " + col.getFieldLength() +
                " exceeds recommended " + maxLength);
        }

        // Reference validation for TableDir
        if (col.getDisplayType() == DisplayType.TableDir) {
            if (!registryService.tableExists(col.getReferenceTable())) {
                result.addError("Column " + col.getName() +
                    ": Reference table " + col.getReferenceTable() + " not found");
            }
        }
    }
}

ValidationResult

/**
 * Result of preflight validation.
 */
public class ValidationResult {
    private boolean valid = true;
    private List<ValidationError> errors = new ArrayList<>();
    private List<ValidationWarning> warnings = new ArrayList<>();

    // Preview outputs (generated if validation passes)
    private String previewSql;
    private String preview2Pack;
    private String previewJava;

    public boolean canProceed() {
        return errors.isEmpty();
    }

    public boolean hasWarnings() {
        return !warnings.isEmpty();
    }

    public record ValidationError(
        String field,
        String message,
        String rule,       // e.g., "MColumn.COLUMNNAME_LENGTH"
        String suggestion  // e.g., "Shorten to 40 characters"
    ) {}

    public record ValidationWarning(
        String field,
        String message,
        String rule
    ) {}
}

Integration with Workflows (ADR-074)

@ApplicationScoped
public class WfDeployToolLogic {

    @Inject
    PreflightValidationService validationService;

    /**
     * Plan deployment with full validation (Nx-style dry-run).
     */
    public ToolResult wfPlanDeployTable(String tableName, String columns, ...) {

        // 1. Parse into in-memory model
        TableSpec spec = TableSpec.parse(tableName, columns);

        // 2. Run preflight validation
        ValidationResult validation = validationService.validateTable(spec);

        // 3. Return plan with validation results
        if (!validation.canProceed()) {
            return ToolResult.error("Validation failed")
                .data("errors", validation.getErrors())
                .data("suggestions", validation.getSuggestions());
        }

        // 4. Return plan with previews
        return ToolResult.success("Plan validated")
            .data("plan", buildPlan(spec))
            .data("previewSql", validation.getPreviewSql())
            .data("preview2Pack", validation.getPreview2Pack());
    }

    /**
     * Execute only if validation passes.
     */
    public ToolResult wfDeployTable(String tableName, String columns, ...) {

        // 1. Parse and validate
        TableSpec spec = TableSpec.parse(tableName, columns);
        ValidationResult validation = validationService.validateTable(spec);

        if (!validation.canProceed()) {
            return ToolResult.error("Cannot execute: validation failed")
                .data("errors", validation.getErrors());
        }

        // 2. Warn about warnings but proceed
        if (validation.hasWarnings()) {
            log.warning("Proceeding with warnings: " + validation.getWarnings());
        }

        // 3. Execute with validated spec
        return executeDeployment(spec);
    }
}

DisplayType Validation Reference

Based on iDempiere's DisplayType.java:

Prefix DisplayType ID Max Length Validation
S# String 10 60 (default) Length 1-2000
T# Text 14 2000 Any length
M# Memo 34 unlimited -
I# Integer 11 - Numeric
N# Number 22 - Decimal
A# Amount 12 - Currency precision
Q# Quantity 29 - Decimal
Y# YesNo 20 1 'Y' or 'N'
D# Date 15 - Date format
d# DateTime 16 - Timestamp
L# List 17 - Must have AD_Reference
ID# TableDir 19 - Reference table must exist
B# Binary 23 - BLOB
U# URL 40 2000 URL format

Migration Validation Rules

For migration scripts, additional validations:

public class MigrationValidator {

    /**
     * Validate SQL migration script syntax.
     */
    public ValidationResult validateMigration(String sql) {
        ValidationResult result = new ValidationResult();

        // 1. PostgreSQL syntax check (basic)
        if (!sql.contains("INSERT INTO") && !sql.contains("ALTER TABLE")) {
            result.addWarning("No INSERT or ALTER statements found");
        }

        // 2. AD_Sequence check
        if (sql.contains("nextval") && !sql.contains("AD_Sequence")) {
            result.addError("Using nextval without AD_Sequence update");
        }

        // 3. UUID format
        Pattern uuidPattern = Pattern.compile(
            "'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'"
        );
        // Validate all UUIDs are proper format

        // 4. Reserved words
        if (sql.matches("(?i).*\\b(DROP|TRUNCATE|DELETE FROM AD_).*")) {
            result.addError("Dangerous operation on core AD tables");
        }

        return result;
    }
}

2Pack Validation Rules

For 2Pack XML, validate:

public class TwoPackValidator {

    public ValidationResult validate2Pack(String xml) {
        ValidationResult result = new ValidationResult();

        // 1. XML well-formed
        try {
            DocumentBuilder builder = DocumentBuilderFactory.newInstance()
                .newDocumentBuilder();
            Document doc = builder.parse(new InputSource(new StringReader(xml)));
        } catch (Exception e) {
            result.addError("Invalid XML: " + e.getMessage());
            return result;
        }

        // 2. Required elements
        if (!xml.contains("<AD_Package_Exp>")) {
            result.addError("Missing AD_Package_Exp element");
        }

        // 3. UUID format in elements
        // Validate all AD_*_UU values are proper UUID format

        // 4. Reference integrity
        // TableDir columns reference tables that are included or exist

        return result;
    }
}

Implementation Roadmap

Phase 1: Core Model Classes

Phase 2: Validation Rules

Phase 3: Format Validators

Phase 4: Integration


Consequences

Positive

  1. Fail-fast - Errors caught before any DB changes
  2. Better UX - Clear validation messages with suggestions
  3. Atomic - No partial failures in database
  4. Preview - See generated outputs before execution
  5. Testable - Validation logic fully unit-testable

Negative

  1. Complexity - Additional abstraction layer
  2. Maintenance - Must keep rules in sync with iDempiere core
  3. Performance - Extra parsing step before execution

Mitigation


References


Document Status: Proposed Next Step: Review and approve, then implement Phase 1 (Core Model Classes)

Path: /docs/developers/architecture/idempiere-hub/075-in-memory-preflight-validation