datus-dws 0.1.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.
@@ -0,0 +1,143 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ *.manifest
32
+ *.spec
33
+
34
+ # Installer logs
35
+ pip-log.txt
36
+ pip-delete-this-directory.txt
37
+
38
+ # Unit test / coverage reports
39
+ htmlcov/
40
+ .tox/
41
+ .nox/
42
+ .coverage
43
+ .coverage.*
44
+ .cache
45
+ nosetests.xml
46
+ coverage.xml
47
+ *.cover
48
+ *.py,cover
49
+ .hypothesis/
50
+ .pytest_cache/
51
+
52
+ # Translations
53
+ *.mo
54
+ *.pot
55
+
56
+ # Django stuff:
57
+ *.log
58
+ local_settings.py
59
+ db.sqlite3
60
+ db.sqlite3-journal
61
+
62
+ # Flask stuff:
63
+ instance/
64
+ .webassets-cache
65
+
66
+ # Scrapy stuff:
67
+ .scrapy
68
+
69
+ # Sphinx documentation
70
+ docs/_build/
71
+
72
+ # PyBuilder
73
+ target/
74
+
75
+ # Jupyter Notebook
76
+ .ipynb_checkpoints
77
+
78
+ # IPython
79
+ profile_default/
80
+ ipython_config.py
81
+
82
+ # pyenv
83
+ .python-version
84
+
85
+ # pipenv
86
+ Pipfile.lock
87
+
88
+ # uv
89
+ uv.lock
90
+
91
+ # PEP 582
92
+ __pypackages__/
93
+
94
+ # Celery stuff
95
+ celerybeat-schedule
96
+ celerybeat.pid
97
+
98
+ # SageMath parsed files
99
+ *.sage.py
100
+
101
+ # Environments
102
+ .env
103
+ .venv
104
+ env/
105
+ venv/
106
+ ENV/
107
+ env.bak/
108
+ venv.bak/
109
+
110
+ # Spyder project settings
111
+ .spyderproject
112
+ .spyproject
113
+
114
+ # Rope project settings
115
+ .ropeproject
116
+
117
+ # mkdocs documentation
118
+ /site
119
+
120
+ # mypy
121
+ .mypy_cache/
122
+ .dmypy.json
123
+ dmypy.json
124
+
125
+ # Pyre type checker
126
+ .pyre/
127
+
128
+ # IDEs
129
+ .vscode/
130
+ .idea/
131
+ *.swp
132
+ *.swo
133
+ *~
134
+
135
+ # OS
136
+ .DS_Store
137
+ Thumbs.db
138
+
139
+
140
+ .omc
141
+
142
+ uv.toml
143
+ */uv.toml
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.5
2
+ Name: datus-dws
3
+ Version: 0.1.0
4
+ Summary: Huawei Cloud GaussDB(DWS) database adapter for Datus
5
+ Project-URL: Homepage, https://github.com/Datus-ai/datus-db-adapters
6
+ Project-URL: Repository, https://github.com/Datus-ai/datus-db-adapters
7
+ Project-URL: Issues, https://github.com/Datus-ai/datus-db-adapters/issues
8
+ Author-email: DatusAI <support@datus.ai>
9
+ License: Apache-2.0
10
+ Keywords: adapter,database,datus,dws,gaussdb,huawei,postgresql
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Requires-Python: >=3.12
17
+ Requires-Dist: datus-db-core>=0.1.6
18
+ Requires-Dist: datus-postgresql>=0.1.8
19
+ Requires-Dist: pydantic>=2.0.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # datus-dws
23
+
24
+ Huawei Cloud GaussDB(DWS) adapter for [Datus](https://github.com/Datus-ai/datus-agent).
25
+
26
+ DWS is a shared-nothing MPP analytical warehouse that speaks the PostgreSQL wire
27
+ protocol. This adapter builds on `datus-postgresql`: it reuses the psycopg2
28
+ transport unchanged and overrides only what DWS does differently — its system
29
+ schema set, its native table-definition function, and the Oracle-compatibility
30
+ semantics of ORA mode.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install datus-dws
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ ```yaml
41
+ datasources:
42
+ dws_analytics:
43
+ type: dws
44
+ host: example.dws.myhuaweicloud.com
45
+ port: 8000
46
+ database: gaussdb
47
+ schema: public
48
+ username: dbadmin
49
+ password: ${DWS_PASSWORD}
50
+ sslmode: verify-ca
51
+ sslrootcert: /path/to/cacert.pem
52
+ timeout_seconds: 30
53
+ ```
54
+
55
+ `host` also accepts the console's `host:port` form, in which case `port` may be
56
+ omitted. The cluster's default database is normally `gaussdb`.
57
+
58
+ ## TLS
59
+
60
+ | `sslmode` | Behaviour |
61
+ |---|---|
62
+ | `prefer` (default) | Encrypts when the server offers it; upgrades automatically if the cluster enforces SSL |
63
+ | `require` | Encrypts without verifying the server certificate |
64
+ | `verify-ca` | Verifies the server certificate against `sslrootcert` |
65
+ | `verify-full` | **Cannot succeed** — see below |
66
+ | `disable` | Fails if the cluster has SSL enforcement switched on |
67
+
68
+ Two things to know about DWS certificates:
69
+
70
+ - **`verify-full` is not usable.** The default server certificate has `CN=server`
71
+ and carries no `subjectAltName`, so hostname verification can never match a
72
+ real endpoint. This is a property of the certificate, not a misconfiguration.
73
+ - **Use the v2 CA.** The console's `dws_ssl_cert` bundle contains both
74
+ `v1/sslcert/cacert.pem` and `v2/sslcert/cacert.pem`. Only v2 matches the server
75
+ certificate issuer; v1 is `CN=Huawei Equipment CA` and fails verification.
76
+
77
+ `sslrootcert` accepts either a filesystem path or inline PEM content, so a hosted
78
+ deployment can pass an uploaded certificate directly.
79
+
80
+ ## Compatibility modes
81
+
82
+ DWS databases run in `ORA`, `TD` or `MySQL` compatibility mode, reported by
83
+ `pg_database.datcompatibility` and surfaced through
84
+ `DWSConnector.get_compatibility_mode()`.
85
+
86
+ ORA mode — the default for new clusters — changes expression semantics in ways
87
+ that can silently produce wrong results:
88
+
89
+ - `7/2` is `3.5` (double precision), not integer `3`.
90
+ - `''` is stored as NULL and `'' IS NULL` is true; `col = ''` never matches.
91
+ - `'a' || NULL` is `'a'`; concatenation absorbs NULL.
92
+ - `DATE` is `timestamp(0)` and `DATE - DATE` yields an `interval`.
93
+
94
+ TD and MySQL modes are not verified by this adapter's test suite.
95
+
96
+ ## Table DDL
97
+
98
+ Table DDL comes from DWS's `pg_get_tabledef()`, which preserves `orientation`,
99
+ `compression`, `DISTRIBUTE BY`, partitioning, `TABLESPACE` and `TO GROUP`.
100
+
101
+ `TABLESPACE` and `TO GROUP` name objects of the source cluster and will not
102
+ replay elsewhere. Use `DWSConnector.strip_cluster_specific_clauses()` to remove
103
+ them before applying the DDL to a migration target.
104
+
105
+ ## Testing
106
+
107
+ ```bash
108
+ # Unit tests (no cluster required)
109
+ python -m pytest tests/unit/ -v
110
+
111
+ # Integration tests against a live cluster
112
+ export DWS_HOST=... DWS_PORT=8000 DWS_DATABASE=gaussdb \
113
+ DWS_USERNAME=dbadmin DWS_PASSWORD=... DWS_SSLROOTCERT=/path/to/cacert.pem
114
+ python -m pytest tests/integration/ -v
115
+ ```
116
+
117
+ Integration tests skip automatically when `DWS_HOST` is unset. They create
118
+ objects in a run-scoped schema and drop it on teardown.
119
+
120
+ ## Verified against
121
+
122
+ DWS 9.1.0.227, storage-decoupled (compute-group mode), 3 CN + 3 DN, ORA
123
+ compatibility mode.
@@ -0,0 +1,102 @@
1
+ # datus-dws
2
+
3
+ Huawei Cloud GaussDB(DWS) adapter for [Datus](https://github.com/Datus-ai/datus-agent).
4
+
5
+ DWS is a shared-nothing MPP analytical warehouse that speaks the PostgreSQL wire
6
+ protocol. This adapter builds on `datus-postgresql`: it reuses the psycopg2
7
+ transport unchanged and overrides only what DWS does differently — its system
8
+ schema set, its native table-definition function, and the Oracle-compatibility
9
+ semantics of ORA mode.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install datus-dws
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ ```yaml
20
+ datasources:
21
+ dws_analytics:
22
+ type: dws
23
+ host: example.dws.myhuaweicloud.com
24
+ port: 8000
25
+ database: gaussdb
26
+ schema: public
27
+ username: dbadmin
28
+ password: ${DWS_PASSWORD}
29
+ sslmode: verify-ca
30
+ sslrootcert: /path/to/cacert.pem
31
+ timeout_seconds: 30
32
+ ```
33
+
34
+ `host` also accepts the console's `host:port` form, in which case `port` may be
35
+ omitted. The cluster's default database is normally `gaussdb`.
36
+
37
+ ## TLS
38
+
39
+ | `sslmode` | Behaviour |
40
+ |---|---|
41
+ | `prefer` (default) | Encrypts when the server offers it; upgrades automatically if the cluster enforces SSL |
42
+ | `require` | Encrypts without verifying the server certificate |
43
+ | `verify-ca` | Verifies the server certificate against `sslrootcert` |
44
+ | `verify-full` | **Cannot succeed** — see below |
45
+ | `disable` | Fails if the cluster has SSL enforcement switched on |
46
+
47
+ Two things to know about DWS certificates:
48
+
49
+ - **`verify-full` is not usable.** The default server certificate has `CN=server`
50
+ and carries no `subjectAltName`, so hostname verification can never match a
51
+ real endpoint. This is a property of the certificate, not a misconfiguration.
52
+ - **Use the v2 CA.** The console's `dws_ssl_cert` bundle contains both
53
+ `v1/sslcert/cacert.pem` and `v2/sslcert/cacert.pem`. Only v2 matches the server
54
+ certificate issuer; v1 is `CN=Huawei Equipment CA` and fails verification.
55
+
56
+ `sslrootcert` accepts either a filesystem path or inline PEM content, so a hosted
57
+ deployment can pass an uploaded certificate directly.
58
+
59
+ ## Compatibility modes
60
+
61
+ DWS databases run in `ORA`, `TD` or `MySQL` compatibility mode, reported by
62
+ `pg_database.datcompatibility` and surfaced through
63
+ `DWSConnector.get_compatibility_mode()`.
64
+
65
+ ORA mode — the default for new clusters — changes expression semantics in ways
66
+ that can silently produce wrong results:
67
+
68
+ - `7/2` is `3.5` (double precision), not integer `3`.
69
+ - `''` is stored as NULL and `'' IS NULL` is true; `col = ''` never matches.
70
+ - `'a' || NULL` is `'a'`; concatenation absorbs NULL.
71
+ - `DATE` is `timestamp(0)` and `DATE - DATE` yields an `interval`.
72
+
73
+ TD and MySQL modes are not verified by this adapter's test suite.
74
+
75
+ ## Table DDL
76
+
77
+ Table DDL comes from DWS's `pg_get_tabledef()`, which preserves `orientation`,
78
+ `compression`, `DISTRIBUTE BY`, partitioning, `TABLESPACE` and `TO GROUP`.
79
+
80
+ `TABLESPACE` and `TO GROUP` name objects of the source cluster and will not
81
+ replay elsewhere. Use `DWSConnector.strip_cluster_specific_clauses()` to remove
82
+ them before applying the DDL to a migration target.
83
+
84
+ ## Testing
85
+
86
+ ```bash
87
+ # Unit tests (no cluster required)
88
+ python -m pytest tests/unit/ -v
89
+
90
+ # Integration tests against a live cluster
91
+ export DWS_HOST=... DWS_PORT=8000 DWS_DATABASE=gaussdb \
92
+ DWS_USERNAME=dbadmin DWS_PASSWORD=... DWS_SSLROOTCERT=/path/to/cacert.pem
93
+ python -m pytest tests/integration/ -v
94
+ ```
95
+
96
+ Integration tests skip automatically when `DWS_HOST` is unset. They create
97
+ objects in a run-scoped schema and drop it on teardown.
98
+
99
+ ## Verified against
100
+
101
+ DWS 9.1.0.227, storage-decoupled (compute-group mode), 3 CN + 3 DN, ORA
102
+ compatibility mode.
@@ -0,0 +1,33 @@
1
+ # Copyright 2025-present DatusAI, Inc.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See http://www.apache.org/licenses/LICENSE-2.0 for details.
4
+
5
+ from .config import DWSConfig
6
+ from .connector import DWSConnector
7
+ from .handlers import (
8
+ build_dws_uri,
9
+ parse_dws_identifier,
10
+ resolve_dws_context,
11
+ )
12
+ from .skills import get_dws_sql_generation_notes
13
+
14
+ __version__ = "0.1.0"
15
+ __all__ = ["DWSConnector", "DWSConfig", "register"]
16
+
17
+
18
+ def register():
19
+ """Register DWS and its generic Agent integration hooks."""
20
+ from datus_db_core import connector_registry
21
+
22
+ connector_registry.register(
23
+ "dws",
24
+ DWSConnector,
25
+ config_class=DWSConfig,
26
+ display_name="Huawei DWS",
27
+ capabilities={"database", "schema"},
28
+ uri_builder=build_dws_uri,
29
+ context_resolver=resolve_dws_context,
30
+ parser_dialect="postgres",
31
+ identifier_parser=parse_dws_identifier,
32
+ sql_generation_notes=get_dws_sql_generation_notes,
33
+ )
@@ -0,0 +1,76 @@
1
+ # Copyright 2025-present DatusAI, Inc.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See http://www.apache.org/licenses/LICENSE-2.0 for details.
4
+
5
+ """Accept a CA certificate as inline PEM, not only as a path on disk.
6
+
7
+ A hosted caller uploads the certificate through a browser, so what reaches the
8
+ adapter is the file's *content*: there is no operator to drop a file on the
9
+ server first, and on a multi-tenant runtime there is nowhere stable to drop it.
10
+
11
+ psycopg2 takes a filename rather than bytes, so inline PEM is spilled to a
12
+ private temp file, once per distinct certificate and reused thereafter.
13
+ """
14
+
15
+ import atexit
16
+ import hashlib
17
+ import os
18
+ import tempfile
19
+ import threading
20
+ from typing import Dict, Optional
21
+
22
+ from datus_db_core import get_logger
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ _PEM_MARKER = "-----BEGIN CERTIFICATE-----"
27
+
28
+ _lock = threading.Lock()
29
+ _materialized: Dict[str, str] = {}
30
+
31
+
32
+ def is_inline_pem(value: Optional[str]) -> bool:
33
+ """Whether *value* is certificate content rather than a path to one."""
34
+ return bool(value) and _PEM_MARKER in value
35
+
36
+
37
+ def as_path(value: Optional[str]) -> Optional[str]:
38
+ """Return a filesystem path for *value*, writing it out if it is inline PEM.
39
+
40
+ Paths pass through untouched, so a self-hosted deployment that mounts its
41
+ CA file keeps working exactly as before.
42
+ """
43
+ if not is_inline_pem(value):
44
+ return value
45
+
46
+ digest = hashlib.sha256(value.encode("utf-8")).hexdigest()
47
+ with _lock:
48
+ path = _materialized.get(digest)
49
+ if path and os.path.exists(path):
50
+ return path
51
+
52
+ fd, path = tempfile.mkstemp(prefix=f"datus-dws-ca-{digest[:12]}-", suffix=".pem")
53
+ try:
54
+ # 0600 before the bytes land: a world-readable trust anchor is an
55
+ # invitation to swap it.
56
+ os.fchmod(fd, 0o600)
57
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
58
+ handle.write(value)
59
+ except BaseException:
60
+ os.unlink(path)
61
+ raise
62
+
63
+ _materialized[digest] = path
64
+ logger.debug("Materialized inline DWS CA certificate to %s", path)
65
+ return path
66
+
67
+
68
+ @atexit.register
69
+ def _cleanup() -> None:
70
+ with _lock:
71
+ for path in _materialized.values():
72
+ try:
73
+ os.unlink(path)
74
+ except OSError:
75
+ pass
76
+ _materialized.clear()
@@ -0,0 +1,104 @@
1
+ # Copyright 2025-present DatusAI, Inc.
2
+ # Licensed under the Apache License, Version 2.0.
3
+ # See http://www.apache.org/licenses/LICENSE-2.0 for details.
4
+
5
+ from typing import Any, Literal, Optional
6
+ from urllib.parse import urlsplit
7
+
8
+ from pydantic import Field, model_validator
9
+
10
+ from datus_postgresql import PostgreSQLConfig
11
+
12
+ DEFAULT_DWS_PORT = 8000
13
+
14
+
15
+ def normalize_dws_endpoint(host: str, port: Any = None) -> tuple[str, int]:
16
+ """Normalize a console endpoint into separate host and port values.
17
+
18
+ The DWS console presents the public endpoint as ``host:port``, so accept
19
+ that form directly rather than making the user split it by hand.
20
+ """
21
+ endpoint = str(host or "").strip()
22
+ if not endpoint:
23
+ raise ValueError("DWS host is required")
24
+ if "://" in endpoint:
25
+ raise ValueError("DWS host must not include a URI scheme")
26
+
27
+ # A bare IPv6 literal has to be bracketed before urlsplit will read it as a
28
+ # host rather than a host:port pair. Config keeps host and port in separate
29
+ # fields, so users write the address unbracketed.
30
+ if endpoint.count(":") > 1 and not endpoint.startswith("["):
31
+ endpoint = f"[{endpoint}]"
32
+
33
+ parsed = urlsplit(f"//{endpoint}")
34
+ if parsed.username is not None or parsed.password is not None:
35
+ raise ValueError("DWS host must not contain user information")
36
+ if parsed.path or parsed.query or parsed.fragment:
37
+ raise ValueError("DWS host must contain only a hostname and optional port")
38
+ if not parsed.hostname:
39
+ raise ValueError("DWS host is invalid")
40
+
41
+ try:
42
+ embedded_port = parsed.port
43
+ except ValueError as exc:
44
+ raise ValueError(f"Invalid DWS endpoint: {endpoint}") from exc
45
+
46
+ explicit_port = None
47
+ if port is not None and str(port).strip():
48
+ try:
49
+ explicit_port = int(port)
50
+ except (TypeError, ValueError) as exc:
51
+ raise ValueError(f"Invalid DWS port: {port}") from exc
52
+
53
+ if embedded_port is not None and explicit_port is not None and embedded_port != explicit_port:
54
+ raise ValueError(f"DWS endpoint port {embedded_port} conflicts with explicit port {explicit_port}")
55
+
56
+ effective_port = embedded_port if embedded_port is not None else explicit_port
57
+ effective_port = DEFAULT_DWS_PORT if effective_port is None else effective_port
58
+ if not 1 <= effective_port <= 65535:
59
+ raise ValueError(f"DWS port must be between 1 and 65535: {effective_port}")
60
+ return parsed.hostname, effective_port
61
+
62
+
63
+ class DWSConfig(PostgreSQLConfig):
64
+ """Connection configuration for Huawei Cloud GaussDB(DWS)."""
65
+
66
+ @model_validator(mode="before")
67
+ @classmethod
68
+ def normalize_endpoint(cls, values):
69
+ if not isinstance(values, dict) or "host" not in values:
70
+ return values
71
+ normalized = dict(values)
72
+ host, port = normalize_dws_endpoint(normalized.get("host"), normalized.get("port"))
73
+ normalized["host"] = host
74
+ normalized["port"] = port
75
+ return normalized
76
+
77
+ host: str = Field(..., min_length=1, description="DWS coordinator endpoint, optionally with a port")
78
+ port: int = Field(default=DEFAULT_DWS_PORT, ge=1, le=65535, description="DWS coordinator port")
79
+ username: str = Field(..., min_length=1, description="DWS database user")
80
+ password: str = Field(
81
+ default="",
82
+ repr=False,
83
+ description="DWS password",
84
+ json_schema_extra={"input_type": "password"},
85
+ )
86
+ database: str = Field(..., min_length=1, description="DWS database name (the cluster default is 'gaussdb')")
87
+ schema_name: str = Field(default="public", alias="schema", min_length=1, description="Default schema name")
88
+ sslmode: Literal["disable", "allow", "prefer", "require", "verify-ca", "verify-full"] = Field(
89
+ default="prefer",
90
+ description=(
91
+ "PostgreSQL SSL mode. 'prefer' upgrades automatically when the cluster enforces SSL. "
92
+ "'verify-full' cannot succeed against the DWS default server certificate, whose CN is "
93
+ "'server' and which carries no subjectAltName"
94
+ ),
95
+ )
96
+ sslrootcert: Optional[str] = Field(
97
+ default=None,
98
+ description=(
99
+ "CA certificate for sslmode=verify-ca, as a file path or inline PEM content. "
100
+ "Use v2/sslcert/cacert.pem from the console's dws_ssl_cert bundle; the v1 CA does "
101
+ "not match the server certificate issuer"
102
+ ),
103
+ )
104
+ timeout_seconds: int = Field(default=30, gt=0, description="Connection and pool timeout in seconds")