Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/actions/build-wheel/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ runs:
CIBW_BEFORE_TEST: >
uv sync --active --inexact --only-group test --no-install-project --directory {project}
CIBW_TEST_COMMAND: >
uv run -v pytest --confcutdir=. --rootdir . -c {project}/pyproject.toml ${{ inputs.testsuite == 'fast' && './tests/fast' || './tests' }}
uv run python -c "import _duckdb; assert not _duckdb._assertions, 'the wheel was built with assertions'"
&& uv run -v pytest --confcutdir=. --rootdir . -c {project}/pyproject.toml ${{ inputs.testsuite == 'fast' && './tests/fast' || './tests' }}

# The stable ABI wheel must import nothing outside the stable ABI, which compiling with Py_LIMITED_API does
# not prove for symbols pulled in by inline functions or dependencies.
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/assert_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ jobs:
import _duckdb
print(_duckdb.__file__)
assert "abi3" not in _duckdb.__file__, "expected a full-API module, got " + _duckdb.__file__
assert _duckdb._assertions, "expected a build with assertions, which keeps nanobind's exit leak report"
EOF

- name: Run the tests
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/packaging_sdist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ jobs:
cd ${{ runner.temp }}
uv venv
uv pip install ${{ github.workspace }}/dist/duckdb-*.tar.gz
# an sdist install is a build without assertions, which keeps nanobind's exit leak report off
uv run python -c "import _duckdb; assert not _duckdb._assertions, 'the sdist was built with assertions'"

- name: Test sdist
if: ${{ inputs.testsuite != 'none' }}
Expand Down
12 changes: 11 additions & 1 deletion .github/workflows/packaging_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,17 @@ jobs:
testrun=$(uv run --no-project --python "$venv" python -c 'import tempfile; print(tempfile.mkdtemp(prefix="duckdb-testrun-"))')
cp -r tests pyproject.toml "$testrun"
cd "$testrun"
uv run --no-project --python "$venv" python -c "import duckdb, sys, os; print(duckdb.__version__, sys.version, os.getcwd())"
# A wheel built with assertions would print nanobind's exit leak report to every user.
uv run --no-project --python "$venv" python - <<'EOF'
import os
import sys

import _duckdb
import duckdb

print(duckdb.__version__, sys.version, os.getcwd())
assert not _duckdb._assertions, "the wheel was built with assertions"
EOF
uv run --no-project --python "$venv" pytest --confcutdir=. --rootdir . -c pyproject.toml ${{ inputs.testsuite == 'fast' && './tests/fast' || './tests' }}

