Files
Joe SpadolaandGitHub 19ffd53f86 Implement native async client (#617)
* implement native async client

* add pylint exceptions

* linting

* improve test config setup

* fix some tests

* enable log tables on startup

* improve error handling

* linting

* fix deprecation warning in aiohttp

* error handling

* separate sync/async stream failure tests

* more error handling issues

* linting

* test updates and improvements

* test adjustments

* increase async stream buffer size

* don't parallelize cloud tests

* apply consistency settings to client factory fixture

* up cloud par to 4

* enforce sequential consistency in helper

* update enable_cleanup_closed vers

* add conn abort to wrong port assertion

* small test refactor

* more consistency

* add deadlock check in async queue

* enforce consistent use of async iter stream consumption

* linting

* DO materialize non-streaming queries

* accept either concurrent session error

* update async client to always reset context

* add pipelining to arrow methods

* feature parity with recent sync client changes

* fix jwt test after bad merge conflict resolution

* 0.12.0rc1 release prep

* fix assertion in racy test

* clean up streaming source on exception

* linting

* more linting

* deprecate legacy async client and replace with async native

* update changelog
2026-03-26 15:56:18 -07:00

194 lines
7.4 KiB
Python

import pytest
import sqlalchemy as db
from sqlalchemy import MetaData, delete, text
from sqlalchemy.engine import Engine
from sqlalchemy.exc import CompileError
from clickhouse_connect.cc_sqlalchemy.datatypes.sqltypes import String, UInt64
from clickhouse_connect.cc_sqlalchemy.ddl.tableengine import engine_map
from tests.integration_tests.test_sqlalchemy.conftest import verify_tables_ready
def test_delete_with_table_object(test_engine: Engine, test_db: str, test_table_engine: str):
"""DELETE using SQLAlchemy Table object"""
engine_cls = engine_map[test_table_engine]
metadata = MetaData(schema=test_db)
test_table = db.Table(
"delete_test",
metadata,
db.Column("id", UInt64),
db.Column("name", String),
db.Column("status", String),
engine_cls("id"),
)
with test_engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS delete_test"))
test_table.create(conn)
conn.execute(db.insert(test_table).values({"id": 1, "name": "hello world", "status": "active"}))
conn.execute(db.insert(test_table).values({"id": 2, "name": "test data", "status": "inactive"}))
conn.execute(db.insert(test_table).values({"id": 3, "name": "hello test", "status": "active"}))
starting = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(starting) == 3
assert [row.id for row in starting] == [1, 2, 3]
delete_stmt = delete(test_table).where(test_table.c.name.like("%hello%"))
conn.execute(delete_stmt)
remaining = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(remaining) == 1
assert remaining[0].name == "test data"
def test_delete_with_multiple_conditions(test_engine: Engine, test_db: str, test_table_engine: str):
"""DELETE with multiple WHERE conditions"""
engine_cls = engine_map[test_table_engine]
metadata = MetaData(schema=test_db)
test_table = db.Table(
"delete_multi_test",
metadata,
db.Column("id", UInt64),
db.Column("category", String),
db.Column("value", UInt64),
engine_cls("id"),
)
with test_engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS delete_multi_test"))
test_table.create(conn)
conn.execute(db.insert(test_table).values({"id": 1, "category": "A", "value": 100}))
conn.execute(db.insert(test_table).values({"id": 2, "category": "B", "value": 200}))
conn.execute(db.insert(test_table).values({"id": 3, "category": "A", "value": 300}))
conn.execute(db.insert(test_table).values({"id": 4, "category": "C", "value": 50}))
starting = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(starting) == 4
assert [row.id for row in starting] == [1, 2, 3, 4]
delete_stmt = delete(test_table).where((test_table.c.category == "A") & (test_table.c.value > 150))
conn.execute(delete_stmt)
remaining = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(remaining) == 3
assert [row.id for row in remaining] == [1, 2, 4]
def test_delete_all_rows_error(test_engine: Engine, test_db: str, test_table_engine: str):
"""DELETE without WHERE should raise"""
engine_cls = engine_map[test_table_engine]
metadata = MetaData(schema=test_db)
test_table = db.Table(
"delete_all_test",
metadata,
db.Column("id", UInt64),
db.Column("data", String),
engine_cls("id"),
)
with test_engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS delete_all_test"))
test_table.create(conn)
conn.execute(db.insert(test_table).values({"id": 1, "data": "test1"}))
delete_stmt = delete(test_table)
with pytest.raises(CompileError, match="require a WHERE clause"):
conn.execute(delete_stmt)
def test_delete_basic_functionality(test_engine: Engine, test_table_engine: str):
"""Basic DELETE statement compilation and execution"""
engine_cls = engine_map[test_table_engine]
metadata = MetaData()
test_table = db.Table(
"delete_simple_test",
metadata,
db.Column("id", UInt64),
db.Column("name", String),
engine_cls("id"),
)
with test_engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS delete_simple_test"))
test_table.create(conn)
conn.execute(db.insert(test_table).values({"id": 1, "name": "test_row"}))
starting = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(starting) == 1
assert starting[0].name == "test_row"
delete_stmt = delete(test_table).where(test_table.c.id == 1)
conn.execute(delete_stmt)
result = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(result) == 0
def test_delete_with_text_condition(test_engine: Engine, test_table_engine: str):
"""Test DELETE with text-based WHERE condition"""
engine_cls = engine_map[test_table_engine]
metadata = MetaData()
test_table = db.Table(
"delete_text_test",
metadata,
db.Column("id", UInt64),
db.Column("status", String),
engine_cls("id"),
)
with test_engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS delete_text_test"))
test_table.create(conn)
conn.execute(db.insert(test_table).values({"id": 1, "status": "active"}))
conn.execute(db.insert(test_table).values({"id": 2, "status": "inactive"}))
conn.execute(db.insert(test_table).values({"id": 3, "status": "active"}))
starting = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(starting) == 3
assert [row.id for row in starting] == [1, 2, 3]
delete_stmt = delete(test_table).where(test_table.c.status == "inactive")
result = conn.execute(delete_stmt)
result = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(result) == 2
assert [row.id for row in result] == [1, 3]
def test_explicit_delete(test_engine: Engine, test_table_engine: str):
"""Test explicit DELETE"""
engine_cls = engine_map[test_table_engine]
metadata = MetaData()
test_table = db.Table(
"delete_explicit_test",
metadata,
db.Column("id", UInt64),
db.Column("name", String),
engine_cls("id"),
)
with test_engine.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS delete_explicit_test"))
test_table.create(conn)
conn.execute(db.insert(test_table).values({"id": 1, "name": "hello world"}))
conn.execute(db.insert(test_table).values({"id": 2, "name": "test data"}))
conn.execute(db.insert(test_table).values({"id": 3, "name": "hello test"}))
# Wait for inserts to complete in cloud environments
verify_tables_ready(conn, {"delete_explicit_test": 3})
starting = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(starting) == 3
assert [row.id for row in starting] == [1, 2, 3]
conn.execute(text("DELETE FROM delete_explicit_test WHERE name LIKE '%hello%'"))
result = conn.execute(db.select(test_table).order_by(test_table.c.id)).fetchall()
assert len(result) == 1
assert result[0].name == "test data"