fix: correct default value backfill during AddField (#45634)

issue: https://github.com/milvus-io/milvus/issues/44585

Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
This commit is contained in:
zhenshan.cao 2025-11-18 23:05:42 +08:00 committed by GitHub
parent 947c8855f3
commit a3b8bcb198
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 454 additions and 400 deletions

2
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/validator/v10 v10.14.0
github.com/gofrs/flock v0.8.1
github.com/golang/protobuf v1.5.4
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/klauspost/compress v1.17.9

40
go.sum
View File

@ -217,12 +217,10 @@ github.com/casbin/casbin/v2 v2.44.2 h1:mlWtgbX872r707frOq+REaHzfvsl+qQw0Eq+ekzJ7
github.com/casbin/casbin/v2 v2.44.2/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
github.com/casbin/json-adapter/v2 v2.0.0 h1:nOCN3TK1CJKSNQQ/MnakbU9/cUcNR3N0AxBDnEBLSDI=
github.com/casbin/json-adapter/v2 v2.0.0/go.mod h1:LvsfPXXr8CD0ZFucAxawcY9Xb0FtLk3mozJ1qcSTUD4=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@ -240,7 +238,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
@ -328,7 +325,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=
@ -458,7 +454,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@ -472,7 +467,6 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
@ -509,7 +503,6 @@ github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/gomodule/redigo v1.7.1-0.20190724094224-574c33c3df38/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI=
@ -937,7 +930,6 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@ -1047,7 +1039,6 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
@ -1176,45 +1167,29 @@ github.com/zilliztech/woodpecker v0.1.11 h1:XYFIqAk6/zOija3Yu3I1mdHj9YGjX4GNHZdN
github.com/zilliztech/woodpecker v0.1.11/go.mod h1:xA7jPkUnnr5S4+LmQghrc/1hNU2kiU8KWZ93Uup2jks=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/bbolt v1.3.12 h1:UAxZAIuJqzFwByP19gZC3zd5robK3FOangrGS+Fdczg=
go.etcd.io/bbolt v1.3.12/go.mod h1:Gi2toLZr1jFkuReJm+yEPn7H8wk6ooptePtHYCbCS1g=
go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw=
go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
go.etcd.io/etcd/api/v3 v3.5.5 h1:BX4JIbQ7hl7+jL+g+2j5UAr0o1bctCm6/Ct+ArBGkf0=
go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
go.etcd.io/etcd/api/v3 v3.5.23 h1:tQi/RaO6peOhmf0c11miU3RQIYPZmiL3UzG9V+f8g6k=
go.etcd.io/etcd/api/v3 v3.5.23/go.mod h1:QP4ZLWROP49Kk/vPLhudxYQcF4ndhMQ1gvJE4rCTAgc=
go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
go.etcd.io/etcd/client/pkg/v3 v3.5.5 h1:9S0JUVvmrVl7wCF39iTQthdaaNIiAaQbmK75ogO6GU8=
go.etcd.io/etcd/client/pkg/v3 v3.5.5/go.mod h1:ggrwbk069qxpKPq8/FKkQ3Xq9y39kbFR4LnKszpRXeQ=
go.etcd.io/etcd/client/pkg/v3 v3.5.23 h1:RzwVV28JgOwGl5TUjA47s9IWxl5qQjM2VqSh8wjFFLM=
go.etcd.io/etcd/client/pkg/v3 v3.5.23/go.mod h1:IdIjxGUGNy+8HWeVlbBXDqmPqe+n8GsVGVYnccAEZ5o=
go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU=
go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
go.etcd.io/etcd/client/v2 v2.305.5 h1:DktRP60//JJpnPC0VBymAN/7V71GHMdjDCBt4ZPXDjI=
go.etcd.io/etcd/client/v2 v2.305.5/go.mod h1:zQjKllfqfBVyVStbt4FaosoX2iYd8fV/GRy/PbowgP4=
go.etcd.io/etcd/client/v2 v2.305.23 h1:lo6nsSHjp3tGsRLrzmM+neVSahXxbhxnfoatEdB6nao=
go.etcd.io/etcd/client/v2 v2.305.23/go.mod h1:Up9T9+5M3MMcCj/V0nDfadBERNMxIYf1tdycva1dXM4=
go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8=
go.etcd.io/etcd/client/v3 v3.5.5 h1:q++2WTJbUgpQu4B6hCuT7VkdwaTP7Qz6Daak3WzbrlI=
go.etcd.io/etcd/client/v3 v3.5.5/go.mod h1:aApjR4WGlSumpnJ2kloS75h6aHUmAyaPLjHMxpc7E7c=
go.etcd.io/etcd/client/v3 v3.5.23 h1:WN7sypGG326sFP5jLkFqD3anf/k6NNlV5Hy/UxvTPvc=
go.etcd.io/etcd/client/v3 v3.5.23/go.mod h1:XTf1oMQi4ZzpXGFoPgvAqbY9JFmTFQqWI1SF4g+hV6o=
go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0/go.mod h1:tV31atvwzcybuqejDoY3oaNRTtlD2l/Ot78Pc9w7DMY=
go.etcd.io/etcd/pkg/v3 v3.5.5 h1:Ablg7T7OkR+AeeeU32kdVhw/AGDsitkKPl7aW73ssjU=
go.etcd.io/etcd/pkg/v3 v3.5.5/go.mod h1:6ksYFxttiUGzC2uxyqiyOEvhAiD0tuIqSZkX3TyPdaE=
go.etcd.io/etcd/pkg/v3 v3.5.23 h1:q2skPu7VFC7oerL3MrFqImVACjAFWPlZMjHU0zll9x4=
go.etcd.io/etcd/pkg/v3 v3.5.23/go.mod h1:zUWlm5U4HgvDg+0PUWb8TgMF7ZrUiQA8aQq7FxE0WHo=
go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0/go.mod h1:FAwse6Zlm5v4tEWZaTjmNhe17Int4Oxbu7+2r0DiD3w=
go.etcd.io/etcd/raft/v3 v3.5.5 h1:Ibz6XyZ60OYyRopu73lLM/P+qco3YtlZMOhnXNS051I=
go.etcd.io/etcd/raft/v3 v3.5.5/go.mod h1:76TA48q03g1y1VpTue92jZLr9lIHKUNcYdZOOGyx8rI=
go.etcd.io/etcd/raft/v3 v3.5.23 h1:NhCdh4xz1VsrqHd2c+h6SLvhE95B1Hs7K+ESaAs6LLQ=
go.etcd.io/etcd/raft/v3 v3.5.23/go.mod h1:NJz9BGkhGvru47lIc1wL0QHsg5yvHTy6tUpEqM69ERM=
go.etcd.io/etcd/server/v3 v3.5.0-alpha.0/go.mod h1:tsKetYpt980ZTpzl/gb+UOJj9RkIyCb1u4wjzMg90BQ=
go.etcd.io/etcd/server/v3 v3.5.5 h1:jNjYm/9s+f9A9r6+SC4RvNaz6AqixpOvhrFdT0PvIj0=
go.etcd.io/etcd/server/v3 v3.5.5/go.mod h1:rZ95vDw/jrvsbj9XpTqPrTAB9/kzchVdhRirySPkUBc=
go.etcd.io/etcd/server/v3 v3.5.23 h1:2g1hz32pp1TYMI7xUyifR8gXOlUnTBCfixV+uJ8BqRU=
go.etcd.io/etcd/server/v3 v3.5.23/go.mod h1:sZwt/lLSwYQ/S5aAbzSQX2xNw1WA0Fo5noUCVxVYB4g=
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
@ -1230,20 +1205,16 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 h1:JRxssobiPg23otYU5SbWtQC//snGVIM3Tx6QRzlQBao=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.25.0/go.mod h1:E5NNboN0UqSAki0Atn9kVwaN7I+l25gGxDqBueo/74E=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0 h1:rgMkmiGfix9vFJDcDi1PK8WEQP4FLQwLDfhp5ZLpFeE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.0.1/go.mod h1:OPEOD4jIT2SlZPMmwT6FqZz2C0ZNdQqiWcoK6M0SNFU=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/jaeger v1.13.0 h1:VAMoGujbVV8Q0JNM/cEbhzUIWWBxnEqH45HP9iBKN04=
go.opentelemetry.io/otel/exporters/jaeger v1.13.0/go.mod h1:fHwbmle6mBFJA1p2ZIhilvffCdq/dM5UTIiCOmEjS+w=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1/go.mod h1:Kv8liBeVNFkkkbilbgWRpV+wWuu+H5xdOT6HAgd30iw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0 h1:DeFD0VgTZ+Cj6hxravYYZE2W4GlneVH81iAOPjZkzk8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.20.0/go.mod h1:GijYcYmNpX1KazD5JmWGsi4P7dDTTTnfv1UbGn84MnU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1/go.mod h1:xOvWoTOrQjxjW61xtOmD/WKGRYb/P4NzRo3bs65U6Rk=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0 h1:gvmNvqrPYovvyRmCSygkUDyL8lC5Tl845MLEwqpxhEU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.20.0/go.mod h1:vNUq47TGFioo+ffTSnKNdob241vePmtNZnAODKapKd0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.20.0 h1:CsBiKCiQPdSjS+MlRiqeTI9JDDpSuk0Hb6QTRfwer8k=
@ -1254,16 +1225,13 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.20.0 h1:4s9HxB4azeeQkhY
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.20.0/go.mod h1:djVA3TUJ2fSdMX0JE5XxFBOaZzprElJoP7fD4vnV2SU=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.0.1/go.mod h1:HrdXne+BiwsOHYYkBE5ysIcv2bvdZstxzmCQhxTcZkI=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.0.1/go.mod h1:5g4i4fKLaX2BQpSBsxw8YYcgKpMMSW3x7ZTuYBr3sUk=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v0.9.0/go.mod h1:1vKfU9rv61e9EVGthD1zNvUbiwPcimSsOPU9brfSHJg=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -1314,7 +1282,6 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
@ -1501,7 +1468,6 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -1566,7 +1532,6 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -1761,14 +1726,9 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc/examples v0.0.0-20220617181431-3e7b97febc7f h1:rqzndB2lIQGivcXdTuY3Y9NBvr70X+y77woofSRluec=

View File

@ -12,7 +12,7 @@ import (
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
parser "github.com/milvus-io/milvus/internal/parser/planparserv2/generated"
"github.com/milvus-io/milvus/pkg/v2/proto/planpb"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -2013,7 +2013,7 @@ func (v *ParserVisitor) VisitTimestamptzCompareForward(ctx *parser.TimestamptzCo
compareOp := cmpOpMap[ctx.GetOp2().GetTokenType()]
timestamptzInt64, err := funcutil.ValidateAndReturnUnixMicroTz(unquotedCompareStr, v.args.Timezone)
timestamptzInt64, err := timestamptz.ValidateAndReturnUnixMicroTz(unquotedCompareStr, v.args.Timezone)
if err != nil {
return err
}
@ -2077,7 +2077,7 @@ func (v *ParserVisitor) VisitTimestamptzCompareReverse(ctx *parser.TimestamptzCo
return fmt.Errorf("unsupported comparison operator for reverse Timestamptz: %s", ctx.GetOp2().GetText())
}
timestamptzInt64, err := funcutil.ValidateAndReturnUnixMicroTz(unquotedCompareStr, v.args.Timezone)
timestamptzInt64, err := timestamptz.ValidateAndReturnUnixMicroTz(unquotedCompareStr, v.args.Timezone)
if err != nil {
return err
}

View File

@ -37,6 +37,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/metricsinfo"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -350,6 +351,13 @@ func describeCollection(node *Proxy) gin.HandlerFunc {
return
}
// Convert TIMESTAMPTZ default values back to string format for the user.
if err := timestamptz.RewriteTimestampTzDefaultValueToString(describeCollectionResp.Schema); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
mhttp.HTTPReturnMessage: err.Error(),
})
}
describePartitionResp, err := rootCoord.ShowPartitions(c, &milvuspb.ShowPartitionsRequest{
Base: &commonpb.MsgBase{
MsgType: commonpb.MsgType_ShowPartitions,

View File

@ -842,7 +842,6 @@ func (node *Proxy) BatchDescribeCollection(ctx context.Context, request *milvusp
CollectionName: collectionName,
}
}
responses = append(responses, describeCollectionResponse)
}

View File

@ -18,6 +18,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timerecord"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -207,6 +208,12 @@ func (node *CachedProxyServiceProvider) DescribeCollection(ctx context.Context,
return nil, err
}
err = timestamptz.RewriteTimestampTzDefaultValueToString(resp.Schema)
if err != nil {
log.Info("failed to rewrite timestamp value", zap.Error(err))
return nil, err
}
resp.CollectionID = c.collID
resp.UpdateTimestamp = c.updateTimestamp
resp.UpdateTimestampStr = fmt.Sprintf("%d", c.updateTimestamp)

