confluent-sql 0.1.3__tar.gz → 0.2.0__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 (47) hide show
  1. confluent_sql-0.2.0/CHANGELOG.md +24 -0
  2. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/DBAPI_EXTENSIONS.md +36 -7
  3. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/PKG-INFO +3 -3
  4. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/README.md +2 -2
  5. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/examples/simple_append_only_streaming_query_example.py +1 -1
  6. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/pyproject.toml +1 -1
  7. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/__init__.py +2 -1
  8. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/connection.py +155 -54
  9. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/cursor.py +15 -12
  10. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/statement.py +17 -1
  11. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/types.py +15 -0
  12. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/conftest.py +13 -8
  13. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/integration/conftest.py +8 -8
  14. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/integration/test_cursor.py +32 -4
  15. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_connection_unit.py +220 -14
  16. confluent_sql-0.2.0/tests/unit/test_connection_unit_properties.py +200 -0
  17. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_types_unit.py +42 -0
  18. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/uv.lock +1 -1
  19. confluent_sql-0.1.3/CHANGELOG.md +0 -7
  20. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/.github/CODEOWNERS +0 -0
  21. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/.gitignore +0 -0
  22. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/.semaphore/publish_to_codeartifact.yml +0 -0
  23. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/.semaphore/publish_to_pypi.yml +0 -0
  24. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/.semaphore/semaphore.yml +0 -0
  25. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/ARCHITECTURE.md +0 -0
  26. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/LICENSE.txt +0 -0
  27. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/Makefile +0 -0
  28. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/STREAMING.md +0 -0
  29. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/TYPES.md +0 -0
  30. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/examples/errors.py +0 -0
  31. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/examples/snapshot_mode_tuple_cursor_simple_example.py +0 -0
  32. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/service.yml +0 -0
  33. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/__version__.py +0 -0
  34. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/changelog_compressor.py +0 -0
  35. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/exceptions.py +0 -0
  36. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/execution_mode.py +0 -0
  37. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/src/confluent_sql/result_readers.py +0 -0
  38. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/__init__.py +0 -0
  39. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/integration/test_connection.py +0 -0
  40. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/integration/test_fetch.py +0 -0
  41. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/conftest.py +0 -0
  42. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_changelog_compressor_unit.py +0 -0
  43. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_changelog_unit.py +0 -0
  44. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_cursor_unit.py +0 -0
  45. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_execution_mode_unit.py +0 -0
  46. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_result_readers_unit.py +0 -0
  47. {confluent_sql-0.1.3 → confluent_sql-0.2.0}/tests/unit/test_statement_unit.py +0 -0
