001package gudusoft.gsqlparser.ir.semantic;
002
003/**
004 * One end of a window frame (slice 22). Represents either {@code start} or
005 * {@code end} of a {@link WindowFrame}.
006 *
007 * <p>{@link Kind#PRECEDING} and {@link Kind#FOLLOWING} optionally carry an
008 * {@code offsetLiteral} string ("1", "INTERVAL '7' DAY", etc.) — the literal
009 * offset spelled by the SQL author.
010 *
011 * <p><b>{@code offsetLiteral} is presentation text, not canonical text.</b>
012 * It is captured directly from {@code TExpression.toString()} on the parser
013 * side, which preserves SQL whitespace and casing. Two SQLs that spell the
014 * same offset differently (e.g. {@code 1} vs {@code 01} vs {@code 1.0}) MUST
015 * NOT be expected to produce equal {@code offsetLiteral}; JSON byte-stability
016 * is only a contract for the same SQL parsed twice.
017 *
018 * <p>The slice-22 builder rejects offsets whose AST type is not
019 * {@code simple_constant_t} — column refs ({@code simple_object_name_t}) and
020 * parenthesised expressions ({@code parenthesis_t}) are reachable in
021 * PostgreSQL / ANSI grammars but rejected at build time.
022 *
023 * <p>{@link Kind#UNBOUNDED_PRECEDING} / {@link Kind#UNBOUNDED_FOLLOWING} /
024 * {@link Kind#CURRENT_ROW} forbid an {@code offsetLiteral}; the constructor
025 * fails fast if one is supplied.
026 *
027 * <p>Bare {@link Kind#PRECEDING} / {@link Kind#FOLLOWING} without an
028 * explicit offset is permitted by this constructor for forward-compat / a
029 * closed domain, but the slice-22 probe across nine vendor grammars showed
030 * no parser surface emits this shape today.
031 */
032public final class FrameBound {
033
034    public enum Kind {
035        UNBOUNDED_PRECEDING,
036        UNBOUNDED_FOLLOWING,
037        CURRENT_ROW,
038        PRECEDING,
039        FOLLOWING
040    }
041
042    private final Kind kind;
043    private final String offsetLiteral;
044
045    public FrameBound(Kind kind, String offsetLiteral) {
046        if (kind == null) {
047            throw new IllegalArgumentException("kind must not be null");
048        }
049        boolean offsetAllowed = (kind == Kind.PRECEDING || kind == Kind.FOLLOWING);
050        if (offsetLiteral != null && !offsetAllowed) {
051            throw new IllegalArgumentException(
052                    "offsetLiteral allowed only with PRECEDING/FOLLOWING, got " + kind);
053        }
054        this.kind = kind;
055        this.offsetLiteral = offsetLiteral;
056    }
057
058    public Kind getKind() {
059        return kind;
060    }
061
062    public String getOffsetLiteral() {
063        return offsetLiteral;
064    }
065}