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}