001package gudusoft.gsqlparser.util;
002
003import java.util.Arrays;
004import java.util.Collections;
005import java.util.HashSet;
006import java.util.Locale;
007import java.util.Set;
008
009/**
010 * The single place that decides whether an Oracle identifier is a documented
011 * <em>pseudocolumn</em> rather than a reference to a column of a table.
012 *
013 * <p>Every caller that either publishes a classification
014 * ({@code TCustomSqlStatement.linkColumnReferenceToTable}) or answers a lookup
015 * ({@code TObjectName.isValidColumnName}, {@code resolver2.ScopeBuilder}) must
016 * go through this class. Keeping two hand-synchronised copies of the list is
017 * what let {@code NEXTVAL} and {@code CURRVAL} drift apart in Mantis #4675: the
018 * lookup path knew about {@code NEXTVAL} and the publish path did not, so the
019 * two answered differently for the same expression.</p>
020 *
021 * <h3>Why the list is split into groups instead of being flat</h3>
022 *
023 * <p>Oracle documents 18 pseudocolumns, but only some of those names are
024 * reserved words. A name that is <em>not</em> reserved is a perfectly legal
025 * column name, and several are common ones: {@code OBJECT_ID} is a real column
026 * of {@code ALL_OBJECTS} and {@code DBA_OBJECTS}. Classifying such a name as a
027 * pseudocolumn on sight would delete correct lineage for
028 * {@code SELECT OBJECT_ID FROM ALL_OBJECTS} while producing no error anywhere -
029 * exactly the silent false negative the project forbids.</p>
030 *
031 * <p>So a name is only treated as a pseudocolumn when that is <em>provable</em>
032 * from the statement itself:</p>
033 *
034 * <ul>
035 *   <li>{@link #isReservedPseudoColumn} - the name is an Oracle reserved word,
036 *       so an unquoted occurrence can never be a column. Always safe.</li>
037 *   <li>{@link #isConnectByPseudoColumn} - only meaningful under a
038 *       {@code CONNECT BY}; the caller supplies that fact.</li>
039 *   <li>{@link #isSequencePseudoColumn} - only meaningful when qualified by
040 *       something that is not a table or alias in scope.</li>
041 * </ul>
042 *
043 * <p>Names that are neither reserved nor context-provable ({@code ORA_ROWSCN},
044 * {@code OBJECT_ID}, {@code OBJECT_VALUE}, {@code XMLDATA},
045 * {@code COLUMN_VALUE}) are deliberately absent. Without catalog metadata there
046 * is no way to tell them from a real column of the same name, and guessing in
047 * either direction is a defect.</p>
048 *
049 * @see <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Pseudocolumns.html">Oracle SQL Language Reference - Pseudocolumns</a>
050 */
051public final class OraclePseudoColumnUtil {
052
053    private OraclePseudoColumnUtil() {
054    }
055
056    /**
057     * Reserved-word pseudocolumns that belong to the <em>query</em>, not to any
058     * one table. Nothing sensible can be said about which table a
059     * {@code ROWNUM} came from, so these are detached from the FROM clause
060     * entirely.
061     */
062    private static final Set<String> DETACHED_RESERVED_PSEUDO_COLUMNS = unmodifiableUpperSet(
063            "ROWNUM",
064            "LEVEL");
065
066    /**
067     * Reserved-word pseudocolumns that belong to one specific table. A
068     * {@code ROWID} is the physical address of a row <em>of a particular
069     * table</em>, so unlike {@code ROWNUM} the table attribution is real
070     * information and is kept.
071     */
072    private static final Set<String> TABLE_SCOPED_PSEUDO_COLUMNS = unmodifiableUpperSet(
073            "ROWID");
074
075    /**
076     * Pseudocolumns of the hierarchical query clause. These names are not
077     * reserved, so they are only pseudocolumns when the query actually has a
078     * {@code CONNECT BY}.
079     */
080    private static final Set<String> CONNECT_BY_PSEUDO_COLUMNS = unmodifiableUpperSet(
081            "CONNECT_BY_ISLEAF",
082            "CONNECT_BY_ISCYCLE");
083
084    /**
085     * Sequence pseudocolumns, written {@code [schema.]sequence.NEXTVAL|CURRVAL}.
086     * Not reserved, so they are only pseudocolumns when the qualifier is not a
087     * table or alias in scope.
088     */
089    private static final Set<String> SEQUENCE_PSEUDO_COLUMNS = unmodifiableUpperSet(
090            "NEXTVAL",
091            "CURRVAL");
092
093    /**
094     * Pseudocolumns of the flashback {@code VERSIONS BETWEEN} clause. Not
095     * reserved, so they are only pseudocolumns when the FROM clause carries a
096     * version query.
097     */
098    private static final Set<String> VERSIONS_PSEUDO_COLUMNS = unmodifiableUpperSet(
099            "VERSIONS_STARTSCN",
100            "VERSIONS_STARTTIME",
101            "VERSIONS_ENDSCN",
102            "VERSIONS_ENDTIME",
103            "VERSIONS_XID",
104            "VERSIONS_OPERATION");
105
106    /**
107     * True when {@code nameOnly} is a pseudocolumn whose name is an Oracle
108     * reserved word, and therefore never a legal unquoted column name.
109     *
110     * <p>The caller is responsible for checking that the identifier was not
111     * quoted: {@code "ROWID"} is a distinct, legal, case-sensitive column
112     * name.</p>
113     *
114     * @param nameOnly the single unqualified name segment, without quotes
115     * @return true if this is always a pseudocolumn
116     */
117    public static boolean isReservedPseudoColumn(String nameOnly) {
118        return isDetachedReservedPseudoColumn(nameOnly)
119                || isTableScopedPseudoColumn(nameOnly);
120    }
121
122    /**
123     * True for a reserved-word pseudocolumn that belongs to the query rather
124     * than to a table ({@code ROWNUM}, {@code LEVEL}). These carry no table
125     * attribution at all.
126     *
127     * @param nameOnly the single unqualified name segment, without quotes
128     * @return true if this pseudocolumn has no owning table
129     */
130    public static boolean isDetachedReservedPseudoColumn(String nameOnly) {
131        return contains(DETACHED_RESERVED_PSEUDO_COLUMNS, nameOnly);
132    }
133
134    /**
135     * True for a reserved-word pseudocolumn that belongs to one specific table
136     * ({@code ROWID}).
137     *
138     * <p>These keep {@code getSourceTable()} and their entry in the table's
139     * linked columns - a {@code ROWID} really did come from that table, and
140     * dropping the attribution would lose information lineage consumers use.
141     * What changes is the reported {@link gudusoft.gsqlparser.EDbObjectType}:
142     * {@code notAColumn} rather than {@code column}, so nothing tries to
143     * resolve the name against a catalog that will never contain it.</p>
144     *
145     * @param nameOnly the single unqualified name segment, without quotes
146     * @return true if this pseudocolumn is scoped to a single table
147     */
148    public static boolean isTableScopedPseudoColumn(String nameOnly) {
149        return contains(TABLE_SCOPED_PSEUDO_COLUMNS, nameOnly);
150    }
151
152    /**
153     * True when {@code nameOnly} is a hierarchical-query pseudocolumn
154     * ({@code CONNECT_BY_ISLEAF}, {@code CONNECT_BY_ISCYCLE}).
155     *
156     * <p>Callers must additionally establish that the enclosing query has a
157     * {@code CONNECT BY} clause; without one the same name is an ordinary
158     * identifier and may well be a real column.</p>
159     *
160     * @param nameOnly the single unqualified name segment, without quotes
161     * @return true if this is a CONNECT BY pseudocolumn name
162     */
163    public static boolean isConnectByPseudoColumn(String nameOnly) {
164        return contains(CONNECT_BY_PSEUDO_COLUMNS, nameOnly);
165    }
166
167    /**
168     * True when {@code nameOnly} is a sequence pseudocolumn
169     * ({@code NEXTVAL}, {@code CURRVAL}).
170     *
171     * <p>Callers must additionally establish that the qualifier is a sequence
172     * rather than a table or alias in scope; {@code x.CURRVAL} where {@code x}
173     * is a table alias is a reference to a column named {@code CURRVAL}.</p>
174     *
175     * @param nameOnly the single unqualified name segment, without quotes
176     * @return true if this is a sequence pseudocolumn name
177     */
178    public static boolean isSequencePseudoColumn(String nameOnly) {
179        return contains(SEQUENCE_PSEUDO_COLUMNS, nameOnly);
180    }
181
182    /**
183     * True when {@code nameOnly} is a flashback version query pseudocolumn
184     * ({@code VERSIONS_STARTSCN} and friends).
185     *
186     * <p>Callers must additionally establish that the FROM clause carries a
187     * {@code VERSIONS BETWEEN} clause.</p>
188     *
189     * @param nameOnly the single unqualified name segment, without quotes
190     * @return true if this is a version query pseudocolumn name
191     */
192    public static boolean isVersionsPseudoColumn(String nameOnly) {
193        return contains(VERSIONS_PSEUDO_COLUMNS, nameOnly);
194    }
195
196    private static boolean contains(Set<String> names, String nameOnly) {
197        if (nameOnly == null || nameOnly.length() == 0) return false;
198        return names.contains(nameOnly.toUpperCase(Locale.ROOT));
199    }
200
201    private static Set<String> unmodifiableUpperSet(String... names) {
202        return Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(names)));
203    }
204}