001package gudusoft.gsqlparser.ir.semantic;
002
003import java.util.Objects;
004
005/**
006 * Per-statement row-limit metadata. Emitted by the builder when a
007 * single-SELECT {@code LIMIT N}, {@code FETCH FIRST N ROWS ONLY},
008 * {@code TOP N}, or {@code OFFSET m [FETCH NEXT n ROWS ONLY]} clause
009 * is present and the count/offset expressions are resolvable from the
010 * parser AST. Column lineage (ORDER BY refs, output sources, etc.) is
011 * <i>unchanged</i> by row-limit metadata.
012 *
013 * <p>{@link #getCount() count} is the verbatim text of the count
014 * expression — what the SQL author wrote, e.g. {@code "10"},
015 * {@code "100"}, {@code "ALL"}, {@code "NULL"}, or even {@code "-10"}
016 * if the parser admits a signed literal. This mirrors the slice-22
017 * {@code FrameBound.offsetLiteral} convention: capture presentation
018 * accurately, do not impose a numeric interpretation that would be
019 * dialect-dependent or lossy for parameterized counts ({@code LIMIT ?},
020 * {@code LIMIT :n}, {@code TOP (@v)}). Downstream governance code is
021 * responsible for parsing the count into a number when it needs one.
022 *
023 * <p>{@link #getCount() count} is normally non-null. The single
024 * exception is {@link RowLimitKind#OFFSET_FETCH} when the SQL author
025 * wrote only an offset and no fetch count
026 * (PG {@code OFFSET 5 [ROWS]}, Oracle/MSSQL {@code OFFSET 5 ROWS}
027 * without {@code FETCH NEXT}). In that case {@code count} is
028 * {@code null} and {@code offset} is non-null.
029 *
030 * <p>{@link #getOffset() offset} is non-null for any row-limit clause
031 * that carries an offset slot in the source SQL:
032 * <ul>
033 *   <li>PG / MySQL / SQLite / BigQuery / Snowflake / Redshift inline
034 *       {@code LIMIT N OFFSET M} (kind {@link RowLimitKind#LIMIT}).</li>
035 *   <li>MySQL old-style {@code LIMIT M, N} (kind
036 *       {@link RowLimitKind#LIMIT}; offset = M).</li>
037 *   <li>PG {@code OFFSET m FETCH FIRST n} (kind
038 *       {@link RowLimitKind#FETCH_FIRST}).</li>
039 *   <li>Informix {@code SKIP m FIRST n} (kind
040 *       {@link RowLimitKind#FETCH_FIRST}) and
041 *       {@code SKIP m LIMIT n} (kind {@link RowLimitKind#LIMIT}).</li>
042 *   <li>Oracle / MSSQL {@code OFFSET m ROWS [FETCH NEXT n ROWS ONLY]}
043 *       (kind {@link RowLimitKind#OFFSET_FETCH}).</li>
044 * </ul>
045 *
046 * <p>Set-op outer row-limit (e.g. {@code UNION ALL ... LIMIT 5})
047 * continues to be rejected by {@code rejectSetOpRowLimit} with
048 * {@code SET_OP_ROW_LIMIT_NOT_SUPPORTED} (slice 72 lifts).
049 *
050 * <p>Immutable. Value equality on (kind, count, offset).
051 */
052public final class RowLimit {
053
054    private final RowLimitKind kind;
055    private final String count;
056    private final String offset;
057
058    public RowLimit(RowLimitKind kind, String count, String offset) {
059        if (kind == null) {
060            throw new IllegalArgumentException("kind must not be null");
061        }
062        // Null count is legal only for OFFSET_FETCH offset-only form,
063        // where the SQL author wrote "OFFSET m" with no FETCH NEXT.
064        if (count == null) {
065            if (kind != RowLimitKind.OFFSET_FETCH) {
066                throw new IllegalArgumentException(
067                        "count must not be null for kind " + kind
068                                + "; null count is reserved for "
069                                + "OFFSET_FETCH offset-only form");
070            }
071            if (offset == null) {
072                throw new IllegalArgumentException(
073                        "OFFSET_FETCH with null count requires a "
074                                + "non-null offset");
075            }
076        }
077        this.kind = kind;
078        this.count = count;
079        this.offset = offset;
080    }
081
082    /**
083     * Non-null. One of {@link RowLimitKind}'s four values; identifies
084     * the SQL surface form the user wrote so downstream consumers can
085     * distinguish e.g. {@code TOP N} from {@code LIMIT N}.
086     */
087    public RowLimitKind getKind() {
088        return kind;
089    }
090
091    /**
092     * Verbatim text of the count expression
093     * ({@code TExpression.toString()}). Non-null for {@link RowLimitKind#LIMIT},
094     * {@link RowLimitKind#FETCH_FIRST}, and {@link RowLimitKind#TOP}.
095     * May be {@code null} only for {@link RowLimitKind#OFFSET_FETCH}
096     * when the SQL author wrote only an offset and no fetch count
097     * ({@code OFFSET 5 ROWS} without {@code FETCH NEXT}).
098     * Downstream code is responsible for any numeric validation of
099     * the count text.
100     */
101    public String getCount() {
102        return count;
103    }
104
105    /**
106     * Verbatim text of the offset expression
107     * ({@code TExpression.toString()}). Non-null whenever the source
108     * SQL carries an offset slot; {@code null} otherwise. See the
109     * class-level javadoc for the full enumeration of offset-bearing
110     * surfaces.
111     */
112    public String getOffset() {
113        return offset;
114    }
115
116    @Override
117    public boolean equals(Object o) {
118        if (this == o) return true;
119        if (!(o instanceof RowLimit)) return false;
120        RowLimit other = (RowLimit) o;
121        return kind == other.kind
122                && Objects.equals(count, other.count)
123                && Objects.equals(offset, other.offset);
124    }
125
126    @Override
127    public int hashCode() {
128        return Objects.hash(kind, count, offset);
129    }
130
131    @Override
132    public String toString() {
133        return "RowLimit{" + kind + ", count=" + count
134                + ", offset=" + offset + "}";
135    }
136}