@@ -0,0 +1,24 @@
1
+ # Change Log
2
+
3
+ All notable changes to this dbapi driver will be documented in this file.
4
+
5
+ ## Unreleased
6
+
7
+ ## 0.2.0
8
+
9
+ ### Changed
10
+ * Respelled the `connect()` parameter `dbname` to `database`. The old spelling `dbname` is deprecated and will be removed in after one release cycle.
11
+ * Class `SqlNone` now gracefully strips trailing `NOT NULL` constraints from type names (case-insensitively), so that `str(SqlNone("DATE NOT NULL"))` returns valid FlinkSQL `"cast (null as DATE)"`.
12
+ * `connect()` is now keyword-only callable.
13
+ * The `host` parameter for `Connection.__init__()` has been renamed to `endpoint`.
14
+
15
+ ### Added
16
+ * New optional keyword parameter `properties: PropertiesDict | None` on `Cursor.execute()` and related methods to allow callers to provide [statement execution properties](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html#table-options). Note: connection or cursor-level properties for default catalog, database, and execution mode cannot be overridden by this parameter.
17
+ * New optional `endpoint` parameter on `connect()` and `Connection.__init__` to allow users to specify a custom Confluent Cloud API base endpoint (e.g., for private networking, staging, etc.). Mutually exclusive with (`cloud_provider`, `cloud_region`) -- either `endpoint` or (`cloud_provider`, `cloud_region`) must be provided. This replaces the `host` parameter in `Connection.__init__()`. (#66)
18
+
19
+ ### Removed
20
+ * The unused control-plane `api_key` and `api_secret` `connect()` parameters have been removed. The Flink regional API key params `flink_api_key` and `flink_api_secret` remain.
21
+
22
+ ## 0.1.x
23
+
24
+ Early access release of the driver.
@@ -808,18 +808,39 @@ cursor.execute(
808
808
  timeout: int = 3000,
809
809
  statement_name: str | None = None,
810
810
  statement_label: str | None = None,
811
+ properties: dict[str, str | int | bool] | None = None,
811
812
  ) -> None
812
813
  ```
813
814
 
814
815
  ### Parameter Reference
815
816
 
816
- | Parameter | Type | Default | Description |
817
- | ----------------- | ----------------------- | ---------- | ------------------------------------------------------------------ |
818
- | `statement_text` | `str` | (required) | SQL statement to execute |
819
- | `parameters` | `tuple \| list \| None` | None | Parameter values for parameterized statements |
820
- | `timeout` | `int` | 3000 | Max seconds to wait for statement to reach RUNNING/COMPLETED phase |
821
- | `statement_name` | `str \| None` | None | Custom statement identifier (defaults to UUID) |
822
- | `statement_label` | `str \| None` | None | Label for grouping related statements |
817
+ | Parameter | Type | Default | Description |
818
+ | ----------------- | --------------------------------- | ---------- | ------------------------------------------------------------------ |
819
+ | `statement_text` | `str` | (required) | SQL statement to execute |
820
+ | `parameters` | `tuple \| list \| None` | None | Parameter values for parameterized statements |
821
+ | `timeout` | `int` | 3000 | Max seconds to wait for statement to reach RUNNING/COMPLETED phase |
822
+ | `statement_name` | `str \| None` | None | Custom statement identifier (defaults to UUID) |
823
+ | `statement_label` | `str \| None` | None | Label for grouping related statements |
824
+ | `properties` | `dict[str, str \| int \| bool] \| None` | None | [Statement properties](#statement-properties) to set for execution |
825
+
826
+ ### Statement Properties
827
+
828
+ The `properties` parameter allows you to set [Flink SQL statement properties](https://docs.confluent.io/cloud/current/flink/reference/statements/set.html#table-options) at query execution time. These are the same properties that can be set with Flink SQL `SET` statements.
829
+
830
+ **Important Precedence Rules:**
831
+ - Connection-level defaults (catalog, database) are always applied
832
+ - Cursor execution mode settings (e.g., `sql.snapshot.mode` for snapshot queries) are always applied
833
+ - User-provided properties in the `properties` parameter can extend these settings but cannot override system properties
834
+
835
+
836
+ **Accessing Properties After Execution:**
837
+ The properties are stored in the Statement object and can be accessed via `statement.properties`:
838
+
839
+ ```python
840
+ cursor.execute(query, properties={"sql.state-ttl": "100 ms"})
841
+ props = cursor.statement.properties
842
+ assert props["sql.state-ttl"] == "100 ms"
843
+ ```
823
844
 
824
845
  **Examples:**
825
846
 
@@ -857,6 +878,14 @@ cursor.execute(
857
878
  statement_name="product-sales-hourly",
858
879
  statement_label="analytics"
859
880
  )
881
+
882
+ # With statement properties
883
+ cursor.execute(
884
+ "SELECT * FROM orders WHERE status = %s",
885
+ ("pending",),
886
+ statement_name="pending-orders-query",
887
+ properties={"sql.state-ttl": "100 ms"}
888
+ )
860
889
  ```
861
890
 
862
891
  ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: confluent-sql
3
- Version: 0.1.3
3
+ Version: 0.2.0
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
@@ -245,7 +245,7 @@ This is pre-production code mainly developed as the lower level portion of a `db
245
245
 
246
246
  The behavior of snapshot-mode cursors, complying with dbapi semantics, are well stable. The streaming query extensions are more of a work in progress at this time. Feedback and suggestions are welcome!
247
247
 
248
- > **⚠️ Early Access:** [Snapshot queries](https://docs.confluent.io/cloud/current/flink/concepts/snapshot-queries.html) on Confluent Cloud Flink SQL are currently in Early Access and may be subject to change. The driver defaults to snapshot mode for all queries unless streaming mode is explicitly requested.
248
+ > **⚠️ Early Access:** [Snapshot queries](https://docs.confluent.io/cloud/current/flink/concepts/snapshot-queries.html) on Confluent Cloud Flink SQL are currently in Early Access and may be subject to change. You will need to request access to snapshot queries for your organization from Confluent. The driver defaults to snapshot mode for all queries unless streaming mode is explicitly requested.
249
249
 
250
250
  ## Prerequisites
251
251
 
@@ -279,7 +279,7 @@ connection = confluent_sql.connect(
279
279
  compute_pool_id="lfcp-789012",
280
280
  cloud_provider="aws",
281
281
  cloud_region="us-east-2",
282
- dbname="your-database-name"
282
+ database="your-database-name"
283
283
  )
284
284
  ```
285
285
 
@@ -14,7 +14,7 @@ This is pre-production code mainly developed as the lower level portion of a `db
14
14
 
15
15
  The behavior of snapshot-mode cursors, complying with dbapi semantics, are well stable. The streaming query extensions are more of a work in progress at this time. Feedback and suggestions are welcome!
16
16
 
17
- > **⚠️ Early Access:** [Snapshot queries](https://docs.confluent.io/cloud/current/flink/concepts/snapshot-queries.html) on Confluent Cloud Flink SQL are currently in Early Access and may be subject to change. The driver defaults to snapshot mode for all queries unless streaming mode is explicitly requested.
17
+ > **⚠️ Early Access:** [Snapshot queries](https://docs.confluent.io/cloud/current/flink/concepts/snapshot-queries.html) on Confluent Cloud Flink SQL are currently in Early Access and may be subject to change. You will need to request access to snapshot queries for your organization from Confluent. The driver defaults to snapshot mode for all queries unless streaming mode is explicitly requested.
18
18
 
19
19
  ## Prerequisites
20
20
 
@@ -48,7 +48,7 @@ connection = confluent_sql.connect(
48
48
  compute_pool_id="lfcp-789012",
49
49
  cloud_provider="aws",
50
50
  cloud_region="us-east-2",
51
- dbname="your-database-name"
51
+ database="your-database-name"
52
52
  )
53
53
  ```
54
54
 
@@ -13,7 +13,7 @@ conn = confluent_sql.connect(
13
13
  compute_pool_id=os.getenv("CONFLUENT_COMPUTE_POOL_ID", ""),
14
14
  cloud_provider=os.getenv("CONFLUENT_CLOUD_PROVIDER", ""),
15
15
  cloud_region=os.getenv("CONFLUENT_CLOUD_REGION", ""),
16
- dbname=os.getenv("CONFLUENT_TEST_DBNAME", "default"),
16
+ database=os.getenv("CONFLUENT_TEST_DBNAME", "default"),
17
17
  )
18
18
 
19
19
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "confluent-sql"
7
- version = "0.1.3"
7
+ version = "0.2.0"
8
8
  description = "DB-API v2 compliant driver for Confluent Cloud Flink SQL"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -27,7 +27,7 @@ from .exceptions import (
27
27
  from .execution_mode import ExecutionMode
28
28
  from .result_readers import ChangeloggedRow
29
29
  from .statement import Op
30
- from .types import SqlNone, YearMonthInterval
30
+ from .types import PropertiesDict, SqlNone, YearMonthInterval
31
31
 
32
32
  # DB-API v2 module globals
33
33
  apilevel = "2.0"
@@ -59,6 +59,7 @@ __all__ = [
59
59
  "apilevel",
60
60
  "threadsafety",
61
61
  "paramstyle",
62
+ "PropertiesDict",
62
63
  "SqlNone",
63
64
  "YearMonthInterval",
64
65
  ]
@@ -24,22 +24,23 @@ from .exceptions import InterfaceError, OperationalError, StatementDeletedError
24
24
  from .execution_mode import ExecutionMode
25
25
  from .statement import LABEL_PREFIX as STATEMENT_LABEL_PREFIX
26
26
  from .statement import ChangelogRow, Statement
27
- from .types import RowPythonTypes
27
+ from .types import PropertiesDict, RowPythonTypes
28
28
 
29
29
  logger = logging.getLogger(__name__)
30
30
 
31
31
 
32
32
  def connect( # noqa: PLR0913
33
+ *,
33
34
  flink_api_key: str,
34
35
  flink_api_secret: str,
35
36
  environment: str,
36
37
  compute_pool_id: str,
37
38
  organization_id: str,
38
- cloud_provider: str,
39
- cloud_region: str,
40
- api_key: str | None = None,
41
- api_secret: str | None = None,
42
- dbname: str | None = None,
39
+ cloud_provider: str | None = None,
40
+ cloud_region: str | None = None,
41
+ database: str | None = None,
42
+ endpoint: str | None = None,
43
+ dbname: str | None = None, # deprecated, use database parameter
43
44
  result_page_fetch_pause_millis: int = 100,
44
45
  http_user_agent: str | None = None,
45
46
  ) -> Connection:
@@ -47,16 +48,23 @@ def connect( # noqa: PLR0913
47
48
  Create a connection to a Confluent SQL service.
48
49
 
49
50
  Args:
50
- flink_api_key: Flink API key
51
- flink_api_secret: Flink API secret
51
+ flink_api_key: Flink Region API key
52
+ flink_api_secret: Flink Region API secret
52
53
  environment: Environment ID
53
54
  compute_pool_id: Compute pool ID for SQL execution
54
55
  organization_id: Organization ID
55
- cloud_provider: Cloud provider (e.g., "aws", "gcp", "azure")
56
- cloud_region: Cloud region (e.g., "us-east-2", "us-west-2")
57
- api_key: Confluent Cloud API key (optional, for general Confluent Cloud resources)
58
- api_secret: Confluent Cloud API secret (optional)
59
- dbname: The name of the database to use (optional)
56
+ cloud_provider: Cloud provider (e.g., "aws", "gcp", "azure"). Required if endpoint is not
57
+ provided; must not be provided if endpoint is specified.
58
+ cloud_region: Cloud region (e.g., "us-east-2", "us-west-2"). Required if endpoint is not
59
+ provided; must not be provided if endpoint is specified.
60
+ database: The default Flink database (Kafka cluster) to use when resolving
61
+ table/view/udf names (optional)
62
+ endpoint: The base URL for Confluent Cloud API (optional). If not provided, the endpoint
63
+ will be constructed based on the cloud_provider and cloud_region parameters in the
64
+ format "https://flink.{cloud_region}.{cloud_provider}.confluent.cloud". A trailing
65
+ slash is optional and will be stripped if provided (e.g., both
66
+ "https://custom.example.com" and "https://custom.example.com/" are accepted).
67
+ dbname: Deprecated alias for database parameter (optional)
60
68
  result_page_fetch_pause_millis: Maximum milliseconds to wait between fetching pages of
61
69
  statement results (per statement). Defaults to 100ms. Prevents tight loops of requests
62
70
  to the statement results API when consuming results for a statement, especially when
@@ -88,15 +96,33 @@ def connect( # noqa: PLR0913
88
96
  if not organization_id:
89
97
  raise InterfaceError("Organization ID is required")
90
98
 
91
- if not cloud_provider:
92
- raise InterfaceError("Cloud provider is required")
99
+ if endpoint:
100
+ if cloud_provider or cloud_region:
101
+ raise InterfaceError(
102
+ "cloud_provider and cloud_region should not be provided when endpoint is specified"
103
+ )
104
+ else:
105
+ if not cloud_provider:
106
+ raise InterfaceError("Cloud provider is required when endpoint is not provided")
93
107
 
94
- if not cloud_region:
95
- raise InterfaceError("Cloud region is required")
108
+ if not cloud_region:
109
+ raise InterfaceError("Cloud region is required when endpoint is not provided")
96
110
 
97
111
  if not flink_api_key or not flink_api_secret:
98
112
  raise InterfaceError("Flink API key and secret are required")
99
113
 
114
+ if dbname is not None:
115
+ warnings.warn(
116
+ "The 'dbname' parameter is deprecated and will be removed in a future release. "
117
+ "Please use the 'database' parameter instead.",
118
+ DeprecationWarning,
119
+ stacklevel=2,
120
+ )
121
+ if database is not None:
122
+ raise InterfaceError(
123
+ "Cannot specify both 'database' and deprecated 'dbname' parameters"
124
+ )
125
+
100
126
  return Connection(
101
127
  flink_api_key,
102
128
  flink_api_secret,
@@ -105,9 +131,8 @@ def connect( # noqa: PLR0913
105
131
  organization_id,
106
132
  cloud_provider,
107
133
  cloud_region,
108
- api_key=api_key,
109
- api_secret=api_secret,
110
- dbname=dbname,
134
+ endpoint,
135
+ database=database or dbname, # dbname is deprecated.
111
136
  statement_results_page_fetch_pause_millis=result_page_fetch_pause_millis,
112
137
  http_user_agent=http_user_agent,
113
138
  )
@@ -128,9 +153,6 @@ class Connection:
128
153
  environment: str
129
154
  organization_id: str
130
155
  compute_pool_id: str
131
- api_key: str | None
132
- api_secret: str | None
133
- host: str | None
134
156
  statement_results_page_fetch_pause_secs: float
135
157
  """Maximum seconds to wait between fetching pages of statement
136
158
  results (per statement). Prevents tight loops of requests to the
@@ -147,7 +169,7 @@ class Connection:
147
169
  """
148
170
 
149
171
  _closed: bool
150
- _dbname: str | None
172
+ _database: str | None
151
173
  _client: httpx.Client
152
174
  _http_user_agent: str
153
175
 
@@ -165,12 +187,10 @@ class Connection:
165
187
  environment: str,
166
188
  compute_pool_id: str,
167
189
  organization_id: str,
168
- cloud_provider: str,
169
- cloud_region: str,
170
- api_key: str | None = None,
171
- api_secret: str | None = None,
172
- host: str | None = None,
173
- dbname: str | None = None,
190
+ cloud_provider: str | None,
191
+ cloud_region: str | None,
192
+ endpoint: str | None,
193
+ database: str | None = None,
174
194
  statement_results_page_fetch_pause_millis: int = 100,
175
195
  http_user_agent: str | None = None,
176
196
  ):
@@ -183,16 +203,18 @@ class Connection:
183
203
  environment: Environment ID
184
204
  compute_pool_id: Compute pool ID for SQL execution
185
205
  organization_id: Organization ID
186
- cloud_provider: Cloud provider
187
- cloud_region: Cloud region (e.g., "us-east-2", "us-west-2")
206
+ cloud_provider: Cloud provider (required if endpoint is not provided)
207
+ cloud_region: Cloud region (e.g., "us-east-2", "us-west-2"). Required if endpoint is
208
+ not provided.
209
+ endpoint: The base URL for Confluent Cloud API (optional). If not provided, the
210
+ endpoint will be constructed based on the cloud_provider and cloud_region
211
+ parameters in the format "https://flink.{cloud_region}.{cloud_provider}.confluent.cloud".
212
+ A trailing slash is optional and will be stripped if provided.
213
+ database: The name of the database to use (optional)
188
214
  result_page_fetch_pause_millis: Milliseconds to possibly wait between fetching pages of
189
215
  statement results. Defaults to 100ms. If most recent fetch of results for a
190
216
  statement was more than this long ago, then no delay will happen when fetching
191
217
  the next page of results for the statement.
192
- api_key: Confluent Cloud API key for general Confluent Cloud resources (optional)
193
- api_secret: Confluent Cloud API secret for general Confluent Cloud resources (optional)
194
- host: The base URL for Confluent Cloud API (optional)
195
- dbname: The name of the database to use (optional)
196
218
  http_user_agent: User-Agent header for HTTP requests. String, 1-100 chars.
197
219
  Defaults to the value of DEFAULT_USER_AGENT, which includes the
198
220
  driver name/version, documentation URL, and support email.
@@ -200,9 +222,6 @@ class Connection:
200
222
  self.environment = environment
201
223
  self.compute_pool_id = compute_pool_id
202
224
  self.organization_id = organization_id
203
- self.api_key = api_key
204
- self.api_secret = api_secret
205
- self.host = host
206
225
 
207
226
  if statement_results_page_fetch_pause_millis < 0:
208
227
  raise InterfaceError("result_page_fetch_pause_millis must be non-negative")
@@ -215,17 +234,27 @@ class Connection:
215
234
 
216
235
  # Internal state
217
236
  self._closed = False
218
- self._dbname = dbname
237
+ self._database = database
219
238
 
220
239
  # Set user agent (validation happens in setter, default if None)
221
240
  self.http_user_agent = (
222
241
  http_user_agent if http_user_agent is not None else self.DEFAULT_USER_AGENT
223
242
  )
224
243
 
244
+ if not endpoint and not (cloud_provider and cloud_region):
245
+ raise InterfaceError(
246
+ "cloud_provider and cloud_region are required when endpoint is not provided"
247
+ )
248
+
225
249
  # Create httpx client for making API calls
226
- if self.host is None:
227
- self.host = f"https://flink.{cloud_region}.{cloud_provider}.confluent.cloud"
228
- base_url = f"{self.host}/sql/v1/organizations/{organization_id}/environments/{environment}"
250
+ if not endpoint:
251
+ # Construct the endpoint URL based on cloud provider and region.
252
+ endpoint = f"https://flink.{cloud_region}.{cloud_provider}.confluent.cloud"
253
+ else:
254
+ # Strip trailing slash if user provided one, to ensure clean URL construction
255
+ endpoint = endpoint.rstrip("/")
256
+
257
+ base_url = f"{endpoint}/sql/v1/organizations/{organization_id}/environments/{environment}"
229
258
 
230
259
  # Create httpx client for making API calls
231
260
  basic_auth = httpx.BasicAuth(username=flink_api_key, password=flink_api_secret)
@@ -477,6 +506,7 @@ class Connection:
477
506
  timeout: int = 3000,
478
507
  statement_name: str | None = None,
479
508
  statement_label: str | None = None,
509
+ properties: PropertiesDict | None = None,
480
510
  ) -> Statement:
481
511
  """Execute bounded DDL that completes after consuming snapshot data.
482
512
 
@@ -499,6 +529,8 @@ class Connection:
499
529
  group and manage related statements. The label will be
500
530
  prefixed with "user.confluent.io/" when stored but you only
501
531
  need to provide the label value itself (e.g., "my-ddl-batch")
532
+ properties: Optional dictionary of statement properties to set for this execution.
533
+ Keys must be strings, values must be str, int, or bool.
502
534
 
503
535
  Returns:
504
536
  Statement for managing the statement lifecycle
@@ -514,6 +546,7 @@ class Connection:
514
546
  timeout=timeout,
515
547
  statement_name=statement_name,
516
548
  statement_label=statement_label,
549
+ properties=properties,
517
550
  )
518
551
 
519
552
  # Return the last version of the statement
@@ -526,6 +559,7 @@ class Connection:
526
559
  timeout: int = 3000,
527
560
  statement_name: str | None = None,
528
561
  statement_label: str | None = None,
562
+ properties: PropertiesDict | None = None,
529
563
  ) -> Statement:
530
564
  """Execute unbounded DDL that starts a streaming job.
531
565
 
@@ -544,6 +578,8 @@ class Connection:
544
578
  group and manage related statements. The label will be
545
579
  prefixed with "user.confluent.io/" when stored but you only
546
580
  need to provide the label value itself (e.g., "streaming-jobs")
581
+ properties: Optional dictionary of statement properties to set for this execution.
582
+ Keys must be strings, values must be str, int, or bool.
547
583
  Returns:
548
584
  Statement for any further management of the statement lifecycle
549
585
  """
@@ -555,6 +591,7 @@ class Connection:
555
591
  timeout=timeout,
556
592
  statement_name=statement_name,
557
593
  statement_label=statement_label,
594
+ properties=properties,
558
595
  )
559
596
 
560
597
  return cur.statement
@@ -739,12 +776,79 @@ class Connection:
739
776
 
740
777
  self._row_type_registry.register_row_type(class_for_flink_row)
741
778
 
779
+ _NEVER_USER_PROVIDED_PROPERTIES = {
780
+ "sql.current-catalog",
781
+ "sql.current-database",
782
+ "sql.snapshot.mode",
783
+ }
784
+
785
+ def _resolve_properties(
786
+ self, properties: PropertiesDict | None, execution_mode: ExecutionMode
787
+ ) -> PropertiesDict:
788
+ """
789
+ Validate and merge user properties with system properties.
790
+
791
+ Validates the properties parameter and merges it with system-level properties
792
+ (catalog, database, snapshot mode). System properties always have precedence
793
+ and cannot be overridden by user input.
794
+
795
+ Args:
796
+ properties: Optional dictionary of user-provided statement properties.
797
+ Keys must be strings, values must be str, int, or bool.
798
+ execution_mode: The execution mode (determines if snapshot.mode is set).
799
+
800
+ Returns:
801
+ Merged properties dictionary with system properties overlaid.
802
+
803
+ Raises:
804
+ InterfaceError: If properties parameter is invalid (not a dict, invalid keys/values).
805
+ """
806
+ # Validate properties parameter if provided
807
+ if properties is not None:
808
+ if not isinstance(properties, dict):
809
+ raise InterfaceError(f"properties must be a dict, got {type(properties).__name__}")
810
+
811
+ for key, value in properties.items():
812
+ if not isinstance(key, str):
813
+ raise InterfaceError(
814
+ f"properties keys must be strings, got {type(key).__name__} for key {key!r}"
815
+ )
816
+ if not isinstance(value, (str, int, bool)):
817
+ raise InterfaceError(
818
+ f"properties values must be str, int, or bool, "
819
+ f"got {type(value).__name__} for key {key!r}"
820
+ )
821
+ if key in self._NEVER_USER_PROVIDED_PROPERTIES:
822
+ raise InterfaceError(f"'{key}' is a reserved system property.")
823
+
824
+ # Start with user properties (if provided), then overlay system properties
825
+ # This ensures system properties always win and cannot be overridden
826
+ merged_properties: PropertiesDict = {}
827
+
828
+ if properties is not None:
829
+ # User properties applied first
830
+ merged_properties.update(properties)
831
+
832
+ # Connection-level properties overlay (always set, cannot be overridden by user)
833
+ merged_properties["sql.current-catalog"] = self.environment
834
+
835
+ if self._database is not None:
836
+ merged_properties["sql.current-database"] = self._database
837
+
838
+ # Cursor-level execution mode properties overlay (always set when applicable)
839
+ if execution_mode.is_snapshot:
840
+ # Ask for snapshot mode behavior -- point-in-time results.
841
+ merged_properties["sql.snapshot.mode"] = "now"
842
+
843
+ return merged_properties
844
+
742
845
  def _execute_statement(
743
846
  self,
744
847
  statement: str,
745
848
  execution_mode: ExecutionMode,
746
849
  statement_name: str | None = None,
747
850
  statement_label: str | None = None,
851
+ properties: PropertiesDict | None = None,
748
852
  ) -> dict[str, Any]:
749
853
  """
750
854
  Execute a SQL statement and return the response.
@@ -755,28 +859,25 @@ class Connection:
755
859
  statement_name: Optional name for the statement (defaults to 'dbapi-{uuid}')
756
860
  statement_label: Optional label for the statement for easier identification in
757
861
  server logs and UIs (defaults to None).
862
+ properties: Optional dictionary of statement properties to set for this execution.
863
+ Keys must be strings, values must be str, int, or bool. System
864
+ properties (sql.current-catalog, sql.current-database, sql.snapshot.mode)
865
+ are always set by the driver and cannot be overridden.
758
866
 
759
867
  Returns:
760
868
  Dictionary containing the API response
761
869
 
762
870
  Raises:
763
871
  OperationalError: If statement execution fails
872
+ InterfaceError: If properties parameter is invalid
764
873
  """
765
874
 
766
875
  # Create the statement payload as per Flink SQL API documentation
767
876
  if statement_name is None:
768
877
  statement_name = f"dbapi-{str(uuid.uuid4())}"
769
878
 
770
- # Each connection uses a single environment, also
771
- # called catalog, so we set the property here
772
- properties = {"sql.current-catalog": self.environment}
773
-
774
- if self._dbname is not None:
775
- properties["sql.current-database"] = self._dbname
776
-
777
- if execution_mode.is_snapshot:
778
- # Ask for snapshot mode behavior -- point-in-time results.
779
- properties["sql.snapshot.mode"] = "now"
879
+ # Resolve and merge user properties with system properties
880
+ merged_properties = self._resolve_properties(properties, execution_mode)
780
881
 
781
882
  payload = {
782
883
  "name": statement_name,
@@ -784,7 +885,7 @@ class Connection:
784
885
  "environment_id": self.environment,
785
886
  "spec": {
786
887
  "statement": statement,
787
- "properties": properties,
888
+ "properties": merged_properties,
788
889
  "compute_pool_id": self.compute_pool_id,
789
890
  "stopped": False,
790
891
  },
@@ -31,7 +31,7 @@ from .result_readers import (
31
31
  ResultTupleOrDict,
32
32
  )
33
33
  from .statement import Statement
34
- from .types import convert_statement_parameters
34
+ from .types import PropertiesDict, convert_statement_parameters
35
35
 
36
36
  if TYPE_CHECKING:
37
37
  from .connection import Connection
@@ -118,9 +118,7 @@ class Cursor:
118
118
 
119
119
  # Statement execution state
120
120
  self._statement: Statement | None = None
121
- self._result_reader: AppendOnlyResultReader | ChangelogEventReader | None = (
122
- None
123
- )
121
+ self._result_reader: AppendOnlyResultReader | ChangelogEventReader | None = None
124
122
 
125
123
  @property
126
124
  def description(self) -> list[tuple] | None:
@@ -158,9 +156,7 @@ class Cursor:
158
156
  f"arraysize must be a positive integer, got {type(value).__name__}"
159
157
  )
160
158
  if value <= 0:
161
- raise InterfaceError(
162
- f"arraysize must be a positive integer, got {value}"
163
- )
159
+ raise InterfaceError(f"arraysize must be a positive integer, got {value}")
164
160
  self._arraysize = value
165
161
 
166
162
  @property
@@ -186,6 +182,7 @@ class Cursor:
186
182
  timeout: int = 3000,
187
183
  statement_name: str | None = None,
188
184
  statement_label: str | None = None,
185
+ properties: PropertiesDict | None = None,
189
186
  ) -> None:
190
187
  """
191
188
  Execute a SQL statement.
@@ -203,9 +200,13 @@ class Cursor:
203
200
  need to provide the label value itself (e.g., "my-batch-job").
204
201
  Use Connection.list_statements(label=...) to retrieve statements
205
202
  by label.
203
+ properties: Optional dictionary of statement properties to set for this execution.
204
+ Keys must be strings, values must be str, int, or bool. System
205
+ properties (sql.current-catalog, sql.current-database, sql.snapshot.mode)
206
+ are always set by the driver and cannot be overridden.
206
207
 
207
208
  Raises:
208
- InterfaceError: If the cursor is closed
209
+ InterfaceError: If the cursor is closed, or if invalid properties are provided.
209
210
  ProgrammingError: If the SQL statement is invalid
210
211
  OperationalError: If the statement cannot be executed
211
212
  """
@@ -235,7 +236,7 @@ class Cursor:
235
236
 
236
237
  # Now submit the statement ...
237
238
  self._statement = self._submit_statement(
238
- statement_text, parameters, statement_name, statement_label
239
+ statement_text, parameters, statement_name, statement_label, properties
239
240
  )
240
241
 
241
242
  if self._statement.is_failed:
@@ -354,9 +355,7 @@ class Cursor:
354
355
  self._raise_if_closed()
355
356
  self._raise_if_ddl_mode()
356
357
 
357
- return self._get_result_reader().fetchmany(
358
- size if size is not None else self.arraysize
359
- )
358
+ return self._get_result_reader().fetchmany(size if size is not None else self.arraysize)
360
359
 
361
360
  def fetchall(self) -> list[ResultRow]:
362
361
  """
@@ -743,6 +742,7 @@ class Cursor:
743
742
  parameters: tuple | list | None = None,
744
743
  statement_name: str | None = None,
745
744
  statement_label: str | None = None,
745
+ properties: PropertiesDict | None = None,
746
746
  ) -> Statement:
747
747
  """
748
748
  Submit a SQL statement for execution.
@@ -754,6 +754,7 @@ class Cursor:
754
754
  not provided)
755
755
  statement_label: Optional label for the statement for easier identification in
756
756
  server logs and UIs (defaults to None)
757
+ properties: Optional dictionary of statement properties (optional)
757
758
 
758
759
  Returns:
759
760
  The submitted Statement object
@@ -761,6 +762,7 @@ class Cursor:
761
762
  Raises:
762
763
  OperationalError: If statement submission fails
763
764
  ProgrammingError: If template parameter interpolation fails
765
+ InterfaceError: If the provided properties are invalid
764
766
  """
765
767
  logger.info(f"Submitting statement {statement_text}")
766
768
 
@@ -773,6 +775,7 @@ class Cursor:
773
775
  self._execution_mode,
774
776
  statement_name,
775
777
  statement_label,
778
+ properties,
776
779
  )
777
780
  return Statement.from_response(self._connection, response)
778
781