diff --git a/entgo/go.mod b/entgo/go.mod index 2be9311..4468168 100644 --- a/entgo/go.mod +++ b/entgo/go.mod @@ -1,26 +1,26 @@ module github.com/tx7do/go-utils/entgo -go 1.23.0 +go 1.24 -toolchain go1.23.2 +toolchain go1.24.4 replace github.com/tx7do/go-utils/id => ../id require ( entgo.io/contrib v0.6.0 entgo.io/ent v0.14.4 - github.com/XSAM/otelsql v0.38.0 + github.com/XSAM/otelsql v0.39.0 github.com/go-kratos/kratos/v2 v2.8.4 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.10.0 - github.com/tx7do/go-utils v1.1.28 + github.com/tx7do/go-utils v1.1.29 github.com/tx7do/go-utils/id v0.0.2 go.opentelemetry.io/otel v1.36.0 google.golang.org/protobuf v1.36.6 ) require ( - ariga.io/atlas v0.34.0 // indirect + ariga.io/atlas v0.35.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect @@ -43,15 +43,15 @@ require ( github.com/sony/sonyflake v1.2.1 // indirect github.com/zclconf/go-cty v1.16.3 // indirect github.com/zclconf/go-cty-yaml v1.1.0 // indirect - go.mongodb.org/mongo-driver v1.17.3 // indirect + go.mongodb.org/mongo-driver v1.17.4 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/mod v0.24.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/entgo/go.sum b/entgo/go.sum index 5dec224..471349e 100644 --- a/entgo/go.sum +++ b/entgo/go.sum @@ -1,13 +1,13 @@ -ariga.io/atlas v0.34.0 h1:4hdy+2x+xNs6Lx2anuJ/4Q7lCaqddbEj5CtRDVOBu0M= -ariga.io/atlas v0.34.0/go.mod h1:WJesu2UCpGQvgUh3oVP94EiRT61nNy1W/VN5g+vqP1I= +ariga.io/atlas v0.35.0 h1:tzco6CEZm1/jGD2ifHhKFlsQB7Bfsc/mty4zwm6Mlbc= +ariga.io/atlas v0.35.0/go.mod h1:9ZAIr/V85596AVxmN8edyVHYKKpnNsDMdnHLsEliW7k= entgo.io/contrib v0.6.0 h1:xfo4TbJE7sJZWx7BV7YrpSz7IPFvS8MzL3fnfzZjKvQ= entgo.io/contrib v0.6.0/go.mod h1:3qWIseJ/9Wx2Hu5zVh15FDzv7d/UvKNcYKdViywWCQg= entgo.io/ent v0.14.4 h1:/DhDraSLXIkBhyiVoJeSshr4ZYi7femzhj6/TckzZuI= entgo.io/ent v0.14.4/go.mod h1:aDPE/OziPEu8+OWbzy4UlvWmD2/kbRuWfK2A40hcxJM= github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/XSAM/otelsql v0.38.0 h1:zWU0/YM9cJhPE71zJcQ2EBHwQDp+G4AX2tPpljslaB8= -github.com/XSAM/otelsql v0.38.0/go.mod h1:5ePOgcLEkWvZtN9H3GV4BUlPeM3p3pzLDCnRG73X8h8= +github.com/XSAM/otelsql v0.39.0 h1:4o374mEIMweaeevL7fd8Q3C710Xi2Jh/c8G4Qy9bvCY= +github.com/XSAM/otelsql v0.39.0/go.mod h1:uMOXLUX+wkuAuP0AR3B45NXX7E9lJS2mERa8gqdU8R0= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= @@ -71,34 +71,34 @@ github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6 github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= -go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ= -go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= -golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= diff --git a/go.sum b/go.sum index ea4b4ac..641c49a 100644 --- a/go.sum +++ b/go.sum @@ -22,12 +22,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI= -golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= diff --git a/query_parser/README.md b/query_parser/README.md index 49f503f..e83efae 100644 --- a/query_parser/README.md +++ b/query_parser/README.md @@ -11,20 +11,39 @@ ## 过滤规则 -过滤器操作本质上是`SQL`里面的`WHERE`条件。 +过滤器操作,其实质上是将用户的查询条件转换为数据库查询语句中的`WHERE`子句。通过这种方式,用户可以根据需要筛选数据,获取更精确的结果。 -过滤器的规则,遵循了Python的ORM的规则,比如: +一个完整的过滤器分为三个部分: -- [Tortoise ORM Filtering](https://tortoise.github.io/query.html#filtering)。 -- [Django Field lookups](https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups) +1. **字段名 (Field)**:要查询的字段。 +2. **操作符 (Operator)**:用于指定查询的类型。 +3. **值 (Value)**:要查询的具体值。 如果只是普通的查询,只需要传递`字段名`即可,但是如果需要一些特殊的查询,那么就需要加入`操作符`了。 -特殊查询的语法规则其实很简单,就是使用双下划线`__`分割字段名和操作符: +过滤器的`操作符`规则,我借鉴并遵循了Python中一些ORM的规则,比如: + +- [Tortoise ORM Filtering][1]。 +- [Django Field lookups][2] + +过滤器通过query参数传递,它必须要遵循某一种格式或者说规则。在这里,我实现了两种格式: + +- **JSON格式**:使用`JSON`对象来表示查询条件。 +- **自定义字符串格式**:使用`自定义的字符串`来表示查询条件。 + +### JSON格式 + +在这套规则里面,我们有2个分隔符: + +- **双下划线** `__`:用于分隔`字段名`和`操作符`,如果没有操作符则视作等于操作。 +- **点号** `.`:用于分隔`字段名`和`JSON字段名`。 ```text -{字段名}__{查找类型} : {值} -{字段名}.{JSON字段名}__{查找类型} : {值} +{字段名}__{操作符} : {查询值} +{字段名}.{JSON字段名}__{操作符} : {查询值} + +{{字段名1}__{操作符1} : {查询值1}, {字段名2}__{操作符2} : {查询值2}} +[{{字段名1}__{操作符1} : {查询值1}}, {{字段名1}__{操作符2} : {查询值2}}] ``` | 查找类型 | 示例 | SQL | 备注 | @@ -68,3 +87,79 @@ | hour | `{"pub_date__hour" : "12"}` | `WHERE EXTRACT('HOUR' FROM pub_date) = '12'` | 小时(0-23) | | minute | `{"pub_date__minute" : "59"}` | `WHERE EXTRACT('MINUTE' FROM pub_date) = '59'` | 分钟 (0-59) | | second | `{"pub_date__second" : "59"}` | `WHERE EXTRACT('SECOND' FROM pub_date) = '59'` | 秒 (0-59) | + +### 自定义字符串格式 + +在这套规则里面,我们有5个分隔符: + +- **逗号** `,`:用于分隔多个`查询条件`,如果没有操作符则视作等于操作。 +- **冒号** `:`:用于分隔`字段名+操作符` 和 `查询值`。 +- **双下划线** `__`:用于分隔`字段名`和`操作符`。 +- **竖线** `|`:用于分隔多个的`查询值`。 +- **点号** `.`:用于分隔`字段名`和`JSON字段名`。 + +```text +{字段名}__{操作符} : {查询值} +{字段名1}__{操作符1} : {查询值1}, {字段名2}__{操作符2} : {查询值2} +{字段名}.{JSON字段名}__{操作符} : {查询值} +``` + +| 查找类型 | 示例 | SQL | 备注 | +|-------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------| +| not | `name__not : tom` | `WHERE NOT ("name" = "tom")` | | +| in | `name__in" : tom \| jimm` | `WHERE name IN ("tom", "jimmy")` | | +| not_in | `name__not_in : tom \| jimm` | `WHERE name NOT IN ("tom", "jimmy")` | | +| gte | `create_time__gte : 2023-10-25` | `WHERE "create_time" >= "2023-10-25"` | | +| gt | `create_time__gt : 2023-10-25` | `WHERE "create_time" > "2023-10-25"` | | +| lte | `create_time__lte : 2023-10-25` | `WHERE "create_time" <= "2023-10-25"` | | +| lt | `create_time__lt : 2023-10-25` | `WHERE "create_time" < "2023-10-25"` | | +| range | `create_time__range : 2023-10-25\| 2024-10-25` | `WHERE "create_time" BETWEEN "2023-10-25" AND "2024-10-25"`

`WHERE "create_time" >= "2023-10-25" AND "create_time" <= "2024-10-25"` | 需要注意的是:
1. 有些数据库的BETWEEN实现的开闭区间可能不一样。
2. 日期`2005-01-01`会被隐式转换为:`2005-01-01 00:00:00`,两个日期一致就会导致查询不到数据。 | +| isnull | `name__isnull : True` | `WHERE name IS NULL` | | +| not_isnull | `name__not_isnull : False` | `WHERE name IS NOT NULL` | | +| contains | `name__contains : L` | `WHERE name LIKE '%L%';` | | +| icontains | `name__icontains : L` | `WHERE name ILIKE '%L%';` | | +| startswith | `name__startswith : La` | `WHERE name LIKE 'La%';` | | +| istartswith | `name__istartswith : La` | `WHERE name ILIKE 'La%';` | | +| endswith | `name__endswith : a` | `WHERE name LIKE '%a';` | | +| iendswith | `name__iendswith : a` | `WHERE name ILIKE '%a';` | | +| exact | `name__exact : a` | `WHERE name LIKE 'a';` | | +| iexact | `name__iexact : a` | `WHERE name ILIKE 'a';` | | +| regex | `title__regex : ^(An?\|The) +` | MySQL: `WHERE title REGEXP BINARY '^(An?\|The) +'`
Oracle: `WHERE REGEXP_LIKE(title, '^(An?\|The) +', 'c');`
PostgreSQL: `WHERE title ~ '^(An?\|The) +';`
SQLite: `WHERE title REGEXP '^(An?\|The) +';` | | +| iregex | `title__iregex : ^(an?\|the) +` | MySQL: `WHERE title REGEXP '^(an?\|the) +'`
Oracle: `WHERE REGEXP_LIKE(title, '^(an?\|the) +', 'i');`
PostgreSQL: `WHERE title ~* '^(an?\|the) +';`
SQLite: `WHERE title REGEXP '(?i)^(an?\|the) +';` | | +| search | | | | + +以及将日期提取出来的查找类型: + +| 查找类型 | 示例 | SQL | 备注 | +|--------------|--------------------------------|---------------------------------------------------|----------------------| +| date | `pub_date__date : 2023-01-01` | `WHERE DATE(pub_date) = '2023-01-01'` | | +| year | `pub_date__year : 2023` | `WHERE EXTRACT('YEAR' FROM pub_date) = '2023'` | 哪一年 | +| iso_year | `pub_date__iso_year : 2023` | `WHERE EXTRACT('ISOYEAR' FROM pub_date) = '2023'` | ISO 8601 一年中的周数 | +| month | `pub_date__month : 12` | `WHERE EXTRACT('MONTH' FROM pub_date) = '12'` | 月份,1-12 | +| day | `pub_date__day : 3` | `WHERE EXTRACT('DAY' FROM pub_date) = '3'` | 该月的某天(1-31) | +| week | `pub_date__week : 7` | `WHERE EXTRACT('WEEK' FROM pub_date) = '7'` | ISO 8601 周编号 一年中的周数 | +| week_day | `pub_date__week_day : tom` | `` | 星期几 | +| iso_week_day | `pub_date__iso_week_day : tom` | `` | | +| quarter | `pub_date__quarter : 1` | `WHERE EXTRACT('QUARTER' FROM pub_date) = '1'` | 一年中的季度 | +| time | `pub_date__time : 12:59:59` | `` | | +| hour | `pub_date__hour : 12` | `WHERE EXTRACT('HOUR' FROM pub_date) = '12'` | 小时(0-23) | +| minute | `pub_date__minute : 59` | `WHERE EXTRACT('MINUTE' FROM pub_date) = '59'` | 分钟 (0-59) | +| second | `pub_date__second : 59` | `WHERE EXTRACT('SECOND' FROM pub_date) = '59'` | 秒 (0-59) | + +## 参考资料 + +- [Tortoise ORM Filtering][1] +- [Django Field lookups][2] +- [PostgreSQL Date/Time Functions and Operators][3] +- [PostgreSQL Regular Expressions][4] +- [PostgreSQL Date/Time Types][5] + +[1]: https://tortoise.github.io/query.html#filtering + +[2]: https://docs.djangoproject.com/en/4.2/ref/models/querysets/#field-lookups + +[3]: https://www.postgresql.org/docs/current/functions-datetime.html + +[4]: https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-REGEXP-TABLE + +[5]: https://www.postgresql.org/docs/current/datatype-datetime.html diff --git a/query_parser/filter.go b/query_parser/filter.go index 59b1205..b32e83a 100644 --- a/query_parser/filter.go +++ b/query_parser/filter.go @@ -53,8 +53,13 @@ const ( ) const ( - QueryDelimiter = "__" // 分隔符 - JsonFieldDelimiter = "." // JSONB字段分隔符 + JSONFilterFieldOperatorDelimiter = "__" // JSON过滤器 - 字段名和操作符的分隔符 + + QueryFilterFieldOperatorDelimiter = ":" // 自定义查询字符串过滤器 - 字段名和操作符的分隔符 + QueryFilterQueriesDelimiter = "," // 自定义查询字符串过滤器 - 多个键值对的分隔符 + QueryFilterValuesDelimiter = "|" // 自定义查询字符串过滤器 - 多个值的分隔符 + + JsonFieldDelimiter = "." // JSON字段分隔符 ) type FilterHandler func(field, operator, value string) @@ -89,6 +94,40 @@ func ParseFilterJSONString(query string, handler FilterHandler) error { return err } +// ParseFilterQueryString 解析过滤条件的查询字符串,调用处理函数 +func ParseFilterQueryString(query string, handler FilterHandler) error { + if query == "" { + return nil // 如果查询字符串为空,直接返回 + } + + // 按逗号分割查询字符串,得到多个键值对 + pairs := SplitQueryQueries(query) + for _, pair := range pairs { + // 按冒号分割键值对,提取字段名和值 + parts := SplitQueryFieldAndOperator(pair) + if len(parts) != 2 { + continue // 跳过无效的键值对 + } + + // 解码字段名 + key, err := DecodeSpecialCharacters(strings.TrimSpace(parts[0])) + if err != nil { + continue // 跳过解码失败的键值对 + } + + // 解码字段值 + value, err := DecodeSpecialCharacters(strings.TrimSpace(parts[1])) + if err != nil { + continue // 跳过解码失败的键值对 + } + + // 调用 ParseFilterField 解析字段和操作符 + ParseFilterField(key, value, handler) + } + + return nil +} + // ParseFilterField 解析过滤条件字符串,调用处理函数 func ParseFilterField(key, value string, handler FilterHandler) { if key == "" || value == "" { @@ -96,7 +135,7 @@ func ParseFilterField(key, value string, handler FilterHandler) { } // 处理字段和操作符 - parts := SplitFieldAndOperator(key) + parts := SplitJsonFieldAndOperator(key) if len(parts) < 1 { return // 无效的字段格式 } @@ -115,9 +154,24 @@ func ParseFilterField(key, value string, handler FilterHandler) { handler(field, op, value) } -// SplitFieldAndOperator 将字段字符串按分隔符分割成字段名和操作符 -func SplitFieldAndOperator(field string) []string { - return strings.Split(field, QueryDelimiter) +// SplitJsonFieldAndOperator JSON过滤器 - 分割“字段名”和“操作符” +func SplitJsonFieldAndOperator(field string) []string { + return strings.Split(field, JSONFilterFieldOperatorDelimiter) +} + +// SplitQueryFieldAndOperator 自定义查询字符串过滤器 - 分割“字段名”和“操作符” +func SplitQueryFieldAndOperator(field string) []string { + return strings.Split(field, QueryFilterFieldOperatorDelimiter) +} + +// SplitQueryQueries 自定义查询字符串过滤器 - 分割多个键值对 +func SplitQueryQueries(field string) []string { + return strings.Split(field, QueryFilterQueriesDelimiter) +} + +// SplitQueryValues 自定义查询字符串过滤器 - 分割多个值 +func SplitQueryValues(field string) []string { + return strings.Split(field, QueryFilterValuesDelimiter) } // SplitJSONField 将JSONB字段字符串按分隔符分割成多个字段 diff --git a/query_parser/filter_test.go b/query_parser/filter_test.go index 32c8738..935123f 100644 --- a/query_parser/filter_test.go +++ b/query_parser/filter_test.go @@ -58,7 +58,7 @@ func TestParseFilterJSONString(t *testing.T) { err = ParseFilterJSONString(`{"na!me__exact":"Jo@hn"}`, handler) assert.NoError(t, err) assert.Equal(t, 1, len(results)) - assert.Equal(t, "na!me", results[0].Field) + assert.Equal(t, "na_me", results[0].Field) assert.Equal(t, "exact", results[0].Operator) assert.Equal(t, "Jo@hn", results[0].Value) @@ -67,10 +67,10 @@ func TestParseFilterJSONString(t *testing.T) { err = ParseFilterJSONString(`[{"ag#e__gte":"30"},{"sta$tus__exact":"ac^tive"}]`, handler) assert.NoError(t, err) assert.Equal(t, 2, len(results)) - assert.Equal(t, "ag#e", results[0].Field) + assert.Equal(t, "ag_e", results[0].Field) assert.Equal(t, "gte", results[0].Operator) assert.Equal(t, "30", results[0].Value) - assert.Equal(t, "sta$tus", results[1].Field) + assert.Equal(t, "sta_tus", results[1].Field) assert.Equal(t, "exact", results[1].Operator) assert.Equal(t, "ac^tive", results[1].Value) @@ -81,6 +81,149 @@ func TestParseFilterJSONString(t *testing.T) { assert.Equal(t, 0, len(results)) } +func TestParseFilterQueryString(t *testing.T) { + var results []struct { + Field string + Operator string + Value string + } + + handler := func(field, operator, value string) { + results = append(results, struct { + Field string + Operator string + Value string + }{Field: field, Operator: operator, Value: value}) + } + + // 测试解析单个过滤条件 + results = nil + err := ParseFilterQueryString("name__exact:John", handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "name", results[0].Field) + assert.Equal(t, "exact", results[0].Operator) + assert.Equal(t, "John", results[0].Value) + + // 测试解析多个过滤条件 + results = nil + err = ParseFilterQueryString("age__gte:30,status__exact:active", handler) + assert.NoError(t, err) + assert.Equal(t, 2, len(results)) + assert.Equal(t, "age", results[0].Field) + assert.Equal(t, "gte", results[0].Operator) + assert.Equal(t, "30", results[0].Value) + assert.Equal(t, "status", results[1].Field) + assert.Equal(t, "exact", results[1].Operator) + assert.Equal(t, "active", results[1].Value) + + // 测试空字符串 + results = nil + err = ParseFilterQueryString("", handler) + assert.NoError(t, err) + assert.Equal(t, 0, len(results)) + + // 测试无效的查询字符串 + results = nil + err = ParseFilterQueryString("invalid_query", handler) + assert.NoError(t, err) + assert.Equal(t, 0, len(results)) + + // 测试包含特殊字符的字段和值 + results = nil + err = ParseFilterQueryString("na!me__exact:Jo@hn", handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "na_me", results[0].Field) + assert.Equal(t, "exact", results[0].Operator) + assert.Equal(t, "Jo@hn", results[0].Value) + + // 测试包含特殊字符的多个过滤条件 + results = nil + err = ParseFilterQueryString("ag#e__gte:30,sta$tus__exact:ac^tive", handler) + assert.NoError(t, err) + assert.Equal(t, 2, len(results)) + assert.Equal(t, "ag_e", results[0].Field) + assert.Equal(t, "gte", results[0].Operator) + assert.Equal(t, "30", results[0].Value) + assert.Equal(t, "sta_tus", results[1].Field) + assert.Equal(t, "exact", results[1].Operator) + assert.Equal(t, "ac^tive", results[1].Value) + + // 测试 Field 中包含分隔符 + results = nil + err = ParseFilterQueryString("na:me__exact:John", handler) + assert.NoError(t, err) + assert.Equal(t, 0, len(results)) + + // 测试 Operator 中包含分隔符 + results = nil + err = ParseFilterQueryString("name__ex:act:John", handler) + assert.NoError(t, err) + assert.Equal(t, 0, len(results)) + + // 测试 Value 中包含分隔符 + results = nil + err = ParseFilterQueryString("name__exact:Jo|hn", handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "name", results[0].Field) + assert.Equal(t, "exact", results[0].Operator) + assert.Equal(t, "Jo|hn", results[0].Value) + + // 测试多个过滤条件中包含分隔符 + results = nil + err = ParseFilterQueryString("ag:e__gte:30,sta|tus__exact:ac|tive", handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + + // 测试 Field 中包含编码后的分隔符 + results = nil + encodedField := EncodeSpecialCharacters("na:me") + err = ParseFilterQueryString(encodedField+"__exact:John", handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "na_me", results[0].Field) // 注意:这里的字段名会被转换为 snake_case + assert.Equal(t, "exact", results[0].Operator) + assert.Equal(t, "John", results[0].Value) + + // 测试 Operator 中包含编码后的分隔符 + results = nil + encodedOperator := EncodeSpecialCharacters("ex:act") + err = ParseFilterQueryString("name__"+encodedOperator+":John", handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "name", results[0].Field) + assert.Equal(t, "ex:act", results[0].Operator) + assert.Equal(t, "John", results[0].Value) + + // 测试 Value 中包含编码后的分隔符 + results = nil + encodedValue := EncodeSpecialCharacters("Jo|hn") + err = ParseFilterQueryString("name__exact:"+encodedValue, handler) + assert.NoError(t, err) + assert.Equal(t, 1, len(results)) + assert.Equal(t, "name", results[0].Field) + assert.Equal(t, "exact", results[0].Operator) + assert.Equal(t, "Jo|hn", results[0].Value) + + // 测试多个过滤条件中包含编码后的分隔符 + results = nil + encodedField1 := EncodeSpecialCharacters("ag:e") + encodedValue1 := EncodeSpecialCharacters("30") + encodedField2 := EncodeSpecialCharacters("sta|tus") + encodedValue2 := EncodeSpecialCharacters("ac|tive") + err = ParseFilterQueryString(encodedField1+"__gte:"+encodedValue1+","+encodedField2+"__exact:"+encodedValue2, handler) + assert.NoError(t, err) + assert.Equal(t, 2, len(results)) + assert.Equal(t, "ag_e", results[0].Field) // 注意:这里的字段名会被转换为 snake_case + assert.Equal(t, "gte", results[0].Operator) + assert.Equal(t, "30", results[0].Value) + assert.Equal(t, "sta_tus", results[1].Field) // 注意:这里的字段名会被转换为 snake_case + assert.Equal(t, "exact", results[1].Operator) + assert.Equal(t, "ac|tive", results[1].Value) +} + func TestParseFilterField(t *testing.T) { var results []struct { Field string @@ -128,25 +271,25 @@ func TestParseFilterField(t *testing.T) { assert.Equal(t, 0, len(results)) } -func TestSplitFieldAndOperator(t *testing.T) { +func TestSplitJsonFieldAndOperator(t *testing.T) { // 测试正常分割 - result := SplitFieldAndOperator("name__exact") + result := SplitJsonFieldAndOperator("name__exact") assert.Equal(t, 2, len(result)) assert.Equal(t, "name", result[0]) assert.Equal(t, "exact", result[1]) // 测试无操作符 - result = SplitFieldAndOperator("name") + result = SplitJsonFieldAndOperator("name") assert.Equal(t, 1, len(result)) assert.Equal(t, "name", result[0]) // 测试空字符串 - result = SplitFieldAndOperator("") + result = SplitJsonFieldAndOperator("") assert.Equal(t, 1, len(result)) assert.Equal(t, "", result[0]) // 测试多个分隔符 - result = SplitFieldAndOperator("name__exact__extra") + result = SplitJsonFieldAndOperator("name__exact__extra") assert.Equal(t, 3, len(result)) assert.Equal(t, "name", result[0]) assert.Equal(t, "exact", result[1]) @@ -179,3 +322,80 @@ func TestSplitJSONField(t *testing.T) { assert.Equal(t, "address", result[2]) assert.Equal(t, "city", result[3]) } + +func TestSplitQueryFieldAndOperator(t *testing.T) { + // 测试正常分割 + result := SplitQueryFieldAndOperator("name:exact") + assert.Equal(t, 2, len(result)) + assert.Equal(t, "name", result[0]) + assert.Equal(t, "exact", result[1]) + + // 测试无操作符 + result = SplitQueryFieldAndOperator("name") + assert.Equal(t, 1, len(result)) + assert.Equal(t, "name", result[0]) + + // 测试空字符串 + result = SplitQueryFieldAndOperator("") + assert.Equal(t, 1, len(result)) + assert.Equal(t, "", result[0]) + + // 测试多个分隔符 + result = SplitQueryFieldAndOperator("name:exact:extra") + assert.Equal(t, 3, len(result)) + assert.Equal(t, "name", result[0]) + assert.Equal(t, "exact", result[1]) + assert.Equal(t, "extra", result[2]) +} + +func TestSplitQueryQueries(t *testing.T) { + // 测试正常分割多个键值对 + result := SplitQueryQueries("name:John,age:30,status:active") + assert.Equal(t, 3, len(result)) + assert.Equal(t, "name:John", result[0]) + assert.Equal(t, "age:30", result[1]) + assert.Equal(t, "status:active", result[2]) + + // 测试单个键值对 + result = SplitQueryQueries("name:John") + assert.Equal(t, 1, len(result)) + assert.Equal(t, "name:John", result[0]) + + // 测试空字符串 + result = SplitQueryQueries("") + assert.Equal(t, 1, len(result)) + assert.Equal(t, "", result[0]) + + // 测试多个分隔符 + result = SplitQueryQueries("name:John,,age:30") + assert.Equal(t, 3, len(result)) + assert.Equal(t, "name:John", result[0]) + assert.Equal(t, "", result[1]) + assert.Equal(t, "age:30", result[2]) +} + +func TestSplitQueryValues(t *testing.T) { + // 测试正常分割多个值 + result := SplitQueryValues("value1|value2|value3") + assert.Equal(t, 3, len(result)) + assert.Equal(t, "value1", result[0]) + assert.Equal(t, "value2", result[1]) + assert.Equal(t, "value3", result[2]) + + // 测试单个值 + result = SplitQueryValues("value1") + assert.Equal(t, 1, len(result)) + assert.Equal(t, "value1", result[0]) + + // 测试空字符串 + result = SplitQueryValues("") + assert.Equal(t, 1, len(result)) + assert.Equal(t, "", result[0]) + + // 测试多个分隔符 + result = SplitQueryValues("value1||value2") + assert.Equal(t, 3, len(result)) + assert.Equal(t, "value1", result[0]) + assert.Equal(t, "", result[1]) + assert.Equal(t, "value2", result[2]) +} diff --git a/query_parser/go.mod b/query_parser/go.mod index b6ece87..1fd9916 100644 --- a/query_parser/go.mod +++ b/query_parser/go.mod @@ -7,14 +7,13 @@ toolchain go1.23.2 require ( github.com/go-kratos/kratos/v2 v2.8.4 github.com/stretchr/testify v1.10.0 - github.com/tx7do/go-utils v1.1.28 + github.com/tx7do/go-utils v1.1.29 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/query_parser/go.sum b/query_parser/go.sum index 439ca77..b749420 100644 --- a/query_parser/go.sum +++ b/query_parser/go.sum @@ -1,10 +1,26 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-kratos/kratos/v2 v2.8.4 h1:eIJLE9Qq9WSoKx+Buy2uPyrahtF/lPh+Xf4MTpxhmjs= github.com/go-kratos/kratos/v2 v2.8.4/go.mod h1:mq62W2101a5uYyRxe+7IdWubu7gZCGYqSNKwGFiiRcw= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/query_parser/utils.go b/query_parser/utils.go new file mode 100644 index 0000000..f351b25 --- /dev/null +++ b/query_parser/utils.go @@ -0,0 +1,13 @@ +package query_parser + +import "net/url" + +// EncodeSpecialCharacters 对字符串进行编码 +func EncodeSpecialCharacters(input string) string { + return url.QueryEscape(input) +} + +// DecodeSpecialCharacters 对字符串进行解码 +func DecodeSpecialCharacters(input string) (string, error) { + return url.QueryUnescape(input) +} diff --git a/stringcase/README.md b/stringcase/README.md new file mode 100644 index 0000000..d1debd6 --- /dev/null +++ b/stringcase/README.md @@ -0,0 +1,80 @@ +# 命名法转换 + +- camelCase 驼峰式命名法(大驼峰) +- PascalCase 帕斯卡命名法(小驼峰) +- snake_case 蛇形命名法 +- kebab-case 烤肉串命名法 + +## camelCase 驼峰式命名法(大驼峰) + +驼峰式命名法(Camel case)是一种不使用空格,将多个单词连起来形成一个标识符的命名方式,其中每个单词的首字母(除了第一个单词,如果使用小驼峰式命名法)都大写,就像骆驼的驼峰一样。 + +驼峰式命名法分为两种:首字母小写的“小驼峰式”(lowerCamelCase)和首字母大写的“大驼峰式”(UpperCamelCase,也称为帕斯卡命名法PascalCase)。 + +- **小驼峰式(lowerCamelCase)**: 第一个单词的首字母小写,后续单词的首字母大写。例如:`myVariableName`。 +- **大驼峰式(UpperCamelCase)**: 每个单词的首字母都大写。例如:MyVariableName,也称为帕斯卡命名法(PascalCase)。 + +在 JavaScript、Java和C#中,驼峰式大小写常用于变量和函数的命名。 + +```javascript +let firstName = "John"; +let lastName = "Doe"; + +function printFullName(firstName, lastName) { + let fullName = firstName + " " + lastName; + console.log(fullName); +} +``` + +## PascalCase 帕斯卡命名法(小驼峰) + +`PascalCase`,也称为`UpperCamelCase`,是一种在编程中使用的命名约定。它要求每个单词(包括第一个单词)的首字母都大写,并且单词之间没有空格或分隔符(如 +`_`)。例如,`ThisIsPascalCase`,`MyClassName` 都是使用PascalCase的例子。 + +PascalCase 通常用于在 C#、Java 和TypeScript等语言中命名类、接口和其他类型。 + +```typescript +class Person { + firstName: string; + lastName: string; + + constructor(firstName: string, lastName: string) { + this.firstName = firstName; + this.lastName = lastName; + } + + printFullName(): void { + let fullName = this.firstName + " " + this.lastName; + console.log(fullName); + } +} +``` + +## snake_case 蛇形命名法 + +蛇形命名法是一种使用下划线 (`_`) 分隔单词的命名方式。之所以叫蛇形命名法,是因为"`snake_case`"的 +下划线的形状类似于蛇腹上的鳞片。蛇形命名法通常用于Python、Ruby 和JavaScript等语言的变量名和函数名。 + +```python +first_name = "John" +last_name = "Doe" + +def print_full_name(first_name, last_name): + full_name = first_name + " " + last_name + print(full_name) +``` + +## kebab-case 烤肉串命名法 + +`kebab-case(烤肉串命名法)`,也被称作 `kebab case`、`dash-case(破折号式)`、`hyphen-case(连字符式)`、`lisp-case(Lisp 式)`。 + +kebab-case 要求短语内的各个单词或缩写之间以`-`(连字符)做间隔。 例如:"`kebab-case`"。 + +短横线命名法通常用于 URL、文件名和 HTML/CSS 类名。 + +```html + +
+

This is a user profile.

+
+``` diff --git a/tag.bat b/tag.bat index c035d4e..9d5d81a 100644 --- a/tag.bat +++ b/tag.bat @@ -10,9 +10,9 @@ git tag slug/v0.0.1 git tag name_generator/v0.0.1 git tag mapper/v0.0.3 git tag password/v0.0.1 -git tag query_parser/v0.0.1 +git tag query_parser/v0.0.2 -git tag entgo/v1.1.31 +git tag entgo/v1.1.32 git tag gorm/v1.1.6 git push origin --tags diff --git a/timeutil/trans.go b/timeutil/trans.go index 7df27bb..916e91d 100644 --- a/timeutil/trans.go +++ b/timeutil/trans.go @@ -296,3 +296,67 @@ func DurationpbToString(in *durationpb.Duration) *string { return trans.Ptr(in.AsDuration().String()) } + +// TimestampToSeconds 将 timestamppb.Timestamp 转换为秒 +func TimestampToSeconds(ts *timestamppb.Timestamp) int64 { + if ts == nil { + return 0 + } + return ts.AsTime().Unix() +} + +// SecondsToTimestamp 将秒转换为 timestamppb.Timestamp +func SecondsToTimestamp(seconds *int64) *timestamppb.Timestamp { + if seconds == nil { + return nil + } + return timestamppb.New(time.Unix(*seconds, 0)) +} + +func TimestampToMilliseconds(ts *timestamppb.Timestamp) int64 { + if ts == nil { + return 0 + } + + return ts.AsTime().UnixMilli() +} + +func MillisecondsToTimestamp(milliseconds *int64) *timestamppb.Timestamp { + if milliseconds == nil { + return nil + } + + return timestamppb.New(time.UnixMilli(*milliseconds)) +} + +func TimestampToMicroseconds(ts *timestamppb.Timestamp) int64 { + if ts == nil { + return 0 + } + + return ts.AsTime().UnixMicro() +} + +func MicrosecondsToTimestamp(microseconds *int64) *timestamppb.Timestamp { + if microseconds == nil { + return nil + } + + return timestamppb.New(time.UnixMicro(*microseconds)) +} + +// TimestampToNanoseconds 将 timestamppb.Timestamp 转换为纳秒 +func TimestampToNanoseconds(ts *timestamppb.Timestamp) int64 { + if ts == nil { + return 0 + } + return ts.AsTime().UnixNano() +} + +// NanosecondsToTimestamp 将纳秒转换为 timestamppb.Timestamp +func NanosecondsToTimestamp(nanoseconds *int64) *timestamppb.Timestamp { + if nanoseconds == nil { + return nil + } + return timestamppb.New(time.Unix(0, *nanoseconds)) +} diff --git a/timeutil/trans_test.go b/timeutil/trans_test.go index 505a837..79207ea 100644 --- a/timeutil/trans_test.go +++ b/timeutil/trans_test.go @@ -442,3 +442,29 @@ func TestDurationpbToString(t *testing.T) { result = DurationpbToString(nil) assert.Nil(t, result) } + +func TestTimestampConversions(t *testing.T) { + // 测试秒转换 + seconds := int64(1672531200) + ts := SecondsToTimestamp(&seconds) + assert.NotNil(t, ts) + assert.Equal(t, seconds, TimestampToSeconds(ts)) + + // 测试毫秒转换 + milliseconds := int64(1672531200123) + ts = MillisecondsToTimestamp(&milliseconds) + assert.NotNil(t, ts) + assert.Equal(t, milliseconds, TimestampToMilliseconds(ts)) + + // 测试微秒转换 + microseconds := int64(1672531200123456) + ts = MicrosecondsToTimestamp(µseconds) + assert.NotNil(t, ts) + assert.Equal(t, microseconds, TimestampToMicroseconds(ts)) + + // 测试纳秒转换 + nanoseconds := int64(1672531200123456789) + ts = NanosecondsToTimestamp(&nanoseconds) + assert.NotNil(t, ts) + assert.Equal(t, nanoseconds, TimestampToNanoseconds(ts)) +}