- 10 translate plugin test files (100% coverage on 12 modules) - assistant/handler tools: 85+ tests covering dispatch, registry, resolvers, routes, llm_planner, 13 tool handlers - clean release: artifact_catalog_loader, mappers, approval, publication tests - API routes: translate_helpers, validation_service extensions, datasets to 100% - notifications: providers/service tests - services: profile_preference_service - docs/orthogonal-test-report.md — full speckit.tests audit - Fixes: 3 git_base async mock failures, 4 assistant handler permission-check patches - .gitignore: coverage artifacts
651 lines
25 KiB
Python
651 lines
25 KiB
Python
# #region Test.TargetSchemaValidation [C:3] [TYPE Module] [SEMANTICS test, translate, schema, validation]
|
|
# @BRIEF Tests for service_target_schema.py — _build_expected_columns, _extract_columns_from_rows,
|
|
# _parse_sqllab_result, validate_target_table_schema.
|
|
# @RELATION BINDS_TO -> [TargetSchemaValidation]
|
|
# @TEST_CONTRACT: _build_expected_columns -> list[TargetSchemaColumnInfo] | deduplicated column list
|
|
# @TEST_CONTRACT: _extract_columns_from_rows -> list[dict] | normalised column info
|
|
# @TEST_CONTRACT: _parse_sqllab_result -> tuple | data_rows + table_exists
|
|
# @TEST_CONTRACT: validate_target_table_schema -> TargetSchemaValidationResponse | full schema diff
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))
|
|
|
|
import os
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from src.schemas.translate import (
|
|
TargetSchemaColumnInfo,
|
|
TargetSchemaValidationRequest,
|
|
TargetSchemaValidationResponse,
|
|
)
|
|
from src.plugins.translate.service_target_schema import (
|
|
_build_expected_columns,
|
|
_extract_columns_from_rows,
|
|
_parse_sqllab_result,
|
|
validate_target_table_schema,
|
|
)
|
|
|
|
|
|
class TestBuildExpectedColumns:
|
|
"""_build_expected_columns — build deduplicated column list from request."""
|
|
|
|
def _make_req(self, overrides: dict | None = None) -> TargetSchemaValidationRequest:
|
|
defaults = {
|
|
"environment_id": "env-1",
|
|
"target_database_id": "db-1",
|
|
"target_table": "my_table",
|
|
"target_schema": "public",
|
|
"target_key_cols": ["id"],
|
|
"target_column": None,
|
|
"translation_column": "name",
|
|
"target_language_column": "lang",
|
|
"target_source_column": "src",
|
|
"target_source_language_column": "src_lang",
|
|
}
|
|
if overrides:
|
|
defaults.update(overrides)
|
|
return TargetSchemaValidationRequest(**defaults)
|
|
|
|
def test_with_all_columns(self):
|
|
"""All column types included in order."""
|
|
req = self._make_req()
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
assert names == ["id", "name", "lang", "src", "src_lang", "context", "is_original"]
|
|
|
|
def test_basic_minimal(self):
|
|
"""Minimal config: only key cols and translation column."""
|
|
req = self._make_req({
|
|
"target_key_cols": ["id"],
|
|
"translation_column": "name",
|
|
"target_column": None,
|
|
"target_language_column": None,
|
|
"target_source_column": None,
|
|
"target_source_language_column": None,
|
|
})
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
assert names == ["id", "name", "context", "is_original"]
|
|
|
|
def test_empty_key_cols(self):
|
|
"""Empty key cols list handled."""
|
|
req = self._make_req({
|
|
"target_key_cols": [],
|
|
"translation_column": "name",
|
|
"target_column": None,
|
|
"target_language_column": None,
|
|
"target_source_column": None,
|
|
"target_source_language_column": None,
|
|
})
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
assert names == ["name", "context", "is_original"]
|
|
|
|
def test_target_column_overrides_translation(self):
|
|
"""target_column used instead of translation_column when set."""
|
|
req = self._make_req({
|
|
"target_key_cols": [],
|
|
"translation_column": "source_col",
|
|
"target_column": "target_col",
|
|
"target_language_column": None,
|
|
"target_source_column": None,
|
|
"target_source_language_column": None,
|
|
})
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
assert "target_col" in names
|
|
assert "source_col" not in names
|
|
|
|
def test_deduplication(self):
|
|
"""Duplicate column names are removed."""
|
|
req = self._make_req({
|
|
"target_key_cols": ["id", "id"],
|
|
"target_column": "name",
|
|
"translation_column": "name",
|
|
"target_language_column": None,
|
|
"target_source_column": None,
|
|
"target_source_language_column": None,
|
|
})
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
assert names == ["id", "name", "context", "is_original"]
|
|
|
|
def test_none_key_cols(self):
|
|
"""None key cols handled."""
|
|
req = self._make_req({
|
|
"target_key_cols": None,
|
|
"translation_column": "name",
|
|
"target_column": None,
|
|
"target_language_column": None,
|
|
"target_source_column": None,
|
|
"target_source_language_column": None,
|
|
})
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
assert names == ["name", "context", "is_original"]
|
|
|
|
def test_all_nullable_fields(self):
|
|
"""All optional column fields are None."""
|
|
req = self._make_req({
|
|
"target_key_cols": None,
|
|
"translation_column": None,
|
|
"target_column": None,
|
|
"target_language_column": None,
|
|
"target_source_column": None,
|
|
"target_source_language_column": None,
|
|
})
|
|
cols = _build_expected_columns(req)
|
|
names = [c.name for c in cols]
|
|
# Only the always-included columns
|
|
assert names == ["context", "is_original"]
|
|
|
|
def test_return_type(self):
|
|
"""Returns list of TargetSchemaColumnInfo."""
|
|
req = self._make_req()
|
|
cols = _build_expected_columns(req)
|
|
for c in cols:
|
|
assert isinstance(c, TargetSchemaColumnInfo)
|
|
assert c.name
|
|
assert c.data_type is None
|
|
assert c.is_nullable is True
|
|
|
|
|
|
class TestExtractColumnsFromRows:
|
|
"""_extract_columns_from_rows — parse column info from data rows."""
|
|
|
|
def test_postgres_format(self):
|
|
"""PostgreSQL information_schema.columns format."""
|
|
rows = [
|
|
{"column_name": "id", "data_type": "integer", "is_nullable": "YES"},
|
|
{"column_name": "name", "data_type": "varchar", "is_nullable": "NO"},
|
|
]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert len(cols) == 2
|
|
assert cols[0]["name"] == "id"
|
|
assert cols[0]["type"] == "integer"
|
|
assert cols[0]["is_nullable"] is True
|
|
assert cols[1]["name"] == "name"
|
|
assert cols[1]["type"] == "varchar"
|
|
assert cols[1]["is_nullable"] is False
|
|
|
|
def test_clickhouse_format(self):
|
|
"""ClickHouse DESCRIBE TABLE format."""
|
|
rows = [
|
|
{"name": "id", "type": "Int32"},
|
|
{"name": "name", "type": "String"},
|
|
]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert len(cols) == 2
|
|
assert cols[0]["name"] == "id"
|
|
assert cols[0]["type"] == "Int32"
|
|
assert cols[0]["is_nullable"] is True # default for CH
|
|
|
|
def test_mysql_format(self):
|
|
"""MySQL SHOW COLUMNS format."""
|
|
rows = [
|
|
{"Field": "id", "Type": "int(11)"},
|
|
{"Field": "name", "Type": "varchar(255)"},
|
|
]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert len(cols) == 2
|
|
assert cols[0]["name"] == "id"
|
|
assert cols[0]["type"] == "int(11)"
|
|
|
|
def test_non_dict_row_skipped(self):
|
|
"""Non-dict rows are skipped."""
|
|
rows = [{"column_name": "id"}, "not a dict", 42]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert len(cols) == 1
|
|
|
|
def test_missing_column_name_skipped(self):
|
|
"""Rows without any known column key are skipped."""
|
|
rows = [{"unknown_key": "value"}]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert len(cols) == 0
|
|
|
|
def test_empty_rows(self):
|
|
"""Empty list returns empty list."""
|
|
assert _extract_columns_from_rows([]) == []
|
|
|
|
def test_is_nullable_string_handling(self):
|
|
"""is_nullable string 'YES'/'NO' converted to bool."""
|
|
rows = [
|
|
{"column_name": "a", "is_nullable": "YES"},
|
|
{"column_name": "b", "is_nullable": "NO"},
|
|
{"column_name": "c", "is_nullable": "yes"},
|
|
{"column_name": "d", "is_nullable": None},
|
|
]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert cols[0]["is_nullable"] is True
|
|
assert cols[1]["is_nullable"] is False
|
|
assert cols[2]["is_nullable"] is True
|
|
assert cols[3]["is_nullable"] is True # None -> default True
|
|
|
|
def test_type_none_handled(self):
|
|
"""type is None when not found in row."""
|
|
rows = [{"column_name": "id"}]
|
|
cols = _extract_columns_from_rows(rows)
|
|
assert cols[0]["type"] is None
|
|
|
|
|
|
class TestParseSqllabResult:
|
|
"""_parse_sqllab_result — extract data rows from SQL Lab response."""
|
|
|
|
def test_format_direct_data(self):
|
|
"""Format 1: direct 'data' key."""
|
|
result = {"data": [{"col": 1}, {"col": 2}]}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 2
|
|
assert exists is True
|
|
|
|
def test_format_async_result(self):
|
|
"""Format 2: nested 'result' dict."""
|
|
result = {"result": {"data": [{"col": 1}]}}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 1
|
|
|
|
def test_format_get_query_results(self):
|
|
"""Format 3: 'results' dict with 'data'."""
|
|
result = {"results": {"data": [{"col": 1}]}}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 1
|
|
|
|
def test_format_get_query_results_nested(self):
|
|
"""Format 3: 'results' -> 'result' -> 'data'."""
|
|
result = {"results": {"result": {"data": [{"col": 1}]}}}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 1
|
|
|
|
def test_format_results_list(self):
|
|
"""Format 4: 'results' as list."""
|
|
result = {"results": [{"col": 1}]}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 1
|
|
|
|
def test_empty_result(self):
|
|
"""None/empty result returns empty."""
|
|
rows, exists = _parse_sqllab_result({})
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
def test_none_result(self):
|
|
"""None result returns empty."""
|
|
rows, exists = _parse_sqllab_result(None)
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
def test_no_data_key(self):
|
|
"""Result without data/result/results returns empty."""
|
|
result = {"status": "success"}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
def test_raw_response_is_falsy(self):
|
|
"""raw_response is a falsy value -> returns empty."""
|
|
result = {"raw_response": None}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
def test_raw_response_is_empty_string(self):
|
|
"""raw_response is empty string -> returns empty."""
|
|
result = {"raw_response": ""}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
def test_with_raw_response_wrapper(self):
|
|
"""Result with raw_response wrapper unwrapped."""
|
|
result = {"raw_response": {"data": [{"col": 1}]}}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 1
|
|
|
|
def test_normalize_list_rows_to_dicts(self):
|
|
"""List-of-lists with columns metadata normalized to dicts."""
|
|
result = {
|
|
"data": [["id1", "name1"], ["id2", "name2"]],
|
|
"columns": [{"name": "id"}, {"name": "name"}],
|
|
}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert len(rows) == 2
|
|
assert rows[0] == {"id": "id1", "name": "name1"}
|
|
assert rows[1] == {"id": "id2", "name": "name2"}
|
|
|
|
def test_table_not_exists_empty_data(self):
|
|
"""Empty data means table does not exist."""
|
|
result = {"data": []}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
def test_data_not_list(self):
|
|
"""data key exists but is not a list."""
|
|
result = {"data": "not a list"}
|
|
rows, exists = _parse_sqllab_result(result)
|
|
assert rows == []
|
|
assert exists is False
|
|
|
|
|
|
class TestValidateTargetTableSchema:
|
|
"""validate_target_table_schema — full schema validation flow."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_table_name_sql_injection(self):
|
|
"""Invalid table name (SQL injection) returns error response."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="users; DROP TABLE",
|
|
target_schema="public",
|
|
)
|
|
config_manager = MagicMock()
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
assert result.table_exists is False
|
|
assert "Invalid target table name" in result.error
|
|
assert len(result.missing_columns) > 0
|
|
assert result.all_present is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_schema_name(self):
|
|
"""Invalid schema name returns error response."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="schema; DROP",
|
|
)
|
|
config_manager = MagicMock()
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
assert result.table_exists is False
|
|
assert "Invalid target schema name" in result.error
|
|
assert result.all_present is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_direct_db_path(self):
|
|
"""Direct DB path uses DbExecutor instead of SQL Lab."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="public",
|
|
insert_method="direct_db",
|
|
connection_id="conn-1",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
mock_column = MagicMock()
|
|
mock_column.name = "id"
|
|
mock_column.data_type = "integer"
|
|
mock_executor = AsyncMock()
|
|
mock_executor.fetch_schema.return_value = [mock_column]
|
|
|
|
with patch("src.plugins.translate.service_target_schema.ConnectionService") as MockCS, \
|
|
patch("src.plugins.translate.service_target_schema.DbExecutor", return_value=mock_executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is True
|
|
assert len(result.actual_columns) == 1
|
|
assert result.actual_columns[0].name == "id"
|
|
assert result.actual_columns[0].data_type == "integer"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_direct_db_no_columns(self):
|
|
"""Direct DB path with None fetch returns table_exists=False."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="public",
|
|
insert_method="direct_db",
|
|
connection_id="conn-1",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
mock_executor = AsyncMock()
|
|
mock_executor.fetch_schema.return_value = None
|
|
|
|
with patch("src.plugins.translate.service_target_schema.ConnectionService"), \
|
|
patch("src.plugins.translate.service_target_schema.DbExecutor", return_value=mock_executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is False
|
|
assert "Failed to fetch schema" in (result.error or "")
|
|
|
|
def _make_sqllab_executor(self, backend="postgresql", db_name="test_db"):
|
|
"""Create a mock executor with proper sync/async methods."""
|
|
executor = MagicMock()
|
|
executor.resolve_database_id = AsyncMock()
|
|
executor.get_database_backend.return_value = backend
|
|
executor.get_database_name.return_value = db_name
|
|
executor.execute_and_poll = AsyncMock()
|
|
return executor
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sqllab_path_success(self):
|
|
"""SQL Lab path: query succeeds, columns match."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="public",
|
|
target_key_cols=["id"],
|
|
translation_column="name",
|
|
target_column=None,
|
|
target_language_column=None,
|
|
target_source_column=None,
|
|
target_source_language_column=None,
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor()
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "success",
|
|
"data": [
|
|
{"column_name": "id", "data_type": "integer", "is_nullable": "NO"},
|
|
{"column_name": "name", "data_type": "varchar", "is_nullable": "YES"},
|
|
{"column_name": "context", "data_type": "text", "is_nullable": "YES"},
|
|
{"column_name": "is_original", "data_type": "boolean", "is_nullable": "YES"},
|
|
],
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is True
|
|
assert result.all_present is True
|
|
assert len(result.expected_columns) == 4 # id, name, context, is_original
|
|
assert len(result.missing_columns) == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sqllab_path_missing_columns(self):
|
|
"""SQL Lab path: some expected columns missing."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="public",
|
|
target_key_cols=["id"],
|
|
translation_column="name",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor()
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "success",
|
|
"data": [
|
|
{"column_name": "id", "data_type": "integer", "is_nullable": "NO"},
|
|
],
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is True
|
|
assert result.all_present is False
|
|
assert len(result.missing_columns) > 0
|
|
missing_names = [c.name for c in result.missing_columns]
|
|
assert "name" in missing_names
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sqllab_path_clickhouse(self):
|
|
"""ClickHouse backend uses system.columns."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="public",
|
|
target_key_cols=["id"],
|
|
translation_column="name",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor(backend="clickhouse", db_name="test_ch")
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "success",
|
|
"data": [
|
|
{"name": "id", "type": "Int32"},
|
|
{"name": "name", "type": "String"},
|
|
{"name": "context", "type": "String"},
|
|
{"name": "is_original", "type": "UInt8"},
|
|
],
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is True
|
|
assert result.all_present is True
|
|
# Verify clickhouse SQL was used
|
|
call_sql = executor.execute_and_poll.call_args[1]["sql"]
|
|
assert "system.columns" in call_sql
|
|
assert "SELECT name, type" in call_sql
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sqllab_path_failed_status(self):
|
|
"""SQL Lab query fails status -> error response."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor()
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "failed",
|
|
"error_message": "Table does not exist",
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is False
|
|
assert "Superset SQL Lab error" in (result.error or "")
|
|
assert result.database_name == "test_db"
|
|
assert result.database_backend == "postgresql"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sqllab_path_exception(self):
|
|
"""Exception during SQL Lab execution -> error response."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = MagicMock()
|
|
executor.resolve_database_id = AsyncMock(side_effect=Exception("Connection refused"))
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.table_exists is False
|
|
assert "Failed to validate" in (result.error or "")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sqllab_with_extra_columns(self):
|
|
"""Extra columns in target table are reported."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_key_cols=["id"],
|
|
translation_column="name",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor()
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "success",
|
|
"data": [
|
|
{"column_name": "id", "data_type": "integer"},
|
|
{"column_name": "name", "data_type": "varchar"},
|
|
{"column_name": "context", "data_type": "text"},
|
|
{"column_name": "is_original", "data_type": "boolean"},
|
|
{"column_name": "extra_col", "data_type": "text"},
|
|
],
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
assert result.all_present is True
|
|
extra_names = [c.name for c in result.extra_columns]
|
|
assert "extra_col" in extra_names
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clickhousedb_normalized_to_clickhouse(self):
|
|
"""Backend 'clickhousedb' normalized to 'clickhouse' for SQL generation."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="default",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor(backend="clickhousedb", db_name="test_ch")
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "success",
|
|
"data": [],
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
call_sql = executor.execute_and_poll.call_args[1]["sql"]
|
|
assert "system.columns" in call_sql
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_greenplum_normalized_to_postgresql(self):
|
|
"""Backend 'greenplum' normalized to 'postgresql' for SQL generation."""
|
|
req = TargetSchemaValidationRequest(
|
|
environment_id="env-1",
|
|
target_database_id="db-1",
|
|
target_table="my_table",
|
|
target_schema="public",
|
|
)
|
|
config_manager = MagicMock()
|
|
|
|
executor = self._make_sqllab_executor(backend="greenplum", db_name="test_gp")
|
|
executor.execute_and_poll.return_value = {
|
|
"status": "success",
|
|
"data": [],
|
|
}
|
|
|
|
with patch("src.plugins.translate.service_target_schema.SupersetSqlLabExecutor", return_value=executor):
|
|
result = await validate_target_table_schema(req, config_manager)
|
|
|
|
call_sql = executor.execute_and_poll.call_args[1]["sql"]
|
|
assert "information_schema.columns" in call_sql
|
|
# #endregion Test.TargetSchemaValidation
|