Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/packaging_wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
uv export --only-group test --no-emit-project --output-file pylock.toml --directory {project} &&
uv pip install -r pylock.toml
CIBW_TEST_COMMAND: >
uv run -v pytest ${{ inputs.testsuite == 'fast' && './tests/fast' || './tests' }} --verbose --ignore=./tests/stubs
uv run -v pytest -n 2 ${{ inputs.testsuite == 'fast' && './tests/fast' || './tests' }} --verbose --ignore=./tests/stubs

steps:
- name: Checkout DuckDB Python
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ test = [ # dependencies used for running tests
"pytest",
"pytest-reraise",
"pytest-timeout",
"pytest-xdist", # multi-processed tests, if `-n <num_workers> | auto`
"pytest-randomly", # randomizes test order to ensure no test dependencies, enabled on install
"mypy",
"coverage",
"gcovr; python_version < '3.14'",
Expand Down Expand Up @@ -306,6 +308,7 @@ filterwarnings = [
"ignore:distutils Version classes are deprecated:DeprecationWarning",
"ignore:is_datetime64tz_dtype is deprecated:DeprecationWarning",
]
timeout = 600 # don't let individual tests "hang"

[tool.coverage.run]
branch = true
Expand Down
16 changes: 12 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,18 @@ def spark():


@pytest.fixture(scope='function')
def duckdb_cursor():
connection = duckdb.connect('')
yield connection
connection.close()
def duckdb_cursor(tmp_path):
with duckdb.connect(tmp_path / "mytest") as connection:
yield connection


@pytest.fixture(scope='function')
def default_con():
# ensures each test uses a fresh default connection to avoid test leakage
# threading_unsafe fixture
duckdb.default_connection().close()
with duckdb.default_connection() as conn:
yield conn


@pytest.fixture(scope='function')
Expand Down
119 changes: 59 additions & 60 deletions tests/fast/api/test_duckdb_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,23 @@ def tmp_database(tmp_path_factory):
# wrapped by the 'duckdb' module, to execute with the 'default_connection'
class TestDuckDBConnection(object):
@pytest.mark.parametrize('pandas', [NumpyPandas(), ArrowPandas()])
def test_append(self, pandas):
duckdb.execute("Create table integers (i integer)")
def test_append(self, pandas, default_con):
default_con.execute("Create table integers (i integer)")
df_in = pandas.DataFrame(
{
'numbers': [1, 2, 3, 4, 5],
}
)
duckdb.append('integers', df_in)
assert duckdb.execute('select count(*) from integers').fetchone()[0] == 5
default_con.append('integers', df_in)
assert default_con.execute('select count(*) from integers').fetchone()[0] == 5
# cleanup
duckdb.execute("drop table integers")
default_con.execute("drop table integers")

def test_default_connection_from_connect(self):
duckdb.sql('create or replace table connect_default_connect (i integer)')
def test_default_connection_from_connect(self, default_con):
default_con.sql('create or replace table connect_default_connect (i integer)')
con = duckdb.connect(':default:')
con.sql('select i from connect_default_connect')
duckdb.sql('drop table connect_default_connect')
default_con.sql('drop table connect_default_connect')
with pytest.raises(duckdb.Error):
con.sql('select i from connect_default_connect')

Expand All @@ -57,31 +57,31 @@ def test_arrow(self):

def test_begin_commit(self):
duckdb.begin()
duckdb.execute("create table tbl as select 1")
duckdb.execute("create table tbl_1 as select 1")
duckdb.commit()
res = duckdb.table("tbl")
duckdb.execute("drop table tbl")
res = duckdb.table("tbl_1")
duckdb.execute("drop table tbl_1")

def test_begin_rollback(self):
duckdb.begin()
duckdb.execute("create table tbl as select 1")
duckdb.rollback()
def test_begin_rollback(self, default_con):
default_con.begin()
default_con.execute("create table tbl_1rb as select 1")
default_con.rollback()
with pytest.raises(duckdb.CatalogException):
# Table does not exist
res = duckdb.table("tbl")
res = default_con.table("tbl_1rb")

def test_cursor(self):
duckdb.execute("create table tbl as select 3")
def test_cursor(self, default_con):
default_con.execute("create table tbl_3 as select 3")
duckdb_cursor = duckdb.cursor()
res = duckdb_cursor.table("tbl").fetchall()
res = duckdb_cursor.table("tbl_3").fetchall()
assert res == [(3,)]
duckdb_cursor.execute("drop table tbl")
duckdb_cursor.execute("drop table tbl_3")
with pytest.raises(duckdb.CatalogException):
# 'tbl' no longer exists
duckdb.table("tbl")
default_con.table("tbl_3")

def test_cursor_lifetime(self):
con = duckdb.connect()
def test_cursor_lifetime(self, default_con):
con = default_con

def use_cursors():
cursors = []
Expand All @@ -103,12 +103,12 @@ def test_df(self):
assert res == ref

def test_duplicate(self):
duckdb.execute("create table tbl as select 5")
duckdb.execute("create table tbl_5 as select 5")
dup_conn = duckdb.duplicate()
dup_conn.table("tbl").fetchall()
duckdb.execute("drop table tbl")
dup_conn.table("tbl_5").fetchall()
duckdb.execute("drop table tbl_5")
with pytest.raises(duckdb.CatalogException):
dup_conn.table("tbl").fetchall()
dup_conn.table("tbl_5").fetchall()

def test_readonly_properties(self):
duckdb.execute("select 42")
Expand All @@ -123,11 +123,11 @@ def test_execute(self):
def test_executemany(self):
# executemany does not keep an open result set
# TODO: shouldn't we also have a version that executes a query multiple times with different parameters, returning all of the results?
duckdb.execute("create table tbl (i integer, j varchar)")
duckdb.executemany("insert into tbl VALUES (?, ?)", [(5, 'test'), (2, 'duck'), (42, 'quack')])
res = duckdb.table("tbl").fetchall()
duckdb.execute("create table tbl_many (i integer, j varchar)")
duckdb.executemany("insert into tbl_many VALUES (?, ?)", [(5, 'test'), (2, 'duck'), (42, 'quack')])
res = duckdb.table("tbl_many").fetchall()
assert res == [(5, 'test'), (2, 'duck'), (42, 'quack')]
duckdb.execute("drop table tbl")
duckdb.execute("drop table tbl_many")

def test_pystatement(self):
with pytest.raises(duckdb.ParserException, match='seledct'):
Expand Down Expand Up @@ -163,8 +163,8 @@ def test_pystatement(self):
duckdb.execute(statements[0])
assert duckdb.execute(statements[0], {'1': 42}).fetchall() == [(42,)]

duckdb.execute("create table tbl(a integer)")
statements = duckdb.extract_statements('insert into tbl select $1')
duckdb.execute("create table tbl_a(a integer)")
statements = duckdb.extract_statements('insert into tbl_a select $1')
assert statements[0].expected_result_type == [
duckdb.ExpectedResultType.CHANGED_ROWS,
duckdb.ExpectedResultType.QUERY_RESULT,
Expand All @@ -174,23 +174,23 @@ def test_pystatement(self):
):
duckdb.executemany(statements[0])
duckdb.executemany(statements[0], [(21,), (22,), (23,)])
assert duckdb.table('tbl').fetchall() == [(21,), (22,), (23,)]
duckdb.execute("drop table tbl")
assert duckdb.table('tbl_a').fetchall() == [(21,), (22,), (23,)]
duckdb.execute("drop table tbl_a")

def test_fetch_arrow_table(self):
# Needed for 'fetch_arrow_table'
pyarrow = pytest.importorskip("pyarrow")

duckdb.execute("Create Table test (a integer)")
duckdb.execute("Create Table test_arrow_tble (a integer)")

for i in range(1024):
for j in range(2):
duckdb.execute("Insert Into test values ('" + str(i) + "')")
duckdb.execute("Insert Into test values ('5000')")
duckdb.execute("Insert Into test values ('6000')")
duckdb.execute("Insert Into test_arrow_tble values ('" + str(i) + "')")
duckdb.execute("Insert Into test_arrow_tble values ('5000')")
duckdb.execute("Insert Into test_arrow_tble values ('6000')")
sql = '''
SELECT a, COUNT(*) AS repetitions
FROM test
FROM test_arrow_tble
GROUP BY a
'''

Expand All @@ -200,7 +200,7 @@ def test_fetch_arrow_table(self):

arrow_df = arrow_table.to_pandas()
assert result_df['repetitions'].sum() == arrow_df['repetitions'].sum()
duckdb.execute("drop table test")
duckdb.execute("drop table test_arrow_tble")

def test_fetch_df(self):
ref = [([1, 2, 3],)]
Expand All @@ -210,22 +210,22 @@ def test_fetch_df(self):
assert res == ref

def test_fetch_df_chunk(self):
duckdb.execute("CREATE table t as select range a from range(3000);")
query = duckdb.execute("SELECT a FROM t")
duckdb.execute("CREATE table t_df_chunk as select range a from range(3000);")
query = duckdb.execute("SELECT a FROM t_df_chunk")
cur_chunk = query.fetch_df_chunk()
assert cur_chunk['a'][0] == 0
assert len(cur_chunk) == 2048
cur_chunk = query.fetch_df_chunk()
assert cur_chunk['a'][0] == 2048
assert len(cur_chunk) == 952
duckdb.execute("DROP TABLE t")
duckdb.execute("DROP TABLE t_df_chunk")

def test_fetch_record_batch(self):
# Needed for 'fetch_arrow_table'
pyarrow = pytest.importorskip("pyarrow")

duckdb.execute("CREATE table t as select range a from range(3000);")
duckdb.execute("SELECT a FROM t")
duckdb.execute("CREATE table t_record_batch as select range a from range(3000);")
duckdb.execute("SELECT a FROM t_record_batch")
record_batch_reader = duckdb.fetch_record_batch(1024)
chunk = record_batch_reader.read_all()
assert len(chunk) == 3000
Expand Down Expand Up @@ -286,13 +286,13 @@ def test_query(self):
def test_register(self):
assert None != duckdb.register

def test_register_relation(self):
con = duckdb.connect()
def test_register_relation(self, default_con):
con = default_con
rel = con.sql('select [5,4,3]')
con.register("relation", rel)
con.register("relation_rr", rel)

con.sql("create table tbl as select * from relation")
assert con.table('tbl').fetchall() == [([5, 4, 3],)]
con.sql("create table tbl_reg_rel as select * from relation_rr")
assert con.table('tbl_reg_rel').fetchall() == [([5, 4, 3],)]

def test_unregister_problematic_behavior(self, duckdb_cursor):
# We have a VIEW called 'vw' in the Catalog
Expand All @@ -314,10 +314,10 @@ def test_unregister_problematic_behavior(self, duckdb_cursor):
assert duckdb_cursor.execute("select * from vw").fetchone() == (0,)

@pytest.mark.parametrize('pandas', [NumpyPandas(), ArrowPandas()])
def test_relation_out_of_scope(self, pandas):
def test_relation_out_of_scope(self, pandas, default_con):
def temporary_scope():
# Create a connection, we will return this
con = duckdb.connect()
con = default_con
# Create a dataframe
df = pandas.DataFrame({'a': [1, 2, 3]})
# The dataframe has to be registered as well
Expand All @@ -333,8 +333,8 @@ def temporary_scope():

def test_table(self):
con = duckdb.connect()
con.execute("create table tbl as select 1")
assert [(1,)] == con.table("tbl").fetchall()
con.execute("create table tbl_test_table as select 1")
assert [(1,)] == con.table("tbl_test_table").fetchall()

def test_table_function(self):
assert None != duckdb.table_function
Expand All @@ -356,16 +356,15 @@ def test_close(self):
def test_interrupt(self):
assert None != duckdb.interrupt

def test_wrap_shadowing(self):
def test_wrap_shadowing(self, default_con):
pd = NumpyPandas()
import duckdb

df = pd.DataFrame({"a": [1, 2, 3]})
res = duckdb.sql("from df").fetchall()
res = default_con.sql("from df").fetchall()
assert res == [(1,), (2,), (3,)]

def test_wrap_coverage(self):
con = duckdb.default_connection
def test_wrap_coverage(self, default_con):
con = default_con

# Skip all of the initial __xxxx__ methods
connection_methods = dir(con)
Expand Down
20 changes: 8 additions & 12 deletions tests/fast/api/test_query_interrupt.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,31 @@
import duckdb
import time
import pytest

import platform
import threading
import _thread as thread


def send_keyboard_interrupt():
# Wait a little, so we're sure the 'execute' has started
time.sleep(0.1)
# Send an interrupt to the main thread
thread.interrupt_main()


class TestQueryInterruption(object):

@pytest.mark.xfail(
condition=platform.system() == "Emscripten",
reason="Emscripten builds cannot use threads",
)
def test_query_interruption(self):
@pytest.mark.timeout(15)
def test_keyboard_interruption(self):
con = duckdb.connect()
thread = threading.Thread(target=send_keyboard_interrupt)
# Start the thread
thread.start()
try:
res = con.execute('select count(*) from range(100000000000)').fetchall()
except RuntimeError:
# If this is not reached, we could not cancel the query before it completed
# indicating that the query interruption functionality is broken
assert True
except KeyboardInterrupt:
pytest.fail()
thread.join()
with pytest.raises((KeyboardInterrupt, RuntimeError)):
res = con.execute('select * from range(100000) t1,range(100000) t2').fetchall()
finally:
# Ensure the thread completes regardless of what happens
thread.join()
Loading