test_pandas_2:
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ target_include_directories(
# link time. No-op on non-Windows.
target_compile_definitions(_duckdb_dependencies INTERFACE DUCKDB_STATIC_BUILD)

# NB_DOMAIN gives the module nanobind state of its own: the exit leak report
# flag, the type registry and the exception translators are otherwise shared
# with every nanobind extension in the process that has the same ABI key. It
# must reach the object library that expands NB_MODULE, so it sits here and not
# on nanobind_add_module.
target_compile_definitions(_duckdb_dependencies INTERFACE NB_DOMAIN=duckdb)

foreach(flag IN LISTS DUCKDB_PY_ARCHITECTURE_FLAGS)
target_compile_options(_duckdb_dependencies
INTERFACE "$<$<CONFIG:Release>:${flag}>")
Expand Down
2 changes: 2 additions & 0 deletions _duckdb-stubs/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ def string_type(collation: str = "", *, connection: DuckDBPyConnection | None =
def struct_type(fields: IntoFields, *, connection: DuckDBPyConnection | None = None) -> sqltypes.DuckDBPyType: ...
def tf(*, connection: DuckDBPyConnection | None = None) -> dict[str, typing.Any]: ...
def _set_arrow_sources(sources: object) -> None: ...
def _set_leak_report(enabled: bool) -> None: ...
def _literal_type(value: object) -> str: ...
def _value_literal(value: object) -> str: ...
def _collation(type: sqltypes.DuckDBPyType) -> str | None: ...
Expand Down Expand Up @@ -776,6 +777,7 @@ __interactive__: bool
__jupyter__: bool
__standard_vector_size__: int
__version__: str
_assertions: bool
_clean_default_connection: typing.Any # value = <capsule object>
apilevel: str
paramstyle: str
Expand Down
10 changes: 10 additions & 0 deletions duckdb/experimental/spark/sql/_type_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""The DuckDBPyType of each spark type text that needs a parse, parsed once: the singletons' names and unnamed structs.

The types module cannot keep them itself: its dict outlives finalization through the functions that hold it, and
nanobind would report every type in it as leaked. This module defines no function, so its dict is cleared with the
module at finalization.
"""

from duckdb.sqltypes import DuckDBPyType

TYPES: dict[str, DuckDBPyType] = {}
15 changes: 9 additions & 6 deletions duckdb/experimental/spark/sql/column.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
FunctionExpression,
LambdaExpression,
SQLExpression,
sqltypes,
)
from duckdb.sqltypes import BLOB, DOUBLE, TIMESTAMP_TZ, VARCHAR, DuckDBPyType
from duckdb.sqltypes import DuckDBPyType

__all__ = ["Column"]

Expand Down Expand Up @@ -359,14 +360,14 @@ def _constant(value: Any) -> Expression: # noqa: ANN401
# ConstantExpression converts a float NaN to NULL, because Python value conversion treats NaN as missing data,
# also inside a list, so lists are built from their elements.
if isinstance(value, float) and math.isnan(value):
return ConstantExpression("NaN").cast(DOUBLE)
return ConstantExpression("NaN").cast(sqltypes.DOUBLE)
if isinstance(value, (list, tuple)):
return FunctionExpression("list_value", *(_constant(v) for v in value))
if isinstance(value, dict):
msg = f"[UNSUPPORTED_FEATURE.LITERAL_TYPE] The feature is not supported: Literal for {value!r} of class dict."
raise SparkRuntimeException(message=msg)
if isinstance(value, datetime.datetime) and value.tzinfo is None:
return ConstantExpression(value).cast(TIMESTAMP_TZ)
return ConstantExpression(value).cast(sqltypes.TIMESTAMP_TZ)
return ConstantExpression(value)


Expand Down Expand Up @@ -421,7 +422,7 @@ def _truncate(value: Expression, target: DuckDBPyType) -> Expression:
"""
source_type = FunctionExpression("typeof", value)
is_string = source_type == ConstantExpression("VARCHAR")
text = value.cast(VARCHAR)
text = value.cast(sqltypes.VARCHAR)
is_number = FunctionExpression(
"regexp_full_match", text, ConstantExpression(r"\s*[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)\s*")
)
Expand All @@ -433,7 +434,7 @@ def _truncate(value: Expression, target: DuckDBPyType) -> Expression:
f'\' of the type "STRING" cannot be cast to "{_sql_type_name(target)}" because it is malformed.'
),
)
as_double = value.cast(DOUBLE)
as_double = value.cast(sqltypes.DOUBLE)
truncated = FunctionExpression("trunc", as_double)
# A decimal or a string can have a fraction that its nearest double drops, so its whole part is cut from its text;
# the leading 0 keeps a bare sign or an empty whole part a valid integer.
Expand All @@ -454,7 +455,9 @@ def _truncate(value: Expression, target: DuckDBPyType) -> Expression:
def _to_binary(value: Expression) -> Expression:
"""Cast to binary like Spark, which takes a string's UTF-8 bytes where DuckDB reads escape sequences."""
is_string = FunctionExpression("typeof", value) == ConstantExpression("VARCHAR")
return CaseExpression(is_string, FunctionExpression("encode", value.cast(VARCHAR))).otherwise(value.cast(BLOB))
return CaseExpression(is_string, FunctionExpression("encode", value.cast(sqltypes.VARCHAR))).otherwise(
value.cast(sqltypes.BLOB)
)


def _children(dtype: DuckDBPyType) -> list[DuckDBPyType]:
Expand Down
Loading
Loading