View File

@ -42,6 +42,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -423,7 +424,7 @@ func (t *createCollectionTask) PreExecute(ctx context.Context) error {
// Validate timezone
tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, t.GetProperties())
if exist && !funcutil.IsTimezoneValid(tz) {
if exist && !timestamptz.IsTimezoneValid(tz) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
}
@ -1214,7 +1215,7 @@ func (t *alterCollectionTask) PreExecute(ctx context.Context) error {
}
// Check the validation of timezone
userDefinedTimezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, t.Properties)
if exist && !funcutil.IsTimezoneValid(userDefinedTimezone) {
if exist && !timestamptz.IsTimezoneValid(userDefinedTimezone) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", userDefinedTimezone)
}
} else if len(t.GetDeleteKeys()) > 0 {

View File

@ -16,6 +16,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
)
type createDatabaseTask struct {
@ -74,7 +75,7 @@ func (cdt *createDatabaseTask) PreExecute(ctx context.Context) error {
return err
}
tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, cdt.GetProperties())
if exist && !funcutil.IsTimezoneValid(tz) {
if exist && !timestamptz.IsTimezoneValid(tz) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
}
return nil
@ -277,7 +278,7 @@ func (t *alterDatabaseTask) PreExecute(ctx context.Context) error {
if len(t.GetProperties()) > 0 {
// Check the validation of timezone
userDefinedTimezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, t.Properties)
if exist && !funcutil.IsTimezoneValid(userDefinedTimezone) {
if exist && !timestamptz.IsTimezoneValid(userDefinedTimezone) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", userDefinedTimezone)
}
}

