Class RowRanges

java.lang.Object
io.github.flink.gcp.connector.bigtable.RowRanges

@Internal public final class RowRanges extends Object
The module's row-key range algebra: emptiness, containment, cutting, coalescing, intersection, truncation and the unsigned key comparison they all rest on, each defined once here.

It began as the bounded scan source's and now serves both source directions and both halves of the table layer — the scan source's builder, planner, split state and split reader, the Change Streams partition model, the table source's range parsing, filter pushdown, point lookups and decode-failure guards, and the table sink's empty-mutation refusal — which is why it sits at the module root rather than under one of them (ADR-0055): 26 importers in the main tree, across ten packages. It lives in one place because every one of those would otherwise have to get the bound types right, and would each get them wrong differently. Row keys are compared as unsigned bytes, which is the order Bigtable stores them in; the natural ordering of ByteString is not that order, and a signed comparison sorts every key whose first byte is above 0x7F before every key whose first byte is below it.

Turning bytes into text: which form, and why there is more than one

A row key, a qualifier and a cell value are all arbitrary bytes, and this module turns them into text four different ways. That is not drift: the reader chooses the form, and choosing by package or by habit is how the wrong one gets used. Before rendering a byte string, ask who reads the result. This is about rendering for something to read and not about decoding a stored value back into what it was — a serializer's readString is neither governed nor contradicted by it.

  • A person, in a log line or an exception message → format(ByteString) or format(ByteStringRange). Printable ASCII stays itself so a text key is recognisable, and every other byte — plus the three that carry structure — becomes \xNN. The rendering is injective, so an operator can tell two of them apart (ADR-0080).
  • A pattern the user wrote → Base64, canonical padded RFC 4648. BigtableChangeStreamMutationFilter matches user regexes against family:qualifierBase64. A pattern needs a form the user can write, which an escape sequence is not. The row-key options take Base64 too, but only when scan.row-key-encoding asks for it — its default is UTF8 — so this arm is about the filter identifier, and an option's own encoding is whatever that option declares. Note it runs the other way from the three below, which are all about output; where the two meet, RowRangeParser is the precedent, and it names the entry number rather than echoing the value back at all.
  • Anyone, when the value is text by construction → toStringUtf8(). A qualifier built from a DDL field name is valid UTF-8 and is the identifier the reader must match against their own DDL; escaping it would render a non-ASCII column as hex, and unlike the family name printed beside it.
  • A user's own code, which is likely to log the object → keep it out of that object's own rendering. A row's key and cell values are that row's data: FailedMutation prints the failed mutation's size, and BigtableChangeStreamMutation has no toString at all. An exception message is the deliberate exception, having one chance to name the offending row and no accessors — so this arm bounds a toString and not a whole object graph: a FailedMutation whose getCause() is a serialization failure may carry that message, escaped key and all, into any handler that logs the cause — a message doing its job through this arm's object, not a leak to close. May, because it is per message: of this connector's own refusals only the empty-mutation one names the key, and a FailedMutation can equally wrap whatever a user's own serializer threw. getRowKey() is null for all of them.

toStringUtf8() on a value that is not text by construction is always wrong. Decoding invalid UTF-8 substitutes U+FFFD rather than failing, so 0xFE and 0xFF arrive as one character — the value is exposed and destroyed in the same breath.

