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}