ray-doris 1.0__py3-none-any.whl
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.
- ray_doris/__init__.py +51 -0
- ray_doris/_api.py +73 -0
- ray_doris/_compat.py +117 -0
- ray_doris/_errors.py +49 -0
- ray_doris/_models.py +292 -0
- ray_doris/_planner.py +401 -0
- ray_doris/_readers.py +396 -0
- ray_doris/_schema.py +152 -0
- ray_doris/_sql.py +116 -0
- ray_doris/datasource.py +161 -0
- ray_doris/py.typed +0 -0
- ray_doris/write/__init__.py +15 -0
- ray_doris/write/api.py +51 -0
- ray_doris/write/connection.py +201 -0
- ray_doris/write/datasink.py +242 -0
- ray_doris/write/metadata.py +367 -0
- ray_doris/write/options.py +148 -0
- ray_doris/write/serialization.py +112 -0
- ray_doris/write/stream_load.py +396 -0
- ray_doris-1.0.dist-info/METADATA +450 -0
- ray_doris-1.0.dist-info/RECORD +24 -0
- ray_doris-1.0.dist-info/WHEEL +4 -0
- ray_doris-1.0.dist-info/licenses/LICENSE +186 -0
- ray_doris-1.0.dist-info/licenses/NOTICE +4 -0
ray_doris/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Apache Doris integration for Ray Data."""
|
|
2
|
+
|
|
3
|
+
from ray_doris._api import read_doris
|
|
4
|
+
from ray_doris._errors import (
|
|
5
|
+
DorisAmbiguousWriteError,
|
|
6
|
+
DorisAuthenticationError,
|
|
7
|
+
DorisConfigurationError,
|
|
8
|
+
DorisError,
|
|
9
|
+
DorisLabelExistsError,
|
|
10
|
+
DorisMetadataError,
|
|
11
|
+
DorisPermissionError,
|
|
12
|
+
DorisPlanningError,
|
|
13
|
+
DorisReadError,
|
|
14
|
+
DorisSchemaError,
|
|
15
|
+
DorisTableCompatibilityError,
|
|
16
|
+
DorisWriteError,
|
|
17
|
+
)
|
|
18
|
+
from ray_doris.datasource import DorisDatasource
|
|
19
|
+
from ray_doris.write import (
|
|
20
|
+
DorisConnection,
|
|
21
|
+
DorisDatasink,
|
|
22
|
+
DorisTable,
|
|
23
|
+
DorisWriteOptions,
|
|
24
|
+
DorisWriteResult,
|
|
25
|
+
write_doris,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"DorisAuthenticationError",
|
|
30
|
+
"DorisAmbiguousWriteError",
|
|
31
|
+
"DorisConfigurationError",
|
|
32
|
+
"DorisConnection",
|
|
33
|
+
"DorisDatasource",
|
|
34
|
+
"DorisDatasink",
|
|
35
|
+
"DorisError",
|
|
36
|
+
"DorisLabelExistsError",
|
|
37
|
+
"DorisMetadataError",
|
|
38
|
+
"DorisPermissionError",
|
|
39
|
+
"DorisPlanningError",
|
|
40
|
+
"DorisReadError",
|
|
41
|
+
"DorisSchemaError",
|
|
42
|
+
"DorisTable",
|
|
43
|
+
"DorisTableCompatibilityError",
|
|
44
|
+
"DorisWriteError",
|
|
45
|
+
"DorisWriteOptions",
|
|
46
|
+
"DorisWriteResult",
|
|
47
|
+
"read_doris",
|
|
48
|
+
"write_doris",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
__version__ = "1.0"
|
ray_doris/_api.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Convenience entry point mirroring Ray Data reader functions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping, Optional, Sequence, cast
|
|
6
|
+
|
|
7
|
+
import ray.data
|
|
8
|
+
from ray.data import Dataset
|
|
9
|
+
|
|
10
|
+
from ray_doris._models import FlightScheme, HttpScheme, QueryPlanPolicy, Transport
|
|
11
|
+
from ray_doris.datasource import DorisDatasource
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_doris(
|
|
15
|
+
*,
|
|
16
|
+
table: str,
|
|
17
|
+
host: str,
|
|
18
|
+
mysql_port: int = 9030,
|
|
19
|
+
http_port: int = 8030,
|
|
20
|
+
flight_port: int = 8070,
|
|
21
|
+
http_scheme: HttpScheme = "http",
|
|
22
|
+
flight_scheme: FlightScheme = "grpc",
|
|
23
|
+
user: str = "root",
|
|
24
|
+
password: str = "",
|
|
25
|
+
password_env: Optional[str] = None,
|
|
26
|
+
columns: Optional[Sequence[str]] = None,
|
|
27
|
+
filter: Optional[str] = None,
|
|
28
|
+
transport: Transport = "mysql",
|
|
29
|
+
on_query_plan_error: QueryPlanPolicy = "single_task",
|
|
30
|
+
tablet_size: int = 1,
|
|
31
|
+
batch_size: int = 10_000,
|
|
32
|
+
connect_timeout: float = 10.0,
|
|
33
|
+
query_plan_timeout: Optional[float] = None,
|
|
34
|
+
http_ca_file: Optional[str] = None,
|
|
35
|
+
client_kwargs: Optional[Mapping[str, Any]] = None,
|
|
36
|
+
flight_options: Optional[Mapping[str, Any]] = None,
|
|
37
|
+
concurrency: Optional[int] = None,
|
|
38
|
+
override_num_blocks: Optional[int] = None,
|
|
39
|
+
ray_remote_args: Optional[Mapping[str, Any]] = None,
|
|
40
|
+
) -> Dataset:
|
|
41
|
+
"""Create a Ray Dataset by reading a Doris table in streaming tablet splits."""
|
|
42
|
+
datasource = DorisDatasource(
|
|
43
|
+
table=table,
|
|
44
|
+
host=host,
|
|
45
|
+
mysql_port=mysql_port,
|
|
46
|
+
http_port=http_port,
|
|
47
|
+
flight_port=flight_port,
|
|
48
|
+
http_scheme=http_scheme,
|
|
49
|
+
flight_scheme=flight_scheme,
|
|
50
|
+
user=user,
|
|
51
|
+
password=password,
|
|
52
|
+
password_env=password_env,
|
|
53
|
+
columns=columns,
|
|
54
|
+
filter=filter,
|
|
55
|
+
transport=transport,
|
|
56
|
+
on_query_plan_error=on_query_plan_error,
|
|
57
|
+
tablet_size=tablet_size,
|
|
58
|
+
batch_size=batch_size,
|
|
59
|
+
connect_timeout=connect_timeout,
|
|
60
|
+
query_plan_timeout=query_plan_timeout,
|
|
61
|
+
http_ca_file=http_ca_file,
|
|
62
|
+
client_kwargs=client_kwargs,
|
|
63
|
+
flight_options=flight_options,
|
|
64
|
+
)
|
|
65
|
+
return cast(
|
|
66
|
+
Dataset,
|
|
67
|
+
ray.data.read_datasource(
|
|
68
|
+
datasource,
|
|
69
|
+
concurrency=concurrency,
|
|
70
|
+
override_num_blocks=override_num_blocks,
|
|
71
|
+
ray_remote_args=dict(ray_remote_args) if ray_remote_args is not None else None,
|
|
72
|
+
),
|
|
73
|
+
)
|
ray_doris/_compat.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Small, explicit compatibility layer for supported Ray V1 releases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Any, Callable, Iterable, Literal, Mapping, Optional
|
|
9
|
+
|
|
10
|
+
import pyarrow as pa
|
|
11
|
+
import ray
|
|
12
|
+
from ray.data.block import BlockMetadata
|
|
13
|
+
from ray.data.datasource import Datasink, ReadTask
|
|
14
|
+
|
|
15
|
+
from ray_doris._errors import DorisConfigurationError
|
|
16
|
+
|
|
17
|
+
_MIN_RAY_VERSION = (2, 49, 2)
|
|
18
|
+
_MAX_RAY_VERSION = (2, 57, 0)
|
|
19
|
+
_RAY_VERSION = ray.__version__
|
|
20
|
+
_FINAL_RELEASE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:\+[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*)?$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def ensure_supported_ray_version() -> None:
|
|
24
|
+
"""Reject Ray releases outside the compatibility matrix before network access."""
|
|
25
|
+
match = _FINAL_RELEASE.fullmatch(_RAY_VERSION)
|
|
26
|
+
release = tuple(int(part) for part in match.groups()) if match is not None else ()
|
|
27
|
+
if not _MIN_RAY_VERSION <= release < _MAX_RAY_VERSION:
|
|
28
|
+
raise DorisConfigurationError(
|
|
29
|
+
"ray-doris supports final Ray releases >=2.49.2,<2.57; "
|
|
30
|
+
"PEP 440 local build suffixes are allowed, but prerelease and post-release builds "
|
|
31
|
+
f"are unsupported; found Ray {_RAY_VERSION!r}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def make_read_task(
|
|
36
|
+
read_fn: Callable[[], Iterable[pa.Table]],
|
|
37
|
+
metadata: BlockMetadata,
|
|
38
|
+
schema: pa.Schema,
|
|
39
|
+
per_task_row_limit: Optional[int],
|
|
40
|
+
) -> ReadTask:
|
|
41
|
+
"""Construct a ReadTask without hiding version or read-function TypeErrors."""
|
|
42
|
+
ensure_supported_ray_version()
|
|
43
|
+
parameters = inspect.signature(ReadTask).parameters
|
|
44
|
+
kwargs: dict[str, Any] = {"read_fn": read_fn, "metadata": metadata}
|
|
45
|
+
if "schema" not in parameters:
|
|
46
|
+
raise DorisConfigurationError(
|
|
47
|
+
"installed Ray ReadTask lacks required schema support within the final "
|
|
48
|
+
"Ray >=2.49.2,<2.57 compatibility window"
|
|
49
|
+
)
|
|
50
|
+
kwargs["schema"] = schema
|
|
51
|
+
if "per_task_row_limit" in parameters:
|
|
52
|
+
kwargs["per_task_row_limit"] = per_task_row_limit
|
|
53
|
+
elif per_task_row_limit is not None:
|
|
54
|
+
raise DorisConfigurationError(
|
|
55
|
+
"this Ray version does not support non-null per_task_row_limit"
|
|
56
|
+
)
|
|
57
|
+
return ReadTask(**kwargs)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class DatasinkCompatibility:
|
|
62
|
+
"""Public Ray Datasink lifecycle facts for one supported Ray generation."""
|
|
63
|
+
|
|
64
|
+
on_write_start_has_schema: bool
|
|
65
|
+
on_write_start_on_empty_dataset: bool
|
|
66
|
+
schema_source: Literal["first_input_bundle", "worker_blocks"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_final_release(version: str) -> tuple[int, int, int]:
|
|
70
|
+
match = _FINAL_RELEASE.fullmatch(version)
|
|
71
|
+
if match is None:
|
|
72
|
+
raise DorisConfigurationError(f"unsupported Ray release format: {version!r}")
|
|
73
|
+
return (int(match.group(1)), int(match.group(2)), int(match.group(3)))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def datasink_compatibility(version: Optional[str] = None) -> DatasinkCompatibility:
|
|
77
|
+
"""Return the explicit Datasink callback contract for a supported Ray release."""
|
|
78
|
+
ensure_supported_ray_version()
|
|
79
|
+
release = _parse_final_release(_RAY_VERSION if version is None else version)
|
|
80
|
+
if release < (2, 53, 0):
|
|
81
|
+
return DatasinkCompatibility(False, True, "worker_blocks")
|
|
82
|
+
return DatasinkCompatibility(True, False, "first_input_bundle")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_datasink_signature() -> DatasinkCompatibility:
|
|
86
|
+
"""Validate the installed public Datasink signature against the release matrix."""
|
|
87
|
+
contract = datasink_compatibility()
|
|
88
|
+
parameters = inspect.signature(Datasink.on_write_start).parameters
|
|
89
|
+
has_schema = "schema" in parameters
|
|
90
|
+
if has_schema != contract.on_write_start_has_schema:
|
|
91
|
+
raise DorisConfigurationError(
|
|
92
|
+
"installed Ray Datasink.on_write_start signature does not match the supported "
|
|
93
|
+
f"Ray {_RAY_VERSION} compatibility contract"
|
|
94
|
+
)
|
|
95
|
+
return contract
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def prepare_write_remote_args(
|
|
99
|
+
ray_remote_args: Optional[Mapping[str, Any]],
|
|
100
|
+
) -> dict[str, Any]:
|
|
101
|
+
"""Freeze write task options and reject configurations that can replay side effects."""
|
|
102
|
+
args = dict(ray_remote_args) if ray_remote_args is not None else {}
|
|
103
|
+
for name in ("max_retries", "max_task_retries"):
|
|
104
|
+
if name not in args:
|
|
105
|
+
continue
|
|
106
|
+
value = args[name]
|
|
107
|
+
if isinstance(value, bool) or not isinstance(value, int) or value != 0:
|
|
108
|
+
raise DorisConfigurationError(
|
|
109
|
+
"Doris writes require Ray write task retry options to be exactly zero"
|
|
110
|
+
)
|
|
111
|
+
if args.get("retry_exceptions"):
|
|
112
|
+
raise DorisConfigurationError(
|
|
113
|
+
"Doris writes do not support Ray exception retries because a request may already "
|
|
114
|
+
"have reached Doris"
|
|
115
|
+
)
|
|
116
|
+
args["max_retries"] = 0
|
|
117
|
+
return args
|
ray_doris/_errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Public exceptions raised by ray-doris."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DorisError(Exception):
|
|
5
|
+
"""Base exception for ray-doris."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DorisConfigurationError(DorisError, ValueError):
|
|
9
|
+
"""Raised when datasource configuration is invalid."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DorisSchemaError(DorisError):
|
|
13
|
+
"""Raised when a Doris schema cannot be represented safely in Arrow."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DorisPlanningError(DorisError):
|
|
17
|
+
"""Raised when tablet planning fails."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DorisAuthenticationError(DorisPlanningError):
|
|
21
|
+
"""Raised when Doris rejects credentials."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DorisPermissionError(DorisPlanningError):
|
|
25
|
+
"""Raised when Doris rejects an operation because permission is missing."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DorisReadError(DorisError):
|
|
29
|
+
"""Raised when a planned split cannot be read."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DorisWriteError(DorisError):
|
|
33
|
+
"""Raised when a Doris Stream Load request has a known failure."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DorisAmbiguousWriteError(DorisWriteError):
|
|
37
|
+
"""Raised when a Stream Load request may have reached Doris with unknown outcome."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DorisLabelExistsError(DorisWriteError):
|
|
41
|
+
"""Raised when Doris reports that a Stream Load label is already retained."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DorisMetadataError(DorisWriteError):
|
|
45
|
+
"""Raised when target table metadata is absent or malformed."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DorisTableCompatibilityError(DorisWriteError):
|
|
49
|
+
"""Raised when the requested operation cannot safely target the table."""
|
ray_doris/_models.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Immutable data passed between the driver and Ray workers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping as MappingABC
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any, Literal, Mapping, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
import pyarrow as pa
|
|
14
|
+
|
|
15
|
+
from ray_doris._errors import DorisConfigurationError
|
|
16
|
+
|
|
17
|
+
Transport = Literal["auto", "mysql", "flight"]
|
|
18
|
+
QueryPlanPolicy = Literal["single_task", "error"]
|
|
19
|
+
HttpScheme = Literal["http", "https"]
|
|
20
|
+
FlightScheme = Literal["grpc", "grpc+tls"]
|
|
21
|
+
|
|
22
|
+
_ADBC_CONNECT_TIMEOUT_OPTION = "adbc.flight.sql.rpc.timeout_seconds.connect"
|
|
23
|
+
_MAX_CONNECT_TIMEOUT_SECONDS = 31_536_000
|
|
24
|
+
_ENVIRONMENT_VARIABLE_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
25
|
+
|
|
26
|
+
_RESERVED_MYSQL_OPTIONS = {
|
|
27
|
+
"charset",
|
|
28
|
+
"connect_timeout",
|
|
29
|
+
"cursorclass",
|
|
30
|
+
"database",
|
|
31
|
+
"db",
|
|
32
|
+
"host",
|
|
33
|
+
"password",
|
|
34
|
+
"passwd",
|
|
35
|
+
"port",
|
|
36
|
+
"user",
|
|
37
|
+
}
|
|
38
|
+
_RESERVED_FLIGHT_OPTIONS = {
|
|
39
|
+
_ADBC_CONNECT_TIMEOUT_OPTION,
|
|
40
|
+
"password",
|
|
41
|
+
"uri",
|
|
42
|
+
"username",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class QualifiedTable:
|
|
48
|
+
"""A validated two-part Doris table name."""
|
|
49
|
+
|
|
50
|
+
database: str
|
|
51
|
+
table: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class DorisInputSplit:
|
|
56
|
+
"""A group of tablets, or an unpartitioned fallback query."""
|
|
57
|
+
|
|
58
|
+
tablet_ids: Optional[Tuple[int, ...]]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class DorisPlan:
|
|
63
|
+
"""The canonical schema and worker splits produced on the Ray driver."""
|
|
64
|
+
|
|
65
|
+
schema: pa.Schema
|
|
66
|
+
splits: Tuple[DorisInputSplit, ...]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class DorisPlanningSnapshot:
|
|
71
|
+
"""Schema and tablet discovery shared across Ray planning calls."""
|
|
72
|
+
|
|
73
|
+
schema: pa.Schema
|
|
74
|
+
tablet_ids: Optional[Tuple[int, ...]]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class DorisReadConfig:
|
|
79
|
+
"""Serializable configuration shared by planning and worker reads."""
|
|
80
|
+
|
|
81
|
+
host: str
|
|
82
|
+
table: QualifiedTable
|
|
83
|
+
user: str
|
|
84
|
+
password: str = field(default="", repr=False)
|
|
85
|
+
password_env: Optional[str] = field(default=None, repr=False)
|
|
86
|
+
mysql_port: int = 9030
|
|
87
|
+
http_port: int = 8030
|
|
88
|
+
flight_port: int = 8070
|
|
89
|
+
http_scheme: HttpScheme = "http"
|
|
90
|
+
http_ca_file: Optional[str] = field(default=None, repr=False)
|
|
91
|
+
flight_scheme: FlightScheme = "grpc"
|
|
92
|
+
columns: Optional[Tuple[str, ...]] = None
|
|
93
|
+
filter: Optional[str] = None
|
|
94
|
+
transport: Transport = "mysql"
|
|
95
|
+
tablet_size: int = 1
|
|
96
|
+
batch_size: int = 10_000
|
|
97
|
+
on_query_plan_error: QueryPlanPolicy = "single_task"
|
|
98
|
+
connect_timeout: float = 10.0
|
|
99
|
+
query_plan_timeout: Optional[float] = None
|
|
100
|
+
client_options: Tuple[Tuple[str, Any], ...] = field(default_factory=tuple, repr=False)
|
|
101
|
+
flight_options: Tuple[Tuple[str, Any], ...] = field(default_factory=tuple, repr=False)
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
"""Validate scalar settings without performing network I/O."""
|
|
105
|
+
if not isinstance(self.host, str) or not self.host.strip():
|
|
106
|
+
raise DorisConfigurationError("host must not be empty")
|
|
107
|
+
if not isinstance(self.user, str) or not self.user:
|
|
108
|
+
raise DorisConfigurationError("user must not be empty")
|
|
109
|
+
if not isinstance(self.password, str):
|
|
110
|
+
raise DorisConfigurationError("password must be a string")
|
|
111
|
+
if self.password_env is not None and (
|
|
112
|
+
not isinstance(self.password_env, str)
|
|
113
|
+
or _ENVIRONMENT_VARIABLE_PATTERN.fullmatch(self.password_env) is None
|
|
114
|
+
):
|
|
115
|
+
raise DorisConfigurationError("password_env must be a portable environment name")
|
|
116
|
+
if self.password and self.password_env is not None:
|
|
117
|
+
raise DorisConfigurationError("password and password_env are mutually exclusive")
|
|
118
|
+
for name, port in (
|
|
119
|
+
("mysql_port", self.mysql_port),
|
|
120
|
+
("http_port", self.http_port),
|
|
121
|
+
("flight_port", self.flight_port),
|
|
122
|
+
):
|
|
123
|
+
if isinstance(port, bool) or not isinstance(port, int) or not 1 <= port <= 65535:
|
|
124
|
+
raise DorisConfigurationError(f"{name} must be between 1 and 65535")
|
|
125
|
+
if (
|
|
126
|
+
isinstance(self.tablet_size, bool)
|
|
127
|
+
or not isinstance(self.tablet_size, int)
|
|
128
|
+
or self.tablet_size <= 0
|
|
129
|
+
):
|
|
130
|
+
raise DorisConfigurationError("tablet_size must be a positive integer")
|
|
131
|
+
if (
|
|
132
|
+
isinstance(self.batch_size, bool)
|
|
133
|
+
or not isinstance(self.batch_size, int)
|
|
134
|
+
or self.batch_size <= 0
|
|
135
|
+
):
|
|
136
|
+
raise DorisConfigurationError("batch_size must be a positive integer")
|
|
137
|
+
self._validate_timeout("connect_timeout", self.connect_timeout)
|
|
138
|
+
if self.query_plan_timeout is not None:
|
|
139
|
+
self._validate_timeout("query_plan_timeout", self.query_plan_timeout)
|
|
140
|
+
if self.transport not in ("auto", "mysql", "flight"):
|
|
141
|
+
raise DorisConfigurationError(f"unsupported transport: {self.transport!r}")
|
|
142
|
+
if self.http_scheme not in ("http", "https"):
|
|
143
|
+
raise DorisConfigurationError(f"unsupported HTTP scheme: {self.http_scheme!r}")
|
|
144
|
+
if self.http_ca_file is not None:
|
|
145
|
+
if not isinstance(self.http_ca_file, str) or not self.http_ca_file:
|
|
146
|
+
raise DorisConfigurationError("http_ca_file must be a non-empty string")
|
|
147
|
+
if self.http_scheme != "https":
|
|
148
|
+
raise DorisConfigurationError("http_ca_file requires http_scheme='https'")
|
|
149
|
+
if self.flight_scheme not in ("grpc", "grpc+tls"):
|
|
150
|
+
raise DorisConfigurationError(f"unsupported Flight SQL scheme: {self.flight_scheme!r}")
|
|
151
|
+
if self.on_query_plan_error not in ("single_task", "error"):
|
|
152
|
+
raise DorisConfigurationError(
|
|
153
|
+
f"unsupported on_query_plan_error: {self.on_query_plan_error!r}"
|
|
154
|
+
)
|
|
155
|
+
self._validate_options("client_options", self.client_options)
|
|
156
|
+
self._validate_options("flight_options", self.flight_options)
|
|
157
|
+
self._reject_reserved_options(
|
|
158
|
+
"client_options", self.client_options, _RESERVED_MYSQL_OPTIONS
|
|
159
|
+
)
|
|
160
|
+
self._reject_reserved_options(
|
|
161
|
+
"flight_options", self.flight_options, _RESERVED_FLIGHT_OPTIONS
|
|
162
|
+
)
|
|
163
|
+
self._validate_mysql_timeouts()
|
|
164
|
+
if any(not isinstance(value, str) for _, value in self.flight_options):
|
|
165
|
+
raise DorisConfigurationError("flight_options values must be strings")
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _validate_timeout(name: str, value: object) -> None:
|
|
169
|
+
if (
|
|
170
|
+
isinstance(value, bool)
|
|
171
|
+
or not isinstance(value, (int, float))
|
|
172
|
+
or value <= 0
|
|
173
|
+
or value > _MAX_CONNECT_TIMEOUT_SECONDS
|
|
174
|
+
or not math.isfinite(value)
|
|
175
|
+
):
|
|
176
|
+
raise DorisConfigurationError(
|
|
177
|
+
f"{name} must be finite, positive, and at most 31536000 seconds"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _validate_mysql_timeouts(self) -> None:
|
|
181
|
+
options = dict(self.client_options)
|
|
182
|
+
for name in ("read_timeout", "write_timeout"):
|
|
183
|
+
if name not in options:
|
|
184
|
+
continue
|
|
185
|
+
value = options[name]
|
|
186
|
+
try:
|
|
187
|
+
finite = (
|
|
188
|
+
not isinstance(value, bool)
|
|
189
|
+
and isinstance(value, (int, float))
|
|
190
|
+
and math.isfinite(value)
|
|
191
|
+
)
|
|
192
|
+
except (TypeError, OverflowError):
|
|
193
|
+
finite = False
|
|
194
|
+
if not finite or value <= 0:
|
|
195
|
+
raise DorisConfigurationError(
|
|
196
|
+
f"client_options {name} must be a finite positive number"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _validate_options(name: str, options: Tuple[Tuple[str, Any], ...]) -> None:
|
|
201
|
+
seen = set()
|
|
202
|
+
for key, _ in options:
|
|
203
|
+
if not isinstance(key, str) or not key:
|
|
204
|
+
raise DorisConfigurationError(f"{name} keys must be non-empty strings")
|
|
205
|
+
if key in seen:
|
|
206
|
+
raise DorisConfigurationError(f"duplicate {name} key: {key!r}")
|
|
207
|
+
seen.add(key)
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _reject_reserved_options(
|
|
211
|
+
name: str, options: Tuple[Tuple[str, Any], ...], reserved: set[str]
|
|
212
|
+
) -> None:
|
|
213
|
+
overridden = sorted(key for key, _ in options if key in reserved)
|
|
214
|
+
if overridden:
|
|
215
|
+
raise DorisConfigurationError(
|
|
216
|
+
f"{name} must not override managed connection options: " + ", ".join(overridden)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def from_options(
|
|
221
|
+
cls,
|
|
222
|
+
*,
|
|
223
|
+
client_options: Optional[Mapping[str, Any]] = None,
|
|
224
|
+
flight_options: Optional[Mapping[str, Any]] = None,
|
|
225
|
+
**kwargs: Any,
|
|
226
|
+
) -> "DorisReadConfig":
|
|
227
|
+
"""Construct a config while snapshotting caller-owned option mappings."""
|
|
228
|
+
return cls(
|
|
229
|
+
client_options=cls._copy_options("client_options", client_options),
|
|
230
|
+
flight_options=cls._copy_options("flight_options", flight_options),
|
|
231
|
+
**kwargs,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _copy_options(
|
|
236
|
+
name: str, options: Optional[Mapping[str, Any]]
|
|
237
|
+
) -> Tuple[Tuple[str, Any], ...]:
|
|
238
|
+
if options is None:
|
|
239
|
+
return ()
|
|
240
|
+
if not isinstance(options, MappingABC):
|
|
241
|
+
raise DorisConfigurationError(f"{name} must be a mapping")
|
|
242
|
+
try:
|
|
243
|
+
copied = deepcopy(dict(options))
|
|
244
|
+
except Exception:
|
|
245
|
+
raise DorisConfigurationError(f"{name} values must be copyable") from None
|
|
246
|
+
return tuple(copied.items())
|
|
247
|
+
|
|
248
|
+
def mysql_options(self) -> Mapping[str, Any]:
|
|
249
|
+
"""Return a fresh mapping for a PyMySQL connection."""
|
|
250
|
+
return deepcopy(dict(self.client_options))
|
|
251
|
+
|
|
252
|
+
def resolve_password(self) -> str:
|
|
253
|
+
"""Resolve a per-process credential without storing it in the config."""
|
|
254
|
+
if self.password_env is None:
|
|
255
|
+
return self.password
|
|
256
|
+
try:
|
|
257
|
+
return os.environ[self.password_env]
|
|
258
|
+
except KeyError:
|
|
259
|
+
raise DorisConfigurationError(
|
|
260
|
+
"configured password environment variable is unavailable"
|
|
261
|
+
) from None
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def effective_query_plan_timeout(self) -> float:
|
|
265
|
+
"""Return the configured query-plan I/O timeout."""
|
|
266
|
+
return self.connect_timeout if self.query_plan_timeout is None else self.query_plan_timeout
|
|
267
|
+
|
|
268
|
+
def adbc_options(self) -> Mapping[str, Any]:
|
|
269
|
+
"""Return a fresh mapping for an ADBC Flight SQL connection."""
|
|
270
|
+
return deepcopy(dict(self.flight_options))
|
|
271
|
+
|
|
272
|
+
def __repr__(self) -> str:
|
|
273
|
+
"""Return a representation that never exposes credentials or option values."""
|
|
274
|
+
rendered_filter = "None" if self.filter is None else "<redacted>"
|
|
275
|
+
rendered_password_env = "None" if self.password_env is None else "<configured>"
|
|
276
|
+
rendered_http_ca = "None" if self.http_ca_file is None else "<configured>"
|
|
277
|
+
return (
|
|
278
|
+
"DorisReadConfig("
|
|
279
|
+
f"host={self.host!r}, table={self.table!r}, user={self.user!r}, "
|
|
280
|
+
f"password=<redacted>, password_env={rendered_password_env}, "
|
|
281
|
+
f"mysql_port={self.mysql_port}, http_port={self.http_port}, "
|
|
282
|
+
f"flight_port={self.flight_port}, http_scheme={self.http_scheme!r}, "
|
|
283
|
+
f"http_ca_file={rendered_http_ca}, "
|
|
284
|
+
f"flight_scheme={self.flight_scheme!r}, columns={self.columns!r}, "
|
|
285
|
+
f"filter={rendered_filter}, transport={self.transport!r}, "
|
|
286
|
+
f"tablet_size={self.tablet_size}, batch_size={self.batch_size}, "
|
|
287
|
+
f"on_query_plan_error={self.on_query_plan_error!r}, "
|
|
288
|
+
f"connect_timeout={self.connect_timeout}, "
|
|
289
|
+
f"query_plan_timeout={self.query_plan_timeout}, "
|
|
290
|
+
f"client_options={tuple(key for key, _ in self.client_options)!r}, "
|
|
291
|
+
f"flight_options={tuple(key for key, _ in self.flight_options)!r})"
|
|
292
|
+
)
|