blob: 19df774176fc7a87cb25a5fb44262355a0a0b512 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Primary-key table example: upsert, lookup, delete, partial update.
Covers fire-and-forget and per-record-acknowledged upserts, point lookups,
deletes, and partial updates by column name and by column index.
Run standalone against a local cluster:
python example/pk_table.py
Or point it at a specific cluster:
FLUSS_BOOTSTRAP_SERVERS=host:port python example/pk_table.py
"""
import asyncio
import os
from datetime import date, datetime
from datetime import time as dt_time
from decimal import Decimal
from typing import Optional
import pyarrow as pa
import fluss
DEFAULT_BOOTSTRAP_SERVERS = "127.0.0.1:9123"
async def main(bootstrap_servers: Optional[str] = None):
bootstrap_servers = bootstrap_servers or os.environ.get(
"FLUSS_BOOTSTRAP_SERVERS", DEFAULT_BOOTSTRAP_SERVERS
)
config = fluss.Config({"bootstrap.servers": bootstrap_servers})
conn = await fluss.FlussConnection.create(config)
try:
await _run(conn)
finally:
await conn.close()
print("\nConnection closed")
async def _run(conn):
fields = [
pa.field("user_id", pa.int32()),
pa.field("name", pa.string()),
pa.field("email", pa.string()),
pa.field("age", pa.int32()),
pa.field("birth_date", pa.date32()),
pa.field("login_time", pa.time32("ms")),
pa.field("created_at", pa.timestamp("us")), # TIMESTAMP (NTZ)
pa.field("updated_at", pa.timestamp("us", tz="UTC")), # TIMESTAMP_LTZ
pa.field("balance", pa.decimal128(10, 2)),
]
schema = fluss.Schema(pa.schema(fields), primary_keys=["user_id"])
table_descriptor = fluss.TableDescriptor(schema, bucket_count=3)
admin = conn.get_admin()
table_path = fluss.TablePath("fluss", "example_pk_table")
await admin.drop_table(table_path, ignore_if_not_exists=True)
await admin.create_table(table_path, table_descriptor, ignore_if_exists=True)
print(f"Created PK table: {table_path}")
table = await conn.get_table(table_path)
print(f"Has primary key: {table.has_primary_key()}")
await _upsert(table)
await _lookup(table)
await _delete(table)
await _partial_update(table)
await _limit_scan(table)
await admin.drop_table(table_path, ignore_if_not_exists=True)
print(f"\nDropped PK table: {table_path}")
async def _upsert(table):
print("\n--- Upsert (fire-and-forget) ---")
upsert_writer = table.new_upsert().create_writer()
rows = [
{
"user_id": 1,
"name": "Alice",
"email": "alice@example.com",
"age": 25,
"birth_date": date(1999, 5, 15),
"login_time": dt_time(9, 30, 45, 123000),
"created_at": datetime(2024, 1, 15, 10, 30, 45, 123456),
"updated_at": datetime(2024, 1, 15, 10, 30, 45, 123456),
"balance": Decimal("1234.56"),
},
{
"user_id": 2,
"name": "Bob",
"email": "bob@example.com",
"age": 30,
"birth_date": date(1994, 3, 20),
"login_time": dt_time(14, 15, 30, 500000),
"created_at": datetime(2024, 1, 16, 11, 22, 33, 444555),
"updated_at": datetime(2024, 1, 16, 11, 22, 33, 444555),
"balance": Decimal("5678.91"),
},
{
"user_id": 3,
"name": "Charlie",
"email": "charlie@example.com",
"age": 35,
"birth_date": date(1989, 11, 8),
"login_time": dt_time(16, 45, 59, 999000),
"created_at": datetime(2024, 1, 17, 23, 59, 59, 999999),
"updated_at": datetime(2024, 1, 17, 23, 59, 59, 999999),
"balance": Decimal("9876.54"),
},
]
for row in rows:
upsert_writer.upsert(row)
await upsert_writer.flush()
print(f"Upserted {len(rows)} rows (flush waits for server acknowledgment)")
print("\n--- Upsert (per-record acknowledgment) ---")
handle = upsert_writer.upsert(
{
"user_id": 1,
"name": "Alice Updated",
"email": "alice.new@example.com",
"age": 26,
"birth_date": date(1999, 5, 15),
"login_time": dt_time(10, 11, 12, 345000),
"created_at": datetime(2024, 1, 15, 10, 30, 45, 123456),
"updated_at": datetime(2024, 1, 20, 15, 45, 30, 678901),
"balance": Decimal("2345.67"),
}
)
await handle.wait()
print("Updated user_id=1 (Alice -> Alice Updated), server acknowledged")
async def _lookup(table):
print("\n--- Lookup ---")
lookuper = table.new_lookup().create_lookuper()
result = await lookuper.lookup({"user_id": 1})
assert result is not None, "Expected to find user_id=1"
assert result["name"] == "Alice Updated", f"Unexpected name: {result['name']}"
print(f"Lookup user_id=1: name={result['name']}, balance={result['balance']}")
result = await lookuper.lookup({"user_id": 2})
assert result is not None, "Expected to find user_id=2"
print(f"Lookup user_id=2: name={result['name']}")
# A missing key returns None (normal control flow, not an error).
result = await lookuper.lookup({"user_id": 999})
assert result is None, "Expected user_id=999 to be absent"
print("Lookup user_id=999: not found (expected)")
async def _delete(table):
print("\n--- Delete ---")
upsert_writer = table.new_upsert().create_writer()
handle = upsert_writer.delete({"user_id": 3})
await handle.wait()
print("Deleted user_id=3, server acknowledged")
lookuper = table.new_lookup().create_lookuper()
result = await lookuper.lookup({"user_id": 3})
assert result is None, "Expected user_id=3 to be deleted"
print("Lookup user_id=3 after delete: not found (deletion confirmed)")
async def _partial_update(table):
print("\n--- Partial update by column names ---")
by_name = (
table.new_upsert()
.partial_update_by_name(["user_id", "balance"])
.create_writer()
)
handle = by_name.upsert({"user_id": 1, "balance": Decimal("9999.99")})
await handle.wait()
lookuper = table.new_lookup().create_lookuper()
result = await lookuper.lookup({"user_id": 1})
assert result is not None, "Expected to find user_id=1"
assert result["balance"] == Decimal("9999.99")
assert result["name"] == "Alice Updated", "name should be unchanged"
print(
f"By name: balance={result['balance']} (updated), "
f"name={result['name']} (unchanged)"
)
print("\n--- Partial update by column indices ---")
# Columns: 0=user_id (PK), 1=name. Update name only.
by_index = table.new_upsert().partial_update_by_index([0, 1]).create_writer()
handle = by_index.upsert([1, "Alice Renamed"])
await handle.wait()
result = await lookuper.lookup({"user_id": 1})
assert result is not None, "Expected to find user_id=1"
assert result["name"] == "Alice Renamed", "name should be updated"
assert result["balance"] == Decimal("9999.99"), "balance should be unchanged"
print(
f"By index: name={result['name']} (updated), "
f"balance={result['balance']} (unchanged)"
)
async def _limit_scan(table):
print("\n--- Limit scan: bounded BatchScanner over current rows (per bucket) ---")
table_info = table.get_table_info()
total = 0
for bucket_id in range(table_info.num_buckets):
bucket = fluss.TableBucket(table_info.table_id, bucket_id)
scanner = table.new_scan().limit(100).create_bucket_batch_scanner(bucket)
arrow_table = await scanner.to_arrow()
total += arrow_table.num_rows
# Users 1 and 2 remain (user 3 was deleted; user 1 was updated in place).
assert total == 2, f"Limit scan returned {total} current rows, expected 2"
print(f"Limit scan across {table_info.num_buckets} bucket(s) returned {total} rows")
if __name__ == "__main__":
asyncio.run(main())