001package gudusoft.gsqlparser.ir.semantic.joinanalysis; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.List; 006import java.util.Objects; 007 008/** 009 * One input side of a {@link JoinEntity}. A join never references a bare 010 * alias directly; it references a {@code JoinEndpoint} so that the left 011 * side of a left-deep chain ({@code t1 JOIN t2 JOIN t3}) can be modelled 012 * as the accumulated {@link JoinEndpointKind#JOIN_RESULT} of the prior 013 * joins rather than a single relation. 014 * 015 * <ul> 016 * <li>A {@link JoinEndpointKind#RELATION} endpoint carries the 017 * FROM-clause {@code alias} and the relation's 018 * {@code qualifiedName} (e.g. {@code t1}).</li> 019 * <li>A {@link JoinEndpointKind#JOIN_RESULT} endpoint carries the 020 * {@code producingJoinOrder} (the {@link JoinEntity#getOrder()} of 021 * the join that produced this result) and the immutable list of 022 * {@code contributingAliases} flowing through it.</li> 023 * </ul> 024 * 025 * <p>Immutable. Introduced by join-analysis slice 162 (S1). 026 */ 027public final class JoinEndpoint { 028 029 /** Sentinel for "no stable relation instance id" (slice 179). */ 030 public static final int NO_INSTANCE_ID = -1; 031 032 /** Sentinel for "no subquery statement index" (R8). */ 033 public static final int NO_STATEMENT_INDEX = -1; 034 035 private final JoinEndpointKind kind; 036 // RELATION fields 037 private final String alias; 038 private final String qualifiedName; 039 private final int instanceId; 040 // JOIN_RESULT fields 041 private final int producingJoinOrder; 042 private final List<String> contributingAliases; 043 // SUBQUERY field (R8): index of the lifted subquery's StatementGraph. 044 private final int statementIndex; 045 046 private JoinEndpoint(JoinEndpointKind kind, String alias, String qualifiedName, 047 int instanceId, int producingJoinOrder, 048 List<String> contributingAliases, int statementIndex) { 049 this.kind = kind; 050 this.alias = alias; 051 this.qualifiedName = qualifiedName; 052 this.instanceId = instanceId; 053 this.producingJoinOrder = producingJoinOrder; 054 this.contributingAliases = contributingAliases == null 055 ? Collections.<String>emptyList() 056 : Collections.unmodifiableList(new ArrayList<String>(contributingAliases)); 057 this.statementIndex = statementIndex; 058 } 059 060 /** 061 * A single FROM-clause relation with no stable instance id. {@code alias} 062 * must be non-empty; {@code qualifiedName} may be {@code null} when the 063 * relation has no resolvable name (e.g. an unaliased derived table). 064 */ 065 public static JoinEndpoint relation(String alias, String qualifiedName) { 066 return relation(alias, qualifiedName, NO_INSTANCE_ID); 067 } 068 069 /** 070 * A single FROM-clause relation carrying its stable 071 * {@code instanceId} (slice 179, R4) — the same per-block FROM-order 072 * ordinal as the matching {@code RelationSource.getInstanceId()}, so an 073 * endpoint links to its exact relation instance without alias-keying. 074 */ 075 public static JoinEndpoint relation(String alias, String qualifiedName, int instanceId) { 076 if (alias == null || alias.isEmpty()) { 077 throw new IllegalArgumentException("alias must be non-empty"); 078 } 079 return new JoinEndpoint(JoinEndpointKind.RELATION, alias, qualifiedName, 080 instanceId, -1, null, NO_STATEMENT_INDEX); 081 } 082 083 /** 084 * The right side of a predicate-derived semi-join (R8): a lifted 085 * {@code EXISTS} / {@code IN} subquery. {@code statementIndex} is the 086 * index of the subquery's own {@link gudusoft.gsqlparser.ir.semantic.StatementGraph} 087 * block in the program; {@code label} is an optional human-facing name 088 * (the synthetic {@code <predicate_subquery_N>} name or the subquery's 089 * alias), may be {@code null}. 090 */ 091 public static JoinEndpoint subquery(int statementIndex, String label) { 092 if (statementIndex < 0) { 093 throw new IllegalArgumentException("statementIndex must be >= 0"); 094 } 095 return new JoinEndpoint(JoinEndpointKind.SUBQUERY, label, null, 096 NO_INSTANCE_ID, -1, null, statementIndex); 097 } 098 099 /** 100 * The accumulated result of prior joins in a left-deep chain. 101 * 102 * @param producingJoinOrder the {@link JoinEntity#getOrder()} of the 103 * join producing this result 104 * @param contributingAliases aliases flowing through this result (may 105 * be empty, never null) 106 */ 107 public static JoinEndpoint joinResult(int producingJoinOrder, List<String> contributingAliases) { 108 if (producingJoinOrder < 0) { 109 throw new IllegalArgumentException("producingJoinOrder must be >= 0"); 110 } 111 return new JoinEndpoint(JoinEndpointKind.JOIN_RESULT, null, null, 112 NO_INSTANCE_ID, producingJoinOrder, contributingAliases, NO_STATEMENT_INDEX); 113 } 114 115 public JoinEndpointKind getKind() { 116 return kind; 117 } 118 119 /** Non-null only for {@link JoinEndpointKind#RELATION}. */ 120 public String getAlias() { 121 return alias; 122 } 123 124 /** May be null even for {@link JoinEndpointKind#RELATION}. */ 125 public String getQualifiedName() { 126 return qualifiedName; 127 } 128 129 /** 130 * Stable relation instance id (slice 179, R4) for a RELATION endpoint — 131 * matches {@code RelationSource.getInstanceId()}. {@link #NO_INSTANCE_ID} 132 * for JOIN_RESULT endpoints or when not assigned. 133 */ 134 public int getInstanceId() { 135 return instanceId; 136 } 137 138 /** Meaningful only for {@link JoinEndpointKind#JOIN_RESULT}; else -1. */ 139 public int getProducingJoinOrder() { 140 return producingJoinOrder; 141 } 142 143 /** Never null; empty for {@link JoinEndpointKind#RELATION}. */ 144 public List<String> getContributingAliases() { 145 return contributingAliases; 146 } 147 148 /** 149 * Index of the lifted subquery's {@code StatementGraph} for a 150 * {@link JoinEndpointKind#SUBQUERY} endpoint (R8); {@link #NO_STATEMENT_INDEX} 151 * for other endpoint kinds. For a SUBQUERY endpoint {@link #getAlias()} 152 * returns the optional label (synthetic name or alias, may be null). 153 */ 154 public int getStatementIndex() { 155 return statementIndex; 156 } 157 158 @Override 159 public boolean equals(Object o) { 160 if (this == o) return true; 161 if (!(o instanceof JoinEndpoint)) return false; 162 JoinEndpoint that = (JoinEndpoint) o; 163 return kind == that.kind 164 && instanceId == that.instanceId 165 && producingJoinOrder == that.producingJoinOrder 166 && statementIndex == that.statementIndex 167 && Objects.equals(alias, that.alias) 168 && Objects.equals(qualifiedName, that.qualifiedName) 169 && contributingAliases.equals(that.contributingAliases); 170 } 171 172 @Override 173 public int hashCode() { 174 return Objects.hash(kind, alias, qualifiedName, instanceId, 175 producingJoinOrder, contributingAliases, statementIndex); 176 } 177 178 @Override 179 public String toString() { 180 if (kind == JoinEndpointKind.RELATION) { 181 return "RELATION{" + alias + (qualifiedName != null ? "/" + qualifiedName : "") 182 + (instanceId != NO_INSTANCE_ID ? "#" + instanceId : "") + "}"; 183 } 184 if (kind == JoinEndpointKind.SUBQUERY) { 185 return "SUBQUERY{stmt=" + statementIndex 186 + (alias != null ? ", label=" + alias : "") + "}"; 187 } 188 return "JOIN_RESULT{order=" + producingJoinOrder + ", aliases=" + contributingAliases + "}"; 189 } 190}