Skip to content

Commit 2f6830a

Browse files
committed
feat: add iso time unit
1 parent efb3491 commit 2f6830a

File tree

6 files changed

+192
-17
lines changed

6 files changed

+192
-17
lines changed

app/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"dependencies": {
1818
"@headlessui/react": "^1.7.14",
1919
"@heroicons/react": "^2.0.8",
20-
"@kanaries/graphic-walker": "0.4.64",
21-
"@kanaries/gw-dsl-parser": "0.1.48-rc5",
20+
"@kanaries/graphic-walker": "0.4.66",
21+
"@kanaries/gw-dsl-parser": "0.1.49",
2222
"@radix-ui/react-checkbox": "^1.0.4",
2323
"@radix-ui/react-dialog": "^1.0.5",
2424
"@radix-ui/react-icons": "^1.3.0",

app/yarn.lock

+25-8
Original file line numberDiff line numberDiff line change
@@ -1045,10 +1045,10 @@
10451045
"@jridgewell/resolve-uri" "3.1.0"
10461046
"@jridgewell/sourcemap-codec" "1.4.14"
10471047

1048-
"@kanaries/graphic-walker@0.4.64":
1049-
version "0.4.64"
1050-
resolved "https://registry.yarnpkg.com/@kanaries/graphic-walker/-/graphic-walker-0.4.64.tgz#bc119475ec2e97e69ba2476cabddcbf36e7735db"
1051-
integrity sha512-6xm4iXWTllDSrcCSmUIr1IA+XF/hei9sr294E0ezroo5mnHrqX+u/jjAag/OvvbNsDDVvU0xJAD1qWMAdqcYmw==
1048+
"@kanaries/graphic-walker@0.4.66":
1049+
version "0.4.66"
1050+
resolved "https://registry.yarnpkg.com/@kanaries/graphic-walker/-/graphic-walker-0.4.66.tgz#8e0d3d43cd39704a9e7ca781b5e9132c6eb4207d"
1051+
integrity sha512-0VmxHhs5tVxVwrXh8AHhK44cnUkdad61dPSFeHnSjHVba0JlBhjnmr4hfm75aZwQlqquE2CRJRBW0zKjBnBlUg==
10521052
dependencies:
10531053
"@headlessui-float/react" "^0.11.4"
10541054
"@headlessui/react" "1.7.12"
@@ -1060,6 +1060,7 @@
10601060
"@radix-ui/react-context-menu" "^2.1.5"
10611061
"@radix-ui/react-dialog" "^1.0.5"
10621062
"@radix-ui/react-dropdown-menu" "^2.0.6"
1063+
"@radix-ui/react-hover-card" "^1.0.7"
10631064
"@radix-ui/react-icons" "^1.3.0"
10641065
"@radix-ui/react-label" "^2.0.2"
10651066
"@radix-ui/react-popover" "^1.0.7"
@@ -1118,10 +1119,10 @@
11181119
vega-lite "^5.6.0"
11191120
vega-webgl-renderer "^1.0.0-beta.2"
11201121

1121-
"@kanaries/gw-dsl-parser@0.1.48-rc5":
1122-
version "0.1.48-rc5"
1123-
resolved "https://registry.yarnpkg.com/@kanaries/gw-dsl-parser/-/gw-dsl-parser-0.1.48-rc5.tgz#b45d92bc85559bc0d477c5a07a9b05d7c2f360eb"
1124-
integrity sha512-o+/rE/I1OkK31D+Fj4Uik1czZuTMM7f4BLswmL3yb6bucFxcNZ+bR4s7SDH5dm9L2KVi1A+QLcd0Zh8VaKg9mw==
1122+
"@kanaries/gw-dsl-parser@0.1.49":
1123+
version "0.1.49"
1124+
resolved "https://registry.yarnpkg.com/@kanaries/gw-dsl-parser/-/gw-dsl-parser-0.1.49.tgz#9f5c6c731ca47e52e41c319b31a1f8348d6525f6"
1125+
integrity sha512-gK95BVQhO0I7wN7VsntRzwpdTXqeXj6ESdvVTau+oXEYp4SQP2EVkQDLp+bbIKO2VVQ6iRQrqmzGTPC8wL2Xzg==
11251126

11261127
"@kanaries/react-beautiful-dnd@^0.1.1":
11271128
version "0.1.1"
@@ -1409,6 +1410,22 @@
14091410
"@radix-ui/react-primitive" "1.0.3"
14101411
"@radix-ui/react-use-callback-ref" "1.0.1"
14111412

1413+
"@radix-ui/react-hover-card@^1.0.7":
1414+
version "1.0.7"
1415+
resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz#684bca2504432566357e7157e087051aa3577948"
1416+
integrity sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==
1417+
dependencies:
1418+
"@babel/runtime" "^7.13.10"
1419+
"@radix-ui/primitive" "1.0.1"
1420+
"@radix-ui/react-compose-refs" "1.0.1"
1421+
"@radix-ui/react-context" "1.0.1"
1422+
"@radix-ui/react-dismissable-layer" "1.0.5"
1423+
"@radix-ui/react-popper" "1.1.3"
1424+
"@radix-ui/react-portal" "1.0.4"
1425+
"@radix-ui/react-presence" "1.0.1"
1426+
"@radix-ui/react-primitive" "1.0.3"
1427+
"@radix-ui/react-use-controllable-state" "1.0.1"
1428+
14121429
"@radix-ui/react-icons@^1.3.0":
14131430
version "1.3.0"
14141431
resolved "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"

pygwalker/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pygwalker.services.global_var import GlobalVarManager
1111
from pygwalker.services.kaggle import show_tips_user_kaggle as __show_tips_user_kaggle
1212

13-
__version__ = "0.4.8.6"
13+
__version__ = "0.4.8.7"
1414
__hash__ = __rand_str()
1515

1616
from pygwalker.api.jupyter import walk, render, table

pygwalker/data_parsers/database_parser.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ class Connector:
5454
"snowflake": {9, 10},
5555
"mysql": {245}
5656
}
57-
PRE_SQL_MAP = {
58-
"snowflake": "ALTER SESSION SET STRICT_JSON_OUTPUT=True;",
57+
PRE_INIT_SQL_MAP = {
58+
"snowflake": "ALTER SESSION SET WEEK_OF_YEAR_POLICY=1, WEEK_START=7, STRICT_JSON_OUTPUT=True;",
5959
}
6060

6161
def __init__(self, url: str, view_sql: str, engine_params: Optional[Dict[str, Any]] = None) -> "Connector":
@@ -73,14 +73,16 @@ def _get_engine(self, engine_params: Dict[str, Any]) -> Engine:
7373
engine = create_engine(self.url, **engine_params)
7474
engine.dialect.requires_name_normalize = False
7575
self.engine_map[self.url] = engine
76+
if engine.dialect.name in self.PRE_INIT_SQL_MAP:
77+
pre_init_sql = self.PRE_INIT_SQL_MAP[engine.dialect.name]
78+
with engine.connect(True) as connection:
79+
connection.execute(text(pre_init_sql))
7680

7781
return self.engine_map[self.url]
7882

7983
def query_datas(self, sql: str) -> List[Dict[str, Any]]:
8084
field_type_map = {}
8185
with self.engine.connect() as connection:
82-
if self.dialect_name in self.PRE_SQL_MAP:
83-
connection.execute(text(self.PRE_SQL_MAP[self.dialect_name]))
8486
result = connection.execute(text(sql))
8587
if self.dialect_name in self.JSON_TYPE_CODE_SET_MAP:
8688
field_type_map = {

pygwalker/utils/custom_sqlglot.py

+157-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
from sqlglot.dialects.duckdb import DuckDB as DuckdbDialect
22
from sqlglot.dialects.postgres import Postgres as PostgresDialect
33
from sqlglot.dialects.mysql import MySQL as MysqlDialect
4+
from sqlglot.dialects.snowflake import Snowflake as SnowflakeDialect
45
from sqlglot import exp
56
from sqlglot.helper import seq_get
67
from sqlglot.generator import Generator
78
from sqlglot.dialects.dialect import (
89
build_date_delta,
910
build_date_delta_with_interval,
11+
rename_func,
12+
unit_to_str
1013
)
1114

1215

@@ -34,15 +37,75 @@ def _postgres_unix_to_time_sql(self: Generator, expression: exp.UnixToTime) -> s
3437
return self.func("to_timestamp", exp.Div(this=timestamp, expression=exp.func("POW", 10, scale)))
3538

3639

40+
# temporary fix for Postgres IN clause(bin filter)
41+
def _postgres_in_sql(self: Generator, expression: exp.In) -> str:
42+
expression.set("expressions", [
43+
exp.Array(expressions=[
44+
exp.cast(item, to=exp.DataType.Type.DOUBLE) if isinstance(item, exp.Literal) and item.args.get("is_string") is False else item
45+
for item in in_item_exp.args.get("expressions", [])
46+
]) if isinstance(in_item_exp, exp.Array) else in_item_exp
47+
for in_item_exp in expression.args.get("expressions", [])
48+
])
49+
return self.in_sql(expression)
50+
51+
52+
def _postgres_timestamp_trunc(self: Generator, expression: exp.TimestampTrunc) -> str:
53+
if expression.unit.this.lower() == "isoyear":
54+
return self.func("to_date", self.func("to_char", expression.this, exp.Literal.string("IYYY-0001")), exp.Literal.string("IYYY-IDDD"))
55+
56+
return self.func("DATE_TRUNC", unit_to_str(expression), expression.this)
57+
58+
59+
def _postgres_time_to_str_sql(self: Generator, expression: exp.TimeToStr) -> str:
60+
if expression.args.get("format").this == "%U":
61+
# postgres not support non-iso week
62+
# current_pass_days = EXTRACT(isodow FROM DATE_TRUNC('year', date))
63+
# week_number = floor((EXTRACT(day from date) + current_pass_days - 1) / 7)
64+
return self.sql(exp.Floor(
65+
this=exp.Div(
66+
this=exp.Paren(this=exp.Add(
67+
this=exp.Sub(
68+
this=exp.Cast(this=self.func("TO_CHAR", expression.this, exp.Literal.string("DDD")), to="int"),
69+
expression=exp.Literal.number(1)
70+
),
71+
expression=exp.Extract(this=exp.Var(this="isodow"), expression=exp.TimestampTrunc(this=expression.this, unit=exp.Literal.string("year")))
72+
)),
73+
expression=exp.Literal.number(7),
74+
)
75+
))
76+
77+
return self.func("TO_CHAR", expression.this, self.format_time(expression))
78+
79+
80+
def _postgres_str_to_time_sql(self: Generator, expression: exp.StrToTime) -> str:
81+
# adapter duckdb non-iso week
82+
if expression.args.get("format").this == "%Y%U" and isinstance(expression.this, exp.TimeToStr) and expression.this.args.get("format").this == "%Y%U":
83+
return self.sql(exp.Sub(
84+
this=exp.TimestampTrunc(this=expression.this.this, unit=exp.Literal.string("day")),
85+
expression=exp.Mul(
86+
this=exp.Extract(this=exp.Var(this="dow"), expression=expression.this.this),
87+
expression=exp.Interval(this=exp.Literal.number(1), unit=exp.Var(this="day"))
88+
)
89+
))
90+
return self.func("TO_TIMESTAMP", expression.this, self.format_time(expression))
91+
92+
3793
PostgresDialect.Generator.TRANSFORMS[exp.Round] = lambda _, e: _postgres_round_generator(e)
3894
PostgresDialect.Generator.TRANSFORMS[exp.UnixToTime] = _postgres_unix_to_time_sql
95+
PostgresDialect.Generator.TRANSFORMS[exp.In] = _postgres_in_sql
96+
PostgresDialect.Generator.TRANSFORMS[exp.TimestampTrunc] = _postgres_timestamp_trunc
97+
PostgresDialect.Generator.TRANSFORMS[exp.TimeToStr] = _postgres_time_to_str_sql
98+
PostgresDialect.Generator.TRANSFORMS[exp.StrToTime] = _postgres_str_to_time_sql
3999

40100

41101
# Mysql Dialect
42102
def _mysql_timestamptrunc_sql(self: Generator, expression: exp.TimestampTrunc) -> str:
43103
unit = expression.args.get("unit")
44104

45-
start_ts = "'0001-01-01 00:00:00'"
105+
if unit.this.lower() == "isoyear":
106+
unit = exp.Var(this="YEAR")
107+
108+
start_ts = "'0006-01-01 00:00:00'"
46109

47110
timestamp_diff = build_date_delta(exp.TimestampDiff)([unit, start_ts, expression.this])
48111
interval = exp.Interval(this=timestamp_diff, unit=unit)
@@ -57,6 +120,13 @@ def _mysql_extract_sql(self: Generator, expression: exp.Extract) -> str:
57120
return self.sql(exp.Sub(this=self.func("DAYOFWEEK", expression.expression), expression=exp.Literal.number(1)))
58121
if unit == "week":
59122
return self.func("WEEK", expression.expression, exp.Literal.number(3))
123+
if unit == "isoyear":
124+
return self.sql(exp.Floor(this=exp.Div(this=self.func("YEARWEEK", expression.expression, exp.Literal.number(3)), expression=exp.Literal.number(100))))
125+
if unit == "isodow":
126+
return self.sql(exp.Add(
127+
this=exp.Mod(this=exp.Add(this=self.func("DAYOFWEEK", expression.expression), expression=exp.Literal.number(5)), expression=exp.Literal.number(7)),
128+
expression=exp.Literal.number(1)
129+
))
60130
return self.extract_sql(expression)
61131

62132

@@ -67,7 +137,93 @@ def _mysql_unix_to_time_sql(self: Generator, expression: exp.UnixToTime) -> str:
67137
return self.func("FROM_UNIXTIME", exp.Div(this=timestamp, expression=exp.func("POW", 10, scale)), self.format_time(expression))
68138

69139

140+
def _mysql_str_to_time_sql(self: Generator, expression: exp.StrToTime) -> str:
141+
# adapter duckdb non-iso week
142+
if expression.args.get("format").this == "%Y%U" and isinstance(expression.this, exp.TimeToStr) and expression.this.args.get("format").this == "%Y%U":
143+
return _mysql_timestamptrunc_sql(self, exp.TimestampTrunc(this=expression.this.this, unit=exp.Literal.string("WEEK")))
144+
return self.func("STR_TO_DATE", expression.this, self.format_time(expression))
145+
146+
70147
MysqlDialect.Generator.TRANSFORMS[exp.Extract] = _mysql_extract_sql
71148
MysqlDialect.Generator.TRANSFORMS[exp.Array] = lambda self, e: self.func("JSON_ARRAY", *e.expressions)
72149
MysqlDialect.Generator.TRANSFORMS[exp.TimestampTrunc] = _mysql_timestamptrunc_sql
73150
MysqlDialect.Generator.TRANSFORMS[exp.UnixToTime] = _mysql_unix_to_time_sql
151+
MysqlDialect.Generator.TRANSFORMS[exp.Mod] = lambda self, e: self.func("MOD", e.this, e.expression)
152+
MysqlDialect.Generator.TRANSFORMS[exp.StrToTime] = _mysql_str_to_time_sql
153+
154+
155+
# Snowflake Dialect
156+
def _snowflake_extract_sql(self: Generator, expression: exp.Extract) -> str:
157+
unit = expression.this.this.lower()
158+
if unit == "isoyear":
159+
return self.func("YEAROFWEEKISO", expression.expression)
160+
if unit == "week":
161+
return self.func("WEEKISO", expression.expression)
162+
if unit == "isodow":
163+
return self.func("DAYOFWEEKISO", expression.expression)
164+
if unit == "dow":
165+
return exp.Sub(this=self.func("DAYOFWEEK", expression.expression), expression=exp.Literal.number(1))
166+
return rename_func("DATE_PART")(self, expression)
167+
168+
169+
def _snowflake_time_to_str(self: Generator, expression: exp.TimeToStr) -> str:
170+
if expression.args.get("format").this == "%U":
171+
# snowflake not support non-iso week
172+
# IFF(TO_CHAR(TO_TIMESTAMP_TZ(TO_CHAR(date, 'YYYY'), 'YYYY'), 'DY') = 'Sun', WEEK(date), WEEK(date)-1)
173+
return self.func(
174+
"IFF",
175+
exp.EQ(
176+
this=self.func("TO_CHAR", self.func("TO_TIMESTAMP_TZ", self.func("TO_CHAR", expression.this, exp.Literal.string('YYYY')), exp.Literal.string('YYYY')), exp.Literal.string("DY")),
177+
expression=exp.Literal.string('Sun')
178+
),
179+
self.func("WEEK", expression.this),
180+
exp.Sub(this=self.func("WEEK", expression.this), expression=exp.Literal.number(1))
181+
)
182+
183+
return self.func("TO_CHAR", exp.cast(expression.this, exp.DataType.Type.TIMESTAMP), self.format_time(expression))
184+
185+
186+
def _snowflake_str_to_time_sql(self: Generator, expression: exp.StrToTime) -> str:
187+
# adapter duckdb non-iso week
188+
if expression.args.get("format").this == "%Y%U" and isinstance(expression.this, exp.TimeToStr) and expression.this.args.get("format").this == "%Y%U":
189+
return self.func("DATE_TRUNC", exp.Literal.string("WEEK"), expression.this.this)
190+
return self.func("TO_TIMESTAMP", expression.this, self.format_time(expression))
191+
192+
193+
def _snowflake_timestamp_trunc_sql(self: Generator, expression: exp.TimestampTrunc) -> str:
194+
unit = expression.unit.this.lower()
195+
196+
# dateadd(day, -((date_extract(DAYOFWEEKISO from date)) - 1), date_trunc('day', date))
197+
trunc_iso_week = self.func(
198+
"dateadd",
199+
exp.Var(this="day"),
200+
exp.Sub(
201+
this=exp.Literal.number(1),
202+
expression=exp.Extract(this=exp.Var(this="DAYOFWEEKISO"), expression=expression.this)
203+
),
204+
self.func("date_trunc", exp.Literal.string("day"), expression.this)
205+
)
206+
207+
# dateadd(week, 1-(WEEKISO(date)), trunc_iso_week)
208+
if unit == "isoyear":
209+
return self.func(
210+
"dateadd",
211+
exp.Var(this="week"),
212+
exp.Sub(
213+
this=exp.Literal.number(1),
214+
expression=self.func("WEEKISO", expression.this)
215+
),
216+
trunc_iso_week
217+
)
218+
219+
# duckdb week means "isoweek"
220+
if unit == "week":
221+
return trunc_iso_week
222+
223+
return self.func("DATE_TRUNC", expression.unit, expression.this)
224+
225+
226+
SnowflakeDialect.Generator.TRANSFORMS[exp.Extract] = _snowflake_extract_sql
227+
SnowflakeDialect.Generator.TRANSFORMS[exp.TimeToStr] = _snowflake_time_to_str
228+
SnowflakeDialect.Generator.TRANSFORMS[exp.StrToTime] = _snowflake_str_to_time_sql
229+
SnowflakeDialect.Generator.TRANSFORMS[exp.TimestampTrunc] = _snowflake_timestamp_trunc_sql

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ dependencies = [
2525
"requests",
2626
"arrow",
2727
"sqlalchemy",
28-
"gw_dsl_parser==0.1.48a6",
28+
"gw_dsl_parser==0.1.49",
2929
"appdirs",
3030
"segment-analytics-python==2.2.3",
3131
"pandas",

0 commit comments

Comments
 (0)