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}