001package gudusoft.gsqlparser.sqlenv; 002 003import gudusoft.gsqlparser.TBaseType; 004import gudusoft.gsqlparser.nodes.TTypeName; 005import gudusoft.gsqlparser.util.SQLUtil; 006 007import java.util.*; 008 009/** 010 * SQL table, includes a list of columns. 011 */ 012public class TSQLTable extends TSQLSchemaObject { 013 014 // IdentifierService for consistent key generation (lazy initialization). 015 // volatile: concurrent lazy init may build duplicate (equivalent) instances, 016 // but must never publish a partially constructed one to another thread. 017 private volatile IdentifierService identifierService; 018 019 // ===== Phase 6: Table seal mechanism for thread-safety hardening ===== 020 /** 021 * Seal state flag (volatile for thread visibility). No explicit initializer: 022 * `this` escapes the base constructor into shared env indexes, so a subclass 023 * initializer could overwrite a seal() issued by a concurrent early reader. 024 */ 025 private volatile boolean sealed; 026 027 /** 028 * Seal this table to prevent further modifications (Phase 6). 029 * 030 * <p>See {@link TSQLEnv#seal()} for detailed documentation. 031 */ 032 public void seal() { 033 this.sealed = true; 034 } 035 036 /** 037 * Check if this table is sealed (Phase 6). 038 * 039 * @return true if sealed, false otherwise 040 */ 041 public boolean isSealed() { 042 return sealed; 043 } 044 045 /** 046 * Check if table is not sealed, throw exception if sealed and enforcement is enabled. 047 * 048 * @throws IllegalStateException if sealed and TBaseType.ENFORCE_CATALOG_SEAL is true 049 */ 050 private void checkNotSealed() { 051 if (sealed && TBaseType.ENFORCE_CATALOG_SEAL) { 052 throw new IllegalStateException("Cannot modify sealed table. Call seal() marks table as read-only."); 053 } 054 } 055 056 /** 057 * Get or create IdentifierService (lazy initialization) 058 */ 059 private IdentifierService getIdentifierService() { 060 if (identifierService == null) { 061 IdentifierProfile profile = IdentifierProfile.forVendor( 062 this.sqlEnv.getDBVendor(), 063 IdentifierProfile.VendorFlags.defaults() 064 ); 065 identifierService = new IdentifierService(profile, null); 066 } 067 return identifierService; 068 } 069 070 // volatile, no explicit initializer: read lock-free by lineage/export paths 071 // while addView on another thread sets it (and see the `sealed` note above) 072 private volatile boolean isView; 073 074 public void setView(boolean view) { 075 isView = view; 076 } 077 078 /** 079 * Used to check whether this is a view. 080 * 081 * @return true if this is a view. 082 */ 083 public boolean isView() { 084 return isView; 085 } 086 087 // volatile: written during registration, read lock-free by other threads 088 private volatile String definition; 089 090 public void setDefinition(String definition) { 091 this.definition = definition; 092 } 093 094 /** 095 * This is the script that used to create this view( {@link #isView()} returns true). 096 * 097 * @return sql script that used to create this view. 098 */ 099 public String getDefinition() { 100 return definition; 101 } 102 103 /** 104 * create a new table belong to a schema 105 * 106 * @param sqlSchema schema 107 * @param tableName table name 108 */ 109 public TSQLTable(TSQLSchema sqlSchema, String tableName){ 110 super(sqlSchema,tableName,ESQLDataObjectType.dotTable); 111 } 112 113 /** 114 * column list 115 * @return a column list 116 */ 117 public List<TSQLColumn> getColumnList() { 118 Set<TSQLColumn> columnList = new LinkedHashSet<>(); 119 Map<String, TSQLColumn> m = columnMap(); 120 synchronized (m) { 121 for(String column: m.keySet()){ 122 columnList.add(m.get(column)); 123 } 124 } 125 return new ArrayList<>(columnList); 126 } 127 128 // No field initializer: the TSQLSchemaObject constructor publishes `this` into 129 // shared env indexes before subclass initializers run, so a field initializer 130 // could overwrite a map a concurrent reader already lazily created. volatile + 131 // double-checked lazy init in columnMap() makes early access safe instead. 132 private volatile Map<String, TSQLColumn> columnMap; 133 134 private Map<String, TSQLColumn> columnMap() { 135 Map<String, TSQLColumn> m = columnMap; 136 if (m == null) { 137 synchronized (this) { 138 if (columnMap == null) { 139 columnMap = Collections.synchronizedMap(new LinkedHashMap<String, TSQLColumn>( )); 140 } 141 m = columnMap; 142 } 143 } 144 return m; 145 } 146 147 /** 148 * add a new column to the table 149 * 150 * @param columnName column name 151 */ 152 public void addColumn(String columnName){ 153 // search-then-put must be atomic: concurrent DDL registration on the same 154 // (now deduplicated) table instance races here otherwise 155 Map<String, TSQLColumn> m = columnMap(); 156 synchronized (m) { 157 checkNotSealed(); // Phase 6: checked under the mutation lock (seal() itself takes no monitor, so this narrows, not closes, the seal race) 158 if (!searchColumn(columnName)){ 159 TSQLColumn newColumn = new TSQLColumn(this, columnName); 160 161 // ===== Legacy path: Update columnMap with SQLUtil normalization ===== 162 String legacyKey = SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), columnName); 163 m.put(legacyKey, newColumn); 164 165 // ===== New path: Also write with IdentifierService key ===== 166 if (TBaseType.USE_HIERARCHICAL_INDEX) { 167 try { 168 String key = getIdentifierService().keyForMap(columnName, ESQLDataObjectType.dotColumn); 169 // Only write if the key is different from legacy key (avoid double-write) 170 if (!key.equals(legacyKey)) { 171 m.put(key, newColumn); 172 } 173 } catch (Throwable ignore) { 174 // Silently fail to maintain backward compatibility 175 } 176 } 177 178 } 179 } 180 } 181 182 public void addColumn(String columnName, TTypeName columnDataType){ 183 // search-then-put must be atomic (see addColumn(String)) 184 Map<String, TSQLColumn> m = columnMap(); 185 synchronized (m) { 186 checkNotSealed(); // Phase 6: checked under the mutation lock (seal() itself takes no monitor, so this narrows, not closes, the seal race) 187 if (!searchColumn(columnName)){ 188 TSQLColumn newColumn = new TSQLColumn(this, columnName, columnDataType); 189 190 // ===== Legacy path: Update columnMap with SQLUtil normalization ===== 191 String legacyKey = SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), columnName); 192 m.put(legacyKey, newColumn); 193 194 // ===== New path: Also write with IdentifierService key ===== 195 if (TBaseType.USE_HIERARCHICAL_INDEX) { 196 try { 197 String key = getIdentifierService().keyForMap(columnName, ESQLDataObjectType.dotColumn); 198 // Only write if the key is different from legacy key (avoid double-write) 199 if (!key.equalsIgnoreCase(legacyKey)) { 200 m.put(key, newColumn); 201 } 202 } catch (Throwable ignore) { 203 // Silently fail to maintain backward compatibility 204 } 205 } 206 } 207 } 208 } 209 210 public boolean searchColumn(String columnName){ 211 // Phase 2: Use IdentifierService when hierarchical index is enabled 212 if (TBaseType.USE_HIERARCHICAL_INDEX) { 213 try { 214 String key = getIdentifierService().keyForMap(columnName, ESQLDataObjectType.dotColumn); 215 if (columnMap().containsKey(key)) { 216 if (TBaseType.LOG_INDEX_HIT_RATE) { 217 System.out.println("[HierarchicalIndex] Table hit: column - " + columnName); 218 } 219 return true; 220 } 221 } catch (Throwable ignore) { 222 // Fall through to legacy path 223 } 224 } 225 226 // Legacy path: use SQLUtil normalization 227 return columnMap().containsKey(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), columnName)); 228 } 229 230 public TSQLColumn getColumn(String columnName){ 231 // Phase 2: Use IdentifierService when hierarchical index is enabled 232 if (TBaseType.USE_HIERARCHICAL_INDEX) { 233 try { 234 String key = getIdentifierService().keyForMap(columnName, ESQLDataObjectType.dotColumn); 235 TSQLColumn result = columnMap().get(key); 236 if (result != null) { 237 if (TBaseType.LOG_INDEX_HIT_RATE) { 238 System.out.println("[HierarchicalIndex] Table hit: column - " + columnName); 239 } 240 return result; 241 } 242 } catch (Throwable ignore) { 243 // Fall through to legacy path 244 } 245 } 246 247 // Legacy path: use SQLUtil normalization 248 return columnMap().get(SQLUtil.getIdentifierNormalColumnName(this.sqlEnv.getDBVendor(), columnName)); 249 } 250 251 252 public ArrayList<String> getColumns(boolean columnNameOnly ){ 253 ArrayList<String> columns = new ArrayList<>(); 254 Map<String, TSQLColumn> m = columnMap(); 255 synchronized (m) { 256 for(String column: m.keySet()){ 257 TSQLColumn s = m.get(column); 258 if (columnNameOnly){ 259 columns.add(s.name); 260 }else{ 261 columns.add(this.name+"."+ s.name); 262 } 263 264 // columns.add(s.name); 265 } 266 } 267 268 return columns; 269 } 270 271}