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}