confluent-sql 0.5.1__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/CHANGELOG.md +12 -1
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/PKG-INFO +1 -1
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/pyproject.toml +1 -1
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/cursor.py +3 -2
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/statement.py +44 -6
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/integration/test_fetch.py +128 -1
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_cursor_unit.py +75 -1
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_statement_unit.py +73 -3
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/uv.lock +1 -1
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/.github/CODEOWNERS +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/.gitignore +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/.semaphore/publish_to_pypi.yml +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/.semaphore/semaphore.yml +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/ARCHITECTURE.md +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/CLAUDE.md +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/DBAPI_EXTENSIONS.md +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/LICENSE.txt +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/Makefile +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/README.md +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/STREAMING.md +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/TYPES.md +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/byoidc_bearer_token_example.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/errors.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/simple_append_only_streaming_query_example.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/statement_properties_example.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/tableflow_lifecycle_example.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/service.yml +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/__init__.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/__version__.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/auth.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/changelog_compressor.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/connection.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/connectors.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/exceptions.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/execution_mode.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/polling.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/result_readers.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/retry.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/statement_properties.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/tableflow.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/src/confluent_sql/types.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/__init__.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/conftest.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/integration/conftest.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/integration/test_connection.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/integration/test_connector.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/integration/test_cursor.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/integration/test_tableflow.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/conftest.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_auth_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_changelog_compressor_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_changelog_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_connection_byoidc_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_connection_connector_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_connection_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_connection_unit_properties.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_connectors_connection_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_connectors_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_execution_mode_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_polling_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_result_readers_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_retry_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_statement_properties_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_tableflow_connection_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_tableflow_unit.py +0 -0
- {confluent_sql-0.5.1 → confluent_sql-0.5.3}/tests/unit/test_types_unit.py +0 -0
|
@@ -2,7 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this dbapi driver will be documented in this file.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 0.5.3, 2026-08-27
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `Statement.from_response()` no longer raises `OperationalError` for a `PENDING` statement with no `status.traits`, which can possibly happen. (#194)
|
|
10
|
+
|
|
11
|
+
## 0.5.2, 2026-08-26
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- `Statement.is_pure_ddl` / `_PURE_DDL_KINDS` now include `CREATE_MATERIALIZED_TABLE`, `CREATE_OR_ALTER_MATERIALIZED_TABLE`, `ALTER_MATERIALIZED_TABLE` (the bare, query-evolving form), and `DROP_MATERIALIZED_TABLE`.
|
|
16
|
+
- Previously these kinds weren't classified as pure or impure DDL, so in **streaming** execution mode, `Statement.can_fetch_results` fell through to the "ready when RUNNING" branch used for CTAS -- meaning a streaming-mode `Cursor.execute()` (and therefore `Connection.execute_streaming_ddl()`) on one of these statements could in principle return control as soon as the statement was RUNNING rather than waiting for it to reach a terminal phase, the same way `execute_streaming_ddl()` correctly waits out a genuinely perpetual CTAS job's RUNNING phase. Snapshot execution mode (the default, and what `execute_snapshot_ddl()` always uses) was unaffected -- it already waits for a terminal phase regardless of `is_pure_ddl`.
|
|
6
17
|
|
|
7
18
|
## 0.5.1, 2026-08-25
|
|
8
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: confluent-sql
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: DB-API v2 compliant driver for Confluent Cloud Flink SQL
|
|
5
5
|
Project-URL: Repository, https://github.com/confluentinc/confluent-sql
|
|
6
6
|
Project-URL: Documentation, https://github.com/confluentinc/confluent-sql?tab=readme-ov-file#confluent-sql
|
|
@@ -259,8 +259,9 @@ class Cursor:
|
|
|
259
259
|
|
|
260
260
|
if self._statement.is_failed:
|
|
261
261
|
raise OperationalError(
|
|
262
|
-
f"Statement
|
|
263
|
-
|
|
262
|
+
f"Statement '{self._statement.name}' submission failed:"
|
|
263
|
+
f" {self._statement.status.get('detail', '')}"
|
|
264
|
+
)
|
|
264
265
|
|
|
265
266
|
# ... and wait for it to be "ready" (either in a terminal state or running) based on
|
|
266
267
|
# execution mode and statement type.
|
|
@@ -170,12 +170,30 @@ class Statement:
|
|
|
170
170
|
"""Represents a Confluent SQL statement, including its metadata, spec, status,
|
|
171
171
|
and parsed traits such as schema, sql kind, etc."""
|
|
172
172
|
|
|
173
|
-
# SQL kinds that represent pure DDL statements (create/modify schema objects)
|
|
173
|
+
# SQL kinds that represent pure DDL statements (create/modify schema objects) which should
|
|
174
|
+
# be waiting for a terminal state before the created/modified objects are usable.
|
|
174
175
|
_PURE_DDL_KINDS = frozenset(
|
|
175
|
-
{
|
|
176
|
+
{
|
|
177
|
+
"CREATE_TABLE",
|
|
178
|
+
"DROP_TABLE",
|
|
179
|
+
"CREATE_VIEW",
|
|
180
|
+
"DROP_VIEW",
|
|
181
|
+
"ALTER_TABLE",
|
|
182
|
+
# The four MATERIALIZED_TABLE kinds stretch "pure" a little: unlike the other
|
|
183
|
+
# members, CREATE_MATERIALIZED_TABLE, CREATE_OR_ALTER_MATERIALIZED_TABLE, and
|
|
184
|
+
# ALTER_MATERIALIZED_TABLE (a query-evolving ALTER, not just a metadata-only one)
|
|
185
|
+
# also kick off (or redeploy) a persistent background refresh job that keeps running
|
|
186
|
+
# long after this statement settles. But they still will reliably reach terminal
|
|
187
|
+
# COMPLETED (or FAILED) phase independent of the background job, which is the one
|
|
188
|
+
# thing this property actually gates.
|
|
189
|
+
"CREATE_MATERIALIZED_TABLE",
|
|
190
|
+
"CREATE_OR_ALTER_MATERIALIZED_TABLE",
|
|
191
|
+
"ALTER_MATERIALIZED_TABLE",
|
|
192
|
+
"DROP_MATERIALIZED_TABLE",
|
|
193
|
+
}
|
|
176
194
|
)
|
|
177
195
|
|
|
178
|
-
# SQL kinds that represent impure DDL (produce no result set but
|
|
196
|
+
# SQL kinds that represent impure DDL (produce no result set but will remain in RUNNING phase)
|
|
179
197
|
_IMPURE_DDL_KINDS = frozenset({"CREATE_TABLE_AS"})
|
|
180
198
|
|
|
181
199
|
# From the cursor that created this statement ...
|
|
@@ -225,6 +243,12 @@ class Statement:
|
|
|
225
243
|
if self.phase.is_terminal:
|
|
226
244
|
return True
|
|
227
245
|
|
|
246
|
+
# Traits aren't sent on the initial PENDING response (see #194) -- without them, the
|
|
247
|
+
# trait-dependent checks below (is_pure_ddl, is_bounded, is_append_only) can't run yet,
|
|
248
|
+
# and a non-terminal statement with no traits can't be ready to fetch results anyway.
|
|
249
|
+
if self.traits is None:
|
|
250
|
+
return False
|
|
251
|
+
|
|
228
252
|
if execution_mode.is_streaming:
|
|
229
253
|
# In streaming mode, readiness depends on statement type.
|
|
230
254
|
if self.is_pure_ddl:
|
|
@@ -336,9 +360,22 @@ class Statement:
|
|
|
336
360
|
Pure DDL statements need to complete fully before the created/modified objects
|
|
337
361
|
are ready for use, unlike streaming queries or CTAS which are ready when RUNNING.
|
|
338
362
|
|
|
363
|
+
"Pure" is stretched a little to also cover CREATE_MATERIALIZED_TABLE,
|
|
364
|
+
CREATE_OR_ALTER_MATERIALIZED_TABLE, and ALTER_MATERIALIZED_TABLE (the query-evolving
|
|
365
|
+
form -- see CREATE_OR_ALTER_MATERIALIZED_TABLE): their completion additionally kicks off
|
|
366
|
+
(or redeploys) a persistent background refresh job that keeps running long after the
|
|
367
|
+
statement settles, which isn't true of the other members. They earn the label anyway
|
|
368
|
+
because they satisfy the one thing this property actually gates: the statement's own
|
|
369
|
+
phase reliably reaches a terminal phase before the created/altered object is usable,
|
|
370
|
+
rather than lingering in RUNNING the way CTAS does (see _PURE_DDL_KINDS for the
|
|
371
|
+
confirming detail). DROP_MATERIALIZED_TABLE needs no such stretch -- like the other
|
|
372
|
+
DROP_* kinds, it's a one-shot action with no background job of its own.
|
|
373
|
+
|
|
339
374
|
Returns:
|
|
340
375
|
True if the statement is one of: CREATE_TABLE, DROP_TABLE, CREATE_VIEW,
|
|
341
|
-
DROP_VIEW, ALTER_TABLE
|
|
376
|
+
DROP_VIEW, ALTER_TABLE, CREATE_MATERIALIZED_TABLE,
|
|
377
|
+
CREATE_OR_ALTER_MATERIALIZED_TABLE, ALTER_MATERIALIZED_TABLE,
|
|
378
|
+
DROP_MATERIALIZED_TABLE. False otherwise.
|
|
342
379
|
"""
|
|
343
380
|
return self.sql_kind in self._PURE_DDL_KINDS
|
|
344
381
|
|
|
@@ -482,8 +519,9 @@ class Statement:
|
|
|
482
519
|
else None
|
|
483
520
|
)
|
|
484
521
|
|
|
485
|
-
# Defensive check: non-failed statements should have traits
|
|
486
|
-
|
|
522
|
+
# Defensive check: non-failed, non-pending statements should have traits. FAILED
|
|
523
|
+
# statements never get traits, and PENDING may or may not have traits. (see #194).
|
|
524
|
+
if traits is None and phase not in (Phase.FAILED, Phase.PENDING):
|
|
487
525
|
raise OperationalError(
|
|
488
526
|
f"Received statement '{name}' in phase {phase} without traits. "
|
|
489
527
|
"This is unexpected and likely indicates a server API change or bug."
|
|
@@ -9,7 +9,7 @@ from typing import NamedTuple
|
|
|
9
9
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
|
-
from confluent_sql import Connection, SqlNone, YearMonthInterval
|
|
12
|
+
from confluent_sql import Connection, OperationalError, SqlNone, YearMonthInterval
|
|
13
13
|
from confluent_sql.execution_mode import ExecutionMode
|
|
14
14
|
|
|
15
15
|
|
|
@@ -678,6 +678,133 @@ class TestExecuteDDL:
|
|
|
678
678
|
assert statement.schema is None, "CTAS schema should be None"
|
|
679
679
|
assert statement.is_deleted, "Snapshot DDL statement should be auto-deleted"
|
|
680
680
|
|
|
681
|
+
def test_execute_streaming_ddl_create_materialized_table_completes(
|
|
682
|
+
self, connection: Connection, auto_dropped_table_name: str
|
|
683
|
+
):
|
|
684
|
+
"""Prove that CREATE MATERIALIZED TABLE, submitted via execute_streaming_ddl(), waits
|
|
685
|
+
for the statement to reach COMPLETED rather than returning as soon as it's RUNNING --
|
|
686
|
+
unlike CTAS, whose backing job runs forever, a materialized table's own CREATE statement
|
|
687
|
+
settles once its background refresh job is started (see statement.py's is_pure_ddl)."""
|
|
688
|
+
try:
|
|
689
|
+
statement_text = f"""
|
|
690
|
+
CREATE MATERIALIZED TABLE `{auto_dropped_table_name}`
|
|
691
|
+
AS SELECT * FROM `sample_data_stock_trades` WHERE quantity > 100
|
|
692
|
+
"""
|
|
693
|
+
|
|
694
|
+
statement = connection.execute_streaming_ddl(statement_text)
|
|
695
|
+
|
|
696
|
+
# execute_streaming_ddl's closing_cursor auto-deletes a terminal statement on exit,
|
|
697
|
+
# which flips Statement.phase to DELETED (it overrides the underlying phase once
|
|
698
|
+
# deleted -- see Statement.phase) -- so we can't read back "COMPLETED" directly here.
|
|
699
|
+
# is_deleted only follows from a *deletable* terminal phase (COMPLETED/FAILED/
|
|
700
|
+
# STOPPED); FAILED would have raised OperationalError out of execute_streaming_ddl
|
|
701
|
+
# instead of returning, and nothing here ever requested a STOPPED, so reaching this
|
|
702
|
+
# line with is_deleted true and no exception raised is exactly the proof that the
|
|
703
|
+
# statement settled to COMPLETED.
|
|
704
|
+
assert not statement.is_running
|
|
705
|
+
assert statement.is_deleted
|
|
706
|
+
|
|
707
|
+
# The table should be immediately queryable.
|
|
708
|
+
with connection.closing_cursor(as_dict=True) as cursor:
|
|
709
|
+
cursor.execute(f"SELECT COUNT(*) AS row_count FROM `{auto_dropped_table_name}`")
|
|
710
|
+
results = cursor.fetchone()
|
|
711
|
+
assert results is not None
|
|
712
|
+
finally:
|
|
713
|
+
with suppress(Exception):
|
|
714
|
+
connection.execute_snapshot_ddl(
|
|
715
|
+
f"DROP MATERIALIZED TABLE IF EXISTS `{auto_dropped_table_name}`"
|
|
716
|
+
)
|
|
717
|
+
|
|
718
|
+
def test_execute_streaming_ddl_create_or_alter_materialized_table_completes(
|
|
719
|
+
self, connection: Connection, auto_dropped_table_name: str
|
|
720
|
+
):
|
|
721
|
+
"""Prove that CREATE OR ALTER MATERIALIZED TABLE, and a bare (query-evolving) ALTER
|
|
722
|
+
MATERIALIZED TABLE, both also wait for COMPLETED, same as a first-time CREATE
|
|
723
|
+
MATERIALIZED TABLE. Each step respells the filter predicate (>100, then >=100, then >50)
|
|
724
|
+
to exercise a genuine redefinition of an already-existing materialized table."""
|
|
725
|
+
|
|
726
|
+
def show_create_ddl_text(table_name: str) -> str:
|
|
727
|
+
# SHOW CREATE MATERIALIZED TABLE is a plain bounded single-row query, so a default
|
|
728
|
+
# (snapshot mode) cursor is all it needs.
|
|
729
|
+
with connection.closing_cursor(as_dict=True) as cursor:
|
|
730
|
+
cursor.execute(f"SHOW CREATE MATERIALIZED TABLE `{table_name}`")
|
|
731
|
+
row = cursor.fetchone()
|
|
732
|
+
assert row is not None
|
|
733
|
+
return row["SHOW CREATE MATERIALIZED TABLE"] # type: ignore[index]
|
|
734
|
+
|
|
735
|
+
try:
|
|
736
|
+
create_statement_text = f"""
|
|
737
|
+
CREATE MATERIALIZED TABLE `{auto_dropped_table_name}`
|
|
738
|
+
AS SELECT * FROM `sample_data_stock_trades` WHERE quantity > 100
|
|
739
|
+
"""
|
|
740
|
+
created = connection.execute_streaming_ddl(create_statement_text)
|
|
741
|
+
assert not created.is_running
|
|
742
|
+
assert created.is_deleted # see the sibling CREATE test for why this implies COMPLETED
|
|
743
|
+
|
|
744
|
+
create_or_alter_statement_text = f"""
|
|
745
|
+
CREATE OR ALTER MATERIALIZED TABLE `{auto_dropped_table_name}`
|
|
746
|
+
AS SELECT * FROM `sample_data_stock_trades` WHERE quantity >= 100
|
|
747
|
+
"""
|
|
748
|
+
create_or_altered = connection.execute_streaming_ddl(create_or_alter_statement_text)
|
|
749
|
+
|
|
750
|
+
assert not create_or_altered.is_running
|
|
751
|
+
assert create_or_altered.is_deleted
|
|
752
|
+
|
|
753
|
+
# Prove the redefinition actually took effect.
|
|
754
|
+
assert ">= 100" in show_create_ddl_text(auto_dropped_table_name)
|
|
755
|
+
|
|
756
|
+
# A bare ALTER (no CREATE OR prefix) with a new AS SELECT is query evolution too --
|
|
757
|
+
# same completion semantics, since ALTER_MATERIALIZED_TABLE is also pure DDL.
|
|
758
|
+
bare_alter_statement_text = f"""
|
|
759
|
+
ALTER MATERIALIZED TABLE `{auto_dropped_table_name}`
|
|
760
|
+
AS SELECT * FROM `sample_data_stock_trades` WHERE quantity > 50
|
|
761
|
+
"""
|
|
762
|
+
bare_altered = connection.execute_streaming_ddl(bare_alter_statement_text)
|
|
763
|
+
|
|
764
|
+
assert not bare_altered.is_running
|
|
765
|
+
assert bare_altered.is_deleted
|
|
766
|
+
|
|
767
|
+
assert "> 50" in show_create_ddl_text(auto_dropped_table_name)
|
|
768
|
+
finally:
|
|
769
|
+
with suppress(Exception):
|
|
770
|
+
connection.execute_snapshot_ddl(
|
|
771
|
+
f"DROP MATERIALIZED TABLE IF EXISTS `{auto_dropped_table_name}`"
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
def test_execute_snapshot_ddl_drop_materialized_table_completes(
|
|
775
|
+
self, connection: Connection, auto_dropped_table_name: str
|
|
776
|
+
):
|
|
777
|
+
"""Prove that DROP MATERIALIZED TABLE, submitted via execute_snapshot_ddl(), completes
|
|
778
|
+
(it's pure DDL, like the other DROP_* kinds -- see statement.py's _PURE_DDL_KINDS)."""
|
|
779
|
+
try:
|
|
780
|
+
create_statement_text = f"""
|
|
781
|
+
CREATE MATERIALIZED TABLE `{auto_dropped_table_name}`
|
|
782
|
+
AS SELECT * FROM `sample_data_stock_trades` WHERE quantity > 100
|
|
783
|
+
"""
|
|
784
|
+
connection.execute_streaming_ddl(create_statement_text)
|
|
785
|
+
|
|
786
|
+
drop_statement = connection.execute_snapshot_ddl(
|
|
787
|
+
f"DROP MATERIALIZED TABLE `{auto_dropped_table_name}`"
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
# is_deleted implies COMPLETED here, not just some other deletable terminal phase --
|
|
791
|
+
# see the CREATE test above for why.
|
|
792
|
+
assert not drop_statement.is_running
|
|
793
|
+
assert drop_statement.is_deleted
|
|
794
|
+
|
|
795
|
+
# The table should be gone: selecting from it should now fail with the backend's
|
|
796
|
+
# missing-object diagnostic, not just some unrelated failure.
|
|
797
|
+
with (
|
|
798
|
+
pytest.raises(OperationalError, match="does not exist"),
|
|
799
|
+
connection.closing_cursor() as cursor,
|
|
800
|
+
):
|
|
801
|
+
cursor.execute(f"SELECT * FROM `{auto_dropped_table_name}`")
|
|
802
|
+
finally:
|
|
803
|
+
with suppress(Exception):
|
|
804
|
+
connection.execute_snapshot_ddl(
|
|
805
|
+
f"DROP MATERIALIZED TABLE IF EXISTS `{auto_dropped_table_name}`"
|
|
806
|
+
)
|
|
807
|
+
|
|
681
808
|
|
|
682
809
|
@pytest.mark.integration
|
|
683
810
|
class TestArrayStatements:
|
|
@@ -13,7 +13,7 @@ from confluent_sql.exceptions import (
|
|
|
13
13
|
)
|
|
14
14
|
from confluent_sql.execution_mode import ExecutionMode
|
|
15
15
|
from confluent_sql.result_readers import ChangelogEventReader, ChangeloggedRow, FetchMetrics
|
|
16
|
-
from confluent_sql.statement import ChangelogRow, Op, Statement
|
|
16
|
+
from confluent_sql.statement import ChangelogRow, Op, Phase, Statement
|
|
17
17
|
from tests.unit.conftest import (
|
|
18
18
|
CursorWithStatementFactory,
|
|
19
19
|
MockConnectionFactory,
|
|
@@ -94,6 +94,33 @@ class TestExecute:
|
|
|
94
94
|
# non-append-only statements.
|
|
95
95
|
assert isinstance(mock_connection_cursor._result_reader, ChangelogEventReader)
|
|
96
96
|
|
|
97
|
+
def test_execute_raises_with_actual_statement_name_on_immediate_submission_failure(
|
|
98
|
+
self,
|
|
99
|
+
mock_connection_cursor: Cursor,
|
|
100
|
+
statement_response_factory: StatementResponseFactory,
|
|
101
|
+
):
|
|
102
|
+
"""Prove that when the *submission* response (not a later poll) already reports FAILED,
|
|
103
|
+
the raised OperationalError names the statement per the parsed response -- not the
|
|
104
|
+
caller's statement_name argument, which is None on the common path where the caller lets
|
|
105
|
+
the server generate a name. Regression test for the bug Copilot's review of #195 caught:
|
|
106
|
+
interpolating the (usually None) statement_name parameter instead of
|
|
107
|
+
self._statement.name."""
|
|
108
|
+
failed_submission = statement_response_factory(
|
|
109
|
+
phase="FAILED",
|
|
110
|
+
status_detail="Invalid SQL syntax",
|
|
111
|
+
name="dbapi-server-generated-name",
|
|
112
|
+
)
|
|
113
|
+
mock_connection_cursor._connection._execute_statement.return_value = ( # type: ignore
|
|
114
|
+
failed_submission
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
with pytest.raises(
|
|
118
|
+
OperationalError,
|
|
119
|
+
match=r"Statement 'dbapi-server-generated-name' submission failed: Invalid SQL syntax",
|
|
120
|
+
):
|
|
121
|
+
# statement_name intentionally omitted, as most callers do -- the server assigns one.
|
|
122
|
+
mock_connection_cursor.execute("SELECT 1 AS col")
|
|
123
|
+
|
|
97
124
|
def test_execute_calls_raise_if_statement_is_broken_for_failed_statement(
|
|
98
125
|
self,
|
|
99
126
|
mock_connection_cursor: Cursor,
|
|
@@ -253,6 +280,53 @@ class TestExecute:
|
|
|
253
280
|
|
|
254
281
|
backoff.assert_called_once_with(30, started_at=0.0)
|
|
255
282
|
|
|
283
|
+
def test_becomes_ready_after_pending_without_traits_then_running_with_traits(
|
|
284
|
+
self,
|
|
285
|
+
mock_connection_cursor: Cursor,
|
|
286
|
+
statement_response_factory: StatementResponseFactory,
|
|
287
|
+
mocker,
|
|
288
|
+
):
|
|
289
|
+
"""End-to-end lifecycle for #194: a statement is submitted PENDING with no traits (the
|
|
290
|
+
documented shape of Confluent Cloud's initial response), stays PENDING with no traits for
|
|
291
|
+
two more polls, then transitions to RUNNING with traits -- at which point execute() must
|
|
292
|
+
return instead of raising or timing out."""
|
|
293
|
+
pending_no_traits = statement_response_factory(phase="PENDING")
|
|
294
|
+
pending_no_traits["status"]["traits"] = None
|
|
295
|
+
|
|
296
|
+
running_with_traits = statement_response_factory(
|
|
297
|
+
sql_statement="SELECT * FROM source_table",
|
|
298
|
+
sql_kind="SELECT",
|
|
299
|
+
phase="RUNNING",
|
|
300
|
+
is_bounded=True,
|
|
301
|
+
is_append_only=True,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
mock_connection_cursor._execution_mode = ExecutionMode.STREAMING_QUERY
|
|
305
|
+
|
|
306
|
+
# The initial submission (POST) response.
|
|
307
|
+
mock_connection_cursor._connection._execute_statement.return_value = ( # type: ignore
|
|
308
|
+
pending_no_traits
|
|
309
|
+
)
|
|
310
|
+
# Subsequent polls (GET): two more PENDING-without-traits observations, then RUNNING
|
|
311
|
+
# with traits on the third.
|
|
312
|
+
mock_connection_cursor._connection._get_statement.side_effect = ( # type: ignore
|
|
313
|
+
[pending_no_traits, pending_no_traits, running_with_traits]
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
mocker.patch("time.sleep", return_value=None)
|
|
317
|
+
start_time = 1000000.0
|
|
318
|
+
time_mock = mocker.patch(
|
|
319
|
+
"time.monotonic", side_effect=lambda: start_time + time_mock.call_count
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
mock_connection_cursor.execute("SELECT * FROM source_table")
|
|
323
|
+
|
|
324
|
+
assert mock_connection_cursor._connection._get_statement.call_count == 3 # type: ignore
|
|
325
|
+
assert mock_connection_cursor._statement is not None
|
|
326
|
+
assert mock_connection_cursor._statement.phase is Phase.RUNNING
|
|
327
|
+
assert mock_connection_cursor._statement.traits is not None
|
|
328
|
+
assert mock_connection_cursor._statement.can_fetch_results(ExecutionMode.STREAMING_QUERY)
|
|
329
|
+
|
|
256
330
|
@pytest.mark.parametrize(
|
|
257
331
|
"streaming_mode",
|
|
258
332
|
[ExecutionMode.STREAMING_QUERY, ExecutionMode.STREAMING_DDL],
|
|
@@ -440,6 +440,10 @@ class TestStatementProperties:
|
|
|
440
440
|
("CREATE_VIEW", True),
|
|
441
441
|
("DROP_VIEW", True),
|
|
442
442
|
("ALTER_TABLE", True),
|
|
443
|
+
("CREATE_MATERIALIZED_TABLE", True),
|
|
444
|
+
("CREATE_OR_ALTER_MATERIALIZED_TABLE", True),
|
|
445
|
+
("ALTER_MATERIALIZED_TABLE", True),
|
|
446
|
+
("DROP_MATERIALIZED_TABLE", True),
|
|
443
447
|
("SELECT", False),
|
|
444
448
|
("INSERT", False),
|
|
445
449
|
("UPDATE", False),
|
|
@@ -613,6 +617,22 @@ class TestStatementFromResponse:
|
|
|
613
617
|
):
|
|
614
618
|
Statement.from_response(mock_connection, response)
|
|
615
619
|
|
|
620
|
+
def test_allows_pending_statement_without_traits(
|
|
621
|
+
self, mock_connection: Connection, statement_response_factory: StatementResponseFactory
|
|
622
|
+
):
|
|
623
|
+
"""Test that from_response accepts a PENDING statement without traits.
|
|
624
|
+
|
|
625
|
+
Confluent Cloud's documented initial response to a statement submission is a PENDING
|
|
626
|
+
phase with no status.traits at all -- see #194. Traits only become available once the
|
|
627
|
+
statement has been polled at least once.
|
|
628
|
+
"""
|
|
629
|
+
response = statement_response_factory(phase="PENDING")
|
|
630
|
+
response["status"]["traits"] = None
|
|
631
|
+
|
|
632
|
+
statement = Statement.from_response(mock_connection, response)
|
|
633
|
+
assert statement.traits is None
|
|
634
|
+
assert statement.phase == Phase.PENDING
|
|
635
|
+
|
|
616
636
|
def test_parses_row_result_schema(
|
|
617
637
|
self, mock_connection: Connection, statement_response_factory: StatementResponseFactory
|
|
618
638
|
):
|
|
@@ -980,32 +1000,63 @@ class TestStatementCanFetchResults:
|
|
|
980
1000
|
statement = Statement.from_response(mock_connection, statement_json)
|
|
981
1001
|
assert statement.can_fetch_results(ExecutionMode.STREAMING_QUERY)
|
|
982
1002
|
|
|
1003
|
+
@pytest.mark.parametrize(
|
|
1004
|
+
"sql_kind",
|
|
1005
|
+
[
|
|
1006
|
+
"CREATE_TABLE",
|
|
1007
|
+
"CREATE_MATERIALIZED_TABLE",
|
|
1008
|
+
"CREATE_OR_ALTER_MATERIALIZED_TABLE",
|
|
1009
|
+
"ALTER_MATERIALIZED_TABLE",
|
|
1010
|
+
"DROP_MATERIALIZED_TABLE",
|
|
1011
|
+
],
|
|
1012
|
+
)
|
|
983
1013
|
@pytest.mark.parametrize("phase", ["PENDING", "RUNNING"])
|
|
984
1014
|
def test_streaming_ddl_pure_ddl_not_ready(
|
|
985
1015
|
self,
|
|
986
1016
|
mock_connection: Connection,
|
|
987
1017
|
statement_response_factory: StatementResponseFactory,
|
|
988
1018
|
phase: str,
|
|
1019
|
+
sql_kind: str,
|
|
989
1020
|
):
|
|
990
|
-
"""In streaming DDL mode, pure DDL must wait for terminal state.
|
|
1021
|
+
"""In streaming DDL mode, pure DDL must wait for terminal state.
|
|
1022
|
+
|
|
1023
|
+
The three MATERIALIZED_TABLE kinds are included alongside CREATE_TABLE here: even though
|
|
1024
|
+
CREATE_MATERIALIZED_TABLE, CREATE_OR_ALTER_MATERIALIZED_TABLE, and
|
|
1025
|
+
ALTER_MATERIALIZED_TABLE all kick off (or redeploy) a persistent background refresh job
|
|
1026
|
+
on completion, each statement's own phase reliably reaches a terminal phase once that job
|
|
1027
|
+
is started (confirmed against a real environment -- unlike CTAS, which the Flink API
|
|
1028
|
+
traits currently mis-report as perpetually RUNNING, see is_bounded's Jan 2026 bug note),
|
|
1029
|
+
so they belong in the same "must wait for terminal" bucket as the other pure DDL kinds.
|
|
1030
|
+
"""
|
|
991
1031
|
statement_json = statement_response_factory(
|
|
992
1032
|
phase=phase,
|
|
993
|
-
sql_kind=
|
|
1033
|
+
sql_kind=sql_kind,
|
|
994
1034
|
)
|
|
995
1035
|
statement = Statement.from_response(mock_connection, statement_json)
|
|
996
1036
|
assert not statement.can_fetch_results(ExecutionMode.STREAMING_DDL)
|
|
997
1037
|
|
|
1038
|
+
@pytest.mark.parametrize(
|
|
1039
|
+
"sql_kind",
|
|
1040
|
+
[
|
|
1041
|
+
"CREATE_TABLE",
|
|
1042
|
+
"CREATE_MATERIALIZED_TABLE",
|
|
1043
|
+
"CREATE_OR_ALTER_MATERIALIZED_TABLE",
|
|
1044
|
+
"ALTER_MATERIALIZED_TABLE",
|
|
1045
|
+
"DROP_MATERIALIZED_TABLE",
|
|
1046
|
+
],
|
|
1047
|
+
)
|
|
998
1048
|
@pytest.mark.parametrize("phase", ["COMPLETED", "STOPPED", "FAILED"])
|
|
999
1049
|
def test_streaming_ddl_pure_ddl_ready(
|
|
1000
1050
|
self,
|
|
1001
1051
|
mock_connection: Connection,
|
|
1002
1052
|
statement_response_factory: StatementResponseFactory,
|
|
1003
1053
|
phase: str,
|
|
1054
|
+
sql_kind: str,
|
|
1004
1055
|
):
|
|
1005
1056
|
"""In streaming DDL mode, pure DDL is ready in terminal states."""
|
|
1006
1057
|
statement_json = statement_response_factory(
|
|
1007
1058
|
phase=phase,
|
|
1008
|
-
sql_kind=
|
|
1059
|
+
sql_kind=sql_kind,
|
|
1009
1060
|
)
|
|
1010
1061
|
statement = Statement.from_response(mock_connection, statement_json)
|
|
1011
1062
|
assert statement.can_fetch_results(ExecutionMode.STREAMING_DDL)
|
|
@@ -1075,3 +1126,22 @@ class TestStatementCanFetchResults:
|
|
|
1075
1126
|
)
|
|
1076
1127
|
statement = Statement.from_response(mock_connection, statement_json)
|
|
1077
1128
|
assert statement.can_fetch_results(ExecutionMode.STREAMING_DDL)
|
|
1129
|
+
|
|
1130
|
+
@pytest.mark.parametrize(
|
|
1131
|
+
"execution_mode",
|
|
1132
|
+
[ExecutionMode.SNAPSHOT, ExecutionMode.STREAMING_QUERY, ExecutionMode.STREAMING_DDL],
|
|
1133
|
+
)
|
|
1134
|
+
def test_pending_without_traits_not_ready(
|
|
1135
|
+
self,
|
|
1136
|
+
mock_connection: Connection,
|
|
1137
|
+
statement_response_factory: StatementResponseFactory,
|
|
1138
|
+
execution_mode: ExecutionMode,
|
|
1139
|
+
):
|
|
1140
|
+
"""A freshly-submitted PENDING statement has no traits yet (see #194) -- it must be
|
|
1141
|
+
reported as not-yet-ready rather than raising while probing trait-dependent properties
|
|
1142
|
+
like is_pure_ddl."""
|
|
1143
|
+
response = statement_response_factory(phase="PENDING")
|
|
1144
|
+
response["status"]["traits"] = None
|
|
1145
|
+
|
|
1146
|
+
statement = Statement.from_response(mock_connection, response)
|
|
1147
|
+
assert not statement.can_fetch_results(execution_mode)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/simple_append_only_streaming_query_example.py
RENAMED
|
File without changes
|
{confluent_sql-0.5.1 → confluent_sql-0.5.3}/examples/snapshot_mode_tuple_cursor_simple_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|