View File

@ -34,6 +34,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timerecord"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/tsoutil"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -242,7 +243,7 @@ func parseQueryParams(queryParamsPair []*commonpb.KeyValuePair) (*queryParams, e
}
timezone, _ = funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, queryParamsPair)
if (timezone != "") && !funcutil.IsTimezoneValid(timezone) {
if (timezone != "") && !timestamptz.IsTimezoneValid(timezone) {
return nil, merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", timezone)
}

View File

@ -39,6 +39,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/metric"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timerecord"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/tsoutil"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -292,7 +293,7 @@ func (t *searchTask) PreExecute(ctx context.Context) error {
timezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, t.request.SearchParams)
if exist {
if !funcutil.IsTimezoneValid(timezone) {
if !timestamptz.IsTimezoneValid(timezone) {
log.Info("get invalid timezone from request", zap.String("timezone", timezone))
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", timezone)
}

View File

@ -60,6 +60,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/metric"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/tsoutil"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -2901,7 +2902,7 @@ func timestamptzUTC2IsoStr(results []*schemapb.FieldData, colTimezone string) er
localTime := t.In(location)
// 3. Format using the optimized logic (max 6 digits, no trailing zeros)
isoStrings[i] = funcutil.FormatTimeMicroWithoutTrailingZeros(localTime)
isoStrings[i] = timestamptz.FormatTimeMicroWithoutTrailingZeros(localTime)
}
// Replace the TimestamptzData with the new StringData in place.

View File

@ -19,6 +19,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/parameterutil"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -590,7 +591,7 @@ func FillWithDefaultValue(field *schemapb.FieldData, fieldSchema *schemapb.Field
// as UTC/the collection's primary timezone, the 'common.DefaultTimezone' passed here
// as the fallback timezone is generally inconsequential (negligible)
// for the final conversion result in this specific context.
defaultValue, _ = funcutil.ValidateAndReturnUnixMicroTz(strDefaultValue, common.DefaultTimezone)
defaultValue, _ = timestamptz.ValidateAndReturnUnixMicroTz(strDefaultValue, common.DefaultTimezone)
}
}
sd.TimestamptzData.Data, err = fillWithDefaultValueImpl(sd.TimestamptzData.Data, defaultValue, field.GetValidData())
@ -1165,8 +1166,8 @@ func (v *validateUtil) checkTimestamptzFieldData(field *schemapb.FieldData, time
// 2. Validation and Conversion Loop
for i, isoStr := range stringData {
// Use the centralized parser (funcutil.ParseTimeTz) for validation and parsing.
t, err := funcutil.ParseTimeTz(isoStr, timezone)
// Use the centralized parser (timestamptz.ParseTimeTz) for validation and parsing.
t, err := timestamptz.ParseTimeTz(isoStr, timezone)
if err != nil {
log.Warn("cannot parse timestamptz string", zap.String("timestamp_string", isoStr), zap.Error(err))
// Use the recommended refined error message structure

View File

@ -40,6 +40,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -153,70 +154,6 @@ func checkGeometryDefaultValue(value string) error {
return nil
}
// checkAndRewriteTimestampTzDefaultValue processes the collection schema to validate
// and rewrite default values for TIMESTAMPTZ fields.
//
// Background:
// 1. TIMESTAMPTZ default values are initially stored as user-provided ISO 8601 strings
// (in ValueField.GetStringData()).
// 2. Milvus stores TIMESTAMPTZ data internally as UTC microseconds (int64).
//
// Logic:
// The function iterates through all fields of type DataType_Timestamptz. For each field
// with a default value:
// 1. It retrieves the collection's default timezone if no offset is present in the string.
// 2. It calls ValidateAndReturnUnixMicroTz to validate the string (including the UTC
// offset range check) and convert it to the absolute UTC microsecond (int64) value.
// 3. It rewrites the ValueField, setting the LongData field with the calculated int64
// value, thereby replacing the initial string representation.
func checkAndRewriteTimestampTzDefaultValue(schema *schemapb.CollectionSchema) error {
// 1. Get the collection-level default timezone.
// Assuming common.TimezoneKey and common.DefaultTimezone are defined constants.
timezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, schema.GetProperties())
if !exist {
timezone = common.DefaultTimezone
}
for _, fieldSchema := range schema.GetFields() {
// Only process TIMESTAMPTZ fields.
if fieldSchema.GetDataType() != schemapb.DataType_Timestamptz {
continue
}
defaultValue := fieldSchema.GetDefaultValue()
if defaultValue == nil {
continue
}
// 2. Read the default value as a string (the input format).
// We expect the default value to be set in string_data initially.
stringTz := defaultValue.GetStringData()
if stringTz == "" {
// Skip or handle empty string default values if necessary.
continue
}
// 3. Validate the string and convert it to UTC microsecond (int64).
// This also performs the critical UTC offset range validation.
utcMicro, err := funcutil.ValidateAndReturnUnixMicroTz(stringTz, timezone)
if err != nil {
// If validation fails (e.g., invalid format or illegal offset), return error immediately.
return err
}
// 4. Rewrite the default value to store the UTC microsecond (int64).
// By setting ValueField_LongData, the oneof field in the protobuf structure
// automatically switches from string_data to long_data.
defaultValue.Data = &schemapb.ValueField_LongData{
LongData: utcMicro,
}
// The original string_data field is now cleared due to the oneof nature,
// and the default value is correctly represented as an int64 microsecond value.
}
return nil
}
func hasSystemFields(schema *schemapb.CollectionSchema, systemFields []string) bool {
for _, f := range schema.GetFields() {
if funcutil.SliceContain(systemFields, f.GetName()) {
@ -239,7 +176,7 @@ func (t *createCollectionTask) validateSchema(ctx context.Context, schema *schem
}
// Validate default
if err := checkAndRewriteTimestampTzDefaultValue(schema); err != nil {
if err := timestamptz.CheckAndRewriteTimestampTzDefaultValue(schema); err != nil {
return err
}
@ -454,7 +391,7 @@ func (t *createCollectionTask) prepareSchema(ctx context.Context) error {
// Validate timezone
tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, t.Req.GetProperties())
if exist && !funcutil.IsTimezoneValid(tz) {
if exist && !timestamptz.IsTimezoneValid(tz) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
}

View File

@ -11,9 +11,12 @@ import (
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
"github.com/milvus-io/milvus/internal/distributed/streaming"
"github.com/milvus-io/milvus/internal/metastore/model"
"github.com/milvus-io/milvus/pkg/v2/common"
"github.com/milvus-io/milvus/pkg/v2/proto/messagespb"
"github.com/milvus-io/milvus/pkg/v2/streaming/util/message"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -39,6 +42,15 @@ func (c *Core) broadcastAlterCollectionForAddField(ctx context.Context, req *mil
if err := checkFieldSchema([]*schemapb.FieldSchema{fieldSchema}); err != nil {
return errors.Wrap(err, "failed to check field schema")
}
if fieldSchema.GetDataType() == schemapb.DataType_Timestamptz {
timezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, coll.Properties)
if !exist {
timezone = common.DefaultTimezone
}
timestamptz.CheckAndRewriteTimestampTzDefaultValueForFieldSchema(fieldSchema, timezone)
}
// check if the field already exists
for _, field := range coll.Fields {
if field.Name == fieldSchema.Name {

View File

@ -21,6 +21,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/streaming/util/message/ce"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -48,7 +49,7 @@ func (c *Core) broadcastAlterCollectionForAlterCollection(ctx context.Context, r
// Validate timezone
tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, req.GetProperties())
if exist && !funcutil.IsTimezoneValid(tz) {
if exist && !timestamptz.IsTimezoneValid(tz) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
}

View File

@ -34,6 +34,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/streaming/util/message/ce"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -55,7 +56,7 @@ func (c *Core) broadcastAlterDatabase(ctx context.Context, req *rootcoordpb.Alte
// Validate timezone
tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, req.GetProperties())
if exist && !funcutil.IsTimezoneValid(tz) {
if exist && !timestamptz.IsTimezoneValid(tz) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
}

View File

@ -62,7 +62,7 @@ func (c *Core) broadcastCreateCollectionV1(ctx context.Context, req *milvuspb.Cr
}
defer broadcaster.Close()
// prepare and validate the create collection message.
// prepare and validate the creation collection message.
createCollectionTask := createCollectionTask{
Core: c,
Req: req,

View File

@ -33,6 +33,7 @@ import (
"github.com/milvus-io/milvus/pkg/v2/streaming/util/message/ce"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
)
func (c *Core) broadcastCreateDatabase(ctx context.Context, req *milvuspb.CreateDatabaseRequest) error {
@ -58,7 +59,7 @@ func (c *Core) broadcastCreateDatabase(ctx context.Context, req *milvuspb.Create
return errors.Wrap(err, "failed to tidy database cipher properties")
}
tz, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, properties)
if exist && !funcutil.IsTimezoneValid(tz) {
if exist && !timestamptz.IsTimezoneValid(tz) {
return merr.WrapErrParameterInvalidMsg("unknown or invalid IANA Time Zone ID: %s", tz)
}
msg := message.NewCreateDatabaseMessageBuilderV2().

View File

@ -66,11 +66,6 @@ func (t *describeCollectionTask) Execute(ctx context.Context) (err error) {
return err
}
t.Rsp = convertModelToDesc(coll, aliases, db.Name)
// NEW STEP: Convert TIMESTAMPTZ default values back to string format for the user.
err = rewriteTimestampTzDefaultValueToString(t.Rsp)
if err != nil {
return err
}
t.Rsp.RequestTime = t.ts
return nil
}

View File

@ -1072,65 +1072,6 @@ func convertModelToDesc(collInfo *model.Collection, aliases []string, dbName str
return resp
}
// rewriteTimestampTzDefaultValueToString converts the default_value of TIMESTAMPTZ fields
// in the DescribeCollectionResponse from the internal int64 (UTC microsecond) format
// back to a human-readable, timezone-aware string (RFC3339Nano).
//
// This is necessary because TIMESTAMPTZ default values are stored internally as int64
// after validation but must be returned to the user as a string, respecting the
// collection's default timezone for display purposes if no explicit offset was stored.
func rewriteTimestampTzDefaultValueToString(resp *milvuspb.DescribeCollectionResponse) error {
if resp.GetSchema() == nil {
return nil
}
// 1. Determine the target timezone for display.
// This is typically stored in the collection properties.
timezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, resp.GetSchema().GetProperties())
if !exist {
timezone = common.DefaultTimezone // Fallback to a default, like "UTC"
}
// 2. Iterate through all fields in the schema.
for _, fieldSchema := range resp.Schema.GetFields() {
// Only process TIMESTAMPTZ fields.
if fieldSchema.GetDataType() != schemapb.DataType_Timestamptz {
continue
}
defaultValue := fieldSchema.GetDefaultValue()
if defaultValue == nil {
continue
}
// 3. Check if the default value is stored in the internal int64 (LongData) format.
// If it's not LongData, we assume it's either unset or already a string (which shouldn't happen
// if the creation flow worked correctly).
utcMicro, ok := defaultValue.GetData().(*schemapb.ValueField_LongData)
if !ok {
continue // Skip if not stored as LongData (int64)
}
ts := utcMicro.LongData
// 4. Convert the int64 microsecond value back to a timezone-aware string.
tzString, err := funcutil.ConvertUnixMicroToTimezoneString(ts, timezone)
if err != nil {
// In a real system, you might log the error and use the raw int64 as a fallback string,
// but here we'll set a placeholder string to avoid crashing.
tzString = fmt.Sprintf("Error converting timestamp: %v", err)
return errors.Wrap(err, tzString)
}
// 5. Rewrite the default value field in the response schema.
// The protobuf oneof structure ensures setting one field clears the others.
fieldSchema.GetDefaultValue().Data = &schemapb.ValueField_StringData{
StringData: tzString,
}
}
return nil
}
func (c *Core) describeCollectionImpl(ctx context.Context, in *milvuspb.DescribeCollectionRequest, allowUnavailable bool) (*milvuspb.DescribeCollectionResponse, error) {
if err := merr.CheckHealthy(c.GetStateCode()); err != nil {
return &milvuspb.DescribeCollectionResponse{

View File

@ -28,9 +28,9 @@ import (
"github.com/milvus-io/milvus/internal/util/importutilv2/common"
"github.com/milvus-io/milvus/internal/util/nullutil"
pkgcommon "github.com/milvus-io/milvus/pkg/v2/common"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/parameterutil"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -446,7 +446,7 @@ func (r *rowParser) parseEntity(field *schemapb.FieldSchema, obj string, useElem
}
return wkbValue, nil
case schemapb.DataType_Timestamptz:
tz, err := funcutil.ValidateAndReturnUnixMicroTz(obj, r.timezone)
tz, err := timestamptz.ValidateAndReturnUnixMicroTz(obj, r.timezone)
if err != nil {
return nil, err
}

View File

@ -27,9 +27,9 @@ import (
"github.com/milvus-io/milvus/internal/util/importutilv2/common"
"github.com/milvus-io/milvus/internal/util/nullutil"
pkgcommon "github.com/milvus-io/milvus/pkg/v2/common"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/parameterutil"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -571,7 +571,7 @@ func (r *rowParser) parseEntity(fieldID int64, obj any) (any, error) {
if !ok {
return nil, r.wrapTypeError(obj, fieldID)
}
tz, err := funcutil.ValidateAndReturnUnixMicroTz(strValue, r.timezone)
tz, err := timestamptz.ValidateAndReturnUnixMicroTz(strValue, r.timezone)
if err != nil {
return nil, err
}

View File

@ -31,9 +31,9 @@ import (
"github.com/milvus-io/milvus/internal/json"
"github.com/milvus-io/milvus/internal/util/importutilv2/common"
pkgcommon "github.com/milvus-io/milvus/pkg/v2/common"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/parameterutil"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -192,7 +192,7 @@ func (c *FieldReader) Next(count int64) (any, any, error) {
}
int64Ts := make([]int64, 0, len(strs))
for _, strValue := range strs {
tz, err := funcutil.ValidateAndReturnUnixMicroTz(strValue, c.timezone)
tz, err := timestamptz.ValidateAndReturnUnixMicroTz(strValue, c.timezone)
if err != nil {
return nil, nil, err
}

View File

@ -33,9 +33,9 @@ import (
"github.com/milvus-io/milvus/internal/util/importutilv2/common"
"github.com/milvus-io/milvus/internal/util/nullutil"
pkgcommon "github.com/milvus-io/milvus/pkg/v2/common"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
"github.com/milvus-io/milvus/pkg/v2/util/merr"
"github.com/milvus-io/milvus/pkg/v2/util/parameterutil"
"github.com/milvus-io/milvus/pkg/v2/util/timestamptz"
"github.com/milvus-io/milvus/pkg/v2/util/typeutil"
)
@ -729,7 +729,7 @@ func ReadNullableTimestamptzData(pcr *FieldReader, count int64) (any, []bool, er
// Convert the ISO 8601 string to int64 (UTC microseconds).
// The pcr.timezone is used as the default timezone if the string (strValue)
// does not contain an explicit UTC offset (e.g., "+08:00").
tz, err := funcutil.ValidateAndReturnUnixMicroTz(strValue, pcr.timezone)
tz, err := timestamptz.ValidateAndReturnUnixMicroTz(strValue, pcr.timezone)
if err != nil {
return nil, nil, err
}
@ -755,7 +755,7 @@ func ReadTimestamptzData(pcr *FieldReader, count int64) (any, error) {
for _, strValue := range data.([]string) {
// Convert the ISO 8601 string to int64 (UTC microseconds).
// The pcr.timezone is used as the default if the string lacks an explicit offset.
tz, err := funcutil.ValidateAndReturnUnixMicroTz(strValue, pcr.timezone)
tz, err := timestamptz.ValidateAndReturnUnixMicroTz(strValue, pcr.timezone)
if err != nil {
return nil, err
}

View File

@ -349,6 +349,7 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gogo/status v1.1.0/go.mod h1:BFv9nrluPLmrS0EmGVvLaPNmRosr9KapBYd5/hpY1WM=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=

View File

@ -1,200 +0,0 @@
package funcutil
import (
"bytes"
"fmt"
"strings"
"time"
)
// Define max/min offset boundaries in seconds for validation, exported for external checks if necessary.
const (
MaxOffsetSeconds = 14 * 3600 // +14:00
MinOffsetSeconds = -12 * 3600 // -12:00
)
// NaiveTzLayouts is a list of common timestamp formats that lack timezone information.
var NaiveTzLayouts = []string{
"2006-01-02T15:04:05.999999999",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05.999999999",
"2006-01-02 15:04:05",
}
// ParseTimeTz is the internal core function for parsing TZ-aware or naive timestamps.
// It includes strict validation for the UTC offset range.
func ParseTimeTz(inputStr string, defaultTimezoneStr string) (time.Time, error) {
// 1. Primary parsing: Attempt to parse a TZ-aware string (RFC3339Nano)
t, err := time.Parse(time.RFC3339Nano, inputStr)
if err == nil {
// Parsing succeeded (TZ-aware string). Now, perform the strict offset validation.
// If the string contains an explicit offset (like +99:00), t.Zone() will reflect it.
_, offsetSeconds := t.Zone()
if offsetSeconds > MaxOffsetSeconds || offsetSeconds < MinOffsetSeconds {
offsetHours := offsetSeconds / 3600
return time.Time{}, fmt.Errorf("UTC offset hour %d is out of the valid range [%d, %d]", offsetHours, MinOffsetSeconds/3600, MaxOffsetSeconds/3600)
}
return t, nil
}
loc, err := time.LoadLocation(defaultTimezoneStr)
if err != nil {
return time.Time{}, fmt.Errorf("invalid default timezone string '%s': %w", defaultTimezoneStr, err)
}
// 2. Fallback parsing: Attempt to parse a naive string using NaiveTzLayouts
var parsed bool
for _, layout := range NaiveTzLayouts {
// For naive strings, time.ParseInLocation assigns the default location (loc).
parsedTime, parseErr := time.ParseInLocation(layout, inputStr, loc)
if parseErr == nil {
t = parsedTime
parsed = true
break
}
}
if !parsed {
return time.Time{}, fmt.Errorf("invalid timestamp string: '%s'. Does not match any known format", inputStr)
}
// No offset validation needed here: The time was assigned the safe defaultTimezoneStr (loc),
// which is already validated via time.LoadLocation.
return t, nil
}
// ValidateTimestampTz checks if the timestamp string is valid (TZ-aware or naive + default TZ).
func ValidateTimestampTz(inputStr string, defaultTimezoneStr string) error {
_, err := ParseTimeTz(inputStr, defaultTimezoneStr)
return err
}
// ValidateAndNormalizeTimestampTz validates the timestamp and normalizes it to a TZ-aware RFC3339Nano string.
func ValidateAndNormalizeTimestampTz(inputStr string, defaultTimezoneStr string) (string, error) {
t, err := ParseTimeTz(inputStr, defaultTimezoneStr)
if err != nil {
return "", err
}
// Normalization: Format the time object to include the timezone offset.
return t.Format(time.RFC3339Nano), nil
}
// ValidateAndReturnUnixMicroTz validates the timestamp and returns its Unix microsecond (int64) representation.
func ValidateAndReturnUnixMicroTz(inputStr string, defaultTimezoneStr string) (int64, error) {
t, err := ParseTimeTz(inputStr, defaultTimezoneStr)
if err != nil {
return 0, err
}
// UnixMicro() returns the number of microseconds since UTC 1970-01-01T00:00:00Z.
return t.UnixMicro(), nil
}
// CompareUnixMicroTz compares two timestamp strings at Unix microsecond precision.
// If both strings are valid and represent the same microsecond moment in time, it returns true.
// Note: It assumes the input strings are guaranteed to be valid as per the requirement.
// If not, it will return an error indicating the invalid input.
func CompareUnixMicroTz(ts1 string, ts2 string, defaultTimezoneStr string) (bool, error) {
// 1. Parse the first timestamp
t1, err := ParseTimeTz(ts1, defaultTimezoneStr)
if err != nil {
return false, fmt.Errorf("error parsing first timestamp '%s': %w", ts1, err)
}
// 2. Parse the second timestamp
t2, err := ParseTimeTz(ts2, defaultTimezoneStr)
if err != nil {
return false, fmt.Errorf("error parsing second timestamp '%s': %w", ts2, err)
}
// 3. Compare their Unix Microsecond values (int64)
// This automatically compares them based on the UTC epoch, regardless of their original location representation.
return t1.UnixMicro() == t2.UnixMicro(), nil
}
// ConvertUnixMicroToTimezoneString converts a Unix microsecond timestamp (UTC epoch)
// into a TZ-aware string formatted as RFC3339Nano, adjusted to the target timezone.
func ConvertUnixMicroToTimezoneString(ts int64, targetTimezoneStr string) (string, error) {
loc, err := time.LoadLocation(targetTimezoneStr)
if err != nil {
return "", fmt.Errorf("invalid target timezone string '%s': %w", targetTimezoneStr, err)
}
// 1. Convert Unix Microsecond (UTC) to a time.Time object (still in UTC).
t := time.UnixMicro(ts).UTC()
// 2. Adjust the time object to the target location.
localTime := t.In(loc)
// 3. Format the result.
return localTime.Format(time.RFC3339Nano), nil
}
// formatTimeMicroWithoutTrailingZeros is an optimized function to format a time.Time
// object. It first truncates the time to microsecond precision (6 digits) and then
// removes all trailing zeros from the fractional seconds part.
//
// Example 1: 2025-03-20T10:30:00.123456000Z -> 2025-03-20T10:30:00.123456Z
// Example 2: 2025-03-20T10:30:00.123000000Z -> 2025-03-20T10:30:00.123Z
// Example 3: 2025-03-20T10:30:00.000000000Z -> 2025-03-20T10:30:00Z
func FormatTimeMicroWithoutTrailingZeros(t time.Time) string {
// 1. Truncate to Microsecond (6 digits max) to ensure we don't exceed the required precision.
tMicro := t.Truncate(time.Microsecond)
// 2. Format the time using the standard high precision format (RFC3339Nano).
// This results in exactly 9 fractional digits, padded with trailing zeros if necessary.
s := tMicro.Format(time.RFC3339Nano)
// 3. Locate the key delimiters ('.' and the Timezone marker 'Z' or '+/-').
dotIndex := strings.LastIndexByte(s, '.')
// Find the Timezone marker index (Z, +, or -)
tzIndex := len(s) - 1
for ; tzIndex >= 0; tzIndex-- {
if s[tzIndex] == 'Z' || s[tzIndex] == '+' || s[tzIndex] == '-' {
break
}
}
// If the format is unexpected, return the original string.
if dotIndex == -1 || tzIndex == -1 {
return s
}
// 4. Extract and efficiently trim the fractional part using bytes.TrimRight.
// Slice the fractional part (e.g., "123456000")
fractionalPart := s[dotIndex+1 : tzIndex]
// Use bytes.TrimRight for efficient removal of trailing '0' characters.
trimmedBytes := bytes.TrimRight([]byte(fractionalPart), "0")
// 5. Reconstruct the final string based on the trimming result.
// Case A: The fractional part was entirely zeros (e.g., .000000000)
if len(trimmedBytes) == 0 {
// Remove the '.' and the fractional part, keep the Timezone marker.
// Result: "2025-03-20T10:30:00Z"
return s[:dotIndex] + s[tzIndex:]
}
// Case B: Fractional part remains (e.g., .123, .123456)
// Recombine: [Time Body] + "." + [Trimmed Fraction] + [Timezone Marker]
// The dot (s[:dotIndex+1]) must be retained here.
return s[:dotIndex+1] + string(trimmedBytes) + s[tzIndex:]
}
// IsTimezoneValid checks if a given string is a valid, recognized timezone name
// (e.g., "Asia/Shanghai" or "UTC").
// It utilizes Go's time.LoadLocation function.
func IsTimezoneValid(tz string) bool {
if tz == "" {
return false
}
_, err := time.LoadLocation(tz)
return err == nil
}

View File

@ -0,0 +1,385 @@
package timestamptz
import (
"bytes"
"fmt"
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/milvus-io/milvus-proto/go-api/v2/schemapb"
"github.com/milvus-io/milvus/pkg/v2/common"
"github.com/milvus-io/milvus/pkg/v2/util/funcutil"
)
// Define max/min offset boundaries in seconds for validation, exported for external checks if necessary.
const (
MaxOffsetSeconds = 14 * 3600 // +14:00
MinOffsetSeconds = -12 * 3600 // -12:00
)
// NaiveTzLayouts is a list of common timestamp formats that lack timezone information.
var NaiveTzLayouts = []string{
"2006-01-02T15:04:05.999999999",
"2006-01-02T15:04:05",
"2006-01-02 15:04:05.999999999",
"2006-01-02 15:04:05",
}
// ParseTimeTz is the internal core function for parsing TZ-aware or naive timestamps.
// It includes strict validation for the UTC offset range.
func ParseTimeTz(inputStr string, defaultTimezoneStr string) (time.Time, error) {
// 1. Primary parsing: Attempt to parse a TZ-aware string (RFC3339Nano)
t, err := time.Parse(time.RFC3339Nano, inputStr)
if err == nil {
// Parsing succeeded (TZ-aware string). Now, perform the strict offset validation.
// If the string contains an explicit offset (like +99:00), t.Zone() will reflect it.
_, offsetSeconds := t.Zone()
if offsetSeconds > MaxOffsetSeconds || offsetSeconds < MinOffsetSeconds {
offsetHours := offsetSeconds / 3600
return time.Time{}, fmt.Errorf("UTC offset hour %d is out of the valid range [%d, %d]", offsetHours, MinOffsetSeconds/3600, MaxOffsetSeconds/3600)
}
return t, nil
}
loc, err := time.LoadLocation(defaultTimezoneStr)
if err != nil {
return time.Time{}, fmt.Errorf("invalid default timezone string '%s': %w", defaultTimezoneStr, err)
}
// 2. Fallback parsing: Attempt to parse a naive string using NaiveTzLayouts
var parsed bool
for _, layout := range NaiveTzLayouts {
// For naive strings, time.ParseInLocation assigns the default location (loc).
parsedTime, parseErr := time.ParseInLocation(layout, inputStr, loc)
if parseErr == nil {
t = parsedTime
parsed = true
break
}
}
if !parsed {
return time.Time{}, fmt.Errorf("invalid timestamp string: '%s'. Does not match any known format", inputStr)
}
// No offset validation needed here: The time was assigned the safe defaultTimezoneStr (loc),
// which is already validated via time.LoadLocation.
return t, nil
}
// ValidateTimestampTz checks if the timestamp string is valid (TZ-aware or naive + default TZ).
func ValidateTimestampTz(inputStr string, defaultTimezoneStr string) error {
_, err := ParseTimeTz(inputStr, defaultTimezoneStr)
return err
}
// ValidateAndNormalizeTimestampTz validates the timestamp and normalizes it to a TZ-aware RFC3339Nano string.
func ValidateAndNormalizeTimestampTz(inputStr string, defaultTimezoneStr string) (string, error) {
t, err := ParseTimeTz(inputStr, defaultTimezoneStr)
if err != nil {
return "", err
}
// Normalization: Format the time object to include the timezone offset.
return t.Format(time.RFC3339Nano), nil
}
// ValidateAndReturnUnixMicroTz validates the timestamp and returns its Unix microsecond (int64) representation.
func ValidateAndReturnUnixMicroTz(inputStr string, defaultTimezoneStr string) (int64, error) {
t, err := ParseTimeTz(inputStr, defaultTimezoneStr)
if err != nil {
return 0, err
}
// UnixMicro() returns the number of microseconds since UTC 1970-01-01T00:00:00Z.
return t.UnixMicro(), nil
}
// CompareUnixMicroTz compares two timestamp strings at Unix microsecond precision.
// If both strings are valid and represent the same microsecond moment in time, it returns true.
// Note: It assumes the input strings are guaranteed to be valid as per the requirement.
// If not, it will return an error indicating the invalid input.
func CompareUnixMicroTz(ts1 string, ts2 string, defaultTimezoneStr string) (bool, error) {
// 1. Parse the first timestamp
t1, err := ParseTimeTz(ts1, defaultTimezoneStr)
if err != nil {
return false, fmt.Errorf("error parsing first timestamp '%s': %w", ts1, err)
}
// 2. Parse the second timestamp
t2, err := ParseTimeTz(ts2, defaultTimezoneStr)
if err != nil {
return false, fmt.Errorf("error parsing second timestamp '%s': %w", ts2, err)
}
// 3. Compare their Unix Microsecond values (int64)
// This automatically compares them based on the UTC epoch, regardless of their original location representation.
return t1.UnixMicro() == t2.UnixMicro(), nil
}
// ConvertUnixMicroToTimezoneString converts a Unix microsecond timestamp (UTC epoch)
// into a TZ-aware string formatted as RFC3339Nano, adjusted to the target timezone.
func ConvertUnixMicroToTimezoneString(ts int64, targetTimezoneStr string) (string, error) {
loc, err := time.LoadLocation(targetTimezoneStr)
if err != nil {
return "", fmt.Errorf("invalid target timezone string '%s': %w", targetTimezoneStr, err)
}
// 1. Convert Unix Microsecond (UTC) to a time.Time object (still in UTC).
t := time.UnixMicro(ts).UTC()
// 2. Adjust the time object to the target location.
localTime := t.In(loc)
// 3. Format the result.
return localTime.Format(time.RFC3339Nano), nil
}
// formatTimeMicroWithoutTrailingZeros is an optimized function to format a time.Time
// object. It first truncates the time to microsecond precision (6 digits) and then
// removes all trailing zeros from the fractional seconds part.
//
// Example 1: 2025-03-20T10:30:00.123456000Z -> 2025-03-20T10:30:00.123456Z
// Example 2: 2025-03-20T10:30:00.123000000Z -> 2025-03-20T10:30:00.123Z
// Example 3: 2025-03-20T10:30:00.000000000Z -> 2025-03-20T10:30:00Z
func FormatTimeMicroWithoutTrailingZeros(t time.Time) string {
// 1. Truncate to Microsecond (6 digits max) to ensure we don't exceed the required precision.
tMicro := t.Truncate(time.Microsecond)
// 2. Format the time using the standard high precision format (RFC3339Nano).
// This results in exactly 9 fractional digits, padded with trailing zeros if necessary.
s := tMicro.Format(time.RFC3339Nano)
// 3. Locate the key delimiters ('.' and the Timezone marker 'Z' or '+/-').
dotIndex := strings.LastIndexByte(s, '.')
// Find the Timezone marker index (Z, +, or -)
tzIndex := len(s) - 1
for ; tzIndex >= 0; tzIndex-- {
if s[tzIndex] == 'Z' || s[tzIndex] == '+' || s[tzIndex] == '-' {
break
}
}
// If the format is unexpected, return the original string.
if dotIndex == -1 || tzIndex == -1 {
return s
}
// 4. Extract and efficiently trim the fractional part using bytes.TrimRight.
// Slice the fractional part (e.g., "123456000")
fractionalPart := s[dotIndex+1 : tzIndex]
// Use bytes.TrimRight for efficient removal of trailing '0' characters.
trimmedBytes := bytes.TrimRight([]byte(fractionalPart), "0")
// 5. Reconstruct the final string based on the trimming result.
// Case A: The fractional part was entirely zeros (e.g., .000000000)
if len(trimmedBytes) == 0 {
// Remove the '.' and the fractional part, keep the Timezone marker.
// Result: "2025-03-20T10:30:00Z"
return s[:dotIndex] + s[tzIndex:]
}
// Case B: Fractional part remains (e.g., .123, .123456)
// Recombine: [Time Body] + "." + [Trimmed Fraction] + [Timezone Marker]
// The dot (s[:dotIndex+1]) must be retained here.
return s[:dotIndex+1] + string(trimmedBytes) + s[tzIndex:]
}
// IsTimezoneValid checks if a given string is a valid, recognized timezone name
// (e.g., "Asia/Shanghai" or "UTC").
// It utilizes Go's time.LoadLocation function.
func IsTimezoneValid(tz string) bool {
if tz == "" {
return false
}
_, err := time.LoadLocation(tz)
return err == nil
}
// CheckAndRewriteTimestampTzDefaultValue processes the collection schema to validate
// and rewrite default values for TIMESTAMPTZ fields.
//
// Background:
// 1. TIMESTAMPTZ default values are initially stored as user-provided ISO 8601 strings
// (in ValueField.GetStringData()).
// 2. Milvus stores TIMESTAMPTZ data internally as UTC microseconds (int64).
//
// Logic:
// The function iterates through all fields of type DataType_Timestamptz. For each field
// with a default value:
// 1. It retrieves the collection's default timezone if no offset is present in the string.
// 2. It calls ValidateAndReturnUnixMicroTz to validate the string (including the UTC
// offset range check) and convert it to the absolute UTC microsecond (int64) value.
// 3. It rewrites the ValueField, setting the LongData field with the calculated int64
// value, thereby replacing the initial string representation.
func CheckAndRewriteTimestampTzDefaultValue(schema *schemapb.CollectionSchema) error {
// 1. Get the collection-level default timezone.
// Assuming common.TimezoneKey and common.DefaultTimezone are defined constants.
timezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, schema.GetProperties())
if !exist {
timezone = common.DefaultTimezone
}
for _, fieldSchema := range schema.GetFields() {
// Only process TIMESTAMPTZ fields.
if fieldSchema.GetDataType() != schemapb.DataType_Timestamptz {
continue
}
defaultValue := fieldSchema.GetDefaultValue()
if defaultValue == nil {
continue
}
// 2. Read the default value as a string (the input format).
// We expect the default value to be set in string_data initially.
stringTz := defaultValue.GetStringData()
if stringTz == "" {
// Skip or handle empty string default values if necessary.
continue
}
// 3. Validate the string and convert it to UTC microsecond (int64).
// This also performs the critical UTC offset range validation.
utcMicro, err := ValidateAndReturnUnixMicroTz(stringTz, timezone)
if err != nil {
// If validation fails (e.g., invalid format or illegal offset), return error immediately.
return err
}
// 4. Rewrite the default value to store the UTC microsecond (int64).
// By setting ValueField_LongData, the oneof field in the protobuf structure
// automatically switches from string_data to timestamptz_data(int64).
defaultValue.Data = &schemapb.ValueField_TimestamptzData{
TimestamptzData: utcMicro,
}
// The original string_data field is now cleared due to the oneof nature,
// and the default value is correctly represented as an int64 microsecond value.
}
return nil
}
// CheckAndRewriteTimestampTzDefaultValueForFieldSchema processes a single FieldSchema
// to validate and rewrite the default value specifically for TIMESTAMPTZ fields.
//
// The function ensures the default value (initially a string) is correctly converted
// and stored internally as an absolute UTC microsecond (int64) value.
//
// Parameters:
//
// fieldSchema: The specific FieldSchema object to be processed.
// collectionTimezone: The collection-level default timezone string (e.g., "UTC", "Asia/Shanghai")
// used to parse timestamps without an explicit offset.
//
// Returns:
//
// error: An error if validation fails (e.g., invalid timestamp format or illegal offset range), otherwise nil.
func CheckAndRewriteTimestampTzDefaultValueForFieldSchema(
fieldSchema *schemapb.FieldSchema,
collectionTimezone string,
) error {
defaultValue := fieldSchema.GetDefaultValue()
if defaultValue == nil {
return nil
}
// log.Info("czsKKK111")
// 2. Read the default value as a string (the initial user-provided format).
// The default value is expected to be stored in string_data initially.
stringTz := defaultValue.GetStringData()
if stringTz == "" {
// Skip or handle empty string default values if necessary.
// log.Info("czsKKK222")
return nil
}
// 3. Validate the string and convert it to UTC microsecond (int64).
// The validation function also applies the collectionTimezone if no offset is present
// in the input stringTz, and performs offset range checks.
utcMicro, err := ValidateAndReturnUnixMicroTz(stringTz, collectionTimezone)
if err != nil {
// log.Info("czsKKK333")
// If validation fails (e.g., invalid format or illegal offset), return error immediately.
return err
}
// 4. Rewrite the default value to store the absolute UTC microsecond (int64).
// By setting ValueField_LongData, the oneof field in the protobuf structure
// automatically switches the internal representation from string_data to timestamptz_data(int64).
defaultValue.Data = &schemapb.ValueField_TimestamptzData{
TimestamptzData: utcMicro,
}
fieldSchema.DefaultValue = defaultValue
// log.Info("czsKKK444", zap.Any("utc", fieldSchema.GetDefaultValue()))
return nil
}
// RewriteTimestampTzDefaultValueToString converts the default_value of TIMESTAMPTZ fields
// in the DescribeCollectionResponse from the internal int64 (UTC microsecond) format
// back to a human-readable, timezone-aware string (RFC3339Nano).
//
// This is necessary because TIMESTAMPTZ default values are stored internally as int64
// after validation but must be returned to the user as a string, respecting the
// collection's default timezone for display purposes if no explicit offset was stored.
func RewriteTimestampTzDefaultValueToString(schema *schemapb.CollectionSchema) error {
if schema == nil {
return nil
}
// 1. Determine the target timezone for display.
// This is typically stored in the collection properties.
timezone, exist := funcutil.TryGetAttrByKeyFromRepeatedKV(common.TimezoneKey, schema.GetProperties())
if !exist {
timezone = common.DefaultTimezone // Fallback to a default, like "UTC"
}
// 2. Iterate through all fields in the schema.
for _, fieldSchema := range schema.GetFields() {
// Only process TIMESTAMPTZ fields.
if fieldSchema.GetDataType() != schemapb.DataType_Timestamptz {
continue
}
defaultValue := fieldSchema.GetDefaultValue()
if defaultValue == nil {
continue
}
// 3. Check if the default value is stored in the internal int64 (LongData) format.
// If it's not LongData, we assume it's either unset or already a string (which shouldn't happen
// if the creation flow worked correctly).
utcMicro, ok := defaultValue.GetData().(*schemapb.ValueField_TimestamptzData)
if !ok {
continue // Skip if not stored as LongData (int64)
}
ts := utcMicro.TimestamptzData
// 4. Convert the int64 microsecond value back to a timezone-aware string.
tzString, err := ConvertUnixMicroToTimezoneString(ts, timezone)
if err != nil {
// In a real system, you might log the error and use the raw int64 as a fallback string,
// but here we'll set a placeholder string to avoid crashing.
tzString = fmt.Sprintf("error converting timestamp: %v", err)
return errors.Wrap(err, tzString)
}
// 5. Rewrite the default value field in the response schema.
// The protobuf oneof structure ensures setting one field clears the others.
fieldSchema.GetDefaultValue().Data = &schemapb.ValueField_StringData{
StringData: tzString,
}
}
return nil
}

View File

@ -1,4 +1,4 @@
package funcutil
package timestamptz
import (
"testing"