|
| 1 | +import os |
1 | 2 | import unittest.mock |
2 | 3 | from datetime import datetime |
| 4 | +from typing import List, Optional |
3 | 5 | from unittest.mock import MagicMock, Mock, patch |
4 | 6 |
|
5 | 7 | import pytest |
|
9 | 11 | from sqlalchemy.engine import Inspector |
10 | 12 | from sqlalchemy.sql import sqltypes |
11 | 13 |
|
12 | | -from datahub.configuration.common import AllowDenyPattern |
| 14 | +from datahub.configuration.common import AllowDenyPattern, ConfigurationError |
13 | 15 | from datahub.ingestion.api.common import PipelineContext |
14 | 16 | from datahub.ingestion.source.sql.oracle import ( |
15 | 17 | VSQL_USAGE_QUERY, |
|
19 | 21 | OracleSource, |
20 | 22 | ProcedureDependencies, |
21 | 23 | VSqlPrerequisiteCheckResult, |
| 24 | + _preload_oracle_client_libs, |
22 | 25 | extra_oracle_types, |
23 | 26 | ) |
24 | 27 | from datahub.ingestion.source.sql.sql_report import SQLSourceReport |
@@ -1423,3 +1426,128 @@ def mock_parent_workunits(): |
1423 | 1426 | list(source.get_workunits()) |
1424 | 1427 |
|
1425 | 1428 | assert all(registered_during_iteration) |
| 1429 | + |
| 1430 | + |
| 1431 | +def _make_thick_mode_config(thick_mode_lib_dir: Optional[str] = None) -> OracleConfig: |
| 1432 | + return OracleConfig( |
| 1433 | + username="user", |
| 1434 | + password="password", |
| 1435 | + host_port="host:1521", |
| 1436 | + service_name="svc01", |
| 1437 | + enable_thick_mode=True, |
| 1438 | + thick_mode_lib_dir=thick_mode_lib_dir, |
| 1439 | + ) |
| 1440 | + |
| 1441 | + |
| 1442 | +def test_preload_oracle_client_libs_loads_in_dependency_order(tmp_path): |
| 1443 | + """Deps must be opened before libclntsh; missing optional libs are skipped silently.""" |
| 1444 | + # Create stub files for some but not all of the patterns the helper looks for. |
| 1445 | + # libipc1 / libmql1 / libociei are intentionally absent — they are optional in |
| 1446 | + # newer Instant Client releases and the helper must tolerate that. |
| 1447 | + files = [ |
| 1448 | + "libnnz12.so", |
| 1449 | + "libclntshcore.so.21.1", |
| 1450 | + "libons.so", |
| 1451 | + "libclntsh.so.21.1", |
| 1452 | + ] |
| 1453 | + for name in files: |
| 1454 | + (tmp_path / name).write_bytes(b"") |
| 1455 | + |
| 1456 | + loaded: List[str] = [] |
| 1457 | + |
| 1458 | + class _FakeCDLL: |
| 1459 | + def __init__(self, path: str, mode: int) -> None: |
| 1460 | + loaded.append(os.path.basename(path)) |
| 1461 | + |
| 1462 | + with patch("datahub.ingestion.source.sql.oracle.ctypes.CDLL", _FakeCDLL): |
| 1463 | + _preload_oracle_client_libs(str(tmp_path)) |
| 1464 | + |
| 1465 | + # Dependencies before libclntsh — that's the whole point of the helper. |
| 1466 | + assert loaded[-1] == "libclntsh.so.21.1" |
| 1467 | + assert "libnnz12.so" in loaded |
| 1468 | + assert "libclntshcore.so.21.1" in loaded |
| 1469 | + assert "libons.so" in loaded |
| 1470 | + # nnz must precede libclntsh; libclntshcore likewise. |
| 1471 | + assert loaded.index("libnnz12.so") < loaded.index("libclntsh.so.21.1") |
| 1472 | + assert loaded.index("libclntshcore.so.21.1") < loaded.index("libclntsh.so.21.1") |
| 1473 | + |
| 1474 | + |
| 1475 | +def test_preload_oracle_client_libs_raises_when_dir_missing(tmp_path): |
| 1476 | + bogus = tmp_path / "does-not-exist" |
| 1477 | + |
| 1478 | + with pytest.raises(ConfigurationError, match="does not exist"): |
| 1479 | + _preload_oracle_client_libs(str(bogus)) |
| 1480 | + |
| 1481 | + |
| 1482 | +def test_preload_oracle_client_libs_raises_when_dir_empty(tmp_path): |
| 1483 | + # Directory exists but contains no Oracle libs — surfaces a clearer error |
| 1484 | + # than the cryptic DPI-1047 we'd otherwise get downstream. |
| 1485 | + with pytest.raises(ConfigurationError, match="No Oracle Instant Client libraries"): |
| 1486 | + _preload_oracle_client_libs(str(tmp_path)) |
| 1487 | + |
| 1488 | + |
| 1489 | +def test_preload_oracle_client_libs_skips_individual_load_failures(tmp_path): |
| 1490 | + """One bad .so should not abort the whole preload — log and continue.""" |
| 1491 | + for name in ["libnnz12.so", "libclntsh.so.21.1"]: |
| 1492 | + (tmp_path / name).write_bytes(b"") |
| 1493 | + |
| 1494 | + attempts: List[str] = [] |
| 1495 | + |
| 1496 | + class _FlakyCDLL: |
| 1497 | + def __init__(self, path: str, mode: int) -> None: |
| 1498 | + attempts.append(os.path.basename(path)) |
| 1499 | + if path.endswith("libnnz12.so"): |
| 1500 | + raise OSError("synthetic load failure") |
| 1501 | + |
| 1502 | + with patch("datahub.ingestion.source.sql.oracle.ctypes.CDLL", _FlakyCDLL): |
| 1503 | + _preload_oracle_client_libs(str(tmp_path)) |
| 1504 | + |
| 1505 | + assert "libnnz12.so" in attempts |
| 1506 | + assert "libclntsh.so.21.1" in attempts |
| 1507 | + |
| 1508 | + |
| 1509 | +@patch("datahub.ingestion.source.sql.oracle.platform.system", return_value="Linux") |
| 1510 | +@patch("datahub.ingestion.source.sql.oracle._preload_oracle_client_libs") |
| 1511 | +@patch("datahub.ingestion.source.sql.oracle.oracledb") |
| 1512 | +def test_oracle_source_linux_preloads_when_lib_dir_set( |
| 1513 | + mock_oracledb, mock_preload, _mock_platform, tmp_path |
| 1514 | +): |
| 1515 | + config = _make_thick_mode_config(thick_mode_lib_dir=str(tmp_path)) |
| 1516 | + |
| 1517 | + OracleSource(config, PipelineContext("test-thick-linux-preload")) |
| 1518 | + |
| 1519 | + mock_preload.assert_called_once_with(str(tmp_path)) |
| 1520 | + # On Linux we must NOT pass lib_dir to init_oracle_client(): the preload |
| 1521 | + # already put the libs in the process namespace, and passing lib_dir is |
| 1522 | + # what triggers the DT_NEEDED resolution failure described in oracle/python-oracledb#578. |
| 1523 | + mock_oracledb.init_oracle_client.assert_called_once_with() |
| 1524 | + |
| 1525 | + |
| 1526 | +@patch("datahub.ingestion.source.sql.oracle.platform.system", return_value="Linux") |
| 1527 | +@patch("datahub.ingestion.source.sql.oracle._preload_oracle_client_libs") |
| 1528 | +@patch("datahub.ingestion.source.sql.oracle.oracledb") |
| 1529 | +def test_oracle_source_linux_skips_preload_when_lib_dir_unset( |
| 1530 | + mock_oracledb, mock_preload, _mock_platform |
| 1531 | +): |
| 1532 | + config = _make_thick_mode_config(thick_mode_lib_dir=None) |
| 1533 | + |
| 1534 | + OracleSource(config, PipelineContext("test-thick-linux-no-preload")) |
| 1535 | + |
| 1536 | + mock_preload.assert_not_called() |
| 1537 | + mock_oracledb.init_oracle_client.assert_called_once_with() |
| 1538 | + |
| 1539 | + |
| 1540 | +@patch("datahub.ingestion.source.sql.oracle.platform.system", return_value="Darwin") |
| 1541 | +@patch("datahub.ingestion.source.sql.oracle._preload_oracle_client_libs") |
| 1542 | +@patch("datahub.ingestion.source.sql.oracle.oracledb") |
| 1543 | +def test_oracle_source_mac_passes_lib_dir_without_preload( |
| 1544 | + mock_oracledb, mock_preload, _mock_platform, tmp_path |
| 1545 | +): |
| 1546 | + config = _make_thick_mode_config(thick_mode_lib_dir=str(tmp_path)) |
| 1547 | + |
| 1548 | + OracleSource(config, PipelineContext("test-thick-mac")) |
| 1549 | + |
| 1550 | + # macOS uses dyld, not glibc's loader, so no preload trick is needed — |
| 1551 | + # init_oracle_client(lib_dir=...) is sufficient. |
| 1552 | + mock_preload.assert_not_called() |
| 1553 | + mock_oracledb.init_oracle_client.assert_called_once_with(lib_dir=str(tmp_path)) |
0 commit comments