001package gudusoft.gsqlparser.catalog.input.model; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.LinkedHashMap; 006import java.util.List; 007import java.util.Map; 008import java.util.Objects; 009 010/** 011 * Table entry inside a {@link SchemaModel}. 012 * 013 * <p>Plan ยง6. {@link #properties()} is the vendor-extension escape hatch 014 * (per plan R6 / Q15) that carries adapter-specific metadata (Hive table 015 * properties, Iceberg snapshot id, OpenMetadata tags, etc.) until a field 016 * earns promotion to a typed accessor.</p> 017 * 018 * <p>Property values held in the map are frozen on build at one level of 019 * depth: {@link List} values are wrapped via {@link Collections#unmodifiableList}, 020 * {@link Map} values via {@link Collections#unmodifiableMap}. Adapters that 021 * stash deeper nested mutable structures (e.g. a {@code List<Map<...>>} whose 022 * inner maps remain mutable) are still expected to supply value objects that 023 * are themselves immutable; the model does not deep-copy.</p> 024 */ 025public final class TableModel { 026 027 private final String name; 028 private final List<ColumnModel> columns; 029 private final List<ConstraintModel> constraints; 030 private final List<IndexModel> indexes; 031 private final Map<String, Object> properties; 032 033 private TableModel(Builder b) { 034 if (b.name == null || b.name.isEmpty()) { 035 throw new IllegalArgumentException("TableModel.name is required"); 036 } 037 this.name = b.name; 038 this.columns = Collections.unmodifiableList(new ArrayList<ColumnModel>(b.columns)); 039 this.constraints = Collections.unmodifiableList(new ArrayList<ConstraintModel>(b.constraints)); 040 this.indexes = Collections.unmodifiableList(new ArrayList<IndexModel>(b.indexes)); 041 // LinkedHashMap to preserve insertion order for deterministic toString / golden output. 042 // Freeze List / Map values on the way in so a caller mutating their original 043 // reference cannot disturb the model's equals/hashCode contract. 044 LinkedHashMap<String, Object> frozen = new LinkedHashMap<String, Object>(b.properties.size()); 045 for (Map.Entry<String, Object> e : b.properties.entrySet()) { 046 frozen.put(e.getKey(), freezePropertyValue(e.getValue())); 047 } 048 this.properties = Collections.unmodifiableMap(frozen); 049 } 050 051 @SuppressWarnings({"unchecked", "rawtypes"}) 052 private static Object freezePropertyValue(Object value) { 053 if (value instanceof List) { 054 return Collections.unmodifiableList(new ArrayList((List) value)); 055 } 056 if (value instanceof Map) { 057 return Collections.unmodifiableMap(new LinkedHashMap((Map) value)); 058 } 059 return value; 060 } 061 062 public static Builder builder() { 063 return new Builder(); 064 } 065 066 public String name() { 067 return name; 068 } 069 070 public List<ColumnModel> columns() { 071 return columns; 072 } 073 074 public List<ConstraintModel> constraints() { 075 return constraints; 076 } 077 078 public List<IndexModel> indexes() { 079 return indexes; 080 } 081 082 public Map<String, Object> properties() { 083 return properties; 084 } 085 086 @Override 087 public boolean equals(Object o) { 088 if (this == o) return true; 089 if (!(o instanceof TableModel)) return false; 090 TableModel that = (TableModel) o; 091 return name.equals(that.name) 092 && columns.equals(that.columns) 093 && constraints.equals(that.constraints) 094 && indexes.equals(that.indexes) 095 && properties.equals(that.properties); 096 } 097 098 @Override 099 public int hashCode() { 100 return Objects.hash(name, columns, constraints, indexes, properties); 101 } 102 103 @Override 104 public String toString() { 105 return "TableModel{name=" + name 106 + ", cols=" + columns.size() 107 + ", constraints=" + constraints.size() 108 + ", indexes=" + indexes.size() 109 + ", props=" + properties.size() + '}'; 110 } 111 112 public static final class Builder { 113 114 private String name; 115 private final List<ColumnModel> columns = new ArrayList<ColumnModel>(); 116 private final List<ConstraintModel> constraints = new ArrayList<ConstraintModel>(); 117 private final List<IndexModel> indexes = new ArrayList<IndexModel>(); 118 private final Map<String, Object> properties = new LinkedHashMap<String, Object>(); 119 120 private Builder() { 121 } 122 123 public Builder name(String v) { 124 this.name = v; 125 return this; 126 } 127 128 public Builder addColumn(ColumnModel v) { 129 if (v == null) { 130 throw new IllegalArgumentException("TableModel column may not be null"); 131 } 132 this.columns.add(v); 133 return this; 134 } 135 136 public Builder addConstraint(ConstraintModel v) { 137 if (v == null) { 138 throw new IllegalArgumentException("TableModel constraint may not be null"); 139 } 140 this.constraints.add(v); 141 return this; 142 } 143 144 public Builder addIndex(IndexModel v) { 145 if (v == null) { 146 throw new IllegalArgumentException("TableModel index may not be null"); 147 } 148 this.indexes.add(v); 149 return this; 150 } 151 152 public Builder property(String key, Object value) { 153 if (key == null) { 154 throw new IllegalArgumentException("TableModel property key may not be null"); 155 } 156 this.properties.put(key, value); 157 return this; 158 } 159 160 public TableModel build() { 161 return new TableModel(this); 162 } 163 } 164}