confluent-sql 0.5.2__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.2 → confluent_sql-0.5.3}/CHANGELOG.md +6 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/PKG-INFO +1 -1
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/pyproject.toml +1 -1
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/cursor.py +3 -2
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/statement.py +9 -2
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_cursor_unit.py +75 -1
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_statement_unit.py +35 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/uv.lock +1 -1
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.github/CODEOWNERS +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.gitignore +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.semaphore/publish_to_pypi.yml +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.semaphore/semaphore.yml +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/ARCHITECTURE.md +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/CLAUDE.md +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/DBAPI_EXTENSIONS.md +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/LICENSE.txt +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/Makefile +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/README.md +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/STREAMING.md +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/TYPES.md +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/byoidc_bearer_token_example.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/errors.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/simple_append_only_streaming_query_example.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/statement_properties_example.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/tableflow_lifecycle_example.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/service.yml +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/__init__.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/__version__.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/auth.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/changelog_compressor.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/connection.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/connectors.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/exceptions.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/execution_mode.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/polling.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/result_readers.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/retry.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/statement_properties.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/tableflow.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/types.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/__init__.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/conftest.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/conftest.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_connection.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_connector.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_cursor.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_fetch.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_tableflow.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/conftest.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_auth_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_changelog_compressor_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_changelog_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_byoidc_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_connector_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_unit_properties.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connectors_connection_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connectors_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_execution_mode_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_polling_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_result_readers_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_retry_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_statement_properties_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_tableflow_connection_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_tableflow_unit.py +0 -0
- {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_types_unit.py +0 -0
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this dbapi driver will be documented in this file.
|
|
4
4
|
|
|
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
|
+
|
|
5
11
|
## 0.5.2, 2026-08-26
|
|
6
12
|
|
|
7
13
|
### Fixed
|
|
@@ -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.
|
|
@@ -243,6 +243,12 @@ class Statement:
|
|
|
243
243
|
if self.phase.is_terminal:
|
|
244
244
|
return True
|
|
245
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
|
+
|
|
246
252
|
if execution_mode.is_streaming:
|
|
247
253
|
# In streaming mode, readiness depends on statement type.
|
|
248
254
|
if self.is_pure_ddl:
|
|
@@ -513,8 +519,9 @@ class Statement:
|
|
|
513
519
|
else None
|
|
514
520
|
)
|
|
515
521
|
|
|
516
|
-
# Defensive check: non-failed statements should have traits
|
|
517
|
-
|
|
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):
|
|
518
525
|
raise OperationalError(
|
|
519
526
|
f"Received statement '{name}' in phase {phase} without traits. "
|
|
520
527
|
"This is unexpected and likely indicates a server API change or bug."
|
|
@@ -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],
|
|
@@ -617,6 +617,22 @@ class TestStatementFromResponse:
|
|
|
617
617
|
):
|
|
618
618
|
Statement.from_response(mock_connection, response)
|
|
619
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
|
+
|
|
620
636
|
def test_parses_row_result_schema(
|
|
621
637
|
self, mock_connection: Connection, statement_response_factory: StatementResponseFactory
|
|
622
638
|
):
|
|
@@ -1110,3 +1126,22 @@ class TestStatementCanFetchResults:
|
|
|
1110
1126
|
)
|
|
1111
1127
|
statement = Statement.from_response(mock_connection, statement_json)
|
|
1112
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.2 → confluent_sql-0.5.3}/examples/simple_append_only_streaming_query_example.py
RENAMED
|
File without changes
|
{confluent_sql-0.5.2 → 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
|
|
File without changes
|