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.
Files changed (67) hide show
  1. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/CHANGELOG.md +6 -0
  2. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/PKG-INFO +1 -1
  3. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/pyproject.toml +1 -1
  4. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/cursor.py +3 -2
  5. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/statement.py +9 -2
  6. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_cursor_unit.py +75 -1
  7. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_statement_unit.py +35 -0
  8. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/uv.lock +1 -1
  9. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.github/CODEOWNERS +0 -0
  10. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.gitignore +0 -0
  11. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.semaphore/publish_to_pypi.yml +0 -0
  12. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/.semaphore/semaphore.yml +0 -0
  13. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/ARCHITECTURE.md +0 -0
  14. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/CLAUDE.md +0 -0
  15. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/DBAPI_EXTENSIONS.md +0 -0
  16. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/LICENSE.txt +0 -0
  17. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/Makefile +0 -0
  18. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/README.md +0 -0
  19. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/STREAMING.md +0 -0
  20. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/TYPES.md +0 -0
  21. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/byoidc_bearer_token_example.py +0 -0
  22. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/errors.py +0 -0
  23. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/simple_append_only_streaming_query_example.py +0 -0
  24. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
  25. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/statement_properties_example.py +0 -0
  26. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/examples/tableflow_lifecycle_example.py +0 -0
  27. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/service.yml +0 -0
  28. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/__init__.py +0 -0
  29. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/__version__.py +0 -0
  30. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/auth.py +0 -0
  31. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/changelog_compressor.py +0 -0
  32. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/connection.py +0 -0
  33. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/connectors.py +0 -0
  34. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/exceptions.py +0 -0
  35. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/execution_mode.py +0 -0
  36. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/polling.py +0 -0
  37. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/result_readers.py +0 -0
  38. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/retry.py +0 -0
  39. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/statement_properties.py +0 -0
  40. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/tableflow.py +0 -0
  41. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/src/confluent_sql/types.py +0 -0
  42. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/__init__.py +0 -0
  43. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/conftest.py +0 -0
  44. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/conftest.py +0 -0
  45. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_connection.py +0 -0
  46. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_connector.py +0 -0
  47. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_cursor.py +0 -0
  48. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_fetch.py +0 -0
  49. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/integration/test_tableflow.py +0 -0
  50. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/conftest.py +0 -0
  51. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_auth_unit.py +0 -0
  52. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_changelog_compressor_unit.py +0 -0
  53. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_changelog_unit.py +0 -0
  54. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_byoidc_unit.py +0 -0
  55. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_connector_unit.py +0 -0
  56. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_unit.py +0 -0
  57. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connection_unit_properties.py +0 -0
  58. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connectors_connection_unit.py +0 -0
  59. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_connectors_unit.py +0 -0
  60. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_execution_mode_unit.py +0 -0
  61. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_polling_unit.py +0 -0
  62. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_result_readers_unit.py +0 -0
  63. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_retry_unit.py +0 -0
  64. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_statement_properties_unit.py +0 -0
  65. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_tableflow_connection_unit.py +0 -0
  66. {confluent_sql-0.5.2 → confluent_sql-0.5.3}/tests/unit/test_tableflow_unit.py +0 -0
  67. {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.2
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "confluent-sql"
7
- version = "0.5.2"
7
+ version = "0.5.3"
8
8
  description = "DB-API v2 compliant driver for Confluent Cloud Flink SQL"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -259,8 +259,9 @@ class Cursor:
259
259
 
260
260
  if self._statement.is_failed:
261
261
  raise OperationalError(
262
- f"Statement submission failed: {self._statement.status.get('detail', '')}"
263
- ) # pragma: no cover
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
- if traits is None and phase != Phase.FAILED:
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)
@@ -36,7 +36,7 @@ wheels = [
36
36
 
37
37
  [[package]]
38
38
  name = "confluent-sql"
39
- version = "0.5.2"
39
+ version = "0.5.3"
40
40
  source = { editable = "." }
41
41
  dependencies = [
42
42
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes