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}