001package gudusoft.gsqlparser.catalog.input.model;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
005import gudusoft.gsqlparser.sqlenv.IdentifierInputForm;
006import gudusoft.gsqlparser.sqlenv.TSQLEnv;
007
008import java.util.ArrayList;
009import java.util.Collections;
010import java.util.List;
011import java.util.Objects;
012
013/**
014 * Top-level immutable catalog model produced by {@code CatalogInputReader}s and consumed by
015 * the runtime + bridge layers.
016 *
017 * <p>Plan §6 (artifact layout) / §7.1 (schema). Format-agnostic: JSON / YAML / DDL / dbt
018 * manifests / vendor dumps all funnel into this shape.</p>
019 *
020 * <p>The build-time checks here cover only structural invariants. Deeper consistency
021 * (duplicate qualified names, identifier round-trip, table↔column referential integrity)
022 * is enforced by {@code CatalogModelValidator} per plan §9.6 — see Round 3 (T1B.2).</p>
023 */
024public final class UnifiedCatalogModel {
025
026    /** Default {@code apiVersion} carried by manifests that don't set one. Plan Q9. */
027    public static final String DEFAULT_API_VERSION = "1";
028
029    private final String apiVersion;
030    private final EDbVendor vendor;
031    private final IdentifierConfig identifierConfig;
032    private final DefaultsConfig defaults;
033    private final List<CatalogModel> catalogs;
034    private final CatalogSourceInfo sourceInfo;
035    private final IdentifierInputForm nameForm;
036    private final boolean nameFormDeclared;
037
038    private UnifiedCatalogModel(Builder b) {
039        if (b.vendor == null) {
040            throw new IllegalArgumentException("UnifiedCatalogModel.vendor is required");
041        }
042        this.vendor = b.vendor;
043        this.apiVersion = (b.apiVersion == null || b.apiVersion.isEmpty())
044            ? DEFAULT_API_VERSION
045            : b.apiVersion;
046        this.identifierConfig = (b.identifierConfig == null)
047            ? IdentifierConfig.defaultsFor(b.vendor)
048            : b.identifierConfig;
049        this.defaults = (b.defaults == null) ? DefaultsConfig.empty() : b.defaults;
050        this.catalogs = Collections.unmodifiableList(new ArrayList<CatalogModel>(b.catalogs));
051        this.sourceInfo = b.sourceInfo;
052        this.nameForm = (b.nameForm == null) ? IdentifierInputForm.SQL_LEXICAL : b.nameForm;
053        this.nameFormDeclared = b.nameFormDeclared && b.nameForm != null;
054    }
055
056    public static Builder builder() {
057        return new Builder();
058    }
059
060    public String apiVersion() {
061        return apiVersion;
062    }
063
064    public EDbVendor vendor() {
065        return vendor;
066    }
067
068    public IdentifierConfig identifierConfig() {
069        return identifierConfig;
070    }
071
072    public DefaultsConfig defaults() {
073        return defaults;
074    }
075
076    public List<CatalogModel> catalogs() {
077        return catalogs;
078    }
079
080    /** Provenance of this model. May be {@code null} for in-memory / programmatic builds. */
081    public CatalogSourceInfo sourceInfo() {
082        return sourceInfo;
083    }
084
085    /**
086     * How every object name in this model is written: as SQL text
087     * ({@link IdentifierInputForm#SQL_LEXICAL}, {@code "Amount"} / {@code [Order ID]}, an
088     * undelimited name folds the way the vendor folds it) or as the name the database
089     * catalog holds ({@link IdentifierInputForm#CATALOG_STORED}, {@code Amount} IS the
090     * stored name). One form per model: a single input must not mix the two.
091     *
092     * <p>A model built in code defaults to {@code SQL_LEXICAL}, which is how names were
093     * always treated. {@code JsonManifestCatalogInputReader} defaults a manifest file to
094     * {@code CATALOG_STORED}; a file overrides either default with a root {@code nameForm}.
095     * See docs/refactor/identifier/README.md, "Catalog inputs and name output".</p>
096     */
097    public IdentifierInputForm nameForm() {
098        return nameForm;
099    }
100
101    /**
102     * Whether the input stated its {@link #nameForm()} itself, as opposed to receiving the
103     * format's default. The validator uses it: a name that the two forms read differently is
104     * worth a warning only while nobody has said which form was meant.
105     */
106    public boolean nameFormDeclared() {
107        return nameFormDeclared;
108    }
109
110    /**
111     * The SQL spelling of one of this model's names, per its {@link #nameForm()}. Every consumer
112     * of a model reads names as SQL text (the validator, {@code SQLEnvCatalogLoader}, the runtime
113     * snapshot built by {@code ModelBackedCatalogProvider}), so each of them passes a name
114     * through here first. A {@code SQL_LEXICAL} name comes back unchanged; a
115     * {@code CATALOG_STORED} name comes back as the delimited spelling of the same stored name
116     * ({@link TSQLEnv#toLexical}). Keeping the rule here is what stops one consumer from
117     * honouring the declaration while another ignores it.
118     *
119     * <p>Only single names follow the form. A dotted reference
120     * ({@code SynonymModel.targetQualifiedName}, {@code ConstraintModel.referencedTable}) is SQL
121     * text by construction: a stored name may itself contain a dot, so stored names joined with
122     * dots could not be split back.</p>
123     */
124    public String lexical(ESQLDataObjectType role, String name) {
125        return TSQLEnv.toLexical(vendor, role, name, nameForm);
126    }
127
128    @Override
129    public boolean equals(Object o) {
130        if (this == o) return true;
131        if (!(o instanceof UnifiedCatalogModel)) return false;
132        UnifiedCatalogModel that = (UnifiedCatalogModel) o;
133        return apiVersion.equals(that.apiVersion)
134            && vendor == that.vendor
135            && identifierConfig.equals(that.identifierConfig)
136            && defaults.equals(that.defaults)
137            && catalogs.equals(that.catalogs)
138            && Objects.equals(sourceInfo, that.sourceInfo)
139            && nameForm == that.nameForm;
140    }
141
142    @Override
143    public int hashCode() {
144        return Objects.hash(apiVersion, vendor, identifierConfig, defaults, catalogs, sourceInfo, nameForm);
145    }
146
147    @Override
148    public String toString() {
149        return "UnifiedCatalogModel{apiVersion=" + apiVersion
150            + ", vendor=" + vendor
151            + ", catalogs=" + catalogs.size()
152            + ", sourceInfo=" + sourceInfo + ", nameForm=" + nameForm + '}';
153    }
154
155    public static final class Builder {
156
157        private String apiVersion;
158        private EDbVendor vendor;
159        private IdentifierConfig identifierConfig;
160        private DefaultsConfig defaults;
161        private final List<CatalogModel> catalogs = new ArrayList<CatalogModel>();
162        private CatalogSourceInfo sourceInfo;
163        private IdentifierInputForm nameForm;
164        private boolean nameFormDeclared;
165
166        private Builder() {
167        }
168
169        public Builder apiVersion(String v) {
170            this.apiVersion = v;
171            return this;
172        }
173
174        public Builder vendor(EDbVendor v) {
175            this.vendor = v;
176            return this;
177        }
178
179        public Builder identifierConfig(IdentifierConfig v) {
180            this.identifierConfig = v;
181            return this;
182        }
183
184        public Builder defaults(DefaultsConfig v) {
185            this.defaults = v;
186            return this;
187        }
188
189        public Builder addCatalog(CatalogModel v) {
190            if (v == null) {
191                throw new IllegalArgumentException("UnifiedCatalogModel catalog may not be null");
192            }
193            this.catalogs.add(v);
194            return this;
195        }
196
197        public Builder sourceInfo(CatalogSourceInfo v) {
198            this.sourceInfo = v;
199            return this;
200        }
201
202        /** See {@link UnifiedCatalogModel#nameForm()}; {@code null} means {@code SQL_LEXICAL}. */
203        public Builder nameForm(IdentifierInputForm v) {
204            this.nameForm = v;
205            this.nameFormDeclared = v != null;
206            return this;
207        }
208
209        /**
210         * The form to use when the input did not declare one: sets {@link #nameForm()} without
211         * marking it as declared. For readers, which know their format's default.
212         */
213        public Builder defaultNameForm(IdentifierInputForm v) {
214            if (!this.nameFormDeclared) {
215                this.nameForm = v;
216            }
217            return this;
218        }
219
220        public UnifiedCatalogModel build() {
221            return new UnifiedCatalogModel(this);
222        }
223    }
224}