continuo-validation-trino 0.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.
- continuo_validation_trino/__init__.py +1 -0
- continuo_validation_trino/adapter.py +130 -0
- continuo_validation_trino-0.1.0.dist-info/METADATA +37 -0
- continuo_validation_trino-0.1.0.dist-info/RECORD +6 -0
- continuo_validation_trino-0.1.0.dist-info/WHEEL +4 -0
- continuo_validation_trino-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Trino warehouse adapter for the continuo validation runner."""
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Trino implementation of the WarehouseAdapter port.
|
|
2
|
+
|
|
3
|
+
Trino's namespace is catalog.schema: the catalog comes from TRINO_CATALOG and every
|
|
4
|
+
statement is fully qualified, so the contract's `schema` argument stays an opaque bare
|
|
5
|
+
name. Two Trino traits shape this adapter: there is no DROP SCHEMA ... CASCADE, so
|
|
6
|
+
teardown drops the schema's tables first; and there are no advisory locks, so
|
|
7
|
+
ensure_schema relies on IF NOT EXISTS plus tolerating a concurrent creation.
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from continuo_validation_contract.port import WarehouseAdapter
|
|
13
|
+
|
|
14
|
+
import trino
|
|
15
|
+
|
|
16
|
+
from trino.auth import BasicAuthentication
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("continuo_validation_trino")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _quote(identifier: str) -> str:
|
|
22
|
+
"""Double-quote a Trino identifier, escaping any embedded double quotes."""
|
|
23
|
+
escaped = identifier.replace('"', '""')
|
|
24
|
+
return f'"{escaped}"'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TrinoAdapter(WarehouseAdapter):
|
|
28
|
+
"""WarehouseAdapter speaking Trino DDL over the trino DBAPI."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, conn: "trino.dbapi.Connection", catalog: str) -> None:
|
|
31
|
+
self._conn = conn
|
|
32
|
+
self._catalog = catalog
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def required_env(cls) -> list[str]:
|
|
36
|
+
"""Vars that must be non-empty before connecting."""
|
|
37
|
+
return ["TRINO_HOST", "TRINO_CATALOG"]
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_env(cls) -> "TrinoAdapter":
|
|
41
|
+
"""Connect from TRINO_* env (port 8080, user continuo, http; password optional)."""
|
|
42
|
+
catalog = os.environ["TRINO_CATALOG"]
|
|
43
|
+
user = os.environ.get("TRINO_USER", "continuo")
|
|
44
|
+
http_scheme = os.environ.get("TRINO_HTTP_SCHEME", "http")
|
|
45
|
+
password = os.environ.get("TRINO_PASSWORD", "")
|
|
46
|
+
if password and http_scheme != "https":
|
|
47
|
+
raise ValueError(
|
|
48
|
+
"TRINO_PASSWORD is set but TRINO_HTTP_SCHEME is not 'https'; "
|
|
49
|
+
"Trino refuses basic auth over plaintext"
|
|
50
|
+
)
|
|
51
|
+
conn = trino.dbapi.connect(
|
|
52
|
+
host=os.environ["TRINO_HOST"],
|
|
53
|
+
port=int(os.environ.get("TRINO_PORT", "8080")),
|
|
54
|
+
user=user,
|
|
55
|
+
catalog=catalog,
|
|
56
|
+
http_scheme=http_scheme,
|
|
57
|
+
auth=BasicAuthentication(user, password) if password else None,
|
|
58
|
+
)
|
|
59
|
+
return cls(conn, catalog)
|
|
60
|
+
|
|
61
|
+
def _schema_ref(self, schema: str) -> str:
|
|
62
|
+
return f"{_quote(self._catalog)}.{_quote(schema)}"
|
|
63
|
+
|
|
64
|
+
def _table_ref(self, schema: str, table: str) -> str:
|
|
65
|
+
return f"{self._schema_ref(schema)}.{_quote(table)}"
|
|
66
|
+
|
|
67
|
+
def _execute(self, statement: str) -> list[tuple]:
|
|
68
|
+
"""Run one statement to completion and return its rows.
|
|
69
|
+
|
|
70
|
+
The trino DBAPI is lazy: execute() only starts the query, so the results must
|
|
71
|
+
be consumed for DDL to actually take effect.
|
|
72
|
+
"""
|
|
73
|
+
cur = self._conn.cursor()
|
|
74
|
+
try:
|
|
75
|
+
cur.execute(statement)
|
|
76
|
+
rows: list[tuple] = cur.fetchall()
|
|
77
|
+
return rows
|
|
78
|
+
finally:
|
|
79
|
+
cur.close()
|
|
80
|
+
|
|
81
|
+
def ensure_schema(self, schema: str) -> None:
|
|
82
|
+
"""Idempotently create *schema*; safe under concurrent callers."""
|
|
83
|
+
logger.info("ensuring candidate schema %s.%s exists", self._catalog, schema)
|
|
84
|
+
try:
|
|
85
|
+
self._execute(f"CREATE SCHEMA IF NOT EXISTS {self._schema_ref(schema)}")
|
|
86
|
+
except trino.exceptions.TrinoUserError as exc:
|
|
87
|
+
# Trino has no advisory locks; a concurrent creator can still win the race
|
|
88
|
+
# between IF NOT EXISTS and the metastore write. The desired end state holds.
|
|
89
|
+
if "already exists" not in str(exc).lower():
|
|
90
|
+
raise
|
|
91
|
+
logger.info("schema %s already exists (concurrent create); continuing", schema)
|
|
92
|
+
|
|
93
|
+
def drop_schema(self, schema: str) -> None:
|
|
94
|
+
"""Idempotently drop *schema* and everything in it; no-op if absent.
|
|
95
|
+
|
|
96
|
+
Trino has no DROP SCHEMA ... CASCADE, so the schema's tables are dropped first.
|
|
97
|
+
"""
|
|
98
|
+
ref = self._schema_ref(schema)
|
|
99
|
+
try:
|
|
100
|
+
rows = self._execute(f"SHOW TABLES FROM {ref}")
|
|
101
|
+
except trino.exceptions.TrinoUserError as exc:
|
|
102
|
+
if "does not exist" not in str(exc).lower():
|
|
103
|
+
raise
|
|
104
|
+
logger.info("candidate schema %s absent; nothing to drop", schema)
|
|
105
|
+
return
|
|
106
|
+
logger.info("dropping candidate schema %s (%d tables)", schema, len(rows))
|
|
107
|
+
for row in rows:
|
|
108
|
+
self._execute(f"DROP TABLE IF EXISTS {self._table_ref(schema, row[0])}")
|
|
109
|
+
self._execute(f"DROP SCHEMA IF EXISTS {ref}")
|
|
110
|
+
|
|
111
|
+
def build_empty_from_sql(self, schema: str, table: str, compiled_sql: str) -> None:
|
|
112
|
+
"""Create ``schema.table`` empty, shaped by the compiled SELECT."""
|
|
113
|
+
# Strip any trailing terminator so the SELECT nests cleanly inside AS ( ... ).
|
|
114
|
+
inner = compiled_sql.strip().rstrip(";").strip()
|
|
115
|
+
ref = self._table_ref(schema, table)
|
|
116
|
+
self._execute(f"DROP TABLE IF EXISTS {ref}")
|
|
117
|
+
self._execute(f"CREATE TABLE {ref} AS ({inner}) WITH NO DATA")
|
|
118
|
+
|
|
119
|
+
def clone_empty_from_prod(self, candidate_schema: str, prod_schema: str, table: str) -> None:
|
|
120
|
+
"""Create ``candidate_schema.table`` empty, shaped like ``prod_schema.table``."""
|
|
121
|
+
ref = self._table_ref(candidate_schema, table)
|
|
122
|
+
self._execute(f"DROP TABLE IF EXISTS {ref}")
|
|
123
|
+
self._execute(
|
|
124
|
+
f"CREATE TABLE {ref} AS "
|
|
125
|
+
f"SELECT * FROM {self._table_ref(prod_schema, table)} WITH NO DATA"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def close(self) -> None:
|
|
129
|
+
"""Release the underlying connection."""
|
|
130
|
+
self._conn.close()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: continuo-validation-trino
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Trino warehouse adapter for the continuo validation runner.
|
|
5
|
+
Author: Simone Carolini
|
|
6
|
+
Maintainer: Simone Carolini
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Requires-Dist: continuo-validation-contract==0.2.0
|
|
12
|
+
Requires-Dist: trino==0.338.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# continuo-validation-trino
|
|
16
|
+
|
|
17
|
+
Trino warehouse adapter for the [continuo](https://github.com/carolsimone/continuo)
|
|
18
|
+
validation runner. Implements `continuo_validation_contract.port.WarehouseAdapter`;
|
|
19
|
+
continuo's harness discovers it through the `continuo_validation.adapters` entry point.
|
|
20
|
+
|
|
21
|
+
Trino's namespace is `catalog.schema`. The catalog comes from `TRINO_CATALOG` and every
|
|
22
|
+
statement is fully qualified, so continuo passes a bare candidate schema name and never
|
|
23
|
+
needs to know about catalogs.
|
|
24
|
+
|
|
25
|
+
## Connection env
|
|
26
|
+
|
|
27
|
+
| Env | Required | Default | Purpose |
|
|
28
|
+
|---|---|---|---|
|
|
29
|
+
| `TRINO_HOST` | yes | — | Coordinator host |
|
|
30
|
+
| `TRINO_CATALOG` | yes | — | Catalog holding candidate schemas (e.g. `iceberg`) |
|
|
31
|
+
| `TRINO_PORT` | no | `8080` | Coordinator port |
|
|
32
|
+
| `TRINO_USER` | no | `continuo` | Trino user |
|
|
33
|
+
| `TRINO_HTTP_SCHEME` | no | `http` | `http` or `https` |
|
|
34
|
+
| `TRINO_PASSWORD` | no | unset | Basic auth; requires `TRINO_HTTP_SCHEME=https` |
|
|
35
|
+
|
|
36
|
+
The catalog must be backed by a connector that supports schema and table creation
|
|
37
|
+
(Iceberg and Hive do). The library is tested live against Iceberg (REST catalog + S3).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
continuo_validation_trino/__init__.py,sha256=xN9flSm3q9z3FPg4ol2bqhsPF3CvoHAaw6rkE8fCXa0,66
|
|
2
|
+
continuo_validation_trino/adapter.py,sha256=2zNv2xLf8lbU-ccoUMpueG5gM8Z5w0qDTUlaTcwRjIY,5478
|
|
3
|
+
continuo_validation_trino-0.1.0.dist-info/METADATA,sha256=ySIHFNs5s5uIbFUxnJcYnJ2MSBxO5aGfVV0TKvid2hk,1585
|
|
4
|
+
continuo_validation_trino-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
continuo_validation_trino-0.1.0.dist-info/entry_points.txt,sha256=2FGDkX5J9yrnyYlmoetrgm59lgMk7JYEq1CJ1jklH4k,86
|
|
6
|
+
continuo_validation_trino-0.1.0.dist-info/RECORD,,
|