001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.TBaseType; 004import gudusoft.gsqlparser.nodes.TTypeName; 005 006import java.util.*; 007 008/** 009 * SQL table, includes a list of columns. 010 */ 011public class TSQLTable extends TSQLSchemaObject { 012 013 // IdentifierService for consistent key generation (lazy initialization). 014 // volatile: concurrent lazy init may build duplicate (equivalent) instances, 015 // but must never publish a partially constructed one to another thread. 016 private volatile IdentifierService identifierService; 017 018 // ===== Phase 6: Table seal mechanism for thread-safety hardening ===== 019 /** 020 * Seal state flag (volatile for thread visibility). No explicit initializer: 021 * `this` escapes the base constructor into shared env indexes, so a subclass 022 * initializer could overwrite a seal() issued by a concurrent early reader. 023 */ 024 private volatile boolean sealed; 025 026 /** 027 * Seal this table to prevent further modifications (Phase 6). 028 * 029 * <p>See {@link TSQLEnv#seal()} for detailed documentation. 030 */ 031 public void seal() { 032 this.sealed = true; 033 } 034 035 /** 036 * Check if this table is sealed (Phase 6). 037 * 038 * @return true if sealed, false otherwise 039 */ 040 public boolean isSealed() { 041 return sealed; 042 } 043 044 /** 045 * Check if table is not sealed, throw exception if sealed and enforcement is enabled. 046 * 047 * @throws IllegalStateException if sealed and TBaseType.ENFORCE_CATALOG_SEAL is true 048 */ 049 private void checkNotSealed() { 050 if (sealed && TBaseType.ENFORCE_CATALOG_SEAL) { 051 throw new IllegalStateException("Cannot modify sealed table. Call seal() marks table as read-only."); 052 } 053 } 054 055 /** 056 * The identifier service of the environment this object belongs to (lazy initialization). 057 * 058 * <p>Inherited, never rebuilt from the vendor: this object's local indexes are part of the 059 * same catalog the environment's {@link CatalogStore} indexes, so keying them under the 060 * vendor default while the environment ran a per-category configuration would let one half 061 * of a lookup answer a question the other half never asked. 062 */ 063 private IdentifierService getIdentifierService() { 064 if (identifierService == null) { 065 if (this.sqlEnv == null) { 066 // Every catalog object is created against an environment; with none there is no 067 // vendor and no configuration to inherit, so say so instead of guessing one. 068 throw new IllegalStateException(getClass().getSimpleName() 069 + " has no TSQLEnv, so it has no identifier configuration to compare with"); 070 } 071 identifierService = this.sqlEnv.getIdentifierService(); 072 } 073 return identifierService; 074 } 075 076 // volatile, no explicit initializer: read lock-free by lineage/export paths 077 // while addView on another thread sets it (and see the `sealed` note above) 078 private volatile boolean isView; 079 080 public void setView(boolean view) { 081 isView = view; 082 } 083 084 /** 085 * Used to check whether this is a view. 086 * 087 * @return true if this is a view. 088 */ 089 public boolean isView() { 090 return isView; 091 } 092 093 // volatile: written during registration, read lock-free by other threads 094 private volatile String definition; 095 096 public void setDefinition(String definition) { 097 this.definition = definition; 098 } 099 100 /** 101 * This is the script that used to create this view( {@link #isView()} returns true). 102 * 103 * @return sql script that used to create this view. 104 */ 105 public String getDefinition() { 106 return definition; 107 } 108 109 /** 110 * create a new table belong to a schema 111 * 112 * @param sqlSchema schema 113 * @param tableName table name 114 */ 115 public TSQLTable(TSQLSchema sqlSchema, String tableName){ 116 super(sqlSchema,tableName,ESQLDataObjectType.dotTable); 117 } 118 119 /** A table of {@code sqlSchema} that the schema does not hold yet ({@link #newVersion()}). */ 120 private TSQLTable(TSQLSchema sqlSchema, String tableName, boolean unregistered){ 121 super(sqlSchema,tableName,ESQLDataObjectType.dotTable,unregistered); 122 } 123 124 /** 125 * column list 126 * @return a column list 127 */ 128 public List<TSQLColumn> getColumnList() { 129 Map<CanonKey, TSQLColumn> m = columnMap(); 130 synchronized (m) { 131 return new ArrayList<>(m.values()); 132 } 133 } 134 135 // No field initializer: the TSQLSchemaObject constructor publishes `this` into 136 // shared env indexes before subclass initializers run, so a field initializer 137 // could overwrite a map a concurrent reader already lazily created. volatile + 138 // double-checked lazy init in columnMap() makes early access safe instead. 139 private volatile Map<CanonKey, TSQLColumn> columnMap; 140 141 private Map<CanonKey, TSQLColumn> columnMap() { 142 Map<CanonKey, TSQLColumn> m = columnMap; 143 if (m == null) { 144 synchronized (this) { 145 if (columnMap == null) { 146 columnMap = Collections.synchronizedMap(new LinkedHashMap<CanonKey, TSQLColumn>()); 147 } 148 m = columnMap; 149 } 150 } 151 return m; 152 } 153 154 /** 155 * add a new column to the table 156 * 157 * @param columnName column name 158 */ 159 public void addColumn(String columnName){ 160 addColumn(columnName, null); 161 } 162 163 public void addColumn(String columnName, TTypeName columnDataType){ 164 // search-then-put must be atomic (see addColumn(String)) 165 Map<CanonKey, TSQLColumn> m = columnMap(); 166 synchronized (m) { 167 checkNotSealed(); // Phase 6: checked under the mutation lock (seal() itself takes no monitor, so this narrows, not closes, the seal race) 168 if (!m.containsKey(columnKey(columnName, IdentifierInputForm.SQL_LEXICAL))){ 169 TSQLColumn newColumn = new TSQLColumn(this, columnName, columnDataType); 170 // getName() is already a stored name; never interpret it as SQL again. 171 m.put(columnKey(newColumn.getName(), IdentifierInputForm.CATALOG_STORED), newColumn); 172 } 173 } 174 } 175 176 /** 177 * Records that the table's width is not fully known: its definition or the lineage that wrote it back read a 178 * star ({@code SELECT *}) it could not expand. The entry is stored as {@code *} and marked 179 * ({@link TSQLColumn#isUnresolvedStar()}); it is not a column of the table, and a column the table already holds 180 * under that name (a column NAMED {@code *}) is left as it is. 181 */ 182 public void addUnresolvedStar(){ 183 Map<CanonKey, TSQLColumn> m = columnMap(); 184 synchronized (m) { 185 checkNotSealed(); 186 if (!m.containsKey(columnKey("*", IdentifierInputForm.SQL_LEXICAL))){ 187 TSQLColumn star = new TSQLColumn(this, "*"); 188 star.setUnresolvedStar(true); 189 m.put(columnKey(star.getName(), IdentifierInputForm.CATALOG_STORED), star); 190 } 191 } 192 } 193 194 /** 195 * Replaces this table in its schema with a new table of the same name, columns, kind and definition, and returns 196 * the new one. A statement that changes the table's columns (ALTER TABLE ... ADD COLUMN) changes the new table, 197 * so a statement placed before it keeps the table it found there, with the columns it had then. The new table is 198 * complete before the schema holds it. 199 * 200 * @return the table the schema now holds under this table's name 201 */ 202 public TSQLTable newVersion(){ 203 // Made from the spellings this table and its columns were made from, so each is stored and keyed as here. 204 TSQLTable next = new TSQLTable(getSchema(), getNameKeepCase(), true); 205 next.setPriority(getPriority()); 206 next.setView(isView()); 207 next.setDefinition(getDefinition()); 208 Map<CanonKey, TSQLColumn> m = next.columnMap(); 209 synchronized (m) { 210 for (TSQLColumn column : getColumnList()) { 211 TSQLColumn copy = new TSQLColumn(next, column.getNameKeepCase(), column.getColumnDataType()); 212 copy.setUnresolvedStar(column.isUnresolvedStar()); 213 m.put(next.columnKey(copy.getName(), IdentifierInputForm.CATALOG_STORED), copy); 214 } 215 } 216 getSchema().addSchemaObject(next); 217 return next; 218 } 219 220 public boolean searchColumn(String columnName){ 221 return getColumn(columnName) != null; 222 } 223 224 /** 225 * Whether every column of the table is known: it has columns, and none is a star it could not expand 226 * ({@link #addUnresolvedStar()}). Only then does {@link #searchColumn} returning false prove the table has no 227 * column of that name. 228 * 229 * @return true when the column list is complete 230 */ 231 public boolean hasAllColumnsKnown(){ 232 List<TSQLColumn> columns = getColumnList(); 233 if (columns.isEmpty()) return false; 234 for (TSQLColumn column : columns) { 235 if ((column == null) || column.isUnresolvedStar()) return false; 236 } 237 return true; 238 } 239 240 public TSQLColumn getColumn(String columnName){ 241 return columnMap().get(columnKey(columnName, IdentifierInputForm.SQL_LEXICAL)); 242 } 243 244 /** 245 * Gives the column {@code columnName} the data type {@code columnDataType} when the column has none: another 246 * definition of the table that knows the type completes the column, as when metadata from several files is 247 * merged. A type the column already has is kept, and nothing happens when the table has no such column. 248 * 249 * @param columnName column name, as for {@link #addColumn(String, TTypeName)} 250 * @param columnDataType the type; nothing happens when it is null 251 */ 252 public void fillColumnDataType(String columnName, TTypeName columnDataType){ 253 if (columnDataType == null) return; 254 Map<CanonKey, TSQLColumn> m = columnMap(); 255 synchronized (m) { 256 checkNotSealed(); 257 TSQLColumn column = m.get(columnKey(columnName, IdentifierInputForm.SQL_LEXICAL)); 258 if ((column != null) && (column.getColumnDataType() == null)){ 259 column.setColumnDataType(columnDataType); 260 } 261 } 262 } 263 264 private CanonKey columnKey(String name, IdentifierInputForm form) { 265 return getIdentifierService().canonKeyOf(name, ESQLDataObjectType.dotColumn, form); 266 } 267 268 public ArrayList<String> getColumns(boolean columnNameOnly ){ 269 ArrayList<String> columns = new ArrayList<>(); 270 Map<CanonKey, TSQLColumn> m = columnMap(); 271 synchronized (m) { 272 for(TSQLColumn s : m.values()){ 273 if (columnNameOnly){ 274 columns.add(s.name); 275 }else{ 276 columns.add(this.name+"."+ s.name); 277 } 278 } 279 } 280 281 return columns; 282 } 283 284}