diff --git a/ibis-server/app/model/connector.py b/ibis-server/app/model/connector.py index ffaa91fe9..08a6604cc 100644 --- a/ibis-server/app/model/connector.py +++ b/ibis-server/app/model/connector.py @@ -152,7 +152,7 @@ def query(self, sql: str, limit: int) -> pa.Table: return super().query(sql, limit) except ValueError as e: # Import here to avoid override the custom datatypes - import ibis.backends.bigquery + import ibis.backends.bigquery # noqa: PLC0415 # Try to match the error message from the google cloud bigquery library matching Arrow type error. # If the error message matches, requries to get the schema from the result and generate a empty pandas dataframe with the mapped schema @@ -190,7 +190,7 @@ def query(self, sql: str, limit: int) -> pa.Table: class DuckDBConnector: def __init__(self, connection_info: ConnectionInfo): - import duckdb + import duckdb # noqa: PLC0415 self.connection = duckdb.connect() if isinstance(connection_info, S3FileConnectionInfo): @@ -221,7 +221,7 @@ def dry_run(self, sql: str) -> None: class RedshiftConnector: def __init__(self, connection_info: RedshiftConnectionUnion): - import redshift_connector + import redshift_connector # noqa: PLC0415 if isinstance(connection_info, RedshiftIAMConnectionInfo): self.connection = redshift_connector.connect( diff --git a/ibis-server/app/model/data_source.py b/ibis-server/app/model/data_source.py index 187acc532..6e3e92f73 100644 --- a/ibis-server/app/model/data_source.py +++ b/ibis-server/app/model/data_source.py @@ -4,7 +4,6 @@ import ssl from enum import Enum, StrEnum, auto from json import loads -from typing import Optional import ibis from google.oauth2 import service_account @@ -241,7 +240,7 @@ def get_trino_connection(info: TrinoConnectionInfo) -> BaseBackend: ) @staticmethod - def _create_ssl_context(info: ConnectionInfo) -> Optional[ssl.SSLContext]: + def _create_ssl_context(info: ConnectionInfo) -> ssl.SSLContext | None: ssl_mode = ( info.ssl_mode.get_secret_value() if hasattr(info, "ssl_mode") and info.ssl_mode diff --git a/ibis-server/app/query_cache/__init__.py b/ibis-server/app/query_cache/__init__.py index efd3aa8d2..6e36db348 100644 --- a/ibis-server/app/query_cache/__init__.py +++ b/ibis-server/app/query_cache/__init__.py @@ -1,6 +1,6 @@ import hashlib import time -from typing import Any, Optional +from typing import Any import ibis import opendal @@ -17,7 +17,7 @@ def __init__(self, root: str = "/tmp/wren-engine/"): self.root = root @tracer.start_as_current_span("get_cache", kind=trace.SpanKind.INTERNAL) - def get(self, data_source: str, sql: str, info) -> Optional[Any]: + def get(self, data_source: str, sql: str, info) -> Any | None: cache_key = self._generate_cache_key(data_source, sql, info) cache_file_name = self._get_cache_file_name(cache_key) op = self._get_dal_operator() diff --git a/ibis-server/app/routers/v2/connector.py b/ibis-server/app/routers/v2/connector.py index c5a351549..d2eeec312 100644 --- a/ibis-server/app/routers/v2/connector.py +++ b/ibis-server/app/routers/v2/connector.py @@ -172,7 +172,7 @@ async def query( # case 5~8 Other cases (cache is not enabled) case (False, _, _): pass - response = ORJSONResponse(to_json(result, headers)) + response = ORJSONResponse(to_json(result, headers, data_source=data_source)) update_response_headers(response, cache_headers) if is_fallback: diff --git a/ibis-server/app/routers/v3/connector.py b/ibis-server/app/routers/v3/connector.py index 78c0a91db..f86201d19 100644 --- a/ibis-server/app/routers/v3/connector.py +++ b/ibis-server/app/routers/v3/connector.py @@ -158,7 +158,7 @@ async def query( case (False, _, _): pass - response = ORJSONResponse(to_json(result, headers)) + response = ORJSONResponse(to_json(result, headers, data_source=data_source)) update_response_headers(response, cache_headers) return response except Exception as e: diff --git a/ibis-server/app/util.py b/ibis-server/app/util.py index 5917c5101..f1a6ab201 100644 --- a/ibis-server/app/util.py +++ b/ibis-server/app/util.py @@ -1,6 +1,6 @@ import base64 -import duckdb +import datafusion import orjson import pandas as pd import pyarrow as pa @@ -40,7 +40,8 @@ def base64_to_dict(base64_str: str) -> dict: @tracer.start_as_current_span("to_json", kind=trace.SpanKind.INTERNAL) -def to_json(df: pa.Table, headers: dict) -> dict: +def to_json(df: pa.Table, headers: dict, data_source: DataSource = None) -> dict: + df = _with_session_timezone(df, headers, data_source) dtypes = {field.name: str(field.type) for field in df.schema} if df.num_rows == 0: return { @@ -49,12 +50,16 @@ def to_json(df: pa.Table, headers: dict) -> dict: "dtypes": dtypes, } + ctx = get_datafusion_context(headers) + ctx.register_record_batches(name="arrow_table", partitions=[df.to_batches()]) + formatted_sql = ( - "SELECT " + ", ".join([_formater(field) for field in df.schema]) + " FROM df" + "SELECT " + + ", ".join([_formater(field) for field in df.schema]) + + " FROM arrow_table" ) logger.debug(f"formmated_sql: {formatted_sql}") - conn = get_duckdb_conn(headers) - formatted_df = conn.execute(formatted_sql).fetch_df() + formatted_df = ctx.sql(formatted_sql).to_pandas() result = formatted_df.to_dict(orient="split") result["dtypes"] = dtypes @@ -62,33 +67,59 @@ def to_json(df: pa.Table, headers: dict) -> dict: return result -def get_duckdb_conn(headers: dict) -> duckdb.DuckDBPyConnection: - """Get a DuckDB connection with the provided headers.""" - conn = duckdb.connect() - if X_WREN_TIMEZONE in headers: - timezone = headers[X_WREN_TIMEZONE] - if timezone.startwith("+") or timezone.startswith("-"): - # If the timezone is an offset, convert it to a named timezone - timezone = get_timezone_from_offset(timezone) - conn.execute("SET TimeZone = ?", [timezone]) - else: - # Default to UTC if no timezone is provided - conn.execute("SET TimeZone = 'UTC'") +def _with_session_timezone( + df: pa.Table, headers: dict, data_source: DataSource +) -> pa.Table: + fields = [] - return conn + for field in df.schema: + if pa.types.is_timestamp(field.type): + if field.type.tz is not None and X_WREN_TIMEZONE in headers: + # change the timezone to the seesion timezone + fields.append( + pa.field( + field.name, + pa.timestamp(field.type.unit, tz=headers[X_WREN_TIMEZONE]), + nullable=True, + ) + ) + continue + if data_source == DataSource.mysql: + timezone = headers.get(X_WREN_TIMEZONE, "UTC") + # TODO: ibis mysql loss the timezone information + # we cast timestamp to timestamp with session timezone for mysql + fields.append( + pa.field( + field.name, + pa.timestamp(field.type.unit, tz=timezone), + nullable=True, + ) + ) + continue + + # TODO: the field's nullable should be Ture if the value contains null but + # the arrow table produced by the ibis clickhouse connector always set nullable to False + # so we set nullable to True here to avoid the casting error + fields.append( + pa.field( + field.name, + field.type, + nullable=True, + ) + ) + return df.cast(pa.schema(fields)) -def get_timezone_from_offset(offset: str) -> str: - if offset.startswith("+"): - offset = offset[1:] # Remove the leading '+' sign +def get_datafusion_context(headers: dict) -> datafusion.SessionContext: + config = datafusion.SessionConfig() + if X_WREN_TIMEZONE in headers: + config.set("datafusion.execution.time_zone", headers[X_WREN_TIMEZONE]) + else: + # Default to UTC if no timezone is provided + config.set("datafusion.execution.time_zone", "UTC") - first = duckdb.execute( - "SELECT name, utc_offset FROM pg_timezone_names() WHERE utc_offset = ?", - [offset], - ).fetchone() - if first is None: - raise ValueError(f"Invalid timezone offset: {offset}") - return first[0] # Return the timezone name + ctx = datafusion.SessionContext(config=config) + return ctx def build_context(headers: Header) -> Context: @@ -166,30 +197,29 @@ def update_response_headers(response, required_headers: dict): response.headers[X_CACHE_OVERRIDE_AT] = required_headers[X_CACHE_OVERRIDE_AT] +def _quote_identifier(identifier: str) -> str: + identifier = identifier.replace('"', '""') # Escape double quotes + return f'"{identifier}"' if identifier else identifier + + def _formater(field: pa.Field) -> str: column_name = _quote_identifier(field.name) - if pa.types.is_decimal(field.type) or pa.types.is_floating(field.type): - return f""" - case when {column_name} = 0 then '0' - when length(CAST({column_name} AS VARCHAR)) > 15 then format('{{:.9g}}', {column_name}) - else RTRIM(RTRIM(format('{{:.8f}}', {column_name}), '0'), '.') - end as {column_name}""" + if pa.types.is_decimal(field.type): + # TODO: maybe implement a to_char udf to fomrat decimal would be better + # Currently, if the nubmer is less than 1, it will show with exponential notation if the lenth of float digits is great than 7 + # e.g. 0.0000123 will be shown without exponential notation but 0.0000123 will be shown with exponential notation 1.23e-6 + return f"case when {column_name} = 0 then '0' else cast({column_name} as double) end as {column_name}" elif pa.types.is_date(field.type): - return f"strftime({column_name}, '%Y-%m-%d') as {column_name}" + return f"to_char({column_name}, '%Y-%m-%d') as {column_name}" elif pa.types.is_timestamp(field.type): if field.type.tz is None: - return f"strftime({column_name}, '%Y-%m-%d %H:%M:%S.%f') as {column_name}" + return f"to_char({column_name}, '%Y-%m-%d %H:%M:%S%.6f') as {column_name}" else: return ( - f"strftime({column_name}, '%Y-%m-%d %H:%M:%S.%f %Z') as {column_name}" + f"to_char({column_name}, '%Y-%m-%d %H:%M:%S%.6f %Z') as {column_name}" ) elif pa.types.is_binary(field.type): - return f"to_hex({column_name}) as {column_name}" + return f"encode({column_name}, 'hex') as {column_name}" elif pa.types.is_interval(field.type): return f"cast({column_name} as varchar) as {column_name}" return column_name - - -def _quote_identifier(identifier: str) -> str: - identifier = identifier.replace('"', '""') # Escape double quotes - return f'"{identifier}"' if identifier else identifier diff --git a/ibis-server/poetry.lock b/ibis-server/poetry.lock index 33373e4ee..d88aae460 100644 --- a/ibis-server/poetry.lock +++ b/ibis-server/poetry.lock @@ -883,7 +883,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} [[package]] name = "cryptography" @@ -945,6 +945,26 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==45.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] +[[package]] +name = "datafusion" +version = "47.0.0" +description = "Build and run queries against data" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "datafusion-47.0.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd83a8e49fb39be06ddfa87023200a9ddc93d181247654ac951fa5720219d08"}, + {file = "datafusion-47.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:43677e6284b165727031aec14d4beaa97296e991960293c61dcb66a3a9ce59b8"}, + {file = "datafusion-47.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d244ed32a2fae7c4dd292a6bfe092cc94b3b86c600eddb7d633609043d406bae"}, + {file = "datafusion-47.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b3304ec63fb89f27e4280226807fd033ed7f0ea36d2d69fecf68f257d975c24d"}, + {file = "datafusion-47.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:73c5d056908185c77eedcaea43a5a8ab5e1c2e747a3e34d36d3625e09a3dc2af"}, + {file = "datafusion-47.0.0.tar.gz", hash = "sha256:19a6976731aa96a6f6e264c390c64b9e32051e866366bd69450bc77e67bc91b1"}, +] + +[package.dependencies] +pyarrow = ">=11.0.0" +typing-extensions = {version = "*", markers = "python_version < \"3.13\""} + [[package]] name = "db-dtypes" version = "1.4.3" @@ -5572,4 +5592,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.12" -content-hash = "510f68190b37a75fdda435c780f810f35f981dc7c906634e969a818d28c4f3fa" +content-hash = "66dd309f8dce5bc8b92b202fab4207fbd818e5bb6fcaf0d6db9457cf7ffee980" diff --git a/ibis-server/pyproject.toml b/ibis-server/pyproject.toml index 05bdf88b6..49a4b1cc8 100644 --- a/ibis-server/pyproject.toml +++ b/ibis-server/pyproject.toml @@ -46,6 +46,7 @@ gunicorn = "^23.0.0" uvicorn-worker = "^0.3.0" jinja2 = ">=3.1.6" redshift_connector = "2.1.7" +datafusion = "^47.0.0" [tool.poetry.group.dev.dependencies] pytest = "8.3.5" diff --git a/ibis-server/tests/routers/v2/connector/test_bigquery.py b/ibis-server/tests/routers/v2/connector/test_bigquery.py index 7df5960c8..88485ef86 100644 --- a/ibis-server/tests/routers/v2/connector/test_bigquery.py +++ b/ibis-server/tests/routers/v2/connector/test_bigquery.py @@ -91,11 +91,11 @@ async def test_query(client, manifest_str): 1, 370, "O", - "172799.49", + 172799.49, "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "616263", ] @@ -317,7 +317,7 @@ async def test_interval(client, manifest_str): ) assert response.status_code == 200 result = response.json() - assert result["data"][0] == ["9 years 4 months 100 days 01:00:00"] + assert result["data"][0] == ["112 mons 100 days 1 hours"] assert result["dtypes"] == {"col": "month_day_nano_interval"} @@ -332,7 +332,7 @@ async def test_avg_interval(client, manifest_str): ) assert response.status_code == 200 result = response.json() - assert result["data"][0] == ["10484 days 08:54:14.4"] + assert result["data"][0] == ["10484 days 8 hours 54 mins 14.400000000 secs"] assert result["dtypes"] == {"col": "month_day_nano_interval"} @@ -362,7 +362,7 @@ async def test_custom_datatypes_no_overrides(client, manifest_str): ) assert response.status_code == 200 result = response.json() - assert result["data"][0] == ["9 years 4 months 100 days 01:00:00"] + assert result["data"][0] == ["112 mons 100 days 1 hours"] assert result["dtypes"] == {"col": "month_day_nano_interval"} diff --git a/ibis-server/tests/routers/v2/connector/test_clickhouse.py b/ibis-server/tests/routers/v2/connector/test_clickhouse.py index aaac2b227..d6cb83edb 100644 --- a/ibis-server/tests/routers/v2/connector/test_clickhouse.py +++ b/ibis-server/tests/routers/v2/connector/test_clickhouse.py @@ -183,7 +183,7 @@ async def test_query(client, manifest_str, clickhouse: ClickHouseContainer): "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "abc", # Clickhouse does not support bytea, so it is returned as string ] diff --git a/ibis-server/tests/routers/v2/connector/test_mssql.py b/ibis-server/tests/routers/v2/connector/test_mssql.py index 80ac46e5d..969a931ca 100644 --- a/ibis-server/tests/routers/v2/connector/test_mssql.py +++ b/ibis-server/tests/routers/v2/connector/test_mssql.py @@ -129,7 +129,7 @@ async def test_query(client, manifest_str, mssql: SqlServerContainer): "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "616263", ] diff --git a/ibis-server/tests/routers/v2/connector/test_mysql.py b/ibis-server/tests/routers/v2/connector/test_mysql.py index 3dfe33603..45c70eb6c 100644 --- a/ibis-server/tests/routers/v2/connector/test_mysql.py +++ b/ibis-server/tests/routers/v2/connector/test_mysql.py @@ -162,8 +162,8 @@ async def test_query(client, manifest_str, mysql: MySqlContainer): "172799.49", "1996-01-02", "1_370", - "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000", + "2024-01-01 23:59:59.000000 +00:00", + "2024-01-01 23:59:59.000000 +00:00", None, "616263", ] @@ -174,9 +174,9 @@ async def test_query(client, manifest_str, mysql: MySqlContainer): "totalprice": "string", "orderdate": "date32[day]", "order_cust_key": "string", - "timestamp": "timestamp[us]", - "timestamptz": "timestamp[us]", - "test_null_time": "timestamp[us]", + "timestamp": "timestamp[us, tz=UTC]", + "timestamptz": "timestamp[us, tz=UTC]", + "test_null_time": "timestamp[us, tz=UTC]", "bytea_column": "binary", } diff --git a/ibis-server/tests/routers/v2/connector/test_oracle.py b/ibis-server/tests/routers/v2/connector/test_oracle.py index 66feb09da..5fecc6858 100644 --- a/ibis-server/tests/routers/v2/connector/test_oracle.py +++ b/ibis-server/tests/routers/v2/connector/test_oracle.py @@ -194,7 +194,7 @@ async def test_query(client, manifest_str, oracle: OracleDbContainer): "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "616263", ] diff --git a/ibis-server/tests/routers/v2/connector/test_postgres.py b/ibis-server/tests/routers/v2/connector/test_postgres.py index f62604736..30ec6b31a 100644 --- a/ibis-server/tests/routers/v2/connector/test_postgres.py +++ b/ibis-server/tests/routers/v2/connector/test_postgres.py @@ -188,7 +188,7 @@ async def test_query(client, manifest_str, postgres: PostgresContainer): "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "616263", ] @@ -1007,7 +1007,7 @@ async def test_postgis_geometry(client, manifest_str, postgis: PostgresContainer ) assert response.status_code == 200 result = response.json() - assert result["data"][0] == ["74.6626535"] + assert result["data"][0] == [74.66265347816136] def _to_connection_info(pg: PostgresContainer): diff --git a/ibis-server/tests/routers/v2/connector/test_redshift.py b/ibis-server/tests/routers/v2/connector/test_redshift.py index 3ba96011a..dec07d3e1 100644 --- a/ibis-server/tests/routers/v2/connector/test_redshift.py +++ b/ibis-server/tests/routers/v2/connector/test_redshift.py @@ -110,7 +110,7 @@ async def test_query(client, manifest_str): "2023-05-21", "1_655", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "abc", ] @@ -150,7 +150,7 @@ async def test_query_with_aws_iam_credential(client, manifest_str): "2023-05-21", "1_655", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, "abc", ] diff --git a/ibis-server/tests/routers/v2/connector/test_snowflake.py b/ibis-server/tests/routers/v2/connector/test_snowflake.py index f126acebc..692e0acbd 100644 --- a/ibis-server/tests/routers/v2/connector/test_snowflake.py +++ b/ibis-server/tests/routers/v2/connector/test_snowflake.py @@ -94,7 +94,7 @@ async def test_query(client, manifest_str): "1996-01-02", "1_36901", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, ] assert result["dtypes"] == { diff --git a/ibis-server/tests/routers/v2/connector/test_trino.py b/ibis-server/tests/routers/v2/connector/test_trino.py index aaa24b3b2..dcecbcac7 100644 --- a/ibis-server/tests/routers/v2/connector/test_trino.py +++ b/ibis-server/tests/routers/v2/connector/test_trino.py @@ -111,7 +111,7 @@ async def test_query(client, manifest_str, trino: TrinoContainer): 1, 370, "O", - "172799.49", + 172799.49, "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", diff --git a/ibis-server/tests/routers/v3/connector/bigquery/test_query.py b/ibis-server/tests/routers/v3/connector/bigquery/test_query.py index 725198202..e353bea9f 100644 --- a/ibis-server/tests/routers/v3/connector/bigquery/test_query.py +++ b/ibis-server/tests/routers/v3/connector/bigquery/test_query.py @@ -82,13 +82,13 @@ async def test_query(client, manifest_str, connection_info): 36485, 1202, "F", - "356711.63", + 356711.63, "1992-06-06", "36485_1202", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", - "2024-01-16 04:00:00.000000 UTC", # utc-5 - "2024-07-16 03:00:00.000000 UTC", # utc-4 + "2024-01-01 23:59:59.000000 +00:00", + "2024-01-16 04:00:00.000000 +00:00", # utc-5 + "2024-07-16 03:00:00.000000 +00:00", # utc-4 ] assert result["dtypes"] == { "o_orderkey": "int64", @@ -293,9 +293,9 @@ async def test_timestamp_func(client, manifest_str, connection_info): assert len(result["columns"]) == 3 assert len(result["data"]) == 1 assert result["data"][0] == [ - "1970-01-01 00:16:40.000000 UTC", - "1970-01-01 00:00:01.000000 UTC", - "1970-01-12 13:46:40.000000 UTC", + "1970-01-01 00:16:40.000000 +00:00", + "1970-01-01 00:00:01.000000 +00:00", + "1970-01-12 13:46:40.000000 +00:00", ] assert result["dtypes"] == { "millis": "timestamp[us, tz=UTC]", diff --git a/ibis-server/tests/routers/v3/connector/local_file/test_query.py b/ibis-server/tests/routers/v3/connector/local_file/test_query.py index 8d918b3bf..3b0111fe3 100644 --- a/ibis-server/tests/routers/v3/connector/local_file/test_query.py +++ b/ibis-server/tests/routers/v3/connector/local_file/test_query.py @@ -141,7 +141,7 @@ async def test_query_calculated_field(client, manifest_str): assert len(result["data"]) == 1 assert result["data"][0] == [ 370, - "2860895.79", + 2860895.79, ] assert result["dtypes"] == { "custkey": "int32", diff --git a/ibis-server/tests/routers/v3/connector/oracle/test_query.py b/ibis-server/tests/routers/v3/connector/oracle/test_query.py index dfef792e7..f79be229f 100644 --- a/ibis-server/tests/routers/v3/connector/oracle/test_query.py +++ b/ibis-server/tests/routers/v3/connector/oracle/test_query.py @@ -97,7 +97,7 @@ async def test_query(client, manifest_str, connection_info): "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", + "2024-01-01 23:59:59.000000 +00:00", None, ] assert result["dtypes"] == { diff --git a/ibis-server/tests/routers/v3/connector/postgres/test_query.py b/ibis-server/tests/routers/v3/connector/postgres/test_query.py index 698e62cdc..144199b57 100644 --- a/ibis-server/tests/routers/v3/connector/postgres/test_query.py +++ b/ibis-server/tests/routers/v3/connector/postgres/test_query.py @@ -141,13 +141,13 @@ async def test_query(client, manifest_str, connection_info): 1, 370, "O", - "172799.49", + 172799.49, "1996-01-02", "1_370", "2024-01-01 23:59:59.000000", - "2024-01-01 23:59:59.000000 UTC", - "2024-01-16 04:00:00.000000 UTC", # utc-5 - "2024-07-16 03:00:00.000000 UTC", # utc-4 + "2024-01-01 23:59:59.000000 +00:00", + "2024-01-16 04:00:00.000000 +00:00", # utc-5 + "2024-07-16 03:00:00.000000 +00:00", # utc-4 ] assert result["dtypes"] == { "o_orderkey": "int32", @@ -180,14 +180,14 @@ async def test_query(client, manifest_str, connection_info): assert result["dtypes"] == { "o_orderkey": "int32", "o_custkey": "int32", - "o_orderstatus": "object", - "o_totalprice_double": "float64", - "o_orderdate": "datetime64[s]", - "order_cust_key": "object", - "timestamp": "datetime64[ns]", - "timestamptz": "datetime64[ns, UTC]", - "dst_utc_minus_5": "datetime64[ns, UTC]", - "dst_utc_minus_4": "datetime64[ns, UTC]", + "o_orderstatus": "string", + "o_totalprice_double": "double", + "o_orderdate": "date32[day]", + "order_cust_key": "string", + "timestamp": "timestamp[us]", + "timestamptz": "timestamp[us, tz=UTC]", + "dst_utc_minus_5": "timestamp[us, tz=UTC]", + "dst_utc_minus_4": "timestamp[us, tz=UTC]", } @@ -624,7 +624,7 @@ async def test_format_floating(client, manifest_str, connection_info): "manifestStr": manifest_str, "sql": """ SELECT - 0.0123e-5 AS case_scientific_original, + 0.0123e-3 AS case_scientific_original, 1.23e+4 AS case_scientific_positive, -4.56e-3 AS case_scientific_negative, 7.89e0 AS case_scientific_zero_exponent, @@ -632,7 +632,7 @@ async def test_format_floating(client, manifest_str, connection_info): 123.456 AS case_decimal_positive, -123.456 AS case_decimal_negative, - 0.0000123 AS case_decimal_small, + 0.00000123 AS show_exponent_decimal, 123.0000 AS case_decimal_trailing_zeros, 0.0 AS case_decimal_zero, @@ -665,31 +665,35 @@ async def test_format_floating(client, manifest_str, connection_info): ) assert response.status_code == 200 result = response.json() - assert result["data"][0][0] == "0.00000012" - assert result["data"][0][1] == "12300" - assert result["data"][0][2] == "-0.00456" - assert result["data"][0][3] == "7.89" - assert result["data"][0][4] == "0" - assert result["data"][0][5] == "123.456" - assert result["data"][0][6] == "-123.456" - assert result["data"][0][7] == "0.0000123" - assert result["data"][0][8] == "123" - assert result["data"][0][9] == "0" - assert result["data"][0][10] == 0 - assert result["data"][0][11] == "0" - assert result["data"][0][12] == -1 - assert result["data"][0][13] == 9999999999 - assert result["data"][0][14] == "12304.56" - assert result["data"][0][15] == "-123.450123" - assert result["data"][0][16] == "0.000123" - assert result["data"][0][17] == "1.00365854" - assert result["data"][0][18] is None - assert result["data"][0][19] == "inf" - assert result["data"][0][20] == "-inf" - assert result["data"][0][21] is None - assert result["data"][0][22] == "123.45600128" - assert result["data"][0][23] == "12300" - assert result["data"][0][24] == "123400000000000" - assert result["data"][0][25] == "1.234e+15" - assert result["data"][0][26] == "1.12345679" - assert result["data"][0][27] == "0.123456789" + assert result["data"][0] == [ + "0.0000123", + "12300.0", + "-0.00456", + "7.89", + "0", + "123.456", + "-123.456", + "1.23e-6", + "123.0", + "0", + 0, + "0", + -1, + 9999999999, + "12304.56", + "-123.450123", + 0.000123, + 1.0036585365853659, + None, # NaN + None, # Infinity + None, # Negative Infinity + None, # NULL + 123.45600128173828, # case_cast_float + "12300.0", # case_cast_decimal + "123400000000000.0", # show_float + # TODO: it's better to show exponent in scientific notation but currently + # DataFusion does not support it, so we show the full number + "1234000000000000.0", # show_exponent + "1.123456789", # round_to_9_decimal_places + "0.12345678912345678", # round_to_18_decimal_places + ] diff --git a/ibis-server/tests/util/sql_generator.py b/ibis-server/tests/util/sql_generator.py index a089d4e0a..8bc97e691 100644 --- a/ibis-server/tests/util/sql_generator.py +++ b/ibis-server/tests/util/sql_generator.py @@ -1,6 +1,5 @@ import re from abc import ABC -from typing import Optional from tests.model import Function @@ -10,7 +9,7 @@ def __init__(self, dialect: str): self.dialect = dialect self._generator = self._get_generator() - def generate_sql(self, function: Function) -> Optional[str]: + def generate_sql(self, function: Function) -> str | None: if function.function_type == "aggregate": return self._generator.generate_aggregate_sql(function) if function.function_type == "scalar": diff --git a/wren-core/core/src/logical_plan/analyze/access_control.rs b/wren-core/core/src/logical_plan/analyze/access_control.rs index b9ff587df..cc9876ecc 100644 --- a/wren-core/core/src/logical_plan/analyze/access_control.rs +++ b/wren-core/core/src/logical_plan/analyze/access_control.rs @@ -101,7 +101,7 @@ pub fn validate_rlac_rule(rule: &RowLevelAccessControl, model: &Model) -> Result "The session property {} is used, but not found in the session properties", missed_properties .iter() - .map(|property| format!("@{}", property)) + .map(|property| format!("@{property}")) .collect::>() .join(", ") ); @@ -364,7 +364,7 @@ mod test { let all_match = name.iter().all(|n| expected.contains(n.as_str())); if !all_match { - panic!("should be all match, but got: {:?}", name); + panic!("should be all match, but got: {name:?}"); } assert_eq!(session_properties.len(), 1); assert_eq!(session_properties[0], "session_id"); diff --git a/wren-core/core/src/logical_plan/analyze/plan.rs b/wren-core/core/src/logical_plan/analyze/plan.rs index a2ba09fb8..482d5330b 100644 --- a/wren-core/core/src/logical_plan/analyze/plan.rs +++ b/wren-core/core/src/logical_plan/analyze/plan.rs @@ -287,7 +287,7 @@ impl ModelPlanNodeBuilder { .get_model(model.table()) .and_then(|m| m.primary_key().and_then(|pk| m.get_column(pk))) else { - debug!("Primary key not found for model {}", model); + debug!("Primary key not found for model {model}"); continue; }; self.model_required_fields @@ -615,15 +615,15 @@ fn collect_model_required_fields( else { return plan_err!("Required fields not found for {}", qualified_column); }; - debug!("Required fields: {:?}", set); + debug!("Required fields: {set:?}"); for c in set { let Some(relation_ref) = &c.relation else { - return plan_err!("Source dataset not found for {}", c); + return plan_err!("Source dataset not found for {c}"); }; let Some(ColumnReference { dataset, column }) = analyzed_wren_mdl.wren_mdl().get_column_reference(c) else { - return plan_err!("Column reference not found for {}", c); + return plan_err!("Column reference not found for {c}"); }; if let Dataset::Model(m) = dataset { if column.is_calculated { @@ -635,8 +635,7 @@ fn collect_model_required_fields( ) else { // skip the semantic expression (e.g. calculated field or relationship column) debug!( - "Error creating expression for calculated field: {}", - expression + "Error creating expression for calculated field: {expression}" ); continue; }; diff --git a/wren-core/core/src/logical_plan/utils.rs b/wren-core/core/src/logical_plan/utils.rs index b309f83a4..866d2fd62 100644 --- a/wren-core/core/src/logical_plan/utils.rs +++ b/wren-core/core/src/logical_plan/utils.rs @@ -69,7 +69,7 @@ fn create_struct_type(struct_type: &str) -> Result { field .field_name .map(|f| f.to_string()) - .unwrap_or_else(|| format!("c{}", counter)), + .unwrap_or_else(|| format!("c{counter}")), data_type, true, ); @@ -96,7 +96,7 @@ fn parse_type(struct_type: &str) -> Result { /// If the data type is not supported, it will return Utf8 pub fn try_map_data_type(data_type: &str) -> Result { Ok(map_data_type(data_type).ok().unwrap_or_else(|| { - debug!("can't parse data type {}, return Utf8", data_type); + debug!("can't parse data type {data_type}, return Utf8"); DataType::Utf8 })) } @@ -178,7 +178,7 @@ pub fn map_data_type(data_type: &str) -> Result { "bit" => DataType::Boolean, // we don't have a BIT type, so we map it to Boolean "timestamp_ns" => DataType::Timestamp(TimeUnit::Nanosecond, None), _ => { - debug!("try parse by arrow {}", lower_data_type); + debug!("try parse by arrow {lower_data_type}"); // the from_str is case sensitive, so we need to use the original string DataType::from_str(data_type)? } @@ -254,7 +254,7 @@ pub fn from_qualified_name_str( /// Use to print the graph for debugging purposes pub fn print_graph(graph: &Graph) { let dot = Dot::with_config(graph, &[Config::EdgeNoLabel]); - println!("graph: {:?}", dot); + println!("graph: {dot:?}"); } /// Check if the table reference belongs to the mdl diff --git a/wren-core/core/src/mdl/context.rs b/wren-core/core/src/mdl/context.rs index 2177f46e4..380c3bb4a 100644 --- a/wren-core/core/src/mdl/context.rs +++ b/wren-core/core/src/mdl/context.rs @@ -39,6 +39,7 @@ use datafusion::optimizer::unwrap_cast_in_comparison::UnwrapCastInComparison; use datafusion::optimizer::{AnalyzerRule, OptimizerRule}; use datafusion::physical_plan::ExecutionPlan; use datafusion::prelude::SessionContext; +use datafusion::scalar::ScalarValue; use datafusion::sql::TableReference; use parking_lot::RwLock; @@ -51,8 +52,16 @@ pub async fn create_ctx_with_mdl( properties: SessionPropertiesRef, is_local_runtime: bool, ) -> Result { + let session_timezone = properties + .get("x-wren-timezone") + .map(|v| v.as_ref().map(|s| s.as_str()).unwrap_or("UTC").to_string()); + let config = ctx .copied_config() + .set( + "datafusion.execution.time_zone", + &ScalarValue::Utf8(session_timezone), + ) .with_create_default_catalog_and_schema(false) .with_default_catalog_and_schema( analyzed_mdl.wren_mdl.catalog(), diff --git a/wren-core/core/src/mdl/function.rs b/wren-core/core/src/mdl/function.rs index 0364510b8..2d96f99ed 100644 --- a/wren-core/core/src/mdl/function.rs +++ b/wren-core/core/src/mdl/function.rs @@ -69,7 +69,7 @@ impl Display for FunctionType { FunctionType::Aggregate => "aggregate".to_string(), FunctionType::Window => "window".to_string(), }; - write!(f, "{}", str) + write!(f, "{str}") } } @@ -81,7 +81,7 @@ impl FromStr for FunctionType { "scalar" => Ok(FunctionType::Scalar), "aggregate" => Ok(FunctionType::Aggregate), "window" => Ok(FunctionType::Window), - _ => Err(format!("Unknown function type: {}", s)), + _ => Err(format!("Unknown function type: {s}")), } } } @@ -105,7 +105,7 @@ pub enum ReturnType { impl Display for ReturnType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - ReturnType::Specific(data_type) => write!(f, "{}", data_type), + ReturnType::Specific(data_type) => write!(f, "{data_type}"), ReturnType::SameAsInput => write!(f, "same_as_input"), ReturnType::SameAsInputFirstArrayElement => { write!(f, "same_as_input_first_array_element") diff --git a/wren-core/core/src/mdl/mod.rs b/wren-core/core/src/mdl/mod.rs index f5ae95afe..ecb4bfd31 100644 --- a/wren-core/core/src/mdl/mod.rs +++ b/wren-core/core/src/mdl/mod.rs @@ -374,9 +374,9 @@ pub async fn transform_sql_with_ctx( properties: SessionPropertiesRef, sql: &str, ) -> Result { - info!("wren-core received SQL: {}", sql); + info!("wren-core received SQL: {sql}"); remote_functions.iter().try_for_each(|remote_function| { - debug!("Registering remote function: {:?}", remote_function); + debug!("Registering remote function: {remote_function:?}"); register_remote_function(ctx, remote_function)?; Ok::<_, DataFusionError>(()) })?; @@ -397,7 +397,7 @@ pub async fn transform_sql_with_ctx( let replaced = sql .to_string() .replace(analyzed_mdl.wren_mdl().catalog_schema_prefix(), ""); - info!("wren-core planned SQL: {}", replaced); + info!("wren-core planned SQL: {replaced}"); Ok(replaced) } Err(e) => Err(e), @@ -471,8 +471,7 @@ mod test { use datafusion::common::format::DEFAULT_FORMAT_OPTIONS; use datafusion::common::not_impl_err; use datafusion::common::Result; - use datafusion::config::ConfigOptions; - use datafusion::prelude::{SessionConfig, SessionContext}; + use datafusion::prelude::SessionContext; use datafusion::sql::unparser::plan_to_sql; use insta::assert_snapshot; use wren_core_base::mdl::{ @@ -529,7 +528,7 @@ mod test { ]; for sql in tests { - println!("Original: {}", sql); + println!("Original: {sql}"); let actual = mdl::transform_sql_with_ctx( &SessionContext::new(), Arc::clone(&analyzed_mdl), @@ -538,7 +537,7 @@ mod test { sql, ) .await?; - println!("After transform: {}", actual); + println!("After transform: {actual}"); assert_sql_valid_executable(&actual).await?; } @@ -554,12 +553,12 @@ mod test { let mdl_json = fs::read_to_string(test_data.as_path())?; let mdl = match serde_json::from_str::(&mdl_json) { Ok(mdl) => mdl, - Err(e) => return not_impl_err!("Failed to parse mdl json: {}", e), + Err(e) => return not_impl_err!("Failed to parse mdl json: {e}"), }; let analyzed_mdl = Arc::new(AnalyzedWrenMDL::analyze(mdl, Arc::new(HashMap::default()))?); let sql = "select * from test.test.customer_view"; - println!("Original: {}", sql); + println!("Original: {sql}"); let _ = transform_sql_with_ctx( &SessionContext::new(), Arc::clone(&analyzed_mdl), @@ -584,7 +583,7 @@ mod test { let mdl_json = fs::read_to_string(test_data.as_path())?; let mdl = match serde_json::from_str::(&mdl_json) { Ok(mdl) => mdl, - Err(e) => return not_impl_err!("Failed to parse mdl json: {}", e), + Err(e) => return not_impl_err!("Failed to parse mdl json: {e}"), }; let analyzed_mdl = Arc::new(AnalyzedWrenMDL::analyze(mdl, Arc::new(HashMap::default()))?); @@ -999,7 +998,7 @@ mod test { let df = ctx.sql(sql).await?; let plan = df.into_optimized_plan()?; let after_roundtrip = plan_to_sql(&plan).map(|sql| sql.to_string())?; - println!("After roundtrip: {}", after_roundtrip); + println!("After roundtrip: {after_roundtrip}"); match ctx.sql(sql).await?.collect().await { Ok(_) => Ok(()), Err(e) => { @@ -1120,17 +1119,17 @@ mod test { #[tokio::test] async fn test_eval_timestamp_with_session_timezone() -> Result<()> { - let mut config = ConfigOptions::new(); - config.execution.time_zone = Some("+08:00".to_string()); - let session_config = SessionConfig::from(config); - let ctx = SessionContext::new_with_config(session_config); + let mut headers = HashMap::new(); + headers.insert("x-wren-timezone".to_string(), Some("+08:00".to_string())); + let headers_ref = Arc::new(headers); + let ctx = SessionContext::new(); let analyzed_mdl = Arc::new(AnalyzedWrenMDL::default()); let sql = "select timestamp '2011-01-01 18:00:00'"; let actual = transform_sql_with_ctx( &ctx, Arc::clone(&analyzed_mdl), &[], - Arc::new(HashMap::new()), + Arc::clone(&headers_ref), sql, ) .await?; @@ -1142,17 +1141,20 @@ mod test { &ctx, Arc::clone(&analyzed_mdl), &[], - Arc::new(HashMap::new()), + Arc::clone(&headers_ref), sql, ) .await?; // TIMESTAMP WITH TIME ZONE will be converted to the session timezone assert_snapshot!(actual, @"SELECT CAST('2011-01-01 10:00:00' AS TIMESTAMP) AS \"Utf8(\"\"2011-01-01 18:00:00\"\")\""); - let mut config = ConfigOptions::new(); - config.execution.time_zone = Some("America/New_York".to_string()); - let session_config = SessionConfig::from(config); - let ctx = SessionContext::new_with_config(session_config); + let ctx = SessionContext::new(); + let mut headers = HashMap::new(); + headers.insert( + "x-wren-timezone".to_string(), + Some("America/New_York".to_string()), + ); + let headers_ref = Arc::new(headers); let analyzed_mdl = Arc::new(AnalyzedWrenMDL::default()); // TIMESTAMP WITH TIME ZONE will be converted to the session timezone with daylight saving (UTC -5) let sql = "select timestamp with time zone '2024-01-15 18:00:00'"; @@ -1160,7 +1162,7 @@ mod test { &ctx, Arc::clone(&analyzed_mdl), &[], - Arc::new(HashMap::new()), + Arc::clone(&headers_ref), sql, ) .await?; @@ -1172,7 +1174,7 @@ mod test { &ctx, Arc::clone(&analyzed_mdl), &[], - Arc::new(HashMap::new()), + Arc::clone(&headers_ref), sql, ) .await?; @@ -1325,7 +1327,7 @@ mod test { .await?; // assert the simplified literal will be casted to the timestamp tz assert_eq!(actual, - "SELECT timestamp_table.timestamptz_col > CAST(CAST('2011-01-01 18:00:00' AS TIMESTAMP) AS TIMESTAMP WITH TIME ZONE) FROM (SELECT timestamp_table.timestamptz_col FROM (SELECT __source.timestamptz_col AS timestamptz_col FROM datafusion.\"public\".timestamp_table AS __source) AS timestamp_table) AS timestamp_table" + "SELECT timestamp_table.timestamptz_col > CAST(CAST('2011-01-01 18:00:00' AS TIMESTAMP WITH TIME ZONE) AS TIMESTAMP WITH TIME ZONE) FROM (SELECT timestamp_table.timestamptz_col FROM (SELECT __source.timestamptz_col AS timestamptz_col FROM datafusion.\"public\".timestamp_table AS __source) AS timestamp_table) AS timestamp_table" ); let sql = r#"select timestamptz_col > '2011-01-01 18:00:00' from wren.test.timestamp_table"#; @@ -1354,7 +1356,7 @@ mod test { .await?; // assert the simplified literal won't be casted to the timestamp tz assert_eq!(actual, - "SELECT timestamp_table.timestamp_col > CAST('2011-01-01 18:00:00' AS TIMESTAMP) \ + "SELECT CAST(timestamp_table.timestamp_col AS TIMESTAMP WITH TIME ZONE) > CAST('2011-01-01 18:00:00' AS TIMESTAMP WITH TIME ZONE) \ FROM (SELECT timestamp_table.timestamp_col FROM (SELECT __source.timestamp_col AS timestamp_col \ FROM datafusion.\"public\".timestamp_table AS __source) AS timestamp_table) AS timestamp_table"); } diff --git a/wren-core/core/src/mdl/utils.rs b/wren-core/core/src/mdl/utils.rs index 6b762e606..770a8c842 100644 --- a/wren-core/core/src/mdl/utils.rs +++ b/wren-core/core/src/mdl/utils.rs @@ -35,7 +35,7 @@ where /// /// For example, a [CompoundIdentifier] with 3 elements: `orders.customer.name` would be represented as `"orders.customer.name"`. pub fn collect_identifiers(expr: &str) -> Result> { - let wrapped = format!("select {}", expr); + let wrapped = format!("select {expr}"); let parsed = match Parser::parse_sql(&GenericDialect {}, &wrapped) { Ok(v) => v, Err(e) => return plan_err!("Error parsing SQL: {}", e), @@ -207,7 +207,7 @@ pub fn quoted_ident(s: &str) -> Ident { #[inline] pub fn quoted(s: &str) -> String { - format!("\"{}\"", s) + format!("\"{s}\"") } /// Transform the column to a datafusion field diff --git a/wren-core/wren-example/examples/calculation-invoke-calculation.rs b/wren-core/wren-example/examples/calculation-invoke-calculation.rs index da12a7c23..ee7876ca0 100644 --- a/wren-core/wren-example/examples/calculation-invoke-calculation.rs +++ b/wren-core/wren-example/examples/calculation-invoke-calculation.rs @@ -88,15 +88,15 @@ async fn main() -> Result<()> { { Ok(sql) => sql, Err(e) => { - eprintln!("Error transforming SQL: {}", e); + eprintln!("Error transforming SQL: {e}"); return Ok(()); } }; - println!("Transformed SQL: {}", transformed); + println!("Transformed SQL: {transformed}"); let df = match ctx.sql(&transformed).await { Ok(df) => df, Err(e) => { - eprintln!("Error executing SQL: {}", e); + eprintln!("Error executing SQL: {e}"); return Ok(()); } }; @@ -114,15 +114,15 @@ async fn main() -> Result<()> { { Ok(sql) => sql, Err(e) => { - eprintln!("Error transforming SQL: {}", e); + eprintln!("Error transforming SQL: {e}"); return Ok(()); } }; - println!("Transformed SQL: {}", transformed); + println!("Transformed SQL: {transformed}"); let df = match ctx.sql(&transformed).await { Ok(df) => df, Err(e) => { - eprintln!("Error executing SQL: {}", e); + eprintln!("Error executing SQL: {e}"); return Ok(()); } }; diff --git a/wren-core/wren-example/examples/datafusion-apply.rs b/wren-core/wren-example/examples/datafusion-apply.rs index 9f95833a3..4cbea2148 100644 --- a/wren-core/wren-example/examples/datafusion-apply.rs +++ b/wren-core/wren-example/examples/datafusion-apply.rs @@ -80,18 +80,18 @@ async fn main() -> Result<()> { let sql = "select * from wrenai.public.order_items"; let sql = transform_sql_with_ctx(&ctx, analyzed_mdl, &[], HashMap::new().into(), sql) .await?; - println!("Wren engine generated SQL: \n{}", sql); + println!("Wren engine generated SQL: \n{sql}"); // create a plan to run a SQL query let df = match ctx.sql(&sql).await { Ok(df) => df, Err(e) => { - eprintln!("Error: {}", e); + eprintln!("Error: {e}"); return Err(e); } }; match df.show().await { Ok(_) => {} - Err(e) => eprintln!("Error: {}", e), + Err(e) => eprintln!("Error: {e}"), } Ok(()) } diff --git a/wren-core/wren-example/examples/plan-sql.rs b/wren-core/wren-example/examples/plan-sql.rs index 71bf9a8a9..82de858cd 100644 --- a/wren-core/wren-example/examples/plan-sql.rs +++ b/wren-core/wren-example/examples/plan-sql.rs @@ -16,7 +16,7 @@ async fn main() -> datafusion::common::Result<()> { )?); let sql = "select customer_state from wrenai.public.orders_model"; - println!("Original SQL: \n{}", sql); + println!("Original SQL: \n{sql}"); let sql = transform_sql_with_ctx( &SessionContext::new(), analyzed_mdl, @@ -25,7 +25,7 @@ async fn main() -> datafusion::common::Result<()> { sql, ) .await?; - println!("Wren engine generated SQL: \n{}", sql); + println!("Wren engine generated SQL: \n{sql}"); Ok(()) } diff --git a/wren-core/wren-example/examples/row-level-access-control.rs b/wren-core/wren-example/examples/row-level-access-control.rs index 32d23f5e9..cc041f535 100644 --- a/wren-core/wren-example/examples/row-level-access-control.rs +++ b/wren-core/wren-example/examples/row-level-access-control.rs @@ -67,7 +67,7 @@ async fn main() -> datafusion::common::Result<()> { ]); let json_str = serde_json::to_string(&manifest).unwrap(); - println!("Manifest JSON: \n{}", json_str); + println!("Manifest JSON: \n{json_str}"); let analyzed_mdl = Arc::new(AnalyzedWrenMDL::analyze_with_tables(manifest, register)?); @@ -116,17 +116,17 @@ async fn main() -> datafusion::common::Result<()> { let df = match ctx.sql(&sql).await { Ok(df) => df, Err(e) => { - eprintln!("Error: {}", e); + eprintln!("Error: {e}"); return Err(e); } }; match df.show().await { Ok(_) => {} - Err(e) => eprintln!("Error: {}", e), + Err(e) => eprintln!("Error: {e}"), } println!("#####################"); - println!("Wren engine generated SQL: \n{}", sql); + println!("Wren engine generated SQL: \n{sql}"); Ok(()) } diff --git a/wren-core/wren-example/examples/to-many-calculation.rs b/wren-core/wren-example/examples/to-many-calculation.rs index cadc7bee0..db1d9a84a 100644 --- a/wren-core/wren-example/examples/to-many-calculation.rs +++ b/wren-core/wren-example/examples/to-many-calculation.rs @@ -84,7 +84,7 @@ async fn main() -> Result<()> { { Ok(df) => df, Err(e) => { - eprintln!("Error executing SQL: {}", e); + eprintln!("Error executing SQL: {e}"); return Ok(()); } }; diff --git a/wren-core/wren-example/examples/view.rs b/wren-core/wren-example/examples/view.rs index bb649cfb9..a2afd061a 100644 --- a/wren-core/wren-example/examples/view.rs +++ b/wren-core/wren-example/examples/view.rs @@ -18,7 +18,7 @@ async fn main() -> datafusion::common::Result<()> { )?); let sql = "select * from wrenai.public.customers_view"; - println!("Original SQL: \n{}", sql); + println!("Original SQL: \n{sql}"); let sql = transform_sql_with_ctx( &SessionContext::new(), analyzed_mdl, @@ -27,7 +27,7 @@ async fn main() -> datafusion::common::Result<()> { sql, ) .await?; - println!("Wren engine generated SQL: \n{}", sql); + println!("Wren engine generated SQL: \n{sql}"); Ok(()) }