001package gudusoft.gsqlparser.resolver2.model; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.List; 006 007/** 008 * Represents a field path for deep/record field access in structured types. 009 * 010 * <p>In BigQuery, Snowflake, and other databases with STRUCT/RECORD types, 011 * column references can include field paths like {@code customer.address.city}. 012 * This class captures the field path portion (segments beyond the base column).</p> 013 * 014 * <p>Design principles:</p> 015 * <ul> 016 * <li>Immutable - once created, cannot be modified</li> 017 * <li>Only stores segments beyond the base column (e.g., for {@code customer.address.city}, 018 * if base column is {@code customer}, fieldPath stores {@code ["address", "city"]})</li> 019 * <li>Empty fieldPath means no field access (regular column reference)</li> 020 * </ul> 021 * 022 * <p>Example:</p> 023 * <pre> 024 * -- SQL: SELECT customer.address.city FROM orders 025 * -- base column: customer 026 * -- fieldPath: ["address", "city"] 027 * </pre> 028 * 029 * @see ColumnSource#getFieldPath() 030 */ 031public class FieldPath { 032 033 /** Empty field path singleton for regular column references */ 034 public static final FieldPath EMPTY = new FieldPath(Collections.emptyList()); 035 036 /** The field path segments (beyond the base column) */ 037 private final List<String> segments; 038 039 /** 040 * Create a FieldPath from a list of segments. 041 * 042 * @param segments The field path segments (will be copied) 043 */ 044 public FieldPath(List<String> segments) { 045 if (segments == null || segments.isEmpty()) { 046 this.segments = Collections.emptyList(); 047 } else { 048 this.segments = Collections.unmodifiableList(new ArrayList<>(segments)); 049 } 050 } 051 052 /** 053 * Create a FieldPath from a single segment. 054 * 055 * @param segment The single field segment 056 * @return A new FieldPath with one segment 057 */ 058 public static FieldPath of(String segment) { 059 if (segment == null || segment.isEmpty()) { 060 return EMPTY; 061 } 062 return new FieldPath(Collections.singletonList(segment)); 063 } 064 065 /** 066 * Create a FieldPath from multiple segments. 067 * 068 * @param segments The field segments 069 * @return A new FieldPath 070 */ 071 public static FieldPath of(String... segments) { 072 if (segments == null || segments.length == 0) { 073 return EMPTY; 074 } 075 List<String> list = new ArrayList<>(segments.length); 076 for (String s : segments) { 077 if (s != null && !s.isEmpty()) { 078 list.add(s); 079 } 080 } 081 return list.isEmpty() ? EMPTY : new FieldPath(list); 082 } 083 084 /** 085 * Create a FieldPath from a list of segments. 086 * 087 * @param segments The field segments 088 * @return A new FieldPath, or EMPTY if null/empty 089 */ 090 public static FieldPath of(List<String> segments) { 091 if (segments == null || segments.isEmpty()) { 092 return EMPTY; 093 } 094 return new FieldPath(segments); 095 } 096 097 /** 098 * Get the field path segments. 099 * 100 * @return Unmodifiable list of segments 101 */ 102 public List<String> getSegments() { 103 return segments; 104 } 105 106 /** 107 * Get the number of segments in this field path. 108 * 109 * @return Number of segments 110 */ 111 public int size() { 112 return segments.size(); 113 } 114 115 /** 116 * Check if this field path is empty (no field access). 117 * 118 * @return true if empty 119 */ 120 public boolean isEmpty() { 121 return segments.isEmpty(); 122 } 123 124 /** 125 * Get the first segment (immediate field). 126 * 127 * @return First segment, or null if empty 128 */ 129 public String getFirst() { 130 return segments.isEmpty() ? null : segments.get(0); 131 } 132 133 /** 134 * Get the last segment (deepest field). 135 * 136 * @return Last segment, or null if empty 137 */ 138 public String getLast() { 139 return segments.isEmpty() ? null : segments.get(segments.size() - 1); 140 } 141 142 /** 143 * Get a segment by index. 144 * 145 * @param index The index 146 * @return The segment at that index 147 * @throws IndexOutOfBoundsException if index is invalid 148 */ 149 public String get(int index) { 150 return segments.get(index); 151 } 152 153 /** 154 * Create a new FieldPath with an additional segment appended. 155 * 156 * @param segment The segment to append 157 * @return New FieldPath with the segment appended 158 */ 159 public FieldPath append(String segment) { 160 if (segment == null || segment.isEmpty()) { 161 return this; 162 } 163 List<String> newSegments = new ArrayList<>(segments.size() + 1); 164 newSegments.addAll(segments); 165 newSegments.add(segment); 166 return new FieldPath(newSegments); 167 } 168 169 /** 170 * Create a new FieldPath with additional segments appended. 171 * 172 * @param other The FieldPath to append 173 * @return New FieldPath with all segments 174 */ 175 public FieldPath append(FieldPath other) { 176 if (other == null || other.isEmpty()) { 177 return this; 178 } 179 if (this.isEmpty()) { 180 return other; 181 } 182 List<String> newSegments = new ArrayList<>(segments.size() + other.segments.size()); 183 newSegments.addAll(segments); 184 newSegments.addAll(other.segments); 185 return new FieldPath(newSegments); 186 } 187 188 /** 189 * Create a sub-path starting from the given index. 190 * 191 * @param fromIndex Start index (inclusive) 192 * @return New FieldPath with segments from fromIndex 193 */ 194 public FieldPath subPath(int fromIndex) { 195 if (fromIndex >= segments.size()) { 196 return EMPTY; 197 } 198 if (fromIndex <= 0) { 199 return this; 200 } 201 return new FieldPath(segments.subList(fromIndex, segments.size())); 202 } 203 204 /** 205 * Convert to dot-separated string representation. 206 * 207 * @return String like "address.city", or empty string if empty 208 */ 209 @Override 210 public String toString() { 211 if (segments.isEmpty()) { 212 return ""; 213 } 214 StringBuilder sb = new StringBuilder(); 215 for (int i = 0; i < segments.size(); i++) { 216 if (i > 0) { 217 sb.append('.'); 218 } 219 sb.append(segments.get(i)); 220 } 221 return sb.toString(); 222 } 223 224 /** 225 * Convert to full reference string with base column. 226 * 227 * @param baseColumn The base column name 228 * @return String like "customer.address.city" 229 */ 230 public String toFullReference(String baseColumn) { 231 if (segments.isEmpty()) { 232 return baseColumn != null ? baseColumn : ""; 233 } 234 StringBuilder sb = new StringBuilder(); 235 if (baseColumn != null && !baseColumn.isEmpty()) { 236 sb.append(baseColumn); 237 sb.append('.'); 238 } 239 sb.append(toString()); 240 return sb.toString(); 241 } 242 243 @Override 244 public boolean equals(Object o) { 245 if (this == o) return true; 246 if (o == null || getClass() != o.getClass()) return false; 247 FieldPath fieldPath = (FieldPath) o; 248 return segments.equals(fieldPath.segments); 249 } 250 251 @Override 252 public int hashCode() { 253 return segments.hashCode(); 254 } 255}