Skip to content

Commit b978bb4

Browse files
authored
add python support object from 2083 (#1088)
1 parent 2b74869 commit b978bb4

4 files changed

Lines changed: 718 additions & 3 deletions

File tree

src/UserGuide/Master/Table/API/Programming-Python-Native-API_timecho.md

Lines changed: 181 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ if has_next:
6666

6767
**Note:** Avoid mixing different traversal methods (e.g., combining `todf()` with `next_df()`), which may cause unexpected errors.
6868

69+
**Since V2.0.8.3**, the Python client has supported `TSDataType.OBJECT` for Tablet batch write and Session value serialization. Query results are read via the `Field` object. The related interfaces are defined as follows:
70+
71+
| Function Name | Description | Parameters | Return Value |
72+
|---------------|-------------|------------|--------------|
73+
| `encode_object_cell` | Encodes a single OBJECT cell into wire-format bytes | `is_eof: bool`,<br>`offset: int`,<br>`content: bytes` | `bytes`: `|[eof 1B]|[offset 8B BE]|[payload]|` |
74+
| `decode_object_cell` | Parses a wire-format cell back into `eof`, `offset`, and `payload` | `cell: bytes` (length ≥ 9) | `Tuple[bool, int, bytes]`: `(is_eof, offset, payload)` |
75+
| `Tablet.add_value_object` | Writes an OBJECT cell at the specified row and column (internally calls `encode_object_cell`) | `row_index: int`,<br>`column_index: int`,<br>`is_eof: bool`,<br>`offset: int`,<br>`content: bytes` | `None` |
76+
| `Tablet.add_value_object_by_name` | Same as above, locates column by name | `column_name: str`,<br>`row_index: int`,<br>`is_eof: bool`,<br>`offset: int`,<br>`content: bytes` | `None` |
77+
| `NumpyTablet.add_value_object` | Same semantics as `Tablet.add_value_object`, column data is stored as `ndarray` | Same as above (`row_index`, `column_index`, ...) | `None` |
78+
| `Field.get_object_value` | Converts the value to a Python value based on the **target type** | `data_type: TSDataType` | Depends on type:<br>For OBJECT: `str` decoded from the entire `self.value` in UTF-8 (see Field.py) |
79+
| `Field.get_string_value` | Returns a string representation | None | `str`;<br>For OBJECT: `self.value.decode("utf-8")` |
80+
| `Field.get_binary_value` | Gets the binary data of TEXT/STRING/BLOB | None | `bytes` or `None`;<br>**Throws an error for OBJECT columns and should not be called** |
81+
6982

7083
#### Sample Code
7184

@@ -511,7 +524,7 @@ def query_data():
511524
while res.has_next():
512525
print(res.next())
513526

514-
print("get data from table1")
527+
print("get data from table1")
515528
with session.execute_query_statement("select * from table1") as res:
516529
while res.has_next():
517530
print(res.next())
@@ -521,7 +534,7 @@ def query_data():
521534
with session.execute_query_statement("select * from table0") as res:
522535
while res.has_next_df():
523536
print(res.next_df())
524-
537+
525538
session.close()
526539

527540

@@ -571,3 +584,169 @@ session_pool.close()
571584
print("example is finished!")
572585
```
573586

587+
**Object Type Usage Example**
588+
589+
```python
590+
import os
591+
592+
import numpy as np
593+
import pytest
594+
595+
from iotdb.utils.IoTDBConstants import TSDataType
596+
from iotdb.utils.NumpyTablet import NumpyTablet
597+
from iotdb.utils.Tablet import Tablet, ColumnType
598+
from iotdb.utils.object_column import decode_object_cell
599+
600+
601+
def _require_thrift():
602+
pytest.importorskip("iotdb.thrift.common.ttypes")
603+
604+
605+
def _session_endpoint():
606+
host = os.environ.get("IOTDB_HOST", "127.0.0.1")
607+
port = int(os.environ.get("IOTDB_PORT", "6667"))
608+
return host, port
609+
610+
611+
@pytest.fixture(scope="module")
612+
def table_session():
613+
_require_thrift()
614+
from iotdb.Session import Session
615+
from iotdb.table_session import TableSession, TableSessionConfig
616+
617+
host, port = _session_endpoint()
618+
cfg = TableSessionConfig(
619+
node_urls=[f"{host}:{port}"],
620+
username=os.environ.get("IOTDB_USER", Session.DEFAULT_USER),
621+
password=os.environ.get("IOTDB_PASSWORD", Session.DEFAULT_PASSWORD),
622+
)
623+
ts = TableSession(cfg)
624+
yield ts
625+
ts.close()
626+
627+
628+
def test_table_numpy_tablet_object_columns(table_session):
629+
"""
630+
Table model: Tablet.add_value_object / add_value_object_by_name,
631+
NumpyTablet.add_value_object, insert + query Field + decode_object_cell;
632+
Also includes writing OBJECT in two segments at the same timestamp
633+
(first with is_eof=False/offset=0, then with is_eof=True/offset=length of the first segment),
634+
and verifies the complete concatenated bytes using read_object(f1).
635+
"""
636+
db = "test_py_object_e2e"
637+
table = "obj_tbl"
638+
table_session.execute_non_query_statement(f"CREATE DATABASE IF NOT EXISTS {db}")
639+
table_session.execute_non_query_statement(f"USE {db}")
640+
table_session.execute_non_query_statement(f"DROP TABLE IF EXISTS {table}")
641+
table_session.execute_non_query_statement(
642+
f"CREATE TABLE {table}("
643+
"device STRING TAG, temp FLOAT FIELD, f1 OBJECT FIELD, f2 OBJECT FIELD)"
644+
)
645+
646+
column_names = ["device", "temp", "f1", "f2"]
647+
data_types = [
648+
TSDataType.STRING,
649+
TSDataType.FLOAT,
650+
TSDataType.OBJECT,
651+
TSDataType.OBJECT,
652+
]
653+
column_types = [
654+
ColumnType.TAG,
655+
ColumnType.FIELD,
656+
ColumnType.FIELD,
657+
ColumnType.FIELD,
658+
]
659+
timestamps = [100, 200]
660+
values = [
661+
["d1", 1.5, None, None],
662+
["d1", 2.5, None, None],
663+
]
664+
665+
tablet = Tablet(
666+
table, column_names, data_types, values, timestamps, column_types
667+
)
668+
tablet.add_value_object(0, 2, True, 0, b"first-row-obj")
669+
# Single-segment write for the entire object: is_eof=True and offset=0;
670+
# Segmented sequential writes must pass server-side offset/length validation
671+
tablet.add_value_object_by_name("f2", 0, True, 0, b"seg")
672+
tablet.add_value_object(1, 2, True, 0, b"second-f1")
673+
tablet.add_value_object(1, 3, True, 0, b"second-f2")
674+
table_session.insert(tablet)
675+
676+
ts_arr = np.array([300, 400], dtype=TSDataType.INT64.np_dtype())
677+
np_vals = [
678+
np.array(["d1", "d1"]),
679+
np.array([1.0, 2.0], dtype=np.float32),
680+
np.array([None, None], dtype=object),
681+
np.array([None, None], dtype=object),
682+
]
683+
np_tab = NumpyTablet(
684+
table, column_names, data_types, np_vals, ts_arr, column_types=column_types
685+
)
686+
np_tab.add_value_object(0, 2, True, 0, b"np-r0-f1")
687+
np_tab.add_value_object(0, 3, True, 0, b"np-r0-f2")
688+
np_tab.add_value_object(1, 2, True, 0, b"np-r1-f1")
689+
np_tab.add_value_object(1, 3, True, 0, b"np-r1-f2")
690+
table_session.insert(np_tab)
691+
692+
# Segmented OBJECT: first with is_eof=False (continue transmission),
693+
# then with is_eof=True (last segment); offset is the length of written bytes
694+
chunk0 = bytes((i % 256) for i in range(512))
695+
chunk1 = b"\xab" * 64
696+
expected_segmented = chunk0 + chunk1
697+
seg1 = Tablet(
698+
table,
699+
column_names,
700+
data_types,
701+
[["d1", 3.0, None, None]],
702+
[500],
703+
column_types,
704+
)
705+
seg1.add_value_object(0, 2, False, 0, chunk0)
706+
seg1.add_value_object(0, 3, True, 0, b"f2-seg")
707+
table_session.insert(seg1)
708+
seg2 = Tablet(
709+
table,
710+
column_names,
711+
data_types,
712+
[["d1", 3.0, None, None]],
713+
[500],
714+
column_types,
715+
)
716+
seg2.add_value_object(0, 2, True, 512, chunk1)
717+
seg2.add_value_object(0, 3, True, 0, b"f2-seg")
718+
table_session.insert(seg2)
719+
720+
with table_session.execute_query_statement(
721+
f"SELECT read_object(f1) FROM {table} WHERE time = 500"
722+
) as ds:
723+
assert ds.has_next()
724+
row = ds.next()
725+
blob = row.get_fields()[0].get_binary_value()
726+
assert blob == expected_segmented
727+
assert not ds.has_next()
728+
729+
seen = 0
730+
with table_session.execute_query_statement(
731+
f"SELECT device, temp, f1, f2 FROM {table} ORDER BY time"
732+
) as ds:
733+
while ds.has_next():
734+
row = ds.next()
735+
fields = row.get_fields()
736+
assert fields[0].get_object_value(TSDataType.STRING) == "d1"
737+
assert fields[1].get_object_value(TSDataType.FLOAT) is not None
738+
for j in (2, 3):
739+
raw = fields[j].value
740+
assert isinstance(raw, (bytes, bytearray))
741+
eof, off, body = decode_object_cell(bytes(raw))
742+
assert isinstance(eof, bool) and isinstance(off, int)
743+
assert isinstance(body, bytes)
744+
fields[j].get_string_value()
745+
fields[j].get_object_value(TSDataType.OBJECT)
746+
seen += 1
747+
assert seen == 5
748+
749+
750+
if __name__ == "__main__":
751+
pytest.main([__file__, "-v", "-rs"])
752+
```

0 commit comments

Comments
 (0)