Skip to main content
Version: 1.0 🚧

Data Types

The Python client uses PyArrow types for schema definitions:

PyArrow TypeFluss TypePython Type
pa.bool_()Booleanbool
pa.int8() / int16() / int32() / int64()TinyInt / SmallInt / Int / BigIntint
pa.float32() / float64()Float / Doublefloat
pa.string()Stringstr
pa.binary()Bytesbytes
pa.binary(n)Binary(n)bytes
pa.date32()Datedatetime.date
pa.time32("ms")Timedatetime.time
pa.timestamp("us")Timestamp (NTZ)datetime.datetime
pa.timestamp("us", tz="UTC")TimestampLTZdatetime.datetime
pa.decimal128(precision, scale)Decimaldecimal.Decimal
pa.list_(type)Arraylist
pa.map_(key_type, value_type)Maplist[(key, value)]
pa.struct([(name, type), ...])Rowdict

All Python native types (date, time, datetime, Decimal) work when appending rows via dicts.

Nullability

PyArrow field nullability is preserved when constructing Fluss schemas. By default, fields are nullable. Use nullable=False on pa.field() to create a NOT NULL column:

schema = pa.schema([
pa.field("id", pa.int32(), nullable=False),
pa.field("name", pa.string()), # nullable by default
])
fluss_schema = fluss.Schema(schema)
fluss_schema.get_column_types() # ["int NOT NULL", "string"]

Primary key columns are automatically forced NOT NULL regardless of the PyArrow field setting.

For nested types, element nullability is also preserved:

schema = pa.schema([
pa.field("tags", pa.list_(pa.field("item", pa.string(), nullable=False))),
])
fluss_schema = fluss.Schema(schema)
fluss_schema.get_column_types() # ["array<string NOT NULL>"]

Writing Data

Rows can be dicts, lists, or tuples:

from datetime import date, time, datetime
from decimal import Decimal

row = {
"user_id": 1,
"name": "Alice",
"active": True,
"score": 95.5,
"balance": Decimal("1234.56"),
"birth_date": date(1990, 3, 15),
"login_time": time(9, 30, 0),
"created_at": datetime(2024, 1, 1, 0, 0, 0),
"nickname": None, # null value
"tags": ["active", "premium"], # Array of strings
"scores": [10, None, 30], # Array with null values
}
handle = writer.append(row)

When a row is written as a dict, a nullable column may be omitted — it defaults to null. A non-nullable column (including primary keys) must be present, otherwise the write is rejected with a clear error. The same rule applies to the fields of a ROW value.

Lists and tuples must have values in column order:

row = [1, "Alice", True, 95.5, Decimal("1234.56"), date(1990, 3, 15), time(9, 30, 0), datetime(2024, 1, 1), None]
handle = writer.append(row)

Writing an Arrow batch

Your batch must have the table's columns, with the table's names, in the table's order. The same applies to the fields of a ROW column. Anything else is rejected with an error naming the column.

Column types must be the table's too, because write_arrow_batch writes your buffers as they are and readers decode them with the table's schema — a timestamp[ns] column written into a TIMESTAMP(3) one would read back as milliseconds. Types that differ only in how they store the same values are converted for you: large_string, large_binary, large_list, the view types, and dictionary encoding. Anything that would change a value, such as a different TIMESTAMP unit or DECIMAL scale, is rejected.

table.arrow_schema is the schema the table expects. Building against it avoids the conversion entirely, which for pandas costs nothing extra:

batch = pa.Table.from_pandas(df, schema=table.arrow_schema).to_batches()[0]
writer.write_arrow_batch(batch)

The nullable flags on your schema do not have to match — actual nulls are checked against the table's NOT NULL columns separately.

Reading Data

records = await scanner.poll(timeout_ms=1000)
for record in records:
row = record.row # dict[str, Any]
print(row["user_id"]) # int
print(row["name"]) # str
print(row["balance"]) # decimal.Decimal
print(row["birth_date"]) # datetime.date
print(row["created_at"]) # datetime.datetime

if row["nickname"] is None:
print("nickname is null")

Complex Types (Array, Map, Row)

ARRAY, MAP, and ROW columns can be nested arbitrarily (for example array<map<string, row<...>>>). On read they materialize to native Python objects; on write they accept the shapes below:

Fluss typeRead-back valueWrite input accepted
ARRAY<T>listlist / tuple
MAP<K, V>list of (key, value) tuplesdict, or a sequence of (key, value) pairs
ROW<...>dict keyed by field namedict (by name) or list/tuple (by position)

The MAP read shape matches pyarrow's MapArray.to_pylist() (it preserves duplicate keys and ordering); ROW matches StructArray.to_pylist().

Arrays

schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("tags", pa.list_(pa.string())),
pa.field("matrix", pa.list_(pa.list_(pa.int32()))), # nested
])
writer.append({"id": 1, "tags": ["a", "b"], "matrix": [[1, 2], [3, 4]]})

row = await lookuper.lookup({"id": 1})
row["tags"] # ["a", "b"]
row["matrix"] # [[1, 2], [3, 4]]

Maps

Use pa.map_(key_type, value_type). Write a dict or a list of (key, value) pairs; reads return a list of (key, value) tuples (wrap with dict(...) for keyed access). Map keys must be non-null.

schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("attrs", pa.map_(pa.string(), pa.int32())),
])
writer.append({"id": 1, "attrs": {"a": 1, "b": None}}) # dict input
# or a sequence of pairs: {"id": 2, "attrs": [("a", 1), ("b", None)]}

row = await lookuper.lookup({"id": 1})
row["attrs"] # [("a", 1), ("b", None)]
dict(row["attrs"]) # {"a": 1, "b": None}

Rows

Use pa.struct([...]). Write a dict keyed by field name (or a list/tuple in field order); reads return a dict.

schema = pa.schema([
pa.field("id", pa.int32()),
pa.field("profile", pa.struct([("age", pa.int32()), ("city", pa.string())])),
])
writer.append({"id": 1, "profile": {"age": 30, "city": "NYC"}})

row = await lookuper.lookup({"id": 1})
row["profile"] # {"age": 30, "city": "NYC"}

Constraints

ARRAY, MAP, and ROW may be used as row values and nested inside one another, but not as primary-key or bucket-key columns — the server rejects complex key types.

Bulk (Arrow) reads

The per-row paths above (append/upsert and the record-based scanner's record.row dict, point lookup) materialize each value into a Python object. For high-throughput scans, prefer the Arrow path — a record-batch scanner's to_arrow() / poll_arrow() returns nested columns as native pyarrow ListArray / MapArray / StructArray with no per-element conversion.