e6data-python-connector 3.0.0__tar.gz → 3.0.1rc2__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.
- {e6data_python_connector-3.0.0/e6data_python_connector.egg-info → e6data_python_connector-3.0.1rc2}/PKG-INFO +134 -2
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/README.md +133 -1
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection.py +53 -2
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_connection_pool.py +2 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cursor.py +379 -19
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_dialect.py +2 -1
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_manager.py +3 -1
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/connection_pool.py +11 -9
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/dialect.py +7 -2
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6data_grpc.py +715 -108
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_batch.py +49 -0
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode.py +469 -0
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_decode_worker.py +50 -0
- e6data_python_connector-3.0.1rc2/e6data_python_connector/result_prefetch.py +421 -0
- e6data_python_connector-3.0.1rc2/e6data_python_connector/server/e6x_engine_pb2.py +209 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2_grpc.py +43 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2/e6data_python_connector.egg-info}/PKG-INFO +134 -2
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/SOURCES.txt +7 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/setup.py +1 -1
- e6data_python_connector-3.0.1rc2/test/integration/benchmark_result_batch_v2.py +149 -0
- e6data_python_connector-3.0.1rc2/test/integration/result_batch_benchmark_support.py +196 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/support.py +3 -0
- e6data_python_connector-3.0.1rc2/test/integration/test_result_batch_v2.py +120 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool.py +3 -0
- e6data_python_connector-3.0.0/e6data_python_connector/server/e6x_engine_pb2.py +0 -207
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/LICENSE +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/MANIFEST.in +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/__init__.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/aio.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_cluster_manager.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_oauth.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/async_work.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/__init__.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/cluster_server/cluster_pb2_grpc.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/common.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/constants.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/datainputstream.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/date_time_utils.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/__init__.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/constants.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/e6x_vector/ttypes.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/exceptions.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_cache.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/oauth_common.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/QueryEngineService.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/__init__.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/constants.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/e6x_engine_pb2.pyi +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/server/ttypes.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/strategy.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector/typeId.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/dependency_links.txt +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/entry_points.txt +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/requires.txt +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/e6data_python_connector.egg-info/top_level.txt +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/pyproject.toml +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/setup.cfg +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/__init__.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_38_nines.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_all_cases.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_binary.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_correct_value.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/analyze_fields.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/check_decimal_errors.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/cleanup_test_files.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/conftest.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/debug_38_nines.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/debug_binary.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/final_test.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/__init__.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_preflight.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/benchmark_sync_async.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/expiry_preflight.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_connection.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_pool.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_queries.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_resume.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_sqlalchemy.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_async_token_lifecycle.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_auth_boundary.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_oauth_continuation.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_e2e_readonly_matrix.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_shared_token_issuance.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/test_sync_oauth_lifecycle.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/integration/token_observation.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/move_tests.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/quick_test.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_38_nines.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_all_decimal128_cases.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_efficiency.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_none_strategy.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_oauth.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_cluster_manager_strategy.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_code_optimizations.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_comprehensive.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_connection_pool_e2e.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_current_implementation.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_binary_parsing.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_decimal128_parsing.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_dialect_columns.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_fix.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_improved_parsing.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_known_case.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_manual_analysis.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_mock_server.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_multiprocessing_fix.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_new_implementation.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_oauth_authentication.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_oauth_auto_resume.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_oauth_resume_integration.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_pool_concurrency_simple.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_pool_threading_multiprocessing.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_simple_connection_pool.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_specific_binary.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy_logic.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy_persistence_fix.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_strategy_sharing_fix.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/test_user_binary_value.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/tests.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/tests_grpc.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/validate_decimal128.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/validate_implementation.py +0 -0
- {e6data_python_connector-3.0.0 → e6data_python_connector-3.0.1rc2}/test/verify_decimal_fix.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: e6data-python-connector
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.1rc2
|
|
4
4
|
Summary: Client for the e6data distributed SQL Engine.
|
|
5
5
|
Home-page: https://github.com/e6x-labs/e6data-python-connector
|
|
6
6
|
Author: e6data, Inc.
|
|
@@ -469,6 +469,122 @@ database = '<new_database_name>' # Replace with the new database.
|
|
|
469
469
|
cursor = conn.cursor(database, catalog_name)
|
|
470
470
|
```
|
|
471
471
|
|
|
472
|
+
### Opt in to multi-chunk result batches
|
|
473
|
+
|
|
474
|
+
Set `enable_result_batch_v2=True` to enable V2 batches, continuous result downloading,
|
|
475
|
+
and bounded parallel chunk decoding together. It defaults to `False`. There
|
|
476
|
+
are no separate public prefetch or decoding switches. `arraysize` and
|
|
477
|
+
`fetchmany(size)` only change the number of rows returned to the application.
|
|
478
|
+
|
|
479
|
+
The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
|
|
480
|
+
`ENABLE_GET_NEXT_CHUNK_V2` controls a separate executor-to-planner boundary.
|
|
481
|
+
These flags do not extend the planner's query lifetime. Finishing a large result
|
|
482
|
+
within 900 seconds still needs a measurement on the target deployment.
|
|
483
|
+
|
|
484
|
+
The optimized path requires ordinary GIL-enabled CPython 3.11 through 3.13 and
|
|
485
|
+
an import-safe application entry point. Put application startup inside
|
|
486
|
+
`if __name__ == "__main__":`, including when using SQLAlchemy. Interactive,
|
|
487
|
+
daemon and frozen applications are rejected before query submission. Two
|
|
488
|
+
workers use the explicit `spawn` method; the application's global multiprocessing
|
|
489
|
+
start method is unchanged. An unguarded main script can run its other import
|
|
490
|
+
side effects, so keep all application startup inside the guard.
|
|
491
|
+
|
|
492
|
+
Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
|
|
493
|
+
custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
|
|
494
|
+
flag-off path keeps its existing options. For async, set
|
|
495
|
+
`max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
|
|
496
|
+
Choose limits from measured envelope sizes and the client's memory budget.
|
|
497
|
+
|
|
498
|
+
These examples use your existing `connection_options`, `sql`, and `consume`:
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
from e6data_python_connector import Connection
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def main():
|
|
505
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
506
|
+
with Connection(**options) as connection:
|
|
507
|
+
with connection.cursor() as cursor:
|
|
508
|
+
cursor.execute(sql)
|
|
509
|
+
for rows in cursor.fetchall_buffer():
|
|
510
|
+
consume(rows)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
main()
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
import asyncio
|
|
519
|
+
from e6data_python_connector.aio import AsyncConnection
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
async def main():
|
|
523
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
524
|
+
async with AsyncConnection(**options) as connection:
|
|
525
|
+
async with connection.cursor() as cursor:
|
|
526
|
+
await cursor.execute(sql)
|
|
527
|
+
async for rows in cursor.fetchall_buffer():
|
|
528
|
+
consume(rows)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
if __name__ == "__main__":
|
|
532
|
+
asyncio.run(main())
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
|
|
536
|
+
inside the guarded application entry point:
|
|
537
|
+
|
|
538
|
+
```python
|
|
539
|
+
connect_args = {**connection_options, "enable_result_batch_v2": True}
|
|
540
|
+
engine = create_engine("e6data://", connect_args=connect_args)
|
|
541
|
+
async_engine = create_async_engine("e6data+asyncio://", connect_args=connect_args)
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
After the first V2 response, a download thread (sync) or task (async) keeps
|
|
545
|
+
fetching serialized batches until the engine returns end-of-stream. Decoding
|
|
546
|
+
runs concurrently and takes batches from the queue in order. Downloading does
|
|
547
|
+
not wait for decoding or application row processing. There is no queue-size
|
|
548
|
+
limit and no process-wide limit on active downloaders. Result RPCs for the same
|
|
549
|
+
query never overlap.
|
|
550
|
+
|
|
551
|
+
Each background RPC gets a fresh configured transport budget when it starts;
|
|
552
|
+
that deadline never extends after dispatch. An empty nonterminal response uses
|
|
553
|
+
backoff with a finite no-progress deadline. Completed responses can wait while
|
|
554
|
+
the application is idle. Public fetch deadlines still cover waiting and decoding,
|
|
555
|
+
and expiry or cancellation stops the downloader. Async `fetchall` keeps one
|
|
556
|
+
deadline for its entire operation.
|
|
557
|
+
|
|
558
|
+
Two shared worker processes can decode one multi-chunk envelope at a time.
|
|
559
|
+
Other envelopes use the sequential path. Workers receive chunk bytes and column
|
|
560
|
+
positions, not connections or credentials. Process startup and copying add cost,
|
|
561
|
+
so parallel decoding is not a promise of better performance for every result.
|
|
562
|
+
|
|
563
|
+
Fetch return shapes and row order stay the same. `fetchall_buffer()` yields the
|
|
564
|
+
original decoded chunks. All chunks in an envelope must decode successfully
|
|
565
|
+
before any are exposed. The serialized queue can hold the entire result when
|
|
566
|
+
downloading is faster than decoding or application reads. Workers add Python
|
|
567
|
+
objects and process copies; the receive limit is not a limit on total memory.
|
|
568
|
+
`fetchall()` still retains the full result.
|
|
569
|
+
|
|
570
|
+
Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
|
|
571
|
+
optimizations. Other RPC failures reach the caller as the original
|
|
572
|
+
`grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
|
|
573
|
+
its envelope is needed, after current valid chunks have been consumed. Failed
|
|
574
|
+
or cancelled queries are not replayed. Later fetches on an incomplete result
|
|
575
|
+
raise `IncompleteResultError`. Clear, cancel, close and pool return retire
|
|
576
|
+
pending work; unsafe connections must not be reused.
|
|
577
|
+
|
|
578
|
+
Use `debug=True` for payload-free RPC, decode and worker diagnostics. Serialized
|
|
579
|
+
Protobuf bytes are uncompressed payload size, not bytes sent over the network.
|
|
580
|
+
|
|
581
|
+
For rollback, close old cursors and connections, then omit
|
|
582
|
+
`enable_result_batch_v2` or set it to `False` on new connections. Closing the
|
|
583
|
+
last V2 connection also stops its shared workers. See the
|
|
584
|
+
[qualification instructions](test/README.md#result-batch-v2-qualification).
|
|
585
|
+
Local synthetic results do not prove customer performance or the 900-second
|
|
586
|
+
query requirement.
|
|
587
|
+
|
|
472
588
|
### Get Query Time Metrics
|
|
473
589
|
```python
|
|
474
590
|
import json
|
|
@@ -914,11 +1030,27 @@ conn = Connection(
|
|
|
914
1030
|
```
|
|
915
1031
|
|
|
916
1032
|
When `debug=True`, the following features are enabled:
|
|
917
|
-
-
|
|
1033
|
+
- Connector logging at DEBUG level, for both sync and async connections. Existing
|
|
1034
|
+
application logging handlers and the root logger level are kept.
|
|
918
1035
|
- Blue-green strategy transition logging
|
|
919
1036
|
- Connection lifecycle logging
|
|
920
1037
|
- Query execution detailed logging
|
|
921
1038
|
|
|
1039
|
+
Each result fetch writes a normal debug message with `protocol`, `rpc_seconds`,
|
|
1040
|
+
`decode_seconds`, `chunk_count`, `serialized_bytes`, and `status`. The timing values
|
|
1041
|
+
are seconds. `rpc_seconds` measures the client RPC call, including waiting for its
|
|
1042
|
+
response. `decode_seconds` measures client response processing. `serialized_bytes`
|
|
1043
|
+
is the protobuf response size, not the number of bytes on the network. A failed
|
|
1044
|
+
attempt reports zero response size and chunk count. The same values remain available
|
|
1045
|
+
as `result_batch_*` attributes for structured log handlers.
|
|
1046
|
+
|
|
1047
|
+
These metrics do not add SQL, credentials, session IDs, result values, or raw metadata
|
|
1048
|
+
to logs. `debug=False` keeps the normal quiet default. Applications can also enable
|
|
1049
|
+
DEBUG on the `e6data_python_connector` logger themselves. `debug=True` adds a connector
|
|
1050
|
+
stream handler only when no existing handler in its logger path accepts DEBUG.
|
|
1051
|
+
As with normal Python logging, enabling the package logger also affects other open
|
|
1052
|
+
connector connections in that process.
|
|
1053
|
+
|
|
922
1054
|
### gRPC Network Tracing
|
|
923
1055
|
|
|
924
1056
|
For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
|
|
@@ -423,6 +423,122 @@ database = '<new_database_name>' # Replace with the new database.
|
|
|
423
423
|
cursor = conn.cursor(database, catalog_name)
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
+
### Opt in to multi-chunk result batches
|
|
427
|
+
|
|
428
|
+
Set `enable_result_batch_v2=True` to enable V2 batches, continuous result downloading,
|
|
429
|
+
and bounded parallel chunk decoding together. It defaults to `False`. There
|
|
430
|
+
are no separate public prefetch or decoding switches. `arraysize` and
|
|
431
|
+
`fetchmany(size)` only change the number of rows returned to the application.
|
|
432
|
+
|
|
433
|
+
The planner must support V2 and have `ENABLE_GET_NEXT_RESULT_BATCH_V2` enabled.
|
|
434
|
+
`ENABLE_GET_NEXT_CHUNK_V2` controls a separate executor-to-planner boundary.
|
|
435
|
+
These flags do not extend the planner's query lifetime. Finishing a large result
|
|
436
|
+
within 900 seconds still needs a measurement on the target deployment.
|
|
437
|
+
|
|
438
|
+
The optimized path requires ordinary GIL-enabled CPython 3.11 through 3.13 and
|
|
439
|
+
an import-safe application entry point. Put application startup inside
|
|
440
|
+
`if __name__ == "__main__":`, including when using SQLAlchemy. Interactive,
|
|
441
|
+
daemon and frozen applications are rejected before query submission. Two
|
|
442
|
+
workers use the explicit `spawn` method; the application's global multiprocessing
|
|
443
|
+
start method is unchanged. An unguarded main script can run its other import
|
|
444
|
+
side effects, so keep all application startup inside the guard.
|
|
445
|
+
|
|
446
|
+
Both APIs default to a 64 MiB receive limit when V2 is enabled. A positive finite
|
|
447
|
+
custom limit is allowed; unlimited receive sizes are rejected for V2. The sync
|
|
448
|
+
flag-off path keeps its existing options. For async, set
|
|
449
|
+
`max_receive_message_bytes`; any receive limit in `grpc_options` must agree.
|
|
450
|
+
Choose limits from measured envelope sizes and the client's memory budget.
|
|
451
|
+
|
|
452
|
+
These examples use your existing `connection_options`, `sql`, and `consume`:
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
from e6data_python_connector import Connection
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def main():
|
|
459
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
460
|
+
with Connection(**options) as connection:
|
|
461
|
+
with connection.cursor() as cursor:
|
|
462
|
+
cursor.execute(sql)
|
|
463
|
+
for rows in cursor.fetchall_buffer():
|
|
464
|
+
consume(rows)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
if __name__ == "__main__":
|
|
468
|
+
main()
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
import asyncio
|
|
473
|
+
from e6data_python_connector.aio import AsyncConnection
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
async def main():
|
|
477
|
+
options = {**connection_options, "enable_result_batch_v2": True}
|
|
478
|
+
async with AsyncConnection(**options) as connection:
|
|
479
|
+
async with connection.cursor() as cursor:
|
|
480
|
+
await cursor.execute(sql)
|
|
481
|
+
async for rows in cursor.fetchall_buffer():
|
|
482
|
+
consume(rows)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
asyncio.run(main())
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
SQLAlchemy accepts the same flag through `connect_args`. Create and use engines
|
|
490
|
+
inside the guarded application entry point:
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
connect_args = {**connection_options, "enable_result_batch_v2": True}
|
|
494
|
+
engine = create_engine("e6data://", connect_args=connect_args)
|
|
495
|
+
async_engine = create_async_engine("e6data+asyncio://", connect_args=connect_args)
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
After the first V2 response, a download thread (sync) or task (async) keeps
|
|
499
|
+
fetching serialized batches until the engine returns end-of-stream. Decoding
|
|
500
|
+
runs concurrently and takes batches from the queue in order. Downloading does
|
|
501
|
+
not wait for decoding or application row processing. There is no queue-size
|
|
502
|
+
limit and no process-wide limit on active downloaders. Result RPCs for the same
|
|
503
|
+
query never overlap.
|
|
504
|
+
|
|
505
|
+
Each background RPC gets a fresh configured transport budget when it starts;
|
|
506
|
+
that deadline never extends after dispatch. An empty nonterminal response uses
|
|
507
|
+
backoff with a finite no-progress deadline. Completed responses can wait while
|
|
508
|
+
the application is idle. Public fetch deadlines still cover waiting and decoding,
|
|
509
|
+
and expiry or cancellation stops the downloader. Async `fetchall` keeps one
|
|
510
|
+
deadline for its entire operation.
|
|
511
|
+
|
|
512
|
+
Two shared worker processes can decode one multi-chunk envelope at a time.
|
|
513
|
+
Other envelopes use the sequential path. Workers receive chunk bytes and column
|
|
514
|
+
positions, not connections or credentials. Process startup and copying add cost,
|
|
515
|
+
so parallel decoding is not a promise of better performance for every result.
|
|
516
|
+
|
|
517
|
+
Fetch return shapes and row order stay the same. `fetchall_buffer()` yields the
|
|
518
|
+
original decoded chunks. All chunks in an envelope must decode successfully
|
|
519
|
+
before any are exposed. The serialized queue can hold the entire result when
|
|
520
|
+
downloading is faster than decoding or application reads. Workers add Python
|
|
521
|
+
objects and process copies; the receive limit is not a limit on total memory.
|
|
522
|
+
`fetchall()` still retains the full result.
|
|
523
|
+
|
|
524
|
+
Only a V2 `UNIMPLEMENTED` response switches that query to V1 and disables its
|
|
525
|
+
optimizations. Other RPC failures reach the caller as the original
|
|
526
|
+
`grpc.RpcError` or `grpc.aio.AioRpcError`. A failed pending RPC is raised when
|
|
527
|
+
its envelope is needed, after current valid chunks have been consumed. Failed
|
|
528
|
+
or cancelled queries are not replayed. Later fetches on an incomplete result
|
|
529
|
+
raise `IncompleteResultError`. Clear, cancel, close and pool return retire
|
|
530
|
+
pending work; unsafe connections must not be reused.
|
|
531
|
+
|
|
532
|
+
Use `debug=True` for payload-free RPC, decode and worker diagnostics. Serialized
|
|
533
|
+
Protobuf bytes are uncompressed payload size, not bytes sent over the network.
|
|
534
|
+
|
|
535
|
+
For rollback, close old cursors and connections, then omit
|
|
536
|
+
`enable_result_batch_v2` or set it to `False` on new connections. Closing the
|
|
537
|
+
last V2 connection also stops its shared workers. See the
|
|
538
|
+
[qualification instructions](test/README.md#result-batch-v2-qualification).
|
|
539
|
+
Local synthetic results do not prove customer performance or the 900-second
|
|
540
|
+
query requirement.
|
|
541
|
+
|
|
426
542
|
### Get Query Time Metrics
|
|
427
543
|
```python
|
|
428
544
|
import json
|
|
@@ -868,11 +984,27 @@ conn = Connection(
|
|
|
868
984
|
```
|
|
869
985
|
|
|
870
986
|
When `debug=True`, the following features are enabled:
|
|
871
|
-
-
|
|
987
|
+
- Connector logging at DEBUG level, for both sync and async connections. Existing
|
|
988
|
+
application logging handlers and the root logger level are kept.
|
|
872
989
|
- Blue-green strategy transition logging
|
|
873
990
|
- Connection lifecycle logging
|
|
874
991
|
- Query execution detailed logging
|
|
875
992
|
|
|
993
|
+
Each result fetch writes a normal debug message with `protocol`, `rpc_seconds`,
|
|
994
|
+
`decode_seconds`, `chunk_count`, `serialized_bytes`, and `status`. The timing values
|
|
995
|
+
are seconds. `rpc_seconds` measures the client RPC call, including waiting for its
|
|
996
|
+
response. `decode_seconds` measures client response processing. `serialized_bytes`
|
|
997
|
+
is the protobuf response size, not the number of bytes on the network. A failed
|
|
998
|
+
attempt reports zero response size and chunk count. The same values remain available
|
|
999
|
+
as `result_batch_*` attributes for structured log handlers.
|
|
1000
|
+
|
|
1001
|
+
These metrics do not add SQL, credentials, session IDs, result values, or raw metadata
|
|
1002
|
+
to logs. `debug=False` keeps the normal quiet default. Applications can also enable
|
|
1003
|
+
DEBUG on the `e6data_python_connector` logger themselves. `debug=True` adds a connector
|
|
1004
|
+
stream handler only when no existing handler in its logger path accepts DEBUG.
|
|
1005
|
+
As with normal Python logging, enabling the package logger also affects other open
|
|
1006
|
+
connector connections in that process.
|
|
1007
|
+
|
|
876
1008
|
### gRPC Network Tracing
|
|
877
1009
|
|
|
878
1010
|
For low-level gRPC network debugging (HTTP/2 frames, TCP events), set environment variables **before** running your Python script:
|
|
@@ -50,7 +50,11 @@ class AsyncConnection:
|
|
|
50
50
|
token_url=None, oauth_scope=None, access_token=None,
|
|
51
51
|
client_auth_method='basic', *, operation_timeout=600.0,
|
|
52
52
|
oauth_timeout=10.0, cleanup_timeout=10.0, auto_resume_timeout=300.0,
|
|
53
|
-
max_receive_message_bytes=64 * 1024 * 1024
|
|
53
|
+
max_receive_message_bytes=64 * 1024 * 1024,
|
|
54
|
+
enable_result_batch_v2=False):
|
|
55
|
+
from .result_decode_worker import is_decode_worker
|
|
56
|
+
if is_decode_worker():
|
|
57
|
+
raise RuntimeError('A result decode worker cannot create a connection.')
|
|
54
58
|
if sys.version_info < (3, 11):
|
|
55
59
|
raise RuntimeError('The async API requires Python 3.11 or newer.')
|
|
56
60
|
try:
|
|
@@ -78,13 +82,19 @@ class AsyncConnection:
|
|
|
78
82
|
raise ValueError('Async OAuth requires verified TLS (secure=True).')
|
|
79
83
|
if ssl_cert is not None and not isinstance(ssl_cert, (str, bytes)):
|
|
80
84
|
raise ValueError('ssl_cert must be a certificate path or PEM bytes.')
|
|
85
|
+
if not isinstance(enable_result_batch_v2, bool):
|
|
86
|
+
raise ValueError('enable_result_batch_v2 must be a boolean.')
|
|
87
|
+
if enable_result_batch_v2:
|
|
88
|
+
from .result_decode import validate_decode_runtime
|
|
89
|
+
validate_decode_runtime()
|
|
81
90
|
settings = dict(host=host, port=port, username=username, password=password,
|
|
82
91
|
catalog=catalog, database=database, cluster_name=cluster_name,
|
|
83
92
|
secure=bool(secure), ssl_cert=ssl_cert, auto_resume=auto_resume,
|
|
84
93
|
scheme=scheme, debug=debug, require_fastbinary=require_fastbinary,
|
|
85
94
|
client_id=client_id, client_secret=client_secret, token_url=token_url,
|
|
86
95
|
oauth_scope=oauth_scope, access_token=access_token,
|
|
87
|
-
client_auth_method=client_auth_method
|
|
96
|
+
client_auth_method=client_auth_method,
|
|
97
|
+
enable_result_batch_v2=enable_result_batch_v2)
|
|
88
98
|
for name, value in [('operation_timeout', operation_timeout), ('oauth_timeout', oauth_timeout),
|
|
89
99
|
('cleanup_timeout', cleanup_timeout), ('auto_resume_timeout', auto_resume_timeout)]:
|
|
90
100
|
settings[name] = validate_positive_timeout(value, name)
|
|
@@ -103,6 +113,9 @@ class AsyncConnection:
|
|
|
103
113
|
settings['grpc_options'] = MappingProxyType(options)
|
|
104
114
|
settings['max_receive_message_bytes'] = max_receive_message_bytes
|
|
105
115
|
self._config = MappingProxyType(settings)
|
|
116
|
+
if debug:
|
|
117
|
+
from .e6data_grpc import _configure_debug_logging
|
|
118
|
+
_configure_debug_logging()
|
|
106
119
|
self._oauth_enabled = not modes[0]
|
|
107
120
|
self._channel = self._client = self._token_provider = None
|
|
108
121
|
self._owns_token_provider = True
|
|
@@ -125,6 +138,11 @@ class AsyncConnection:
|
|
|
125
138
|
self._cleanup_error = None
|
|
126
139
|
self._cluster_manager = None
|
|
127
140
|
self._owns_cluster_manager = True
|
|
141
|
+
self._decoder_started = False
|
|
142
|
+
self._decoder_lease = None
|
|
143
|
+
if enable_result_batch_v2:
|
|
144
|
+
from .result_decode import DecoderLease
|
|
145
|
+
self._decoder_lease = DecoderLease()
|
|
128
146
|
|
|
129
147
|
def __getattr__(self, name):
|
|
130
148
|
config = self.__dict__.get('_config', {})
|
|
@@ -300,6 +318,24 @@ class AsyncConnection:
|
|
|
300
318
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
301
319
|
return metadata
|
|
302
320
|
|
|
321
|
+
async def _ensure_decoder(self, deadline):
|
|
322
|
+
self._check_owner()
|
|
323
|
+
if self._decoder_lease is not None and not self._decoder_started:
|
|
324
|
+
from .async_work import run_blocking
|
|
325
|
+
await run_blocking(self._decoder_lease.start, deadline, deadline=deadline)
|
|
326
|
+
self._check_owner()
|
|
327
|
+
self._decoder_started = True
|
|
328
|
+
|
|
329
|
+
async def _raw_result_rpc(self, method_name, request, metadata, deadline):
|
|
330
|
+
"""Transport only: the owning task already froze identity and metadata."""
|
|
331
|
+
call = getattr(self._client, method_name)(
|
|
332
|
+
request, metadata=metadata, timeout=self._remaining(deadline))
|
|
333
|
+
self._calls.add(call)
|
|
334
|
+
try:
|
|
335
|
+
return await call
|
|
336
|
+
finally:
|
|
337
|
+
self._calls.discard(call)
|
|
338
|
+
|
|
303
339
|
async def _rpc(self, method_name, request, *, deadline, route=None, safe_retry=False, _cleanup=False, _on_dispatch=None, _response_metadata=None):
|
|
304
340
|
self._check_owner(_cleanup=_cleanup, _internal=True)
|
|
305
341
|
refreshed = switched = False
|
|
@@ -500,6 +536,15 @@ class AsyncConnection:
|
|
|
500
536
|
# Native channel close with no grace cancels transports without waiting.
|
|
501
537
|
if self._channel is not None:
|
|
502
538
|
await self._channel.close()
|
|
539
|
+
if self._decoder_lease is not None:
|
|
540
|
+
# Retirement must survive loop shutdown and saturated work slots.
|
|
541
|
+
self._decoder_lease.retire(deadline)
|
|
542
|
+
try:
|
|
543
|
+
async with asyncio.timeout_at(deadline):
|
|
544
|
+
while self._decoder_lease.cleanup_pending:
|
|
545
|
+
await asyncio.sleep(min(0.005, self._remaining(deadline)))
|
|
546
|
+
except (Exception, asyncio.CancelledError):
|
|
547
|
+
self._cleanup_error = OperationalError('Result decoder cleanup remains pending.')
|
|
503
548
|
for resource, owned in ((self._cluster_manager, self._owns_cluster_manager),
|
|
504
549
|
(self._token_provider, self._owns_token_provider)):
|
|
505
550
|
if owned and resource is not None:
|
|
@@ -538,6 +583,12 @@ class AsyncConnection:
|
|
|
538
583
|
if self._owner is not None and self._owner != (os.getpid(), threading.get_ident(), asyncio.get_running_loop()):
|
|
539
584
|
raise ProgrammingError('Connection belongs to another owner.')
|
|
540
585
|
await self.close()
|
|
586
|
+
if self._decoder_lease is not None and self._decoder_lease.cleanup_pending:
|
|
587
|
+
raise ProgrammingError('Cannot reopen while result decoder cleanup remains pending.')
|
|
588
|
+
if self.enable_result_batch_v2:
|
|
589
|
+
from .result_decode import DecoderLease
|
|
590
|
+
self._decoder_lease = DecoderLease()
|
|
591
|
+
self._decoder_started = False
|
|
541
592
|
self._state = 'new'
|
|
542
593
|
self._session_id = self._session_task = self._close_task = self._open_task = None
|
|
543
594
|
if self._owns_token_provider:
|
|
@@ -316,6 +316,8 @@ class AsyncConnectionPool:
|
|
|
316
316
|
healthy = healthy and cursor._state != 'SUBMISSION_UNKNOWN'
|
|
317
317
|
await cursor._close_owned(deadline)
|
|
318
318
|
healthy = healthy and cursor.cleanup_error is None
|
|
319
|
+
healthy = (healthy and cursor._pending_result is None
|
|
320
|
+
and not cursor._retired_results)
|
|
319
321
|
healthy = (healthy and not connection._routes and not connection._ambiguous_submissions
|
|
320
322
|
and connection._state == 'open')
|
|
321
323
|
except (Exception, asyncio.CancelledError):
|