11from __future__ import annotations
22
3+ import contextlib
34from functools import reduce
45from operator import and_
56from typing import TYPE_CHECKING
2122from narwhals .exceptions import ColumnNotFoundError
2223from narwhals .exceptions import InvalidOperationError
2324from narwhals .typing import CompliantDataFrame
25+ from narwhals .typing import CompliantLazyFrame
2426from narwhals .utils import Implementation
2527from narwhals .utils import Version
2628from narwhals .utils import generate_temporary_column_name
4850 from narwhals .typing import LazyUniqueKeepStrategy
4951 from narwhals .utils import _FullContext
5052
51- from narwhals .typing import CompliantLazyFrame
53+ with contextlib .suppress (ImportError ): # requires duckdb>=1.3.0
54+ from duckdb import SQLExpression # type: ignore[attr-defined, unused-ignore]
5255
5356
5457class DuckDBLazyFrame (CompliantLazyFrame ["DuckDBExpr" , "duckdb.DuckDBPyRelation" ]):
@@ -338,7 +341,7 @@ def join_asof(
338341 ):
339342 select .append (f'rhs."{ name } " as "{ name } { suffix } "' )
340343 elif right_on is None or name not in {right_on , * by_right }:
341- select .append (f'" { name } "' )
344+ select .append (str ( col ( name )) )
342345 # Replace with Python API call once
343346 # https://github.com/duckdb/duckdb/discussions/16947 is addressed.
344347 query = f"""
@@ -359,23 +362,28 @@ def unique(
359362 self , subset : Sequence [str ] | None , * , keep : LazyUniqueKeepStrategy
360363 ) -> Self :
361364 if subset_ := subset if keep == "any" else (subset or self .columns ):
365+ if self ._backend_version < (1 , 3 ):
366+ msg = (
367+ "At least version 1.3 of DuckDB is required for `unique` operation\n "
368+ "with `subset` specified."
369+ )
370+ raise NotImplementedError (msg )
362371 # Sanitise input
363372 if any (x not in self .columns for x in subset_ ):
364373 msg = f"Columns { set (subset_ ).difference (self .columns )} not found in { self .columns } ."
365374 raise ColumnNotFoundError (msg )
366375 idx_name = generate_temporary_column_name (8 , self .columns )
367376 count_name = generate_temporary_column_name (8 , [* self .columns , idx_name ])
368377 partition_by_sql = generate_partition_by_sql (* (subset_ ))
369- rel = self .native # noqa: F841
370- query = f"""
371- select *,
372- row_number() over ({ partition_by_sql } ) as "{ idx_name } ",
373- count(*) over ({ partition_by_sql } ) as "{ count_name } "
374- from rel
375- """ # noqa: S608
376378 name = count_name if keep == "none" else idx_name
379+ idx_expr = SQLExpression (
380+ f"{ FunctionExpression ('row_number' )} over ({ partition_by_sql } )"
381+ ).alias (idx_name )
382+ count_expr = SQLExpression (
383+ f"{ FunctionExpression ('count' , StarExpression ())} over ({ partition_by_sql } )"
384+ ).alias (count_name )
377385 return self ._with_native (
378- duckdb . sql ( query )
386+ self . native . select ( StarExpression (), idx_expr , count_expr )
379387 .filter (col (name ) == lit (1 ))
380388 .select (StarExpression (exclude = [count_name , idx_name ]))
381389 )
@@ -465,7 +473,7 @@ def unpivot(
465473 msg = "`value_name` cannot be empty string for duckdb backend."
466474 raise NotImplementedError (msg )
467475
468- unpivot_on = ", " .join (f'" { name } "' for name in on_ )
476+ unpivot_on = ", " .join (str ( col ( name )) for name in on_ )
469477 rel = self .native # noqa: F841
470478 # Replace with Python API once
471479 # https://github.com/duckdb/duckdb/discussions/16980 is addressed.
0 commit comments