Skip to content

Migrating from HappyBase

Documentation home · General migration checklist · Limitations

This is an application migration, not an import-only replacement. HappyBase connects through HBase Thrift and returns flat family-qualified mappings for row reads. Its scan stop bound is exclusive. Check your installed version against the HappyBase API reference.

Map the application boundary

Existing application assumption hbase-driver change
Connect to a Thrift host/port Configure ZooKeeper; reach advertised Master and RegionServers
A table name is one string Pass namespace and table separately
Row is a flat column mapping Row object with nested kv and get(family, qualifier)
Multiple reads produce ordered tuples BatchGet returns a dictionary keyed by row key
Scan end is exclusive Call with_end_row(..., inclusive=False) explicitly
Filter is a textual expression Construct Python filter/comparator objects
Client scan limit is enforced Consume next_batch(n) or use islice and close
Read timestamps/versions for each cell No equivalent complete history through current Row
Batch implies throughput improvement Current convenience batch APIs issue per-row requests

Do not remove Thrift infrastructure used by other applications as part of changing this client.

Connection and table access

import os

from hbasedriver.client.client import Client
from hbasedriver.hbase_constants import HConstants
from hbasedriver.operations.get import Get

config = {HConstants.ZOOKEEPER_QUORUM: os.getenv("HBASE_ZK", "127.0.0.1:2181")}
with Client(config) as client:
    with client.get_table(b"default", b"users") as table:
        row = table.get(Get(b"user:001"))
        if row is not None:
            print(row.get(b"cf", b"name"))

The table must already exist. The application explicitly maps HBASE_ZK to the driver's configuration; it is not an automatically recognized driver environment variable.

Convert flat input data

This contextual adapter accepts byte-string keys such as b"cf:name". Split only at the first colon so qualifiers containing colons are preserved. Reject family-only input for a Put.

from hbasedriver.operations.put import Put

def make_put(rowkey: bytes, flat_data: dict[bytes, bytes]) -> Put:
    put = Put(rowkey)
    for column, value in flat_data.items():
        family, separator, qualifier = column.partition(b":")
        if not separator or not family:
            raise ValueError("Put columns must use family:qualifier")
        put.add_column(family, qualifier, value)
    return put

table.put(make_put(b"user:001", {b"cf:name": b"Alice", b"cf:metric:count": b"12"}))

Convert output only at the compatibility boundary:

def flatten_row(row):
    if row is None:
        return {}
    return {
        family + b":" + qualifier: value
        for family, columns in row.kv.items()
        for qualifier, value in columns.items()
    }

This intentionally maps missing rows to an empty mapping. Decide whether your application should instead preserve None. It cannot restore timestamps or version history that Row has already discarded.

Multiple reads and order

from hbasedriver.operations.batch import BatchGet

requested = [b"user:002", b"user:001", b"missing"]
operation = BatchGet(requested)
operation.add_column(b"cf", b"name")
by_key = table.batch_get(operation)
ordered = [(key, by_key.get(key)) for key in requested]

Keep the original requested list when order or duplicate keys matter. Do not iterate the returned dictionary as if each element were a Row.

Scan conversion

from hbasedriver.filter import PrefixFilter
from hbasedriver.operations.scan import Scan

scan = (
    Scan(start_row=b"user:")
    .with_end_row(b"user;", inclusive=False)
    .add_family(b"cf")
    .set_filter(PrefixFilter(b"user:"))
)
with table.scan(scan) as scanner:
    first_100 = scanner.next_batch(100)
for row in first_100:
    print(row.rowkey, flatten_row(row))

This consumes at most 100 rows and closes the scanner. It is not a continuous full scan. For multiple pages, use the exclusive-resume recipe in the cookbook; preserve filters and bounds. Use family selection plus qualifier filtering to avoid the current Scan.add_column serialization issue.

Batches and counters

A flat-column convenience batch is available now:

with table.batch(batch_size=100) as batch:
    batch.put(b"user:001", {b"cf:name": b"Alice"})
    batch.put(b"user:002", {b"cf:name": b"Bob"})

Its presence does not imply combined RPCs, rollback, or all-or-nothing behavior. Writes can occur before context exit when thresholds are reached. BufferedMutator has the same durability/retry caveats described in limitations.

For counters, use Increment on a compatible 8-byte numeric cell; do not convert existing numeric counters to decimal text. The Table method returns one int, so avoid multi-column Increment when callers need all resulting values.

Schema and rollout checklist

  1. Inventory byte encodings, column-family descriptors, timestamps, versions, filters, and pagination assumptions.
  2. Run the documented operations against a staging copy, including absent rows, empty values, and boundary keys.
  3. Verify schemas using cluster tooling, especially TTL, versions, and compression settings.
  4. Compare deterministic reads without replaying side-effecting counters/appends.
  5. Test timeouts and partial writes with a reconciliation plan.
  6. Measure the same workload in both clients; no blanket performance advantage is claimed.
  7. Roll out gradually and retain an application-level fallback plan.

Do not treat unsupported time/history reads, secure-cluster authentication, or connection-sharing semantics as compatible without dedicated changes and tests.