|
| 1 | +import time |
| 2 | +import random |
| 3 | +import pytest |
| 4 | +from datetime import datetime, timezone, timedelta |
| 5 | + |
| 6 | +from base.testbase import TestBase |
| 7 | +from utils.utils import gen_collection_name |
| 8 | +from utils.util_log import test_log as logger |
| 9 | + |
| 10 | + |
| 11 | +@pytest.mark.L1 |
| 12 | +class TestTimestamptz(TestBase): |
| 13 | + """ |
| 14 | + RESTful e2e coverage for timestamptz field: |
| 15 | + - create collection with default timestamptz |
| 16 | + - describe schema to confirm defaultValue |
| 17 | + - insert rows (one missing timestamptz -> use default) |
| 18 | + - flush + load |
| 19 | + - get entities and validate timestamptz values are preserved/defaulted |
| 20 | + """ |
| 21 | + |
| 22 | + def test_timestamptz_default_value_and_get(self): |
| 23 | + name = gen_collection_name() |
| 24 | + dim = 5 |
| 25 | + default_time = "2025-01-01T00:00:00Z" |
| 26 | + |
| 27 | + # 1. create collection with timestamptz default value and vector index |
| 28 | + payload = { |
| 29 | + "collectionName": name, |
| 30 | + "schema": { |
| 31 | + "autoId": False, |
| 32 | + "enableDynamicField": False, |
| 33 | + "fields": [ |
| 34 | + {"fieldName": "id", "dataType": "Int64", "isPrimary": True}, |
| 35 | + {"fieldName": "time", "dataType": "Timestamptz", "defaultValue": default_time, "nullable": True}, |
| 36 | + {"fieldName": "color", "dataType": "VarChar", "elementTypeParams": {"max_length": "30"}}, |
| 37 | + {"fieldName": "vector", "dataType": "FloatVector", "elementTypeParams": {"dim": f"{dim}"}}, |
| 38 | + ], |
| 39 | + }, |
| 40 | + "indexParams": [ |
| 41 | + {"fieldName": "vector", "indexName": "vector_index", "metricType": "L2"}, |
| 42 | + ], |
| 43 | + } |
| 44 | + logger.info(f"create collection {name} with payload: {payload}") |
| 45 | + rsp = self.collection_client.collection_create(payload) |
| 46 | + assert rsp["code"] == 0 |
| 47 | + self.wait_load_completed(name) |
| 48 | + |
| 49 | + # 2. describe collection and verify defaultValue is returned |
| 50 | + desc = self.collection_client.collection_describe(name) |
| 51 | + assert desc["code"] == 0 |
| 52 | + fields = desc.get("data", {}).get("fields", []) |
| 53 | + time_field = next( |
| 54 | + (f for f in fields if f.get("fieldName") == "time" or f.get("name") == "time"), |
| 55 | + None, |
| 56 | + ) |
| 57 | + assert time_field is not None, f"timestamptz field not found in describe: {desc}" |
| 58 | + # defaultValue is returned in protobuf-like structure, keep loose check on the string payload |
| 59 | + assert "defaultValue" in time_field |
| 60 | + |
| 61 | + # 3. insert rows (one row omits timestamptz to trigger default) |
| 62 | + now_utc = datetime.now(timezone.utc) |
| 63 | + one_hour_ago = now_utc - timedelta(hours=1) |
| 64 | + rows = [ |
| 65 | + {"id": 1, "time": now_utc.isoformat(), "color": "red_9392", "vector": [random.random() for _ in range(dim)]}, |
| 66 | + {"id": 3, "time": one_hour_ago.isoformat(), "color": "pink_9298", "vector": [random.random() for _ in range(dim)]}, |
| 67 | + {"id": 4, "color": "green_0004", "vector": [random.random() for _ in range(dim)]}, # default timestamptz |
| 68 | + {"id": 504, "time": one_hour_ago.isoformat(), "color": "blue_0000", "vector": [random.random() for _ in range(dim)]}, |
| 69 | + ] |
| 70 | + insert_payload = {"collectionName": name, "data": rows} |
| 71 | + insert_rsp = self.vector_client.vector_insert(insert_payload) |
| 72 | + assert insert_rsp["code"] == 0 |
| 73 | + assert insert_rsp["data"]["insertCount"] == len(rows) |
| 74 | + |
| 75 | + # 4. flush and load collection to make data queryable |
| 76 | + flush_rsp = self.collection_client.flush(name) |
| 77 | + assert flush_rsp["code"] == 0 |
| 78 | + load_rsp = self.collection_client.collection_load(collection_name=name) |
| 79 | + assert load_rsp["code"] == 0 |
| 80 | + # wait a moment for load state |
| 81 | + time.sleep(2) |
| 82 | + |
| 83 | + # 5. get entities by id and validate timestamptz values |
| 84 | + get_payload = { |
| 85 | + "collectionName": name, |
| 86 | + "id": [1, 3, 4], |
| 87 | + "outputFields": ["color", "time"], |
| 88 | + } |
| 89 | + get_rsp = self.vector_client.vector_get(get_payload) |
| 90 | + assert get_rsp["code"] == 0 |
| 91 | + result = {int(item["id"]): item for item in get_rsp["data"]} |
| 92 | + assert set(result.keys()) == {1, 3, 4} |
| 93 | + |
| 94 | + def to_dt(ts: str) -> datetime: |
| 95 | + return datetime.fromisoformat(ts.replace("Z", "+00:00")) |
| 96 | + |
| 97 | + # default applied for id=4 |
| 98 | + assert to_dt(result[4]["time"]) == to_dt(default_time) |
| 99 | + # provided values preserved (allow small drift if server trims precision) |
| 100 | + assert abs((to_dt(result[1]["time"]) - now_utc).total_seconds()) < 1 |
| 101 | + assert abs((to_dt(result[3]["time"]) - one_hour_ago).total_seconds()) < 1 |
| 102 | + # colors round-trip |
| 103 | + assert result[1]["color"] == "red_9392" |
| 104 | + assert result[3]["color"] == "pink_9298" |
| 105 | + assert result[4]["color"] == "green_0004" |
| 106 | + |
0 commit comments