Three facts about the vendor's Range.ByteStringRange, measured against google-cloud-bigtable 2.80.0 on 2026-08-09 and relied on below:

  • Method Summary

    Modifier and Type
    Method
    Description
    static List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange>
    coalesce(List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> ranges)
    Merges ranges that overlap or run into one another, and returns them in key order.
    static int
    compareEnds(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
    Orders two ranges by where they end; an unbounded end comes last.
    static int
    compareKeys(com.google.protobuf.ByteString left, com.google.protobuf.ByteString right)
    Compares two row keys in Bigtable's own order.
    static int
    compareStarts(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
    Orders two ranges by where they begin; an unbounded start comes first.
    static boolean
    contains(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range, com.google.protobuf.ByteString key)
    Returns whether a row key belongs to a range.
    static List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange>
    copyAll(List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> ranges)
    Returns independent copies of every range in a list.
    static com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange
    copyOf(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
    Returns an independent, normalised copy of a range.
    static boolean
    cuts(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range, com.google.protobuf.ByteString key)
    Returns whether a sampled row key cuts a range into two non-empty pieces.
    static String
    format(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
    Renders a range the way a log reader needs to see it.
    static String
    format(com.google.protobuf.ByteString key)
    Renders one row key under format(ByteStringRange)'s escaping, for a caller holding a key rather than a range.
    static List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange>
    intersect(List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> left, List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> right)
    Intersects two unions of row-key ranges.
    static boolean
    isEmpty(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
    Returns whether a range provably holds no row key at all.
    static boolean
    isUnboundedEnd(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
    Returns whether a range continues past every row key.
    static boolean
    isUnboundedStart(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
    Returns whether a range begins before every row key.
    static boolean
    sameEnd(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
    Returns whether two ranges end at exactly the same point.
    static boolean
    sameStart(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
    Returns whether two ranges begin at exactly the same point.
    static com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange
    truncateStartOpen(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range, com.google.protobuf.ByteString lastEmittedKey)
    Returns the work a split has left after successfully deserializing a row, as a range starting just past it.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Method Details

    • compareKeys

      public static int compareKeys(com.google.protobuf.ByteString left, com.google.protobuf.ByteString right)
      Compares two row keys in Bigtable's own order.
      Parameters:
      left - the first key
      right - the second key
      Returns:
      a negative number, zero or a positive number as left sorts before, at or after right
    • copyOf

      public static com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange copyOf(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
      Returns an independent, normalised copy of a range.

      Every range that crosses into this connector — from a builder setter, from the planner, from a deserialised split, from the Change Streams service — goes through here, for two reasons. A Range.ByteStringRange is mutable, so shared references would let a caller change a plan after it was made. And it is the connector's one normalisation point: rebuilding through the four setters folds an empty key on a bounded side into UNBOUNDED, which is the spelling everything else here assumes. A range built by ByteStringRange.create — every partition and continuation token the service returns — uses the other spelling, and the two are not equal to one another.

      Parameters:
      range - the range to copy
      Returns:
      a normalised range denoting the same rows, sharing no mutable state with it
    • copyAll

      public static List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> copyAll(List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> ranges)
      Returns independent copies of every range in a list.
      Parameters:
      ranges - the ranges to copy
      Returns:
      copies, in the same order, sharing no mutable state with the originals
    • isEmpty

      public static boolean isEmpty(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
      Returns whether a range provably holds no row key at all.

      Four shapes qualify, the last of which is easy to write by accident and impossible to spot: a start at or after the end; a start equal to a non-inclusive end; and startOpen(k) paired with endOpen(k + 0x00), where the only key the bounds admit is the one they both exclude.

      A user-configured empty range is rejected by the builder, because a range that reads nothing under a green job is indistinguishable from a job with nothing to read. A truncated range is a different matter and is normal — see truncateStartOpen(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange, com.google.protobuf.ByteString).

      Parameters:
      range - the range to test
      Returns:
      true when no row key lies inside it
    • cuts

      public static boolean cuts(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range, com.google.protobuf.ByteString key)
      Returns whether a sampled row key cuts a range into two non-empty pieces.

      A split point k means "rows below k belong to the section before it, rows from k onwards to the section after it", so cutting at k is worthwhile exactly when both sides would hold something. Both start bound types answer the same way — cutting at a key equal to the start yields an empty left-hand piece whether the start includes that key or not — while the end bounds differ, because a cut at an inclusive end leaves a right-hand piece holding exactly that one row.

      Parameters:
      range - the range being cut
      key - the candidate split point
      Returns:
      true when the key lies strictly inside the range
    • contains

      public static boolean contains(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range, com.google.protobuf.ByteString key)
      Returns whether a row key belongs to a range.

      This is deliberately separate from cuts(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange, com.google.protobuf.ByteString): a key on a closed start belongs to the range but cannot cut a non-empty left-hand piece from it. Point lookups need membership, while split planning needs the stricter cut relation.

      Parameters:
      range - the range to test
      key - the row key
      Returns:
      true when the range contains the key
    • truncateStartOpen

      public static com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange truncateStartOpen(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range, com.google.protobuf.ByteString lastEmittedKey)
      Returns the work a split has left after successfully deserializing a row, as a range starting just past it.

      The end bound is carried over untouched and the original start bound is discarded, which is safe because it sits strictly below the processed key and so constrains nothing. An exclusive start is what makes a restore resume rather than replay, and it is also what the client's own resumption strategy uses when it reconnects a broken stream mid-range.

      The result may be empty — a range ending endClosed(e) whose row e was successfully deserialized has nothing left, even if it produced no output — and that is a normal end-of-split state, not an error. The split reader finishes such a split without opening a stream, so an inverted range is never sent to the service.

      The empty-key case is not hypothetical enough to leave out: startOpen(EMPTY) is silently turned into an unbounded start by the SDK — which would widen the split back to the whole table and replay it forever. Progress past the empty key is expressed as an inclusive start at its successor instead. That is written against the SDK's normalisation rather than against what a server admits, which is what keeps it correct on both ends.

      Parameters:
      range - the range the split was assigned
      lastEmittedKey - the key of the last successfully deserialized row
      Returns:
      the remaining range
    • coalesce

      public static List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> coalesce(List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> ranges)
      Merges ranges that overlap or run into one another, and returns them in key order.

      Overlapping ranges are easy to configure by accident — prefix("user") beside prefix("user1") is enough — and left alone they are not merely wasteful: the overlapping rows land in two different splits, which two different subtasks read, so a single successful run emits them twice. Deduplication inside one request does not reach across splits, so it has to happen here.

      Two ranges that merely touch are merged when the key between them belongs to one of them, and left apart when it belongs to neither: endOpen(k) beside startOpen(k) excludes k deliberately, and merging would put a row back that the user removed.

      Parameters:
      ranges - the ranges to merge, in any order
      Returns:
      the merged ranges, sorted by start
    • intersect

      public static List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> intersect(List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> left, List<com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange> right)
      Intersects two unions of row-key ranges.

      The inputs may overlap and arrive in any order. Each side is coalesced first, then the two sorted lists are walked once. Empty intersections are omitted; the result is therefore an empty list when the two unions share no row key.

      Parameters:
      left - the first range union
      right - the second range union
      Returns:
      independent, coalesced ranges present in both unions
    • format

      public static String format(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
      Renders a range the way a log reader needs to see it.

      Range.ByteStringRange inherits Object.toString(), so a range in a log line is otherwise an identity hash. Printable ASCII is shown as itself and every other byte as \xNN, so a key that is not text stays readable and a key that is text stays recognisable.

      Three printable bytes are shown escaped rather than as themselves, because each carries structure here: \ introduces an escape, * is the sentinel for an absent bound, and , separates the two bounds. A key holding one of them would otherwise make two different ranges render as one string — [a, b, c) is both "from a, b to c" and "from a to b, c" — and these strings are what an operator reads to tell two ranges apart in a warning. The rendering is therefore injective, and a test asserts that as a property.

      That is a readability property, not a contract: nothing decides identity from a rendering, and nothing should. Range identity is ByteStringRange.equals.

      Parameters:
      range - the range to render
      Returns:
      a rendering such as [row-1, row-9) or (\x00ff, *]
    • format

      public static String format(com.google.protobuf.ByteString key)
      Renders one row key under format(ByteStringRange)'s escaping, for a caller holding a key rather than a range.

      Escaping only — no sentinel, and the empty key renders as the empty string. Every caller that names a row in a message quotes the value ('%s'), where an empty rendering reads as '' and needs no marker. A caller that does not quote, and for which an empty key carries a meaning, supplies its own: RowKeySample marks it *, because there it is the service's "end of table" rather than a key. Deciding that here would impose one caller's meaning on the rest, which is the whole reason this method does not.

      Same caveat as the range form: a rendering is what a person reads in a log, and nothing decides identity from one.

      Parameters:
      key - the key to render
      Returns:
      a rendering such as row-1 or \x00\xff; empty for the empty key
    • compareStarts

      public static int compareStarts(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
      Orders two ranges by where they begin; an unbounded start comes first.

      Like compareKeys(ByteString, ByteString), and unlike the range predicates above, this does not null-check. It is called once per comparison inside a sort or a TreeSet, over ranges the caller has already accepted.

      Parameters:
      left - the first range
      right - the second range
      Returns:
      a negative number, zero or a positive number as left begins before, at or after right
    • compareEnds

      public static int compareEnds(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
      Orders two ranges by where they end; an unbounded end comes last.
      Parameters:
      left - the first range
      right - the second range
      Returns:
      a negative number, zero or a positive number as left ends before, at or after right
    • sameStart

      public static boolean sameStart(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
      Returns whether two ranges begin at exactly the same point.

      Two unbounded starts are the same start. Otherwise the bound type has to match as well as the key, because startClosed(k) and startOpen(k) disagree about the one row k. This is the equality compareStarts(ByteStringRange, ByteStringRange) induces, written out directly so that a caller asking "is this the same edge?" does not have to read a comparator's result against zero.

      Parameters:
      left - the first range
      right - the second range
      Returns:
      true when both begin at the same key with the same bound type, or both are unbounded
    • sameEnd

      public static boolean sameEnd(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange left, com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange right)
      Returns whether two ranges end at exactly the same point.
      Parameters:
      left - the first range
      right - the second range
      Returns:
      true when both end at the same key with the same bound type, or both are unbounded
    • isUnboundedStart

      public static boolean isUnboundedStart(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
      Returns whether a range begins before every row key.

      The bound type alone answers this, which is correct only for a normalised range. Copies of this helper elsewhere in the module used to add || range.getStart().isEmpty(), and that disjunct was not decoration: it absorbed the spelling ByteStringRange.create produces, in which an absent bound is a bounded one at the empty key. The copies are gone and the disjunct with them, so every caller owes a range that has been through copyOf(ByteStringRange) — see the third measured fact above.

      Parameters:
      range - the range to test
      Returns:
      true when the range has no lower bound
    • isUnboundedEnd

      public static boolean isUnboundedEnd(com.google.cloud.bigtable.data.v2.models.Range.ByteStringRange range)
      Returns whether a range continues past every row key.
      Parameters:
      range - the range to test
      Returns:
      true when the range has no upper bound