bf2343b7e4
Integration Tests - MySQL + Elasticsearch / Detect Changes (push) Has been cancelled
Integration Tests - MySQL + Elasticsearch / integration-tests-mysql-elasticsearch (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + Elasticsearch + Redis / integration-tests-postgres-elasticsearch-redis (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / Detect Changes (push) Has been cancelled
Integration Tests - PostgreSQL + OpenSearch / integration-tests-postgres-opensearch (push) Has been cancelled
Java Checkstyle / java-checkstyle (push) Has been cancelled
Maven Collate Tests / maven-collate-ci (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests-status (push) Has been cancelled
Publish Package to Maven Central Repository / publish-maven-packages (push) Has been cancelled
OpenMetadata Service Unit Tests / Detect Changes (push) Has been cancelled
OpenMetadata Service Unit Tests / openmetadata-service-unit-tests (push) Has been cancelled
OpenMetadata Service Unit Tests / k8s_operator-unit-tests (push) Has been cancelled
167 lines
5.5 KiB
Markdown
167 lines
5.5 KiB
Markdown
# SQL & SQLAlchemy Standards
|
|
|
|
## Connection URL Building
|
|
|
|
Use `get_connection_url_common` for standard `scheme://user:pass@host:port/db` patterns:
|
|
|
|
```python
|
|
from metadata.ingestion.connections.builders import (
|
|
get_connection_url_common,
|
|
create_generic_db_connection,
|
|
init_empty_connection_arguments,
|
|
)
|
|
|
|
def get_connection(connection: MyDbConnection) -> Engine:
|
|
url = get_connection_url_common(connection)
|
|
return create_generic_db_connection(
|
|
connection=connection,
|
|
get_connection_url_fn=lambda _: url,
|
|
get_connection_args_fn=lambda _: init_empty_connection_arguments(connection),
|
|
)
|
|
```
|
|
|
|
Override `get_connection_url_common` only when the database has non-standard URL structure (BigQuery project IDs, Databricks workspaces, etc.).
|
|
|
|
## Password and Secret Handling
|
|
|
|
Passwords are extracted through `get_password_secret()` which handles:
|
|
- Direct `password` field
|
|
- `authType.password` from `BasicAuth`
|
|
- AWS IAM token generation from `IamAuthConfigurationSource`
|
|
|
|
Passwords are URL-quoted via `quote_plus()` before inclusion in the connection string. Never log or print connection URLs with credentials.
|
|
|
|
```python
|
|
# CORRECT — framework handles quoting
|
|
url = get_connection_url_common(connection)
|
|
|
|
# WRONG — manual password handling
|
|
url = f"{scheme}://{user}:{password}@{host}" # No quoting, leaks secrets
|
|
```
|
|
|
|
## Engine Creation
|
|
|
|
`create_generic_db_connection` creates a SQLAlchemy Engine with:
|
|
- `QueuePool` for connection pooling
|
|
- Query tracking via `attach_query_tracker`
|
|
- Optional query comment injection (`supportsQueryComment`)
|
|
- `max_overflow=-1` (unlimited overflow connections)
|
|
|
|
```python
|
|
engine = create_generic_db_connection(
|
|
connection=connection,
|
|
get_connection_url_fn=get_connection_url_fn,
|
|
get_connection_args_fn=get_connection_args_fn,
|
|
)
|
|
```
|
|
|
|
## Time Window Standardization
|
|
|
|
Query log extraction uses `get_start_and_end()` to compute time ranges from config:
|
|
|
|
```python
|
|
from metadata.ingestion.source.database.query_parser_source import QueryParserSource
|
|
|
|
class MyDbQueryParserSource(QueryParserSource):
|
|
def get_sql_statement(self, start_time: datetime, end_time: datetime) -> str:
|
|
return self.sql_stmt.format(
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
filters=self.get_filters(),
|
|
result_limit=self.source_config.resultLimit,
|
|
)
|
|
```
|
|
|
|
Always parameterize time windows — never hardcode durations.
|
|
|
|
## Auth Patterns for SQL Databases
|
|
|
|
### BasicAuth (username/password)
|
|
Standard pattern. `get_connection_url_common` handles it automatically.
|
|
|
|
### IAM Auth (AWS RDS/Redshift)
|
|
Uses `IamAuthConfigurationSource` to generate temporary tokens:
|
|
|
|
```python
|
|
# Framework handles this in builders.py
|
|
aws_client = AWSClient(config=connection.authType.awsConfig).get_rds_client()
|
|
password = aws_client.generate_db_auth_token(
|
|
DBHostname=host, Port=port,
|
|
DBUsername=connection.username,
|
|
Region=connection.authType.awsConfig.awsRegion,
|
|
)
|
|
```
|
|
|
|
Connector-specific IAM logic belongs in the connector's `connection.py`, not in shared `builders.py`.
|
|
|
|
### Azure AD Auth
|
|
Uses `AzureConfig` with service principal credentials.
|
|
|
|
### Kerberos
|
|
Some databases (Hive, Impala) use Kerberos. Handle in `connect_args`:
|
|
|
|
```python
|
|
def get_connection_args(connection) -> dict:
|
|
args = init_empty_connection_arguments(connection)
|
|
if connection.authMechanism == AuthMechanism.GSSAPI:
|
|
args["auth_mechanism"] = "GSSAPI"
|
|
args["kerberos_service_name"] = connection.kerberosServiceName
|
|
return args
|
|
```
|
|
|
|
## Schema and Table Filtering
|
|
|
|
Use framework filter utilities — do not implement custom filtering:
|
|
|
|
```python
|
|
from metadata.utils.filters import filter_by_database, filter_by_schema, filter_by_table
|
|
|
|
# Applied automatically by CommonDbSourceService:
|
|
if filter_by_table(self.source_config.tableFilterPattern, table_name):
|
|
self.status.filter(table_name, "Table filtered out")
|
|
continue
|
|
```
|
|
|
|
## System Schema Exclusion
|
|
|
|
Most databases have system schemas to skip. Override in the source:
|
|
|
|
```python
|
|
def get_default_schema_filter(self):
|
|
return ["information_schema", "pg_catalog", "sys", "mysql", "performance_schema"]
|
|
```
|
|
|
|
## Multi-Database vs Single-Database
|
|
|
|
### When to Use MultiDBSource
|
|
|
|
Add `MultiDBSource` mixin when the database server hosts multiple independent databases (Postgres, Snowflake, BigQuery projects, etc.):
|
|
|
|
```python
|
|
class MyDbSource(CommonDbSourceService, MultiDBSource):
|
|
def get_configured_database(self) -> Optional[str]:
|
|
return self.service_connection.databaseName
|
|
|
|
def get_database_names_raw(self) -> Iterable[str]:
|
|
yield from self._execute_database_query(MY_DB_GET_DATABASES)
|
|
```
|
|
|
|
### When NOT to Use MultiDBSource
|
|
|
|
Skip it when the database has a flat namespace (MySQL without cross-DB queries, SQLite, embedded databases).
|
|
|
|
## Decision Tree: Architecture Selection
|
|
|
|
```
|
|
Is it a SQL database with a SQLAlchemy dialect?
|
|
├── YES → CommonDbSourceService + BaseConnection[Config, Engine]
|
|
│ ├── Multiple databases? → Add MultiDBSource mixin
|
|
│ ├── Query logs available? → Add LineageSource + UsageSource
|
|
│ └── Stored procedures? → Framework handles via Inspector
|
|
└── NO → Does it have a proprietary API/SDK?
|
|
├── YES → DatabaseServiceSource + get_connection()/test_connection()
|
|
│ ├── Document store? → CommonNoSQLSource (MongoDB, Couchbase, DynamoDB)
|
|
│ └── Cloud catalog? → DatabaseServiceSource directly (Glue, Unity Catalog)
|
|
└── NO → Consider if it belongs as a database connector at all
|
|
```
|