From bdd89e4c703b4bacf471720d97e4be318a2efe52 Mon Sep 17 00:00:00 2001 From: "pushpendra.garg@oracle.com" Date: Mon, 9 Feb 2026 09:54:17 +0000 Subject: [PATCH] Selet AI Tests for Vector Index --- tests/credential/test_2200_create_cred.py | 350 ++++++++++ tests/credential/test_2300_drop_cred.py | 207 ++++++ tests/provider/test_2400_enable.py | 243 +++++++ tests/provider/test_2500_disable.py | 255 ++++++++ tests/test_1010_connection.py | 304 +++++++++ tests/test_env.py | 210 ++++++ tests/vector_index/test_5000_create_index.py | 418 ++++++++++++ tests/vector_index/test_5100_drop_index.py | 520 +++++++++++++++ .../test_5200_setindex_attributes.py | 616 ++++++++++++++++++ .../test_5300_getindex_attributes.py | 401 ++++++++++++ tests/vector_index/test_5400_list_index.py | 330 ++++++++++ .../test_5500_enable_disable_index.py | 337 ++++++++++ 12 files changed, 4191 insertions(+) create mode 100644 tests/credential/test_2200_create_cred.py create mode 100644 tests/credential/test_2300_drop_cred.py create mode 100644 tests/provider/test_2400_enable.py create mode 100644 tests/provider/test_2500_disable.py create mode 100644 tests/test_1010_connection.py create mode 100644 tests/test_env.py create mode 100644 tests/vector_index/test_5000_create_index.py create mode 100644 tests/vector_index/test_5100_drop_index.py create mode 100644 tests/vector_index/test_5200_setindex_attributes.py create mode 100644 tests/vector_index/test_5300_getindex_attributes.py create mode 100644 tests/vector_index/test_5400_list_index.py create mode 100644 tests/vector_index/test_5500_enable_disable_index.py diff --git a/tests/credential/test_2200_create_cred.py b/tests/credential/test_2200_create_cred.py new file mode 100644 index 0000000..e352c72 --- /dev/null +++ b/tests/credential/test_2200_create_cred.py @@ -0,0 +1,350 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb +from select_ai.errors import DatabaseNotConnectedError + +logger = logging.getLogger("TestCreateCredential") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def credential_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + } + request.cls.credential_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown_cred(request, credential_params): + logger.info("=== Setting up TestCreateCredential class ===") + test_env.create_connection(use_wallet=request.cls.credential_params["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Initial connection successful") + yield + logger.info("=== Tearing down TestCreateCredential class ===") + try: + select_ai.disconnect() + logger.info("Disconnected from DB") + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("credential_params", "setup_and_teardown_cred") +class TestCreateCredential: + def __init__(self): + self.logger = logging.getLogger("TestCreateCredential") + + @staticmethod + def get_native_cred_param(params, cred_name=None): + return dict( + credential_name = cred_name, + user_ocid = params["user_ocid"], + tenancy_ocid = params["tenancy_ocid"], + private_key = params["private_key"], + fingerprint = params["fingerprint"] + ) + @staticmethod + def get_cred_param(params, cred_name=None): + return dict( + credential_name = cred_name, + username = params["cred_username"], + password = params["cred_password"] + ) + @staticmethod + def drop_credential_cursor(cursor, cred_name='GENAI_CRED'): + logger.info(f"Dropping credential: {cred_name}") + cursor.callproc( + "DBMS_CLOUD.DROP_CREDENTIAL", + keyword_parameters={ + "credential_name": cred_name + }, + ) + logger.info(f"Dropped credential: {cred_name}") + + def test_2201(self): + """Testing basic credential creation""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + self.logger.info(f"Creating credential: {credential}") + try: + select_ai.create_credential(credential=credential, replace=False) + self.logger.info("Credential created successfully.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2202(self): + """Testing creating credential twice without replace""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential) + self.logger.info("First credential creation successful.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + self.logger.info("Attempting to create credential again (expected to fail)...") + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.create_credential(credential=credential) + self.logger.info(f"Caught expected DatabaseError: {cm.value}") + assert "ORA-20022" in str(cm.value) + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2203(self): + """Testing repeated credential creation with replace=True""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + for i in range(5): + self.logger.info(f"Creating credential iteration {i+1}...") + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Repeated creation succeeded.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2204(self): + """Testing credential creation with replace=True""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Credential created successfully with replace=True.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2205(self): + """Testing credential creation twice with replace=True""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Credential created successfully with replace=True.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + try: + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Credential created successfully with replace=True.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + assert True, "Credential creation and replacement passed without exception." + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2206(self): + """Testing replace=True then replace=False behavior""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("First creation succeeded.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + self.logger.info("Second creation without replace (expected to fail)...") + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.create_credential(credential=credential) + self.logger.info(f"Caught expected error: {cm.value}") + assert "ORA-20022" in str(cm.value) + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2207(self): + """Testing replace=False followed by replace=True""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential) + self.logger.info("Credential created (replace=False).") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + try: + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Credential replaced successfully (replace=True).") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + assert True, "Credential creation and replacement passed without exception." + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2208(self): + """Testing native credential creation""" + credential = self.get_native_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential, replace=False) + self.logger.info("Native credential created successfully.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2209(self): + """Testing native credential creation twice""" + credential = self.get_native_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential) + self.logger.info("First native credential created.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.create_credential(credential=credential) + self.logger.info(f"Expected error caught: {cm.value}") + assert "ORA-20022" in str(cm.value) + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2210(self): + """Testing native credential creation with replace=True""" + credential = self.get_native_cred_param(self.credential_params, 'GENAI_CRED') + try: + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Native credential created successfully.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2211(self): + """Testing native credential creation with replace=True twice""" + credential = self.get_native_cred_param(self.credential_params, 'GENAI_CRED') + for i in range(2): + self.logger.info(f"Creating native credential iteration {i+1} (replace=True)...") + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Native credential replaced successfully twice.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2212(self): + """Testing creation with empty credential name""" + credential = self.get_cred_param(self.credential_params) + with pytest.raises(Exception) as cm: + select_ai.create_credential(credential=credential) + self.logger.info(f"Expected exception caught: {cm.value}") + assert "ORA-20010: Missing credential name" in str(cm.value) + + def test_2213(self): + """Testing credential creation with empty dictionary""" + credential = dict() + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.create_credential(credential=credential) + self.logger.info(f"Expected exception caught: {cm.value}") + assert ( + "PLS-00306: wrong number or types of arguments in call to 'CREATE_CREDENTIAL'" in str(cm.value) + ) + + def test_2214(self): + """Testing credential creation with invalid username""" + credential = dict( + credential_name = 'GENAI_CRED', + username = 'invalid_username', + password = self.credential_params["cred_password"] + ) + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Credential with invalid username created successfully.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2215(self): + """Testing credential creation with invalid password""" + credential = dict( + credential_name = 'GENAI_CRED', + username = self.credential_params["cred_username"], + password = 'invalid_pwd' + ) + select_ai.create_credential(credential=credential, replace=True) + self.logger.info("Credential with invalid password created successfully.") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor) + + def test_2216(self): + """Testing credential creation when DB is disconnected""" + select_ai.disconnect() + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED') + with pytest.raises(DatabaseNotConnectedError): + select_ai.create_credential(credential=credential, replace=False) + self.logger.info("Expected DatabaseNotConnectedError raised.") + + def test_2217(self): + """Test Credential creation for a local test user""" + self.logger.info("Connecting as admin user...") + test_env.create_connection(use_wallet=self.credential_params["use_wallet"]) + self.logger.info("Admin connection established.") + test_username = "TEST_USER1" + test_password = self.credential_params["password"] + self.logger.info(f"Ensuring test user '{test_username}' does not exist...") + with select_ai.cursor() as admin_cursor: + try: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + self.logger.info(f"Existing user '{test_username}' dropped.") + except oracledb.DatabaseError: + self.logger.info(f"User '{test_username}' did not exist, continuing...") + self.logger.info(f"Creating test user '{test_username}'...") + admin_cursor.execute(f"CREATE USER {test_username} IDENTIFIED BY {test_password}") + admin_cursor.execute(f"grant create session, create table, unlimited tablespace to {test_username}") + admin_cursor.execute(f"grant execute on dbms_cloud to {test_username}") + self.logger.info(f"User '{test_username}' created and granted privileges.") + self.logger.info(f"Connecting as test user '{test_username}'...") + test_env.create_connection( + user=test_username, + password=test_password, + use_wallet=self.credential_params["use_wallet"] + ) + self.logger.info("Test user connection established.") + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED_USER1') + self.logger.info(f"Creating credential '{credential['credential_name']}' for test user...") + try: + select_ai.create_credential(credential=credential, replace=False) + self.logger.info("Credential created successfully.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + self.logger.info(f"Dropping credential '{credential['credential_name']}'...") + with select_ai.cursor() as cursor: + self.drop_credential_cursor(cursor, 'GENAI_CRED_USER1') + self.logger.info("Credential dropped.") + self.logger.info("Disconnecting test user...") + select_ai.disconnect() + self.logger.info("Disconnected test user.") + self.logger.info(f"Reconnecting as admin to drop test user '{test_username}'...") + test_env.create_connection(use_wallet=self.credential_params["use_wallet"]) + with select_ai.cursor() as admin_cursor: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + self.logger.info(f"Test user '{test_username}' dropped successfully.") + + def test_2218(self): + """Testing credential name with special characters""" + credential = self.get_cred_param(self.credential_params, 'GENAI_CRED!@#') + with pytest.raises(oracledb.DatabaseError, match="ORA-20010: Invalid credential name"): + select_ai.create_credential(credential=credential, replace=False) + self.logger.info("Invalid name test passed.") + + def test_2219(self): + """Testing credential name exceeding 128 characters""" + long_name = "GENAI_CRED" + "_" + "a" * (128 - len('GENAI_CRED')) + credential = self.get_cred_param(self.credential_params, long_name) + with pytest.raises( + oracledb.DatabaseError, + match=r"ORA-20008: Credential name length \(129\) exceeds maximum length \(128\)" + ): + select_ai.create_credential(credential=credential, replace=False) + self.logger.info("Long credential name test passed.") diff --git a/tests/credential/test_2300_drop_cred.py b/tests/credential/test_2300_drop_cred.py new file mode 100644 index 0000000..286dbe1 --- /dev/null +++ b/tests/credential/test_2300_drop_cred.py @@ -0,0 +1,207 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb + +from select_ai.errors import DatabaseNotConnectedError + +logger = logging.getLogger("TestDropCredential") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def drop_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + } + request.cls.drop_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, drop_params): + logger.info("=== Setting up TestDropCredential class ===") + test_env.create_connection(use_wallet=request.cls.drop_params["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Initial connection successful") + yield + logger.info("=== Tearing down TestDropCredential class ===") + try: + select_ai.disconnect() + logger.info("Disconnected from DB") + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("drop_params", "setup_and_teardown") +class TestDropCredential: + @staticmethod + def get_cred_param(params, cred_name=None): + logger.info(f"Preparing credential params for: {cred_name}") + return dict( + credential_name = cred_name, + username = params["cred_username"], + password = params["cred_password"] + ) + @classmethod + def create_test_credential(cls, cred_name="GENAI_CRED"): + logger.info(f"Creating test credential: {cred_name}") + credential = cls.get_cred_param(cls.drop_params, cred_name) + try: + select_ai.create_credential(credential=credential, replace=False) + logger.info(f"Credential '{cred_name}' created successfully.") + except Exception as e: + pytest.fail(f"create_credential() raised {e} unexpectedly.") + @classmethod + def create_local_user(cls, test_username="TEST_USER1"): + logger.info(f"Creating local user: {test_username}") + test_password = cls.drop_params["password"] + with select_ai.cursor() as admin_cursor: + try: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + except oracledb.DatabaseError: + pass # Ignore if user doesn't exist + admin_cursor.execute(f"CREATE USER {test_username} IDENTIFIED BY {test_password}") + admin_cursor.execute(f"grant create session, create table, unlimited tablespace to {test_username}") + admin_cursor.execute(f"grant execute on dbms_cloud to {test_username}") + logger.info(f"Local user '{test_username}' ready.") + + def test_2301(self): + """Deleting existing credential (force=True)""" + logger.info("Deleting existing credential (force=True)") + self.create_test_credential() + try: + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info("Credential deleted successfully.") + except Exception as e: + pytest.fail(f"delete_credential() raised {e} unexpectedly.") + + def test_2302(self): + """Deleting same credential twice (force=True)""" + logger.info("Deleting same credential twice (force=True)") + self.create_test_credential() + select_ai.delete_credential("GENAI_CRED", force=True) + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info("Double deletion succeeded (force=True).") + + def test_2303(self): + """Deleting same credential twice (force=False)""" + logger.info("Deleting same credential twice (force=False)") + self.create_test_credential() + select_ai.delete_credential("GENAI_CRED", force=False) + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.delete_credential("GENAI_CRED", force=False) + logger.info(f"Expected DatabaseError for second delete (force=False): {cm.value}") + + def test_2304(self): + """Deleting nonexistent credential (default force=False)""" + logger.info("Deleting nonexistent credential (default force=False)") + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.delete_credential("nonexistent_cred") + logger.info(f"Expected DatabaseError for nonexistent credential: {cm.value}") + + def test_2305(self): + """Deleting nonexistent credential (force=False)""" + logger.info("Deleting nonexistent credential (force=False)") + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.delete_credential("nonexistent_cred", force=False) + logger.info(f"Expected DatabaseError for nonexistent credential: {cm.value}") + + def test_2306(self): + """Deleting nonexistent credential (force=True)""" + logger.info("Deleting nonexistent credential (force=True)") + try: + select_ai.delete_credential("nonexistent_cred", force=True) + logger.info("No error raised (expected behavior).") + except Exception as e: + pytest.fail(f"delete_credential(force=True) raised {e} unexpectedly.") + + def test_2307(self): + """Deleting credential as local user""" + logger.info("Deleting credential as local user") + test_username = "TEST_USER1" + self.create_local_user(test_username) + test_env.create_connection( + user=test_username, + password=self.drop_params["password"], + use_wallet=self.drop_params["use_wallet"] + ) + credential = self.get_cred_param(self.drop_params, "GENAI_CRED_USER1") + try: + select_ai.delete_credential("GENAI_CRED_USER1", force=True) + logger.info("Local user credential deleted successfully.") + except Exception as e: + pytest.fail(f"delete_credential() raised {e} unexpectedly.") + finally: + select_ai.disconnect() + test_env.create_connection(use_wallet=self.drop_params["use_wallet"]) + with select_ai.cursor() as admin_cursor: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + logger.info("Local user cleanup complete.") + + def test_2308(self): + """Deleting credential with invalid name""" + logger.info("Deleting credential with invalid name") + with pytest.raises(oracledb.DatabaseError, match=r"ORA-20010: Invalid credential name"): + select_ai.delete_credential("invalid!@#", force=True) + logger.info("Caught expected ORA-20010 for invalid name.") + + def test_2309(self): + """Deleting credential without active connection""" + logger.info("Deleting credential without active connection") + select_ai.disconnect() + with pytest.raises(select_ai.errors.DatabaseNotConnectedError) as cm: + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info(f"Expected DatabaseNotConnectedError raised: {cm.value}") + + def test_2310(self): + """Deleting credential with name exceeding max length""" + logger.info("Deleting credential with name exceeding max length") + test_env.create_connection(use_wallet=self.drop_params["use_wallet"]) + long_name = "GENAI_CRED_" + "a" * 120 + with pytest.raises( + oracledb.DatabaseError, + match=r"ORA-20008: Credential name length .* exceeds maximum length" + ): + select_ai.delete_credential(long_name, force=True) + logger.info("Caught expected ORA-20008 for long credential name.") + + def test_2311(self): + """Deleting credential with lowercase name""" + logger.info("Deleting credential with lowercase name") + self.create_test_credential("GENAI_CRED") + try: + select_ai.delete_credential(credential_name="genai_cred") + logger.info("Credential deleted successfully (case-insensitive).") + except Exception as e: + pytest.fail(f"async_delete_credential raised {e} unexpectedly for lowercase name") + + def test_2312(self): + """Deleting credential with empty or None name""" + logger.info("Deleting credential with empty or None name") + with pytest.raises(oracledb.DatabaseError, match=r"ORA-20010: Missing credential name"): + select_ai.delete_credential(credential_name="", force=True) + with pytest.raises(oracledb.DatabaseError, match=r"ORA-20010: Missing credential name"): + select_ai.delete_credential(credential_name=None, force=True) + logger.info("Caught expected ORA-20010 for missing credential name.") \ No newline at end of file diff --git a/tests/provider/test_2400_enable.py b/tests/provider/test_2400_enable.py new file mode 100644 index 0000000..4cada67 --- /dev/null +++ b/tests/provider/test_2400_enable.py @@ -0,0 +1,243 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb + +logger = logging.getLogger("TestEnableProvider") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def provider_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + } + request.cls.provider_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, provider_params): + logger.info("=== Setting up TestEnableProvider class ===") + test_env.create_connection() + assert select_ai.is_connected(), "Connection to DB failed" + cls = request.cls + cls.user = cls.provider_params["user"] + cls.password = cls.provider_params["password"] + cls.dsn = cls.provider_params["dsn"] + cls.db_users = [] + # Create multiple DB users (DB_USER1 ... DB_USER5) + for i in range(1, 6): + user = f"DB_USER{i}" + cls.create_local_user(user) + cls.db_users.append(user) + yield + logger.info("=== Tearing down TestEnableProvider class ===") + # Drop DB users + with select_ai.cursor() as admin_cursor: + for user in cls.db_users: + try: + admin_cursor.execute(f"DROP USER {user} CASCADE") + except oracledb.DatabaseError: + pass # Ignore if already dropped + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("provider_params", "setup_and_teardown") +class TestEnableProvider: + @classmethod + def create_local_user(cls, test_username="TEST_USER1"): + test_password = cls.password + with select_ai.cursor() as admin_cursor: + try: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + except oracledb.DatabaseError: + pass + admin_cursor.execute(f"CREATE USER {test_username} IDENTIFIED BY {test_password}") + admin_cursor.execute(f"grant create session, create table, unlimited tablespace to {test_username}") + admin_cursor.execute(f"grant execute on dbms_cloud to {test_username}") + + def setup_method(self, method): + logger.info(f"SetUp for {method.__name__}") + self.provider_endpoint = "*.openai.azure.com" + self.db_users = self.__class__.db_users + + # ---- TESTS ---- + + def test_2401(self): + "Test enabling provider with valid users and endpoint" + logger.info("Testing enable_provider() with valid users and valid endpoint") + try: + select_ai.enable_provider( + users=self.db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider enabled successfully for all test users.") + except Exception as e: + logger.error(f"enable_provider() raised {e} unexpectedly.") + pytest.fail(f"enable_provider() raised {e} unexpectedly.") + + def test_2402(self): + "Test enabling provider with a non-existent username" + logger.info("Testing enable_provider() with a mix of existing and non-existent usernames") + db_users = ["DB_USER1", "TEST_USER2"] + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.enable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info(f"Expected DatabaseError caught: {cm.value}") + assert "ORA-01917: user or role 'TEST_USER2' does not exist" in str(cm.value) + logger.info("Test for non-existent username completed.") + + def test_2403(self): + "Test enabling provider with all non-existent usernames" + logger.info("Testing enable_provider() with all non-existent usernames") + db_users = ["TEST_USER1", "TEST_USER2"] + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.enable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info(f"Expected DatabaseError caught: {cm.value}") + assert "ORA-01917: user or role 'TEST_USER1' does not exist" in str(cm.value) + logger.info("Test for all non-existent usernames completed.") + + def test_2404(self): + "Test enabling provider with empty users list" + logger.info("Testing enable_provider() with empty users list") + try: + select_ai.enable_provider( + users=[], + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider enabled successfully with empty users (expected and allowed).") + except Exception as e: + logger.error(f"enable_provider() raised {e} unexpectedly with empty users.") + pytest.fail(f"enable_provider() raised {e} unexpectedly with empty users.") + + def test_2405(self): + "Test enabling provider with users as string instead of list" + logger.info("Testing enable_provider() with users as a string (should be list; verify no TypeError is raised)") + try: + select_ai.enable_provider( + users="DB_USER1", # not a list + provider_endpoint=self.provider_endpoint + ) + logger.info("No TypeError raised. Library may accept strings for 'users'.") + except Exception as e: + logger.warning(f"Unexpected exception caught: {e}") + pytest.fail(f"enable_provider() raised unexpected exception: {e}") + + def test_2406(self): + "Test enabling provider with users as int - expect TypeError" + logger.info("Testing enable_provider() with users as an integer (type error expected)") + with pytest.raises(TypeError) as cm: + select_ai.enable_provider( + users=2, # not a list + provider_endpoint=self.provider_endpoint + ) + logger.info(f"Expected TypeError caught: {cm.value}") + + def test_2407(self): + "Test enabling provider with missing provider_endpoint" + logger.info("Testing enable_provider() with None as provider_endpoint (ValueError expected)") + with pytest.raises(ValueError) as cm: + select_ai.enable_provider( + users=self.db_users, + provider_endpoint=None + ) + logger.info(f"Expected ValueError caught: {cm.value}") + + def test_2408(self): + "Test enabling provider with invalid endpoint" + logger.info("Testing enable_provider() with an invalid endpoint (ValueError expected)") + with pytest.raises(ValueError) as cm: + select_ai.enable_provider( + users=self.db_users, + provider_endpoint="invalid.endpoint" + ) + logger.info(f"Expected ValueError caught: {cm.value}") + + def test_2409(self): + "Test enabling provider with duplicate usernames" + logger.info("Testing enable_provider() with duplicate usernames") + try: + select_ai.enable_provider( + users=[self.db_users[0], self.db_users[0]], + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider enabled successfully with duplicate users (expected and allowed).") + except Exception as e: + logger.error(f"enable_provider() raised {e} unexpectedly with duplicate users.") + pytest.fail(f"enable_provider() raised {e} unexpectedly with duplicate users.") + + def test_2410(self): + "Test enabling provider with lowercase username (case-insensitive)" + logger.info("Testing enable_provider() with username in lowercase (should succeed on case-insensitive DB)") + try: + select_ai.enable_provider( + users=[self.db_users[0].lower()], + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider enabled successfully for lowercase username.") + except Exception as e: + logger.error(f"enable_provider() raised {e} unexpectedly with lowercase username.") + pytest.fail(f"enable_provider() raised {e} unexpectedly with lowercase username.") + + def test_2411(self): + "Test enabling provider with username containing whitespace" + logger.info("Testing enable_provider() with username containing leading/trailing whitespace") + db_users = [f" {self.db_users[0]} "] + try: + select_ai.enable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider enabled successfully with username containing whitespace.") + except Exception as e: + logger.error(f"enable_provider() raised {e} unexpectedly with whitespace in username.") + pytest.fail(f"enable_provider() raised {e} unexpectedly with whitespace in username.") + + def test_2412(self): + "Test enabling provider with large user list" + logger.info("Testing enable_provider() with a very large list of users (DatabaseError expected)") + db_users = [f"DB_USER_{i}" for i in range(1000)] + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.enable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info(f"Expected DatabaseError caught: {cm.value}") + + def test_2413(self): + "Test enabling provider with a valid custom endpoint (ORA-24244 expected)" + logger.info("Testing enable_provider() with a custom endpoint (ORA-24244 expected)") + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.enable_provider( + users=self.db_users, + provider_endpoint="https://custom.openai.azure.com" + ) + logger.info(f"Expected DatabaseError caught: {cm.value}") + assert "ORA-24244: invalid host or port for access control list (ACL) assignment" in str(cm.value) \ No newline at end of file diff --git a/tests/provider/test_2500_disable.py b/tests/provider/test_2500_disable.py new file mode 100644 index 0000000..b521476 --- /dev/null +++ b/tests/provider/test_2500_disable.py @@ -0,0 +1,255 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb + +logger = logging.getLogger("TestDisableProvider") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def disable_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + } + request.cls.disable_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, disable_params): + logger.info("\n=== Setting up TestDisableProvider class ===") + test_env.create_connection(use_wallet=request.cls.disable_params["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + db_users = [] + for i in range(1, 6): + user = f"DB_USER{i}" + request.cls.create_local_user(user) + db_users.append(user) + request.cls.db_users = db_users + # Create Additional user + request.cls.create_local_user("DB_USER6") + logger.info("Setup complete.\n") + yield + + logger.info("\n=== Tearing down TestDisableProvider class ===") + db_users.append("DB_USER6") + with select_ai.cursor() as admin_cursor: + for user in db_users: + try: + admin_cursor.execute(f"DROP USER {user} CASCADE") + logger.info(f"Dropped user {user}") + except oracledb.DatabaseError as e: + logger.warning(f"Disconnect failed: {e}") + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("disable_params", "setup_and_teardown") +class TestDisableProvider: + + @classmethod + def create_local_user(cls, test_username="TEST_USER1"): + logger.info(f"Creating local user: {test_username}") + test_password = cls.disable_params["password"] + with select_ai.cursor() as admin_cursor: + try: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + except oracledb.DatabaseError: + pass # Ignore if user doesn't exist + admin_cursor.execute(f"CREATE USER {test_username} IDENTIFIED BY {test_password}") + admin_cursor.execute(f"grant create session, create table, unlimited tablespace to {test_username}") + admin_cursor.execute(f"grant execute on dbms_cloud to {test_username}") + logger.info(f"User {test_username} created successfully.") + + def setup_method(self, method): + logger.info(f"\n--- Starting test: {method.__name__} ---") + self.provider_endpoint = "*.openai.azure.com" + try: + select_ai.enable_provider( + users=self.db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info(f"Provider enabled for {len(self.db_users)} users.") + except Exception as e: + pytest.fail(f"enable_provider() raised {e} unexpectedly.") + + def teardown_method(self, method): + logger.info(f"--- Finished test: {method.__name__} ---") + + # === TEST CASES === + + def test_2501(self): + "Test disabling provider with all valid users and endpoint" + try: + select_ai.disable_provider( + users=self.db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider disabled successfully for all valid users.") + except Exception as e: + pytest.fail(f"disable_provider() raised {e} unexpectedly.") + + def test_2502(self): + "Test disabling provider with a mix of existing and non-existent usernames" + db_users = ["DB_USER1", "TEST_USER2"] + with pytest.raises(oracledb.DatabaseError): + select_ai.disable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info("Caught expected DatabaseError for nonexistent user.") + + def test_2503(self): + "Test disabling provider with all invalid usernames" + with pytest.raises(oracledb.DatabaseError): + select_ai.disable_provider( + users=["INVALID_USER1", "INVALID_USER2"], + provider_endpoint=self.provider_endpoint + ) + logger.info("Caught expected DatabaseError for invalid users input.") + + def test_2504(self): + "Test disabling provider with users as integer (TypeError/ValueError expected)" + with pytest.raises((TypeError, ValueError)): + select_ai.disable_provider( + users=123, + provider_endpoint=self.provider_endpoint + ) + logger.info("Caught expected TypeError/ValueError for int users input.") + + def test_2505(self): + "Test disabling provider with users as string" + try: + select_ai.disable_provider( + users="DB_USER1", + provider_endpoint=self.provider_endpoint + ) + logger.info("Provider disabled successfully for string user input.") + except Exception as e: + pytest.fail(f"disable_provider() raised {e} unexpectedly.") + + def test_2506(self): + "Test disabling provider with users as None (TypeError/ValueError expected)" + with pytest.raises((TypeError, ValueError)): + select_ai.disable_provider( + users=None, + provider_endpoint=self.provider_endpoint + ) + logger.info("Caught expected TypeError/ValueError for none users input.") + + def test_2507(self): + "Test disabling provider with missing provider_endpoint (ValueError expected)" + with pytest.raises(ValueError): + select_ai.disable_provider( + users=self.db_users, + provider_endpoint=None + ) + logger.info("Caught expected ValueError for missing endpoint.") + + def test_2508(self): + "Test disabling provider with invalid endpoint (DatabaseError expected)" + with pytest.raises(oracledb.DatabaseError): + select_ai.disable_provider( + users=self.db_users, + provider_endpoint="invalid.endpoint" + ) + logger.info("Caught expected DatabaseError for invalid endpoint.") + + def test_2509(self): + "Test disabling provider with empty users list" + try: + select_ai.disable_provider( + users=[], + provider_endpoint=self.provider_endpoint + ) + logger.info("disable_provider() succeeded with empty users list.") + except Exception as e: + pytest.fail(f"disable_provider() raised {e} unexpectedly with empty users list.") + + def test_2510(self): + "Test disabling provider with duplicate usernames (ORA-01927 expected)" + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.disable_provider( + users=[self.db_users[0], self.db_users[0]], + provider_endpoint=self.provider_endpoint + ) + assert "ORA-01927" in str(cm.value) + logger.info("Caught expected ORA-01927 for duplicate users.") + + def test_2511(self): + "Test disabling provider with lowercase username" + try: + select_ai.disable_provider( + users=[self.db_users[0].lower()], + provider_endpoint=self.provider_endpoint + ) + logger.info("disable_provider() succeeded with lowercase username.") + except Exception as e: + pytest.fail(f"disable_provider() raised {e} unexpectedly with lowercase username.") + + def test_2512(self): + "Test disabling provider with username containing whitespace" + db_users = [f" {self.db_users[0]} "] + try: + select_ai.disable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info("disable_provider() succeeded with whitespace username.") + except Exception as e: + pytest.fail(f"disable_provider() raised {e} unexpectedly with whitespace in username.") + + def test_2513(self): + "Test disabling provider with valid custom endpoint (ORA-24244 expected)" + with pytest.raises( + oracledb.DatabaseError, + match=r"ORA-24244: invalid host or port for access control list \(ACL\) assignment" + ): + select_ai.disable_provider( + users=self.db_users, + provider_endpoint="https://custom.openai.azure.com" + ) + logger.info("Caught expected ORA-24244 for custom endpoint.") + + def test_2514(self): + "Test disabling provider with non-granted user (ORA-01927 expected)" + non_granted_user = "DB_USER6" + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.disable_provider( + users=[non_granted_user], + provider_endpoint=self.provider_endpoint + ) + assert "ORA-01927" in str(cm.value) + logger.info("Caught expected ORA-01927 for non-granted user.") + + def test_2515(self): + "Test disabling provider with a large user list (DatabaseError expected)" + db_users = [f"DB_USER_{i}" for i in range(1000)] + with pytest.raises(oracledb.DatabaseError): + select_ai.disable_provider( + users=db_users, + provider_endpoint=self.provider_endpoint + ) + logger.info("Caught expected DatabaseError for large user list.") \ No newline at end of file diff --git a/tests/test_1010_connection.py b/tests/test_1010_connection.py new file mode 100644 index 0000000..17519c8 --- /dev/null +++ b/tests/test_1010_connection.py @@ -0,0 +1,304 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb +from select_ai.errors import DatabaseNotConnectedError + +logger = logging.getLogger("TestConnection") + +@pytest.fixture(scope="session", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def connection_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "wallet_location": test_env.get_wallet_location(), + "wallet_password": test_env.get_wallet_password(), + "connect_kwargs": {}, + } + request.cls.connection_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, connection_params): + logger.info("=== Setting up TestConnection class ===") + test_env.create_connection( + dsn=request.cls.connection_params["dsn"], + use_wallet=request.cls.connection_params["use_wallet"] + ) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Initial connection successful") + yield + logger.info("=== Tearing down TestConnection class ===") + try: + select_ai.disconnect() + logger.info("Disconnected from DB") + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("connection_params", "setup_and_teardown") +class TestConnection: + def test_1011(self): + """Testing connection success with wallet.""" + logger.info("Testing connection success with wallet...") + test_env.create_connection(use_wallet=True) + is_connected = select_ai.is_connected() + assert is_connected, "Connection to DB failed" + logger.info("Connection successful") + select_ai.disconnect() + assert not select_ai.is_connected(), "Connection should be closed after disconnect" + logger.info("Disconnected after test_01_connection_success") + + def test_1012(self): + """Testing connection without wallet.""" + logger.info("Testing connection without wallet...") + test_env.create_connection(use_wallet=False) + is_connected = select_ai.is_connected() + assert is_connected, "Connection to DB failed without wallet" + logger.info("Connection successful without wallet") + select_ai.disconnect() + assert not select_ai.is_connected(), "Connection should be closed after close()" + logger.info("Disconnected after test_02_without_wallet") + + def test_1013(self): + """Testing is_connected returns bool.""" + logger.info("Testing is_connected returns bool...") + test_env.create_connection(use_wallet=self.connection_params["use_wallet"]) + assert isinstance(select_ai.is_connected(), bool) + select_ai.disconnect() + logger.info("is_connected check complete and disconnected") + + def test_1014(self): + """Testing failure with wrong password.""" + logger.info("Testing failure with wrong password...") + connect_kwargs = {} + if self.connection_params["use_wallet"]: + connect_kwargs = { + "config_dir": self.connection_params["wallet_location"], + "wallet_location": self.connection_params["wallet_location"], + "wallet_password": self.connection_params["wallet_password"], + } + with pytest.raises(oracledb.DatabaseError): + select_ai.connect( + user=self.connection_params["user"], + password="wrong_pass", + dsn=self.connection_params["dsn"], + **connect_kwargs + ) + logger.info("Correctly raised DatabaseError for wrong password") + + def test_1015(self): + """Testing connection with bad string.""" + logger.info("Testing connection with bad string...") + with pytest.raises(TypeError) as e: + select_ai.connect("not a valid connect string!!") + assert "missing 2 required positional arguments" in str(e.value) + logger.info("Correctly raised TypeError for bad string") + + def test_1016(self): + """Testing connection with bad DSN.""" + logger.info("Testing connection with bad DSN...") + with pytest.raises(oracledb.DatabaseError) as excinfo: + select_ai.connect( + user=self.connection_params["user"], + password=self.connection_params["password"], + dsn="invalid_dsn", + **self.connection_params["connect_kwargs"] + ) + msg = str(excinfo.value) + logger.info(f"Database exception message was: {msg}") + assert ("DPY-4026" in msg) or ("DPY-4027" in msg) + logger.info("Correctly raised DatabaseError for bad DSN") + + def test_1017(self): + """Testing connection with bad password.""" + logger.info("Testing connection with bad password...") + with pytest.raises(oracledb.DatabaseError) as excinfo: + select_ai.connect( + user=self.connection_params["user"], + password=self.connection_params["password"] + "X", + dsn=self.connection_params["dsn"], + **self.connection_params["connect_kwargs"] + ) + assert "ORA-01017" in str(excinfo.value) + logger.info("Correctly raised DatabaseError for wrong password") + + def test_1018(self): + """Testing simple query execution.""" + logger.info("Testing simple query execution...") + test_env.create_connection(use_wallet=self.connection_params["use_wallet"]) + with select_ai.cursor() as cr: + cr.execute("SELECT 1 FROM DUAL") + result = cr.fetchone() + assert result[0] == 1 + logger.info(f"Query executed successfully, result: {result[0]}") + # select_ai.disconnect() + + def test_1019(self): + """Testing query with parameters.""" + logger.info("Testing query with parameters...") + with select_ai.cursor() as cr: + cr.execute("SELECT :val FROM dual", val=42) + result = cr.fetchone() + assert result[0] == 42 + logger.info(f"Query with parameters successful, result: {result[0]}") + + def test_1020(self): + """Testing fetchall.""" + logger.info("Testing fetchall...") + with select_ai.cursor() as cursor: + cursor.execute("SELECT level FROM dual CONNECT BY level <= 5") + results = cursor.fetchall() + assert len(results) == 5 + logger.info(f"Fetched rows: {len(results)}") + + def test_1021(self): + """Testing invalid query.""" + logger.info("Testing invalid query...") + with select_ai.cursor() as cursor: + with pytest.raises(oracledb.DatabaseError): + cursor.execute("SELECT * FROM non_existent_table") + logger.info("Correctly raised DatabaseError for invalid query") + + def test_1022(self): + """Testing commit and rollback.""" + logger.info("Testing commit and rollback...") + with select_ai.cursor() as cursor: + cursor.execute(""" + begin + execute immediate 'create table test_cr_tab (id int)'; + exception + when others then + if sqlcode != -955 then + raise; + end if; + end; + """) + cursor.execute("commit") + cursor.execute("truncate table test_cr_tab") + cursor.execute("insert into test_cr_tab values (1)") + cursor.execute("rollback") + cursor.execute("select count(*) from test_cr_tab") + (count,) = cursor.fetchone() + assert count == 0 + logger.info("Rollback verified successfully") + + def test_1023(self): + """Testing connection close error.""" + logger.info("Testing connection close error...") + select_ai.disconnect() + with pytest.raises(DatabaseNotConnectedError): + with select_ai.cursor() as cr: + cr.execute("SELECT 1 FROM DUAL") + logger.info("DatabaseNotConnectedError correctly raised on disconnected cursor") + + def test_1024(self): + """Testing repeated disconnect.""" + logger.info("Testing repeated disconnect...") + test_env.create_connection(use_wallet=self.connection_params["use_wallet"]) + select_ai.disconnect() + select_ai.disconnect() + is_connected = select_ai.is_connected() + assert not is_connected, "Connection should be closed after repeated disconnects" + logger.info("Repeated disconnect handled successfully") + + def test_1025(self): + """Testing DBMS_OUTPUT package.""" + logger.info("Testing DBMS_OUTPUT package...") + test_env.create_connection(use_wallet=self.connection_params["use_wallet"]) + test_string = "Testing DBMS_OUTPUT package" + with select_ai.cursor() as cursor: + cursor.callproc("dbms_output.enable") + cursor.callproc("dbms_output.put_line", [test_string]) + string_var = cursor.var(str) + number_var = cursor.var(int) + cursor.callproc("dbms_output.get_line", (string_var, number_var)) + assert string_var.getvalue() == test_string + logger.info(f"DBMS_OUTPUT verified: {string_var.getvalue()}") + # select_ai.disconnect() + + def test_1026(self): + """Testing instance name retrieval.""" + logger.info("Testing instance name retrieval...") + with select_ai.cursor() as cursor: + cursor.execute( + "select upper(sys_context('userenv', 'instance_name')) from dual" + ) + (instance_name,) = cursor.fetchone() + assert isinstance(instance_name, str) + logger.info(f"Instance name: {instance_name}") + + def test_1027(self): + """Testing max open cursors.""" + logger.info("Testing max open cursors...") + with select_ai.cursor() as cursor: + cursor.execute( + "select value from V$PARAMETER where name='open_cursors'" + ) + (max_open_cursors,) = cursor.fetchone() + assert int(max_open_cursors) == 1000 + logger.info(f"Max open cursors: {max_open_cursors}") + + def test_1028(self): + """Testing service name retrieval.""" + logger.info("Testing service name retrieval...") + with select_ai.cursor() as cursor: + cursor.execute( + "select sys_context('userenv', 'service_name') from dual" + ) + (service_name,) = cursor.fetchone() + assert isinstance(service_name, str) + logger.info(f"Service name: {service_name}") + + def test_1029(self): + """Testing user and table creation.""" + logger.info("Testing user and table creation...") + test_username = "TEST_USER1" + test_password = self.connection_params["password"] + with select_ai.cursor() as admin_cursor: + try: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + except oracledb.DatabaseError: + logger.info(f"User {test_username} did not exist before test") + admin_cursor.execute(f"CREATE USER {test_username} IDENTIFIED BY {test_password}") + admin_cursor.execute(f"grant create session, create table, unlimited tablespace to {test_username}") + logger.info(f"Created test user: {test_username}") + test_env.create_connection( + user=test_username, + password=test_password, + dsn=self.connection_params["dsn"], + use_wallet=self.connection_params["use_wallet"] + ) + with select_ai.cursor() as test_cursor: + test_cursor.execute("CREATE TABLE test_table (id INT)") + test_cursor.execute("INSERT INTO test_table (id) VALUES (100)") + test_cursor.execute("SELECT id FROM test_table") + result = test_cursor.fetchone() + assert result[0] == 100 + logger.info("Test table created and verified successfully") + select_ai.disconnect() + test_env.create_connection(use_wallet=self.connection_params["use_wallet"]) + with select_ai.cursor() as admin_cursor: + admin_cursor.execute(f"DROP USER {test_username} CASCADE") + logger.info(f"Dropped test user: {test_username}") \ No newline at end of file diff --git a/tests/test_env.py b/tests/test_env.py new file mode 100644 index 0000000..038ccd5 --- /dev/null +++ b/tests/test_env.py @@ -0,0 +1,210 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import os +import sys +import secrets +import string +import select_ai + +# dictionary containing all parameters; these are acquired as needed by the +# methods below (which should be used instead of consulting this dictionary +# directly) and then stored so that a value is not requested more than once +PARAMETERS = {} + + +# ------------------------- +# PARAMETER ACCESS HELPERS +# ------------------------- +def get_value(name, default_value=None, **kwargs): + """Retrieve a value from PARAMETERS or environment.""" + if name in PARAMETERS: + return PARAMETERS[name] + + env_name = "SAI_TEST_" + name + value = os.environ.get(env_name) + + if not value: + value = default_value + + PARAMETERS[name] = value + return value + + +def get_connection_args(): + """Get and return connection parameters""" + return { + "user": get_test_user(), + "password": get_test_password(), + "dsn": get_connect_string(), + "wallet_location": get_wallet_location(), + "wallet_password": get_wallet_password(), + } + + +def create_connection(use_wallet=True, **kwargs): + """Create a synchronous connection.""" + conn_args = get_connection_args() + + connect_kwargs = { + "user": kwargs.get("user", conn_args["user"]), + "password": kwargs.get("password", conn_args["password"]), + "dsn": kwargs.get("dsn", conn_args["dsn"]), + } + + if use_wallet: + connect_kwargs.update({ + "config_dir": kwargs.get("wallet_location", conn_args["wallet_location"]), + "wallet_location": kwargs.get("wallet_location", conn_args["wallet_location"]), + "wallet_password": kwargs.get("wallet_password", conn_args["wallet_password"]) + }) + + select_ai.connect(**connect_kwargs) + + +async def create_async_connection(use_wallet=True, **kwargs): + """Create an asynchronous connection.""" + conn_args = get_connection_args() + + connect_kwargs = { + "user": kwargs.get("user", conn_args["user"]), + "password": kwargs.get("password", conn_args["password"]), + "dsn": kwargs.get("dsn", conn_args["dsn"]), + } + + if use_wallet: + connect_kwargs.update({ + "config_dir": kwargs.get("wallet_location", conn_args["wallet_location"]), + "wallet_location": kwargs.get("wallet_location", conn_args["wallet_location"]), + "wallet_password": kwargs.get("wallet_password", conn_args["wallet_password"]) + }) + + print(connect_kwargs) + await select_ai.async_connect(**connect_kwargs) + + + +# --------------------- +# SIMPLE VALUE HELPERS +# --------------------- +def get_connect_string(): + """Retrieve the database connection string used for testing.""" + return get_value("CONNECT_STRING", default_value="Connect String") + + +def get_test_password(): + """Retrieve the test password, possibly from environment or set a default.""" + return get_value("PASSWORD", default_value=f"Password for {get_test_user()}", password=True) + + +def get_test_user(): + """Retrieve the test user name, or use a default value.""" + return get_value("USER", default_value="Test User Name") + + +def get_use_wallet(): + """Check if a PDB wallet should be used for the connection.""" + return get_value("USE_WALLET", default_value=False) + + +def get_wallet_location(): + """Retrieve the file path for the wallet location, if specified.""" + return get_value("WALLET_LOCATION", default_value=None) + + +def get_wallet_password(): + """Retrieve the password for accessing the wallet.""" + return get_value("WALLET_PASSWORD", default_value="Wallet Password", password=True) + + +def get_cred_username(): + """Retrieve the OCI credential username (for cloud authentication).""" + return get_value("CRED_USERNAME", default_value="OCI credential username") + + +def get_cred_password(): + """Retrieve the OCI credential password (for cloud authentication).""" + return get_value("CRED_PASSWORD", default_value="OCI credential password") + + +def get_user_ocid(): + """Retrieve the OCID for the user in OCI.""" + return get_value("USER_OCID", default_value="user ocid") + + +def get_tenancy_ocid(): + """Retrieve the OCID for the tenancy in OCI.""" + return get_value("TENANCY_OCID", default_value="tenancy ocid") + + +def get_private_key(): + """Retrieve the private key used for authentication or signing.""" + return get_value("PRIVATE_KEY", default_value="private key") + + +def get_fingerprint(): + """Retrieve the fingerprint for the private key used in OCI.""" + return get_value("FINGERPRINT", default_value="fingerprint") + + +def get_compartment_id(provider="OCI"): + """Retrieve the compartment OCID for a given cloud provider (default: OCI).""" + return get_value(f"{provider}_COMP_ID", default_value="Compartment ID") + + +def get_embedding_location(): + """Retrieve the file or folder location for storing vector embeddings.""" + return get_value("EMBEDDING_LOCATION", default_value="Vector Embedding Location") + + +def get_random_string(length=10): + """Generate a random string of the specified length, using ASCII letters.""" + return "".join(secrets.choice(string.ascii_letters) for i in range(length)) + + +# ------------------ +# SQL SCRIPT HELPER +# ------------------ +def run_sql_script(conn, script_name, **kwargs): + statement_parts = [] + cursor = conn.cursor() + replace_values = [("&" + k + ".", v) for k, v in kwargs.items()] + [ + ("&" + k, v) for k, v in kwargs.items() + ] + script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + file_name = os.path.join(script_dir, "sql", script_name + ".sql") + for line in open(file_name): + if line.strip() == "/": + statement = "".join(statement_parts).strip() + if statement: + for search_value, replace_value in replace_values: + statement = statement.replace(search_value, replace_value) + try: + cursor.execute(statement) + except: + print("Failed to execute SQL:", statement) + raise + statement_parts = [] + else: + statement_parts.append(line) + cursor.execute( + """ + select name, type, line, position, text + from dba_errors + where owner = upper(:owner) + order by name, type, line, position + """, + owner=get_test_user(), + ) + prev_name = prev_obj_type = None + for name, obj_type, line_num, position, text in cursor: + if name != prev_name or obj_type != prev_obj_type: + print("%s (%s)" % (name, obj_type)) + prev_name = name + prev_obj_type = obj_type + print(" %s/%s %s" % (line_num, position, text)) + diff --git a/tests/vector_index/test_5000_create_index.py b/tests/vector_index/test_5000_create_index.py new file mode 100644 index 0000000..f8e6aa4 --- /dev/null +++ b/tests/vector_index/test_5000_create_index.py @@ -0,0 +1,418 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb +from select_ai import OracleVectorIndexAttributes + +logger = logging.getLogger("TestCreateVectorIndex") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def vector_index_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + "oci_compartment_id": test_env.get_compartment_id(), + "embedding_location": test_env.get_embedding_location(), + } + request.cls.vector_index_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, vector_index_params): + logger.info("\n=== Setting up TestCreateVectorIndex class ===") + test_env.create_connection(use_wallet=request.cls.vector_index_params["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Fetching credential secrets and OCI configuration...") + # Create credentials and profile + request.cls.create_credential() + request.cls.profile = request.cls.create_profile() + logger.info("Setup complete.\n") + yield + + logger.info("\n=== Tearing down TestCreateVectorIndex class ===") + request.cls.delete_profile(request.cls.profile) + request.cls.delete_credential() + logger.info("Disconnecting from DB...") + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("vector_index_params", "setup_and_teardown") +class TestCreateVectorIndex: + + @classmethod + def get_native_cred_param(cls, cred_name=None) -> dict: + logger.info(f"Preparing native credential params for: {cred_name}") + params = cls.vector_index_params + return dict( + credential_name = cred_name, + user_ocid = params["user_ocid"], + tenancy_ocid = params["tenancy_ocid"], + private_key = params["private_key"], + fingerprint = params["fingerprint"] + ) + + @classmethod + def get_cred_param(cls, cred_name=None) -> dict: + logger.info(f"Preparing basic credential params for: {cred_name}") + params = cls.vector_index_params + return dict( + credential_name = cred_name, + username = params["cred_username"], + password = params["cred_password"] + ) + @classmethod + def create_credential(cls, genai_cred="GENAI_CRED", objstore_cred="OBJSTORE_CRED"): + logger.info(f"Creating credentials: {genai_cred}, {objstore_cred}") + genai_credential = cls.get_native_cred_param(genai_cred) + objstore_credential = cls.get_cred_param(objstore_cred) + try: + logger.info(f"Creating GenAI credential: {genai_cred}") + select_ai.create_credential(credential=genai_credential, replace=True) + logger.info("GenAI credential created.") + except Exception as e: + raise AssertionError(f"create_credential() raised {e} unexpectedly.") + try: + logger.info(f"Creating ObjectStore credential: {objstore_cred}") + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("ObjectStore credential created.") + except Exception as e: + raise AssertionError(f"create_credential() raised {e} unexpectedly.") + + @classmethod + def create_profile(cls, profile_name="vector_ai_profile"): + logger.info(f"Creating Profile: {profile_name}") + params = cls.vector_index_params + provider = select_ai.OCIGenAIProvider( + oci_compartment_id=params["oci_compartment_id"], + oci_apiformat="GENERIC" + ) + profile_attributes = select_ai.ProfileAttributes( + credential_name="GENAI_CRED", + provider=provider + ) + profile = select_ai.Profile( + profile_name=profile_name, + attributes=profile_attributes, + description="OCI GENAI Profile", + replace=True + ) + logger.info(f"Profile '{profile_name}' created successfully.") + return profile + + @classmethod + def delete_profile(cls, profile): + logger.info("Deleting profile...") + try: + profile.delete() + logger.info(f"Profile '{profile.profile_name}' deleted successfully.") + except Exception as e: + raise AssertionError(f"profile.delete() raised {e} unexpectedly.") + + @classmethod + def delete_credential(cls): + logger.info("Deleting credentials...") + try: + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info("Deleted credential 'GENAI_CRED'") + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.delete_credential("OBJSTORE_CRED", force=True) + logger.info("Deleted credential 'OBJSTORE_CRED'") + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + + def setup_method(self, method): + logger.info(f"\n--- Starting test: {method.__name__} ---") + self.objstore_cred = "OBJSTORE_CRED" + params = self.vector_index_params + self.vector_index_attributes = OracleVectorIndexAttributes( + location=params["embedding_location"], + object_storage_credential_name=self.objstore_cred + ) + self.profile = self.profile + self.vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=self.vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + + def teardown_method(self, method): + logger.info(f"--- Finished test: {method.__name__} ---") + try: + vector_index = select_ai.VectorIndex(index_name="test_vector_index") + vector_index.delete(force=True) + logger.info("Vector index deleted successfully.") + except Exception as e: + logger.warning(f"Warning: vector index cleanup failed: {e}") + + def test_5001(self): + """Test successful vector index creation.""" + try: + self.vector_index.create(replace=True) + logger.info("Vector index created successfully.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + logger.info("Verifying created vector index...") + vector_index = select_ai.VectorIndex() + indexes = [i.index_name for i in vector_index.list()] + logger.info(f"Indexes found: {indexes}") + assert "TEST_VECTOR_INDEX" in indexes + logger.info("Verified vector index creation successfully.") + + def test_5002(self): + """Test vector index creation with replace=False.""" + try: + self.vector_index.create(replace=False) + logger.info("Vector index created successfully.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + logger.info("Verifying created vector index...") + vector_index = select_ai.VectorIndex() + indexes = [i.index_name for i in vector_index.list()] + logger.info(f"Indexes found: {indexes}") + assert "TEST_VECTOR_INDEX" in indexes + logger.info("Verified vector index presence.") + + def test_5003(self): + """Test vector index creation with empty description.""" + params = self.vector_index_params + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=self.vector_index_attributes, + description="", + profile=self.profile + ) + try: + vector_index.create(replace=True) + logger.info("Vector index created successfully with empty description.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + logger.info("Verifying created vector index...") + vector_index = select_ai.VectorIndex() + indexes = [i.index_name for i in vector_index.list()] + logger.info(f"Indexes found: {indexes}") + assert "TEST_VECTOR_INDEX" in indexes + logger.info("Verified vector index creation with empty description.") + + def test_5004(self): + """Test vector index recreation with replace=True.""" + try: + self.vector_index.create(replace=True) + logger.info("First creation successful.") + self.vector_index.create(replace=True) + logger.info("Second creation successful with replace=True.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + + def test_5005(self): + """Test vector index recreation with replace=False (expect failure).""" + try: + self.vector_index.create(replace=False) + logger.info("First creation successful.") + except Exception as e: + pytest.fail(f"Create vector index failed unexpectedly: {e}") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.create(replace=False) + logger.info(f"Expected DatabaseError raised: {cm.value}") + assert "ORA-20048" in str(cm.value) + assert "already exists" in str(cm.value) + logger.info("Verified error on duplicate creation with replace=False.") + + def test_5006(self): + """Test minimal attribute vector index creation.""" + params = self.vector_index_params + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=self.vector_index_attributes, + profile=self.profile + ) + try: + vector_index.create(replace=True) + logger.info("Vector index created successfully with minimal attributes.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + + def test_5007(self): + """Test vector index recreation after delete.""" + try: + self.vector_index.create(replace=True) + logger.info("Vector index created successfully.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + logger.info("Deleting vector index...") + vector_index = select_ai.VectorIndex(index_name="test_vector_index") + vector_index.delete(force=True) + logger.info("Vector index deleted successfully.") + logger.info("Recreating vector index...") + try: + self.vector_index.create(replace=True) + logger.info("Vector index recreated successfully.") + except Exception as e: + pytest.fail(f"VectorIndex.create raised an unexpected exception: {e}") + + def test_5008(self): + """Test vector index creation with invalid credential.""" + params = self.vector_index_params + vector_index_attributes = OracleVectorIndexAttributes( + location=params["embedding_location"], + object_storage_credential_name="invalidObjStore_cred" + ) + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + with pytest.raises(oracledb.DatabaseError) as cm: + vector_index.create(replace=True) + logger.info(f"Expected DatabaseError raised: {cm.value}") + + def test_5009(self): + """Test vector index creation with invalid location.""" + params = self.vector_index_params + vector_index_attributes = OracleVectorIndexAttributes( + location="invalid_location", + object_storage_credential_name=self.objstore_cred + ) + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + with pytest.raises(oracledb.DatabaseError) as cm: + vector_index.create(replace=True) + logger.info(f"Expected DatabaseError raised: {cm.value}") + + def test_5010(self): + """Test vector index creation with missing attributes.""" + with pytest.raises(AttributeError): + select_ai.VectorIndex( + index_name="test_vector_index", + attributes=None, + profile=self.profile + ).create() + logger.info("Expected AttributeError raised for missing attributes.") + + def test_5011(self): + """Test vector index creation with invalid attributes type.""" + with pytest.raises(TypeError): + select_ai.VectorIndex( + index_name="test_vector_index", + attributes="invalid_attributes", + profile=self.profile + ).create() + logger.info("Expected TypeError raised for invalid attribute type.") + + def test_5012(self): + """Test vector index creation with invalid name type.""" + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.VectorIndex( + index_name=12345, + attributes=self.vector_index_attributes, + profile=self.profile + ).create() + assert "ORA-20048" in str(cm.value) + assert "Invalid vector index name" in str(cm.value) + logger.info(f"Expected DatabaseError raised: {cm.value}") + + def test_5013(self): + """Test vector index creation with empty name.""" + with pytest.raises(oracledb.DatabaseError) as cm: + select_ai.VectorIndex( + index_name="", + attributes=self.vector_index_attributes, + profile=self.profile + ).create() + assert "ORA-20048" in str(cm.value) + assert "Missing vector index name" in str(cm.value) + logger.info(f"Expected DatabaseError raised: {cm.value}") + + def test_5014(self): + """Test vector index creation with invalid profile.""" + with pytest.raises(TypeError) as cm: + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=self.vector_index_attributes, + description="Test vector index", + profile="invalid_profile" + ) + vector_index.create() + logger.info(f"Expected ValueError raised for invalid profile: {cm.value}") + + def test_5015(self): + """Test vector index creation with None attributes.""" + with pytest.raises(TypeError) as cm: + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=None, + description="Test vector index", + profile="invalid_profile" + ) + vector_index.create() + logger.info(f"Expected TypeError raised for None attributes: {cm.value}") + + def test_5016(self): + """Test vector index creation with long name (>128 chars).""" + long_name = "X" * 150 + vector_index = select_ai.VectorIndex( + index_name=long_name, + attributes=self.vector_index_attributes, + profile=self.profile + ) + with pytest.raises(oracledb.DatabaseError) as cm: + vector_index.create() + logger.info(f"Expected DatabaseError raised for long name: {cm.value}") + + def test_5017(self): + """Test vector index creation with long description.""" + long_desc = "D" * 5000 + vector_index = select_ai.VectorIndex( + index_name="test_vector_index", + attributes=self.vector_index_attributes, + description=long_desc, + profile=self.profile + ) + with pytest.raises(oracledb.DatabaseError) as cm: + vector_index.create(replace=True) + assert "ORA-20045" in str(cm.value) + assert "description is too long" in str(cm.value) + logger.info(f"Expected DatabaseError raised: {cm.value}") + + def test_5018(self): + """Test multiple recreations of vector index (10x).""" + for _ in range(10): + self.vector_index.create(replace=True) + logger.info("Successfully recreated vector index multiple times.") diff --git a/tests/vector_index/test_5100_drop_index.py b/tests/vector_index/test_5100_drop_index.py new file mode 100644 index 0000000..a65b2b2 --- /dev/null +++ b/tests/vector_index/test_5100_drop_index.py @@ -0,0 +1,520 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb +import time +from select_ai import OracleVectorIndexAttributes + +# Set up global logger (one per module) +logger = logging.getLogger("TestDeleteVectorIndex") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def delete_vec_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + "oci_compartment_id": test_env.get_compartment_id(), + "embedding_location": test_env.get_embedding_location(), + } + request.cls.delete_vec_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, delete_vec_params): + logger.info("=== Setting up TestDeleteVectorIndex class ===") + test_env.create_connection(use_wallet=request.cls.delete_vec_params["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Fetching credential secrets and OCI configuration...") + request.cls.create_credential() + request.cls.profile = request.cls.create_profile() + logger.info("Setup complete.") + yield + logger.info("=== Tearing down TestDeleteVectorIndex class ===") + request.cls.delete_profile(request.cls.profile) + request.cls.delete_credential() + logger.info("Disconnecting from DB...") + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("delete_vec_params", "setup_and_teardown") +class TestDeleteVectorIndex: + @classmethod + def get_native_cred_param(cls, cred_name=None) -> dict: + logger.info(f"Preparing native credential params for: {cred_name}") + params = cls.delete_vec_params + return dict( + credential_name = cred_name, + user_ocid = params["user_ocid"], + tenancy_ocid = params["tenancy_ocid"], + private_key = params["private_key"], + fingerprint = params["fingerprint"] + ) + @classmethod + def get_cred_param(cls, cred_name=None) -> dict: + logger.info(f"Preparing basic credential params for: {cred_name}") + params = cls.delete_vec_params + return dict( + credential_name = cred_name, + username = params["cred_username"], + password = params["cred_password"] + ) + @classmethod + def create_credential(cls, genai_cred="GENAI_CRED", objstore_cred="OBJSTORE_CRED"): + logger.info(f"Creating credentials: {genai_cred}, {objstore_cred}") + genai_credential = cls.get_native_cred_param(genai_cred) + objstore_credential = cls.get_cred_param(objstore_cred) + try: + logger.info(f"Creating GenAI credential: {genai_cred}") + select_ai.create_credential(credential=genai_credential, replace=True) + logger.info("GenAI credential created.") + except Exception as e: + logger.error(f"create_credential() raised {e} unexpectedly.") + raise AssertionError(f"create_credential() raised {e} unexpectedly.") + try: + logger.info(f"Creating ObjectStore credential: {objstore_cred}") + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("ObjectStore credential created.") + except Exception as e: + logger.error(f"create_credential() raised {e} unexpectedly.") + raise AssertionError(f"create_credential() raised {e} unexpectedly.") + @classmethod + def create_profile(cls, profile_name="vector_ai_profile"): + logger.info(f"Creating Profile: {profile_name}") + params = cls.delete_vec_params + provider = select_ai.OCIGenAIProvider( + oci_compartment_id=params["oci_compartment_id"], + oci_apiformat="GENERIC" + ) + profile_attributes = select_ai.ProfileAttributes( + credential_name="GENAI_CRED", + provider=provider + ) + profile = select_ai.Profile( + profile_name=profile_name, + attributes=profile_attributes, + description="OCI GENAI Profile", + replace=True + ) + logger.info(f"Profile '{profile_name}' created successfully.") + return profile + @classmethod + def delete_profile(cls, profile): + logger.info("Deleting profile...") + try: + profile.delete() + logger.info(f"Profile '{profile.profile_name}' deleted successfully.") + except Exception as e: + logger.error(f"profile.delete() raised {e} unexpectedly.") + raise AssertionError(f"profile.delete() raised {e} unexpectedly.") + @classmethod + def delete_credential(cls): + logger.info("Deleting credentials...") + try: + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info("Deleted credential 'GENAI_CRED'") + except Exception as e: + logger.error(f"delete_credential() raised {e} unexpectedly.") + raise AssertionError(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.delete_credential("OBJSTORE_CRED", force=True) + logger.info("Deleted credential 'OBJSTORE_CRED'") + except Exception as e: + logger.error(f"delete_credential() raised {e} unexpectedly.") + raise AssertionError(f"delete_credential() raised {e} unexpectedly.") + + def delete_and_wait(self, force=True, pattern=".*", wait_seconds=1): + logger.info("Deleting indexes matching pattern.") + all_indexes = list(self.vecidx.list(index_name_pattern=pattern)) + if not all_indexes: + logger.info("No indexes found to delete.") + return + for idx in all_indexes: + try: + idx.delete(force=force) + logger.info(f"Deleted index: {idx.index_name}") + time.sleep(wait_seconds) + except Exception as e: + logger.warning(f"Warning: failed to delete index {idx.index_name}: {e}") + remaining = list(self.vecidx.list(index_name_pattern=pattern)) + logger.info(f"Remaining indexes after delete: {[i.index_name for i in remaining]}") + + def setup_method(self, method): + logger.info(f"SetUp for {method.__name__}") + self.objstore_cred = "OBJSTORE_CRED" + self.vecidx = select_ai.VectorIndex() + params = self.delete_vec_params + self.vector_index_attributes = select_ai.OracleVectorIndexAttributes( + location=params["embedding_location"], + object_storage_credential_name=self.objstore_cred + ) + self.delete_and_wait() + self.index_name = "test_vector_index" + self.vector_index = select_ai.VectorIndex( + index_name=self.index_name, + attributes=self.vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + self.vector_index.create(replace=True) + logger.info(f"Vector index '{self.index_name}' created for test.") + + def teardown_method(self, method): + logger.info(f"TearDown for {method.__name__}") + try: + self.vector_index.delete(force=True) + logger.info(f"Vector index '{self.index_name}' deleted successfully.") + except Exception as e: + logger.warning(f"Warning: vector index cleanup failed: {e}") + + def assert_index_count(self, pattern, expected): + actual = list(self.vecidx.list(index_name_pattern=pattern)) + logger.info(f"Indexes matching '{pattern}': {[i.index_name for i in actual]}") + assert len(actual) == expected, f"Expected {expected} indexes, got {len(actual)}" + + def verify_and_cleanup_vectab(self, vector_index_name: str): + table_name = f"{vector_index_name}$vectab".upper() + logger.info(f"Verifying and cleaning up vector table: {table_name}") + with select_ai.cursor() as cursor: + cursor.execute(""" + SELECT column_name + FROM user_tab_columns + WHERE table_name = :table_name + ORDER BY column_id + """, {"table_name": table_name}) + cols = [c[0] for c in cursor.fetchall()] + logger.info(f"Columns found in {table_name}: {cols}") + expected_cols = ["CONTENT", "ATTRIBUTES", "EMBEDDING"] + assert cols == expected_cols, f"Unexpected columns for {table_name}: {cols}" + cursor.execute(f"DROP TABLE {table_name} PURGE") + logger.info(f"Table {table_name} dropped successfully.") + + def test_5101(self): + """Test single vector index deletion removes the index.""" + logger.info("Deleting vector index (single delete)") + self.assert_index_count("^test_vector_index", 1) + self.vector_index.delete(force=True) + logger.info("Delete called on vector index.") + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + logger.info("Single-delete verified: index removed") + + # def test_5102(self): + # """Test multiple creates then bulk delete.""" + # logger.info("Creating multiple vector indexes") + # # Create multiple indexes + # for i in range(5): + # idx = select_ai.VectorIndex( + # index_name=f"TEST_VECTOR_INDEX_{i}", + # attributes=self.vector_index_attributes, + # profile=self.profile + # ) + # idx.create(replace=True) + # logger.info(f"Created index TEST_VECTOR_INDEX_{i}") + # logger.info("Deleting all created indexes") + # # Delete all indexes one by one + # self.delete_and_wait(force=True, pattern=f"^TEST_VECTOR_INDEX_{i}$") + # # Ensure all are gone + # actual_indexes = list(self.vector_index.list(index_name_pattern="^TEST_VECTOR_INDEX_")) + # logger.info(f"Indexes found for bulk delete test: {actual_indexes}") + # assert len(actual_indexes) == 0 + # logger.info("All TEST_VECTOR_INDEX_* deleted successfully.") + + def test_5103(self): + """Test deleting the same vector index twice.""" + logger.info("Deleting vector index first time") + self.vector_index.delete(force=True) + logger.info("Deleting vector index second time (no-op expected)") + time.sleep(1) + self.vector_index.delete(force=True) # no-op + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + logger.info("Double-delete verified: index removed") + + def test_5104(self): + """Test delete with include_data=True also removes table.""" + logger.info("Deleting index with include_data=True (metadata + table)") + self.vector_index.delete(include_data=True, force=True) + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + table_name = "TEST_VECTOR_INDEX$VECTAB" + with select_ai.cursor() as cursor: + cursor.execute(""" + SELECT COUNT(*) + FROM user_tables + WHERE table_name = :table_name + """, {"table_name": table_name}) + (count,) = cursor.fetchone() + logger.info(f"Verified vector table '{table_name}' removed: {count==0}") + assert count == 0 + + def test_5105(self): + """Test delete with include_data=False doesn't remove table.""" + logger.info("Deleting index with include_data=False (metadata only)") + self.vector_index.delete(include_data=False, force=True) + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + logger.info("Attempting to recreate index (should fail due to leftover table)") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.create(replace=True) + logger.info(f"Expected DatabaseError on recreate: {cm.value}") + assert "ORA-00955" in str(cm.value) + self.verify_and_cleanup_vectab("test_vector_index") + logger.info("Vector table cleaned up after failed recreate") + + def test_5106(self): + """Test delete twice with include_data=False then cleanup.""" + logger.info("Deleting index metadata only first time") + self.vector_index.delete(include_data=False, force=True) + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + logger.info("Attempting to recreate index (should fail)") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.create(replace=True) + assert "ORA-00955" in str(cm.value) + self.verify_and_cleanup_vectab("test_vector_index") + logger.info("Vector table cleaned up") + logger.info("Deleting index metadata only second time (no-op)") + self.vector_index.delete(include_data=False, force=True) + self.assert_index_count("^test_vector_index", 0) + + def test_5107(self): + """Test delete twice with include_data=False and cleanup after failed recreate.""" + logger.info("Deleting metadata only first time") + self.vector_index.delete(include_data=False, force=True) + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + logger.info("Attempting recreate (expected failure)") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.create(replace=True) + logger.info(f"Recreate failed (expected): {cm.value}") + assert "ORA-00955" in str(cm.value) + logger.info("Deleting metadata second time (no-op)") + self.vector_index.delete(include_data=False, force=True) + self.verify_and_cleanup_vectab("test_vector_index") + self.assert_index_count("^test_vector_index", 0) + logger.info("Cleanup complete") + + def test_5108(self): + """Test delete and then recreate a vector index.""" + logger.info("Deleting index before recreation") + self.vector_index.delete(force=True) + time.sleep(1) + logger.info("Recreating vector index") + self.vector_index.create(replace=True) + time.sleep(1) + self.assert_index_count("^test_vector_index", 1) + logger.info("Recreate verified: index exists") + + def test_5109(self): + """Test delete of a nonexistent index (should not error).""" + idx = select_ai.VectorIndex( + index_name="nonexistent_index", + attributes=self.vector_index_attributes, + profile=self.profile + ) + logger.info("Attempting to delete nonexistent index") + idx.delete(force=True) + time.sleep(1) + self.assert_index_count("^nonexistent_index", 0) + logger.info("Nonexistent delete verified (no error)") + + def test_5110(self): + """Test delete after set_attributes was called.""" + logger.info("Setting temporary attributes before delete") + self.vector_index.create(replace=True) + try: + self.vector_index.set_attributes( + attribute_name="match_limit", + attribute_value=10 + ) + except Exception as e: + logger.error(f"set_attributes() raised {e} unexpectedly.") + pytest.fail(f"set_attributes() raised {e} unexpectedly.") + logger.info("Deleting index after setting attributes") + self.vector_index.delete(force=True) + time.sleep(1) + actual_indexes = list(self.vector_index.list(index_name_pattern="^test_vector_index$")) + logger.info(f"Indexes remaining after delete: {actual_indexes}") + assert len(actual_indexes) == 0 + logger.info("Delete after attributes verified") + + def test_5111(self): + """Test case-sensitive name for create and delete.""" + idx = select_ai.VectorIndex( + index_name="CaseSensitiveIndex", + attributes=self.vector_index_attributes, + profile=self.profile + ) + logger.info("Creating case-sensitive index") + idx.create(replace=True) + logger.info("Deleting case-sensitive index") + idx.delete(force=True) + time.sleep(1) + self.assert_index_count("^CaseSensitiveIndex", 0) + logger.info("Case-sensitive index delete verified") + + def test_5112(self): + """Test creation and deletion with long index name.""" + long_name = "index_" + "x" * 40 + idx = select_ai.VectorIndex( + index_name=long_name, + attributes=self.vector_index_attributes, + profile=self.profile + ) + logger.info(f"Creating long-name index: {long_name}") + idx.create(replace=True) + logger.info(f"Deleting long-name index: {long_name}") + idx.delete(force=True) + time.sleep(1) + self.assert_index_count(f"^{long_name}$", 0) + logger.info("Long-name index delete verified") + + def test_5113(self): + """Test creation and bulk deletion of indexes.""" + names = [f"bulk_idx_{i}" for i in range(3)] + logger.info("Creating bulk indexes") + for n in names: + select_ai.VectorIndex( + index_name=n, + attributes=self.vector_index_attributes, + profile=self.profile + ).create(replace=True) + logger.info(f"Created {n}") + logger.info("Deleting bulk indexes") + for n in names: + select_ai.VectorIndex( + index_name=n, + attributes=self.vector_index_attributes, + profile=self.profile + ).delete(force=True) + time.sleep(1) + logger.info(f"Deleted {n}") + self.assert_index_count("^bulk_idx_", 0) + logger.info("Bulk delete verified") + + def test_5114(self): + """Test that list returns empty after index is deleted.""" + logger.info("Deleting index and verifying list is empty") + self.vector_index.delete(force=True) + time.sleep(1) + actual_indexes = list(self.vector_index.list(index_name_pattern=".*")) + index_names = [idx.index_name for idx in actual_indexes] + logger.info(f"Actual indexes after delete: {index_names}") + actual = list(self.vector_index.list(index_name_pattern="^test_vector_index")) + assert actual == [] + logger.info("List verification successful: no remaining indexes") + + def test_5115(self): + """Test delete then recreate index with same name.""" + logger.info("Deleting index before recreate with same name") + self.vector_index.delete(force=True) + time.sleep(1) + logger.info("Recreating index with same name") + self.vector_index.create(replace=False) + time.sleep(1) + self.assert_index_count("^test_vector_index", 1) + logger.info("Recreate same-name index verified") + + def test_5116(self): + """Test delete of one out of multiple indexes.""" + idx1 = select_ai.VectorIndex(index_name="IDX_1", attributes=self.vector_index_attributes, profile=self.profile) + idx2 = select_ai.VectorIndex(index_name="IDX_2", attributes=self.vector_index_attributes, profile=self.profile) + logger.info("Creating two indexes IDX_1 and IDX_2") + idx1.create(replace=True) + idx2.create(replace=True) + logger.info("Deleting IDX_1 only") + self.delete_and_wait(force=True, pattern="^IDX_1$") + remaining_idx2 = list(self.vector_index.list(index_name_pattern="^IDX_2$")) + logger.info(f"IDX_2 entries after IDX_1 delete: {remaining_idx2}") + assert len(remaining_idx2) == 1 + logger.info("IDX_2 remains after IDX_1 delete") + + def test_5117(self): + """Test deletion by pattern.""" + logger.info("Deleting index with pattern '^test_vector_index$'") + self.vector_index.create(replace=True) + self.vector_index.delete(force=True) + time.sleep(1) + actual = list(self.vector_index.list(index_name_pattern="^test_vector_index$")) + logger.info(f"List entries after pattern delete: {actual}") + assert len(actual) == 0 + logger.info("Pattern delete verified") + + def test_5118(self): + """Test delete with force=True option.""" + logger.info("Deleting index with force=True") + self.vector_index.create(replace=True) + self.vector_index.delete(force=True) + time.sleep(1) + self.assert_index_count("^test_vector_index$", 0) + logger.info("Force delete verified") + + def test_5119(self): + """Test delete with force=False option.""" + logger.info("Creating index before delete (force=False)") + self.vector_index.create(replace=True) + logger.info("Deleting index with force=False") + self.vector_index.delete(force=False) + time.sleep(1) + self.assert_index_count("^test_vector_index$", 0) + logger.info("Delete verified successfully with force=False") + + def test_5120(self): + """Test delete with force=False called twice in a row.""" + logger.info("Deleting index first time (force=False)") + self.vector_index.delete(force=False) + time.sleep(1) + self.assert_index_count("^test_vector_index$", 0) + logger.info("First delete succeeded") + logger.info("Attempting second delete (expected to fail)") + with pytest.raises(Exception) as cm: + self.vector_index.delete(force=False) + assert "does not exist" in str(cm.value) + logger.info(f"Expected failure confirmed: {cm.value}") + self.assert_index_count("^test_vector_index$", 0) + logger.info("Index still absent after failed second delete") + + def test_5121(self): + """Test delete include_data=False and force=False (leftover vectab)""" + logger.info("Deleting index with include_data=False and force=False") + self.vector_index.delete(include_data=False, force=False) + time.sleep(1) + self.assert_index_count("^test_vector_index", 0) + logger.info("Attempting to recreate index (expected to fail - leftover data)") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.create(replace=False) + assert "ORA-00955" in str(cm.value) + logger.info(f"Expected recreate failure confirmed: {cm.value}") + logger.info("Cleaning up leftover vector table") + self.verify_and_cleanup_vectab("test_vector_index") + logger.info("Cleanup complete after include_data=False, force=False delete") diff --git a/tests/vector_index/test_5200_setindex_attributes.py b/tests/vector_index/test_5200_setindex_attributes.py new file mode 100644 index 0000000..a96d4e0 --- /dev/null +++ b/tests/vector_index/test_5200_setindex_attributes.py @@ -0,0 +1,616 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + + +import logging +import pytest +import select_ai +import test_env +import oracledb +from select_ai import VectorIndex, VectorIndexAttributes, OracleVectorIndexAttributes +from select_ai.errors import DatabaseNotConnectedError + +logger = logging.getLogger("TestSetVectorIndexAttributes") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def set_vec_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + "oci_compartment_id": test_env.get_compartment_id(), + "embedding_location": test_env.get_embedding_location(), + } + request.cls.set_vec_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, set_vec_params): + logger.info("=== Setting up TestSetVectorIndexAttributes class ===") + p = request.cls.set_vec_params + test_env.create_connection(use_wallet=p["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + request.cls.user = p["user"] + request.cls.password = p["password"] + request.cls.dsn = p["dsn"] + request.cls.use_wallet = p["use_wallet"] + request.cls.user_ocid = p["user_ocid"] + request.cls.tenancy_ocid = p["tenancy_ocid"] + request.cls.private_key = p["private_key"] + request.cls.fingerprint = p["fingerprint"] + request.cls.cred_username = p["cred_username"] + request.cls.cred_password = p["cred_password"] + request.cls.oci_compartment_id = p["oci_compartment_id"] + request.cls.embedding_location = p["embedding_location"] + request.cls.objstore_cred = "OBJSTORE_CRED" + + def get_native_cred_param(cred_name=None): + logger.info(f"Preparing native credential params for: {cred_name}") + return dict( + credential_name = cred_name, + user_ocid = p["user_ocid"], + tenancy_ocid = p["tenancy_ocid"], + private_key = p["private_key"], + fingerprint = p["fingerprint"] + ) + def get_cred_param(cred_name=None): + logger.info(f"Preparing basic credential params for: {cred_name}") + return dict( + credential_name = cred_name, + username = p["cred_username"], + password = p["cred_password"] + ) + request.cls.get_native_cred_param = staticmethod(get_native_cred_param) + request.cls.get_cred_param = staticmethod(get_cred_param) + request.cls.create_profile = staticmethod(lambda profile_name="vector_ai_profile": select_ai.Profile( + profile_name=profile_name, + attributes=select_ai.ProfileAttributes( + credential_name="GENAI_CRED", + provider=select_ai.OCIGenAIProvider( + oci_compartment_id=p["oci_compartment_id"], + oci_apiformat="GENERIC" + ) + ), + description="OCI GENAI Profile", + replace=True + )) + request.cls.delete_profile = staticmethod(lambda profile: profile.delete()) + + logger.info("Creating credentials: GENAI_CRED, OBJSTORE_CRED") + genai_credential = get_native_cred_param("GENAI_CRED") + objstore_credential = get_cred_param("OBJSTORE_CRED") + select_ai.create_credential(credential=genai_credential, replace=True) + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("Credentials created.") + + request.cls.profile = request.cls.create_profile() + logger.info("Profile 'vector_ai_profile' created successfully.") + + request.cls.index_name = "test_vector_index_attr" + vi_attrs = OracleVectorIndexAttributes( + location=p["embedding_location"], + object_storage_credential_name="OBJSTORE_CRED" + ) + request.cls.vector_index_attributes = vi_attrs + vi = VectorIndex( + index_name=request.cls.index_name, + attributes=vi_attrs, + description="Test vector index", + profile=request.cls.profile + ) + vi.create(replace=True) + created_indexes = [idx.index_name for idx in VectorIndex.list()] + assert request.cls.index_name.upper() in created_indexes, f"VectorIndex {request.cls.index_name} was not created" + yield + logger.info("=== Tearing down TestSetVectorIndexAttributes class ===") + try: + vector_index = VectorIndex(index_name=request.cls.index_name) + vector_index.delete(force=True) + except Exception as e: + logger.warning(f"Warning: drop vector index failed: {e}") + try: + request.cls.profile.delete() + except Exception as e: + logger.warning(f"profile.delete() raised {e} unexpectedly.") + try: + select_ai.delete_credential("GENAI_CRED", force=True) + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.delete_credential("OBJSTORE_CRED", force=True) + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("set_vec_params", "setup_and_teardown") +class TestSetVectorIndexAttributes: + def setup_method(self, method): + logger.info(f"SetUp for {method.__name__}") + vecidx = VectorIndex() + self.vector_index = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + + def teardown_method(self, method): + logger.info(f"TearDown for {method.__name__}") + + def test_5201(self): + """Update 'match_limit' attribute.""" + logger.info("Testing update of 'match_limit' attribute...") + self.vector_index.set_attributes("match_limit", 10) + attrs = self.vector_index.get_attributes() + logger.info(f"Updated match_limit: {attrs.match_limit}") + assert attrs.match_limit == 10 + logger.info("Match limit update verified successfully.") + + def test_5202(self): + """Update 'similarity_threshold' attribute.""" + logger.info("Testing update of 'similarity_threshold' attribute...") + self.vector_index.set_attributes("similarity_threshold", 0.8) + attrs = self.vector_index.get_attributes() + logger.info(f"Updated similarity_threshold: {attrs.similarity_threshold}") + assert attrs.similarity_threshold == 0.8 + logger.info("Similarity threshold update verified successfully.") + + def test_5203(self): + """Update multiple attributes with VectorIndexAttributes object.""" + logger.info("Testing update of multiple attributes via VectorIndexAttributes object...") + update_attrs = VectorIndexAttributes( + match_limit=5, + similarity_threshold=0.5, + location=self.embedding_location, + refresh_rate=40 + ) + self.vector_index.set_attributes(attributes=update_attrs) + attrs = self.vector_index.get_attributes() + logger.info(f"Updated attributes: {attrs.__dict__}") + assert attrs.match_limit == 5 + assert attrs.similarity_threshold == 0.5 + assert attrs.location == self.embedding_location + assert attrs.refresh_rate == 40 + logger.info("Multiple attributes update verified successfully.") + + def test_5204(self): + """Repeated update of the same attribute 'similarity_threshold'.""" + logger.info("Testing repeated update of the same attribute 'similarity_threshold'...") + self.vector_index.set_attributes("similarity_threshold", 0.8) + self.vector_index.set_attributes("similarity_threshold", 0.5) + attrs = self.vector_index.get_attributes() + logger.info(f"Final similarity_threshold value: {attrs.similarity_threshold}") + assert attrs.similarity_threshold == 0.5 + logger.info("Repeated attribute update verified successfully.") + + def test_5205(self): + """Update 'match_limit' with maximum allowed value.""" + logger.info("Testing update of 'match_limit' with maximum allowed value...") + max_limit = 8192 + self.vector_index.set_attributes("match_limit", max_limit) + attrs = self.vector_index.get_attributes() + logger.info(f"Set match_limit to: {attrs.match_limit}") + assert attrs.match_limit == max_limit + logger.info("Max value for match_limit verified successfully.") + + def test_5206(self): + """Update match_limit with minimum value.""" + logger.info("Testing update of match_limit with minimum value...") + min_limit = 1 + self.vector_index.set_attributes("match_limit", min_limit) + logger.info(f"Set match_limit to {min_limit}, fetching attributes for verification...") + attrs = self.vector_index.get_attributes() + assert attrs.match_limit == min_limit + logger.info("match_limit minimum value update verified successfully.") + + def test_5207(self): + """Update match_limit with zero value.""" + logger.info("Testing update of match_limit with zero value...") + min_limit = 0 + self.vector_index.set_attributes("match_limit", min_limit) + logger.info("Fetching attributes to verify zero value update...") + attrs = self.vector_index.get_attributes() + assert attrs.match_limit == min_limit + logger.info("match_limit zero value update verified successfully.") + + def test_5208(self): + """Update profile_name with temporary profile.""" + temp_profile_name = "vector_ai_profile_temp" + temp_profile = self.create_profile(profile_name=temp_profile_name) + logger.info(f"Temporary profile created: {temp_profile_name}") + self.vector_index.set_attributes("profile_name", temp_profile_name) + logger.info(f"Set profile_name to {temp_profile_name}, fetching attributes...") + attrs = self.vector_index.get_attributes() + logger.info(f"VectorIndex attributes after profile update: {attrs.__dict__}") + assert attrs.profile_name == temp_profile_name + vecidx = VectorIndex() + vec_index = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + logger.info(f"Persisted VectorIndex after profile update: {vec_index.__dict__}") + assert attrs.profile_name == vec_index.profile.profile_name + logger.info("Persisted VectorIndex reflects updated profile correctly.") + self.delete_profile(temp_profile) + logger.info(f"Temporary profile deleted: {temp_profile_name}") + self.vector_index.set_attributes("profile_name", self.profile.profile_name) + attrs = self.vector_index.get_attributes() + logger.info("VectorIndex reset to default profile, verifying...") + assert attrs.profile_name == self.profile.profile_name + logger.info("VectorIndex profile reset verified successfully.") + + def test_5209(self): + """Update profile_name and then delete profile scenario.""" + logger.info("Testing update of profile_name followed by delete scenario...") + temp_profile_name = "vector_ai_profile_temp" + temp_profile = self.create_profile(profile_name=temp_profile_name) + logger.info(f"Temporary profile created: {temp_profile_name}") + self.vector_index.set_attributes("profile_name", temp_profile_name) + logger.info(f"Set profile_name to {temp_profile_name}, verifying update...") + attrs = self.vector_index.get_attributes() + assert attrs.profile_name == temp_profile_name + vecidx = VectorIndex() + vec_index = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + logger.info(f"Persisted VectorIndex after profile update: {vec_index.__dict__}") + assert attrs.profile_name == vec_index.profile.profile_name + self.delete_profile(temp_profile) + logger.info(f"Temporary profile deleted: {temp_profile_name}") + logger.info("Verifying VectorIndex profile cleared after delete...") + vecidx = VectorIndex() + vec_index = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + attrs = vec_index.get_attributes() + assert attrs.profile_name is None + logger.info("Profile cleared successfully after deletion.") + self.vector_index.set_attributes("profile_name", self.profile.profile_name) + attrs = self.vector_index.get_attributes() + logger.info(f"Reset VectorIndex profile to default: {attrs.__dict__}") + assert attrs.profile_name == self.profile.profile_name + logger.info("Profile reset after delete verified successfully.") + + def test_5210(self): + """Update refresh_rate attribute.""" + logger.info("Testing update of refresh_rate attribute...") + self.vector_index.set_attributes("refresh_rate", 30) + attrs = self.vector_index.get_attributes() + assert attrs.refresh_rate == 30 + + def test_5211(self): + """Update object_storage_credential_name, handle pipeline.""" + logger.info("Testing update of object_storage_credential_name with pipeline handling...") + attrs = self.vector_index.get_attributes() + pipeline_name = attrs.pipeline_name + logger.info(f"Retrieved pipeline name: {pipeline_name}") + logger.info(f"Stopping pipeline: {pipeline_name}") + with select_ai.cursor() as cursor: + cursor.execute("BEGIN dbms_cloud_pipeline.stop_pipeline(pipeline_name => :1); END;", [pipeline_name]) + logger.info(f"Pipeline '{pipeline_name}' stopped successfully.") + objstore_credential = self.get_cred_param("TEMP_OBJSTORE_CRED") + logger.info("Creating temporary Object Store credential: TEMP_OBJSTORE_CRED") + try: + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("TEMP_OBJSTORE_CRED created successfully.") + except Exception as e: + raise AssertionError(f"create_credential() raised an unexpected exception: {e}") + logger.info("Updating vector index with TEMP_OBJSTORE_CRED...") + self.vector_index.set_attributes("object_storage_credential_name", "TEMP_OBJSTORE_CRED") + attrs = self.vector_index.get_attributes() + logger.info(f"Updated credential: {attrs.object_storage_credential_name}") + assert attrs.object_storage_credential_name == "TEMP_OBJSTORE_CRED" + logger.info("Deleting temporary credential: TEMP_OBJSTORE_CRED") + try: + select_ai.delete_credential("TEMP_OBJSTORE_CRED", force=True) + logger.info("TEMP_OBJSTORE_CRED deleted successfully.") + except Exception as e: + pytest.fail(f"delete_credential() raised unexpected exception: {e}") + logger.info("Restoring original Object Store credential: OBJSTORE_CRED") + self.vector_index.set_attributes("object_storage_credential_name", "OBJSTORE_CRED") + logger.info(f"Restarting pipeline: {pipeline_name}") + with select_ai.cursor() as cursor: + cursor.execute("BEGIN dbms_cloud_pipeline.start_pipeline(pipeline_name => :1); END;", [pipeline_name]) + logger.info(f"Pipeline '{pipeline_name}' restarted successfully.") + attrs = self.vector_index.get_attributes() + assert attrs.object_storage_credential_name == "OBJSTORE_CRED" + logger.info("Object Store credential restored successfully.") + + def test_5212(self): + """Update object_storage_credential_name with delete handling.""" + logger.info("Testing update of object_storage_credential_name with delete handling...") + attrs = self.vector_index.get_attributes() + pipeline_name = attrs.pipeline_name + logger.info(f"Retrieved pipeline name: {pipeline_name}") + logger.info(f"Stopping pipeline: {pipeline_name}") + with select_ai.cursor() as cursor: + cursor.execute("BEGIN dbms_cloud_pipeline.stop_pipeline(pipeline_name => :1); END;", [pipeline_name]) + logger.info(f"Pipeline '{pipeline_name}' stopped successfully.") + objstore_credential = self.get_cred_param("TEMP_OBJSTORE_CRED") + logger.info("Creating temporary Object Store credential: TEMP_OBJSTORE_CRED") + try: + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("TEMP_OBJSTORE_CRED created successfully.") + except Exception as e: + raise AssertionError(f"create_credential() raised an unexpected exception: {e}") + logger.info("Updating vector index with TEMP_OBJSTORE_CRED...") + self.vector_index.set_attributes("object_storage_credential_name", "TEMP_OBJSTORE_CRED") + attrs = self.vector_index.get_attributes() + assert attrs.object_storage_credential_name == "TEMP_OBJSTORE_CRED" + logger.info(f"Credential updated to: {attrs.object_storage_credential_name}") + logger.info("Deleting temporary credential: TEMP_OBJSTORE_CRED") + try: + select_ai.delete_credential("TEMP_OBJSTORE_CRED", force=True) + logger.info("TEMP_OBJSTORE_CRED deleted successfully.") + except Exception as e: + pytest.fail(f"delete_credential() raised unexpected exception: {e}") + logger.info("Verifying that VectorIndex no longer holds deleted credential...") + vecidx = VectorIndex() + vec_index = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + attrs = vec_index.get_attributes() + assert attrs.object_storage_credential_name is None + logger.info("Credential removal reflected in VectorIndex attributes.") + logger.info("Restoring original Object Store credential: OBJSTORE_CRED") + self.vector_index.set_attributes("object_storage_credential_name", "OBJSTORE_CRED") + logger.info(f"Restarting pipeline: {pipeline_name}") + with select_ai.cursor() as cursor: + cursor.execute("BEGIN dbms_cloud_pipeline.start_pipeline(pipeline_name => :1); END;", [pipeline_name]) + logger.info(f"Pipeline '{pipeline_name}' restarted successfully.") + attrs = self.vector_index.get_attributes() + assert attrs.object_storage_credential_name == "OBJSTORE_CRED" + logger.info("Object Store credential restoration after delete verified successfully.") + + def test_5213(self): + """Update multiple attributes together.""" + logger.info("Testing update of multiple attributes together...") + updates = { + "refresh_rate": 50, + "similarity_threshold": 0.8, + "match_limit": 10 + } + for field, value in updates.items(): + logger.info(f"Updating {field} to {value}...") + self.vector_index.set_attributes(field, value) + attrs = self.vector_index.get_attributes() + logger.info(f"Fetched attributes after updates: {attrs.__dict__}") + assert attrs.refresh_rate == updates["refresh_rate"] + assert attrs.similarity_threshold == updates["similarity_threshold"] + assert attrs.match_limit == updates["match_limit"] + logger.info("All multiple attribute updates verified successfully.") + + def test_5214(self): + """Update description (should raise DatabaseError).""" + logger.info("Testing update of description attribute (should raise DatabaseError)...") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("description", "updated description") + assert "ORA-20048" in str(cm.value) + logger.info("DatabaseError correctly raised for invalid description update.") + + def test_5215(self): + """Update pipeline_name (should raise DatabaseError).""" + logger.info("Testing update of pipeline_name (expected DatabaseError)...") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("pipeline_name", "test_pipeline") + assert "ORA-20048" in str(cm.value) + attrs = self.vector_index.get_attributes() + assert attrs.pipeline_name == "TEST_VECTOR_INDEX_ATTR$VECPIPELINE" + logger.info("Pipeline update correctly raised error and original value retained.") + + def test_5216(self): + """Update chunk_size (should fail).""" + logger.info("Testing update of chunk_size (should fail with ORA-20047)...") + attrs = self.vector_index.get_attributes() + logger.info(f"Current attributes: {attrs.__dict__}") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("chunk_size", 2048) + assert "ORA-20047" in str(cm.value) + attrs = self.vector_index.get_attributes() + assert attrs.chunk_size == 1024 + logger.info("chunk_size update prevented successfully; original value verified.") + + def test_5217(self): + """Update chunk_overlap (should fail).""" + logger.info("Testing update of chunk_overlap (should fail with ORA-20047)...") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("chunk_overlap", 256) + assert "ORA-20047" in str(cm.value) + attrs = self.vector_index.get_attributes() + assert attrs.chunk_overlap == 128 + logger.info("chunk_overlap update prevented successfully; original value verified.") + + def test_5218(self): + """Update vector_distance_metric (should fail).""" + logger.info("Testing update of vector_distance_metric (should fail with ORA-20047)...") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("vector_distance_metric", "EUCLIDEAN") + assert "ORA-20047" in str(cm.value) + attrs = self.vector_index.get_attributes() + assert attrs.vector_distance_metric == "COSINE" + logger.info("vector_distance_metric update prevented successfully.") + + def test_5219(self): + """Partial update with VectorIndexAttributes object.""" + logger.info("Testing partial update with VectorIndexAttributes object...") + update_attrs = VectorIndexAttributes(match_limit=20, chunk_size=2048) + self.vector_index.set_attributes(attributes=update_attrs) + attrs = self.vector_index.get_attributes() + logger.info(f"Attributes after update attempt: {attrs.__dict__}") + assert attrs.match_limit == 20 + assert attrs.chunk_size == 1024 + logger.info("Partial update applied correctly; restricted fields unchanged.") + + def test_5220(self): + """Update with invalid attribute combinations.""" + logger.info("Testing update with invalid attribute combinations...") + update_attrs = VectorIndexAttributes(chunk_size=2048, chunk_overlap=256) + self.vector_index.set_attributes(attributes=update_attrs) + attrs = self.vector_index.get_attributes() + logger.info(f"Attributes after invalid update: {attrs.__dict__}") + assert attrs.chunk_overlap == 128 + assert attrs.chunk_size == 1024 + logger.info("Invalid updates ignored; original attribute values retained.") + + def test_5221(self): + """Update location (should raise ORA-20047).""" + logger.info("Testing update of location (expected ORA-20047)...") + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("location", self.embedding_location) + assert "ORA-20047" in str(cm.value) + attrs = self.vector_index.get_attributes() + assert attrs.location == self.embedding_location + logger.info("Location update prevented successfully.") + + def test_5222(self): + """Update using profile object directly.""" + logger.info("Testing update of vector index using profile object directly...") + temp_profile_name = "vector_ai_profile_temp" + temp_profile = self.create_profile(profile_name=temp_profile_name) + logger.info(f"Created temporary profile: {temp_profile_name}") + try: + self.vector_index.set_attributes("profile", temp_profile) + except oracledb.NotSupportedError as e: + logger.info(f"Expected NotSupportedError caught: {e}") + except Exception as e: + raise AssertionError(f"Unexpected exception: {e}") + attrs = self.vector_index.get_attributes() + assert attrs.profile_name in [self.profile.profile_name, temp_profile_name] + logger.info(f"Attributes after attempted profile object update: {attrs.__dict__}") + try: + self.delete_profile(temp_profile) + logger.info(f"Temporary profile '{temp_profile_name}' deleted successfully.") + except Exception as e: + logger.warning(f"Profile cleanup failed: {e}") + + def test_5223(self): + """Update with invalid attribute name.""" + logger.info("Testing update with invalid attribute name...") + with pytest.raises(oracledb.DatabaseError): + self.vector_index.set_attributes("invalid_attr", "value") + logger.info("Invalid attribute name correctly raised DatabaseError.") + + def test_5224(self): + """Update with invalid type for integer field.""" + logger.info("Testing update with invalid type for integer field...") + with pytest.raises(oracledb.DatabaseError): + self.vector_index.set_attributes("chunk_size", "not_an_int") + logger.info("Invalid integer type correctly raised DatabaseError.") + + def test_5225(self): + """Update with invalid type for float field.""" + logger.info("Testing update with invalid type for float field...") + with pytest.raises(oracledb.DatabaseError): + self.vector_index.set_attributes("similarity_threshold", "NaN") + logger.info("Invalid float type correctly raised DatabaseError.") + + def test_5226(self): + """Update with invalid enum value for vector_distance_metric.""" + logger.info("Testing update with invalid enum value for vector_distance_metric...") + with pytest.raises(oracledb.DatabaseError): + self.vector_index.set_attributes("vector_distance_metric", "INVALID") + logger.info("Invalid enum value correctly raised DatabaseError.") + + def test_5227(self): + """Update on nonexistent vector index.""" + logger.info("Testing update on nonexistent vector index...") + temp_index = VectorIndex(index_name="does_not_exist") + with pytest.raises(AttributeError): + temp_index.set_attributes("chunk_size", 512) + logger.info("Nonexistent index update correctly raised AttributeError.") + + def test_5228(self): + """Update with None as attribute name (should fail).""" + logger.info("Testing update with None as attribute name...") + with pytest.raises(TypeError): + self.vector_index.set_attributes(None, 128) + logger.info("None attribute name correctly raised TypeError.") + + def test_5229(self): + """Update with None as attribute name for second time.""" + logger.info("Testing update with None as attribute name for second time...") + with pytest.raises(TypeError): + self.vector_index.set_attributes(None, 128) + logger.info("None attribute name correctly raised TypeError.") + + def test_5230(self): + """Update with invalid attributes object (non-object input).""" + logger.info("Testing update with invalid attributes object (non-object input)...") + with pytest.raises(AttributeError): + self.vector_index.set_attributes(attributes="not_an_object") + logger.info("Invalid attributes object correctly raised AttributeError.") + + def test_5231(self): + """Update after disconnecting from the database.""" + logger.info("Testing update after disconnecting from the database...") + select_ai.disconnect() + with pytest.raises(DatabaseNotConnectedError): + self.vector_index.set_attributes("chunk_size", 256) + logger.info("DatabaseNotConnectedError correctly raised after disconnect.") + logger.info("Reconnecting for further tests...") + test_env.create_connection(use_wallet=self.use_wallet) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Reconnection successful.") + + def test_5232(self): + """Update with None as attribute value (should fail).""" + logger.info("Testing update with None as attribute value...") + with pytest.raises(oracledb.DatabaseError): + self.vector_index.set_attributes("chunk_size", None) + logger.info("None value correctly raised DatabaseError.") + + def test_5233(self): + """Concurrent updates on the same vector index.""" + logger.info("Testing concurrent updates on the same vector index...") + vecidx = VectorIndex() + index1 = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + index2 = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + index1.set_attributes("match_limit", 15) + index2.set_attributes("match_limit", 20) + attrs = self.vector_index.get_attributes() + logger.info(f"Final match_limit value after concurrent updates: {attrs.match_limit}") + assert attrs.match_limit in [15, 20] + logger.info("Concurrent update behavior verified (last writer wins).") + + def test_5234(self): + """Update with excessively large attribute value.""" + logger.info("Testing update with excessively large attribute value...") + long_name = "X" * 500 + with pytest.raises(oracledb.DatabaseError) as cm: + self.vector_index.set_attributes("profile_name", long_name) + assert "ORA-20048" in str(cm.value) + logger.info("Large attribute value correctly raised DatabaseError.") + + def test_5235(self): + """Repeated updates to match_limit (last writer wins).""" + logger.info("Testing repeated updates to match_limit...") + for i in range(5): + self.vector_index.set_attributes("match_limit", i * 10) + logger.info(f"Set match_limit to {i * 10}") + attrs = self.vector_index.get_attributes() + assert attrs.match_limit == 40 + logger.info("Repeated update test passed; last value retained.") + + def test_5236(self): + """Update attribute after delete and recreate of vector index.""" + logger.info("Testing attribute update after deleting and recreating the vector index...") + self.vector_index.delete(force=True) + logger.info("Vector index deleted.") + self.vector_index.create(replace=True) + logger.info("Vector index recreated.") + self.vector_index.set_attributes("match_limit", 10) + attrs = self.vector_index.get_attributes() + assert attrs.match_limit == 10 + logger.info("Update after recreation verified successfully.") diff --git a/tests/vector_index/test_5300_getindex_attributes.py b/tests/vector_index/test_5300_getindex_attributes.py new file mode 100644 index 0000000..fe11906 --- /dev/null +++ b/tests/vector_index/test_5300_getindex_attributes.py @@ -0,0 +1,401 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +from select_ai import VectorIndex, OracleVectorIndexAttributes +from select_ai.errors import VectorIndexNotFoundError + +logger = logging.getLogger("TestGetVectorIndexAttributes") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def vector_attr_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + "oci_compartment_id": test_env.get_compartment_id(), + "embedding_location": test_env.get_embedding_location(), + } + request.cls.vector_attr_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, vector_attr_params): + logger.info("=== Setting up TestGetVectorIndexAttributes class ===") + p = request.cls.vector_attr_params + test_env.create_connection(use_wallet=p["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + request.cls.user = p["user"] + request.cls.password = p["password"] + request.cls.dsn = p["dsn"] + request.cls.use_wallet = p["use_wallet"] + request.cls.user_ocid = p["user_ocid"] + request.cls.tenancy_ocid = p["tenancy_ocid"] + request.cls.private_key = p["private_key"] + request.cls.fingerprint = p["fingerprint"] + request.cls.cred_username = p["cred_username"] + request.cls.cred_password = p["cred_password"] + request.cls.oci_compartment_id = p["oci_compartment_id"] + request.cls.embedding_location = p["embedding_location"] + + # helpers for credentials/profiles + def get_native_cred_param(cred_name=None): + logger.info(f"Preparing native credential params for: {cred_name}") + return dict( + credential_name=cred_name, + user_ocid=p["user_ocid"], + tenancy_ocid=p["tenancy_ocid"], + private_key=p["private_key"], + fingerprint=p["fingerprint"] + ) + def get_cred_param(cred_name=None): + logger.info(f"Preparing basic credential params for: {cred_name}") + return dict( + credential_name=cred_name, + username=p["cred_username"], + password=p["cred_password"] + ) + + # create creds + logger.info("Creating credentials: GENAI_CRED, OBJSTORE_CRED") + genai_credential = get_native_cred_param("GENAI_CRED") + objstore_credential = get_cred_param("OBJSTORE_CRED") + select_ai.create_credential(credential=genai_credential, replace=True) + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("Credentials created.") + + # create profile + provider = select_ai.OCIGenAIProvider( + oci_compartment_id=p["oci_compartment_id"], + oci_apiformat="GENERIC" + ) + profile_attributes = select_ai.ProfileAttributes( + credential_name="GENAI_CRED", + provider=provider + ) + request.cls.profile = select_ai.Profile( + profile_name="vector_ai_profile", + attributes=profile_attributes, + description="OCI GENAI Profile", + replace=True + ) + logger.info("Profile 'vector_ai_profile' created successfully.") + + # create vector index + request.cls.index_name = "test_vector_index_attr" + vi_attrs = select_ai.OracleVectorIndexAttributes( + location=p["embedding_location"], + object_storage_credential_name="OBJSTORE_CRED" + ) + request.cls.vector_index_attributes = vi_attrs + vi = select_ai.VectorIndex( + index_name=request.cls.index_name, + attributes=vi_attrs, + description="Test vector index", + profile=request.cls.profile + ) + vi.create(replace=True) + created_indexes = [idx.index_name for idx in VectorIndex.list()] + assert request.cls.index_name.upper() in created_indexes, f"VectorIndex {request.cls.index_name} was not created" + yield + logger.info("=== Tearing down TestGetVectorIndexAttributes class ===") + # Delete Vector Index + try: + vector_index = VectorIndex(index_name=request.cls.index_name) + vector_index.delete(force=True) + except Exception as e: + logger.warning(f"drop vector index failed: {e}") + # Delete Profile + try: + request.cls.profile.delete() + except Exception as e: + logger.warning(f"profile.delete() raised {e} unexpectedly.") + # Delete Credential + try: + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info("Deleted credential 'GENAI_CRED'") + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.delete_credential("OBJSTORE_CRED", force=True) + logger.info("Deleted credential 'OBJSTORE_CRED'") + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + # Disconnect from DB + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("vector_attr_params", "setup_and_teardown") +class TestGetVectorIndexAttributes: + def setup_method(self, method): + logger.info(f"SetUp for {method.__name__}") + vecidx = VectorIndex() + self.vector_index = (list(vecidx.list(index_name_pattern=self.index_name)))[0] + def teardown_method(self, method): + logger.info(f"TearDown for {method.__name__}") + + # ---------------- + # Positive tests + # ---------------- + def test_5301(self): + """Get vector index attributes and verify type.""" + logger.info("Getting vector index attributes and verifying type...") + attrs = self.vector_index.get_attributes() + assert isinstance(attrs, OracleVectorIndexAttributes) + logger.info("Attributes type verified successfully.") + + def test_5302(self): + """Verify core values of vector index attributes.""" + logger.info("Getting vector index attributes and verifying core values...") + attrs = self.vector_index.get_attributes() + assert attrs.location == self.embedding_location + assert attrs.object_storage_credential_name == "OBJSTORE_CRED" + assert attrs.profile_name == "vector_ai_profile" + assert attrs.pipeline_name == f"{self.index_name.upper()}$VECPIPELINE" + logger.info("Core attribute values verified successfully.") + + def test_5303(self): + """Additional sanity checks on vector index attributes.""" + logger.info("Performing additional sanity checks on vector index attributes...") + attrs = self.vector_index.get_attributes() + assert attrs.chunk_size == 1024 + assert attrs.chunk_overlap == 128 + assert attrs.match_limit == 5 + assert attrs.refresh_rate == 1440 + assert attrs.vector_distance_metric == "COSINE" + assert attrs.vector_db_provider.value == "oracle" + logger.info("Additional sanity checks passed successfully.") + + def test_5304(self): + """Verify required fields in attributes object.""" + logger.info("Verifying attributes object contains required fields...") + attrs = self.vector_index.get_attributes() + logger.info(f"Attributes dict: {attrs.__dict__}") + assert hasattr(attrs, "location") + assert hasattr(attrs, "object_storage_credential_name") + logger.info("Attributes contain all expected fields.") + + def test_5305(self): + """Repeatability: fetch attributes twice and compare.""" + logger.info("Fetching attributes twice to confirm repeatability...") + attrs1 = self.vector_index.get_attributes() + attrs2 = self.vector_index.get_attributes() + assert attrs1.location == attrs2.location + logger.info("Attribute values are repeatable across calls.") + + def test_5306(self): + """Test case-insensitive index name handling.""" + logger.info("Testing case-insensitive index name handling...") + vecidx = VectorIndex() + vector_index = (list(vecidx.list(index_name_pattern=self.index_name.lower())))[0] + attrs = vector_index.get_attributes() + assert attrs.location == self.embedding_location + logger.info("Case-insensitive index name test passed.") + + def test_5307(self): + """Type check on key vector index attributes.""" + logger.info("Performing type check on key vector index attributes...") + attrs = self.vector_index.get_attributes() + logger.info(f"{attrs}") + assert isinstance(attrs.location, str) + assert isinstance(attrs.profile_name, str) + assert isinstance(attrs.object_storage_credential_name, str) + logger.info("All attribute fields are of correct type.") + + # ---------------- + # Negative tests + # ---------------- + def test_5308(self): + """Calling get_attributes on nonexistent index raises error.""" + logger.info("Testing get_attributes() with a nonexistent index...") + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name="does_not_exist").get_attributes() + logger.info("Nonexistent index correctly raised VectorIndexNotFoundError.") + + def test_5309(self): + """verify error after deleting a temporary vector index.""" + logger.info("Testing error after deleting a temporary vector index...") + vector_index_attributes = OracleVectorIndexAttributes( + location=self.embedding_location, + object_storage_credential_name="OBJSTORE_CRED" + ) + logger.info("Creating temporary vector index...") + temp_index = VectorIndex( + index_name="temp_index_for_delete", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + temp_index.create(replace=True) + logger.info("Temporary vector index created.") + temp_index.delete(force=True) + logger.info("Temporary vector index deleted. Attempting to fetch attributes...") + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name="temp_index_for_delete").get_attributes() + logger.info("Expected error raised after deleting index.") + + def test_5310(self): + """Access attributes after deleting the vector index (should use cache).""" + logger.info("Testing access to attributes object after deleting the vector index...") + vector_index_attributes = OracleVectorIndexAttributes( + location=self.embedding_location, + object_storage_credential_name="OBJSTORE_CRED" + ) + logger.info("Creating temporary vector index for deletion test...") + temp_index = VectorIndex( + index_name="temp_index_for_delete", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + temp_index.create(replace=True) + logger.info("Fetching attributes before deletion...") + attrs = temp_index.get_attributes() + assert isinstance(attrs, OracleVectorIndexAttributes) + assert attrs.object_storage_credential_name == "OBJSTORE_CRED" + assert attrs.location == self.embedding_location + logger.info("Deleting temporary index...") + temp_index.delete(force=True) + logger.info("Accessing cached attributes after deletion...") + logger.info(f"After delete: {attrs}") + assert isinstance(attrs, OracleVectorIndexAttributes) + assert attrs.object_storage_credential_name == "OBJSTORE_CRED" + assert attrs.location == self.embedding_location + logger.info("Attributes object remains valid after deletion.") + + def test_5311(self): + """get_attributes with empty index name raises error.""" + logger.info("Testing get_attributes() with empty index name...") + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name="").get_attributes() + logger.info("Empty name correctly raised VectorIndexNotFoundError.") + + def test_5312(self): + """get_attributes with None as index name raises error.""" + logger.info("Testing get_attributes() with None as index name...") + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name=None).get_attributes() + logger.info("None name correctly raised VectorIndexNotFoundError.") + + def test_5313(self): + """get_attributes with special characters in index name.""" + logger.info("Testing get_attributes() with special characters in index name...") + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name='@@invalid!!').get_attributes() + logger.info("Special character name correctly raised VectorIndexNotFoundError.") + + def test_5314(self): + """get_attributes with Unicode index name raises error.""" + logger.info("Testing get_attributes() with Unicode index name...") + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name='ใƒ†ใ‚นใƒˆ').get_attributes() + logger.info("Unicode name correctly raised VectorIndexNotFoundError.") + + # ---------------- + # Stress / Edge cases + # ---------------- + def test_5315(self): + """Multiple indices: check their attribute differences.""" + logger.info("Creating multiple vector indices to compare their attributes...") + vector_index_attributes = OracleVectorIndexAttributes( + location=self.embedding_location, + object_storage_credential_name="OBJSTORE_CRED" + ) + logger.info("Creating index_a...") + index_a = VectorIndex( + index_name="index_a", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + index_a.create(replace=True) + logger.info("Creating index_b...") + index_b = VectorIndex( + index_name="index_b", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + index_b.create(replace=True) + logger.info("Fetching attributes for both indices...") + attrs_a = VectorIndex(index_name="index_a").get_attributes() + logger.info(f"Attrs_a: {attrs_a}") + attrs_b = VectorIndex(index_name="index_b").get_attributes() + assert attrs_a.pipeline_name != attrs_b.pipeline_name + logger.info("Indices have distinct pipeline names as expected.") + logger.info("Deleting both indices...") + index_a.delete(force=True) + index_b.delete(force=True) + logger.info("Both indices deleted successfully.") + + def test_5316(self): + """Attributes remain consistent after index delete and recreate.""" + logger.info("Testing attributes consistency after delete and recreate...") + vector_index_attributes = OracleVectorIndexAttributes( + location=self.embedding_location, + object_storage_credential_name="OBJSTORE_CRED" + ) + logger.info("Creating temporary vector index for recreate test...") + temp_index = VectorIndex( + index_name="temp_recreate", + attributes=vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + temp_index.create(replace=True) + logger.info("Deleting temporary index...") + temp_index.delete(force=True) + logger.info("Recreating temporary index...") + temp_index.create(replace=True) + logger.info("Fetching attributes after recreation...") + attrs = VectorIndex(index_name="temp_recreate").get_attributes() + assert attrs.object_storage_credential_name == "OBJSTORE_CRED" + temp_index.delete(force=True) + logger.info("Recreate test completed successfully.") + + def test_5317(self): + """get_attributes with very long index name raises error.""" + logger.info("Testing get_attributes() with very long index name...") + long_name = "X" * 100 + with pytest.raises(VectorIndexNotFoundError): + VectorIndex(index_name=long_name).get_attributes() + logger.info("Long name correctly raised VectorIndexNotFoundError.") + + def test_5318(self): + """get_attributes after disconnecting from database raises error.""" + logger.info("Testing get_attributes() after disconnecting from database...") + select_ai.disconnect() + with pytest.raises(Exception): + VectorIndex(index_name=self.index_name).get_attributes() + logger.info("Expected error raised after disconnect.") + logger.info("Reconnecting for remaining tests...") + test_env.create_connection(use_wallet=self.use_wallet) + logger.info("Reconnection successful.") \ No newline at end of file diff --git a/tests/vector_index/test_5400_list_index.py b/tests/vector_index/test_5400_list_index.py new file mode 100644 index 0000000..3342e79 --- /dev/null +++ b/tests/vector_index/test_5400_list_index.py @@ -0,0 +1,330 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb + +logger = logging.getLogger("TestListVectorIndex") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def list_vec_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + "oci_compartment_id": test_env.get_compartment_id(), + "embedding_location": test_env.get_embedding_location(), + } + request.cls.list_vec_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, list_vec_params): + logger.info("=== Setting up TestListVectorIndex class ===") + p = request.cls.list_vec_params + test_env.create_connection(use_wallet=p["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + request.cls.user = p["user"] + request.cls.password = p["password"] + request.cls.dsn = p["dsn"] + request.cls.use_wallet = p["use_wallet"] + request.cls.user_ocid = p["user_ocid"] + request.cls.tenancy_ocid = p["tenancy_ocid"] + request.cls.private_key = p["private_key"] + request.cls.fingerprint = p["fingerprint"] + request.cls.cred_username = p["cred_username"] + request.cls.cred_password = p["cred_password"] + request.cls.oci_compartment_id = p["oci_compartment_id"] + request.cls.embedding_location = p["embedding_location"] + request.cls.objstore_cred = "OBJSTORE_CRED" + + def get_native_cred_param(cred_name=None): + logger.info(f"Preparing native credential params for: {cred_name}") + return dict( + credential_name = cred_name, + user_ocid = p["user_ocid"], + tenancy_ocid = p["tenancy_ocid"], + private_key = p["private_key"], + fingerprint = p["fingerprint"] + ) + def get_cred_param(cred_name=None): + logger.info(f"Preparing basic credential params for: {cred_name}") + return dict( + credential_name = cred_name, + username = p["cred_username"], + password = p["cred_password"] + ) + + logger.info("Creating credentials: GENAI_CRED, OBJSTORE_CRED") + genai_credential = get_native_cred_param("GENAI_CRED") + objstore_credential = get_cred_param("OBJSTORE_CRED") + select_ai.create_credential(credential=genai_credential, replace=True) + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("Credentials created.") + + provider = select_ai.OCIGenAIProvider( + oci_compartment_id=p["oci_compartment_id"], + oci_apiformat="GENERIC" + ) + profile_attributes = select_ai.ProfileAttributes( + credential_name="GENAI_CRED", + provider=provider + ) + request.cls.profile = select_ai.Profile( + profile_name="vector_ai_profile", + attributes=profile_attributes, + description="OCI GENAI Profile", + replace=True + ) + logger.info("Profile 'vector_ai_profile' created successfully.") + + def create_vector_index(index_name): + logger.info(f"Creating vector index: {index_name}") + vector_index_attributes = select_ai.OracleVectorIndexAttributes( + location=p["embedding_location"], + object_storage_credential_name="OBJSTORE_CRED" + ) + vector_index = select_ai.VectorIndex( + index_name=index_name, + attributes=vector_index_attributes, + description="Test vector index", + profile=request.cls.profile + ) + vector_index.create(replace=True) + logger.info(f"Vector index '{index_name}' created successfully.") + + request.cls.indexes = [f"test_vector_index{i}" for i in range(1, 6)] + \ + [f"test_vecidx{i}" for i in range(1, 3)] + for idx in request.cls.indexes: + try: + create_vector_index(index_name=idx) + except Exception as exc: + logger.warning(f"Index creation failed or already exists for {idx}: {exc}") + + yield + + logger.info("=== Tearing down TestListVectorIndex class ===") + for idx in request.cls.indexes: + try: + vector_index = select_ai.VectorIndex(index_name=idx) + vector_index.delete(force=True) + except Exception as e: + logger.warning(f"Warning: drop vector index failed: {e}") + try: + request.cls.profile.delete() + except Exception as e: + logger.warning(f"profile.delete() raised {e} unexpectedly.") + try: + select_ai.delete_credential("GENAI_CRED", force=True) + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.delete_credential("OBJSTORE_CRED", force=True) + except Exception as e: + logger.warning(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("list_vec_params", "setup_and_teardown") +class TestListVectorIndex: + def setup_method(self, method): + logger.info(f"SetUp for {method.__name__}") + self.vector_index = select_ai.VectorIndex() + + def teardown_method(self, method): + logger.info(f"TearDown for {method.__name__}") + + # ---------------- + # Positive tests + # ---------------- + def test_5401(self): + """Verify list of vector indexes with matching names.""" + logger.info("Verifying list of vector indexes with matching names...") + expected_index_names = [f"TEST_VECTOR_INDEX{i}" for i in range(1, 6)] + \ + [f"TEST_VECIDX{i}" for i in range(1, 3)] + actual_indexes = list(self.vector_index.list(index_name_pattern=".*")) + logger.info(f"Found {len(actual_indexes)} indexes, verifying names match expectations...") + assert len(actual_indexes) == len(expected_index_names), \ + f"Expected {len(expected_index_names)} indexes, got {len(actual_indexes)}" + actual_index_names = [idx.index_name for idx in actual_indexes] + assert sorted(actual_index_names) == sorted(expected_index_names), \ + f"Expected names {sorted(expected_index_names)}, got {sorted(actual_index_names)}" + logger.info("All index names matched as expected.") + + def test_5402(self): + """Verify each index has correct profile name.""" + logger.info("Verifying each index has correct profile name...") + expected_profile = "vector_ai_profile" + for index in self.vector_index.list(index_name_pattern=".*"): + assert index.profile.profile_name == expected_profile, \ + f"Profile mismatch for {index.index_name}: expected {expected_profile}, got {index.profile.profile_name}" + logger.info("All indexes have correct profile name.") + + def test_5403(self): + """Verify each index has correct object store credential name.""" + logger.info("Verifying each index has correct object store credential name...") + expected_credential = "OBJSTORE_CRED" + for index in self.vector_index.list(index_name_pattern=".*"): + assert index.attributes.object_storage_credential_name == expected_credential, \ + f"Credential mismatch for {index.index_name}: expected {expected_credential}, got {index.attributes.object_storage_credential_name}" + logger.info("All indexes have correct object store credential name.") + + def test_5404(self): + """Verify descriptions for all indexes.""" + logger.info("Verifying descriptions for all indexes...") + expected_description = "Test vector index" + for index in self.vector_index.list(index_name_pattern=".*"): + assert index.description == expected_description, \ + f"Description mismatch for {index.index_name}: expected {expected_description}, got {index.description}" + logger.info("All indexes have correct descriptions.") + + def test_5405(self): + """Test exact match listing for index name.""" + logger.info("Testing exact match listing for index name 'test_vector_index1'...") + indexes = self.vector_index.list(index_name_pattern="^test_vector_index1$") + assert list(indexes)[0].index_name == "TEST_VECTOR_INDEX1" + logger.info("Exact match returned correct index.") + + def test_5406(self): + """Verify multiple matches for pattern.""" + logger.info("Verifying multiple matches for pattern '^test_vector_index'...") + actual_indexes = list(self.vector_index.list(index_name_pattern="^test_vector_index")) + expected_count = 5 + assert len(actual_indexes) == expected_count, \ + f"Expected {expected_count} indexes, got {len(actual_indexes)}" + actual_index_names = [index.index_name for index in actual_indexes] + expected_index_names = [f"TEST_VECTOR_INDEX{i}" for i in range(1, 6)] + assert sorted(actual_index_names) == sorted(expected_index_names), \ + f"Expected names {sorted(expected_index_names)}, got {sorted(actual_index_names)}" + logger.info("Multiple index names verified successfully.") + + def test_5407(self): + """Test case-sensitive regex pattern for listing indexes.""" + logger.info("Testing case-sensitive regex pattern for listing indexes...") + indexes = self.vector_index.list("^TEST_VECTOR_INDEX?") + assert any(idx.index_name == "TEST_VECTOR_INDEX2" for idx in indexes) + logger.info("Case-sensitive pattern matched correctly.") + + def test_5408(self): + """Test case-insensitive regex pattern for listing indexes.""" + logger.info("Testing case-insensitive regex pattern for listing indexes...") + indexes = self.vector_index.list("^TEST") + assert any(idx.index_name.upper() == "TEST_VECTOR_INDEX1" for idx in indexes) + logger.info("Case-insensitive pattern matched correctly.") + + def test_5409(self): + """Test complex regex pattern with OR operator.""" + logger.info("Testing complex regex pattern with OR operator...") + indexes = self.vector_index.list("^(test_vector_index|test_vecidx)") + names = [idx.index_name for idx in indexes] + assert "TEST_VECTOR_INDEX1" in names + assert "TEST_VECIDX1" in names + assert "INVALID_VECIDX1" not in names + logger.info("Complex regex OR pattern matched correctly.") + + # ---------------- + # Negative tests + # ---------------- + def test_5410(self): + """Test non-matching regex pattern returns nothing.""" + logger.info("Testing non-matching regex pattern...") + indexes = self.vector_index.list(index_name_pattern="^xyz") + assert len(list(indexes)) == 0 + logger.info("Non-matching pattern returned no results as expected.") + + def test_5411(self): + """Test invalid regex pattern expecting ORA-12726 error.""" + logger.info("Testing invalid regex pattern expecting ORA-12726 error...") + with pytest.raises(oracledb.DatabaseError) as cm: + list(self.vector_index.list("[unclosed")) + assert "ORA-12726" in str(cm.value) + logger.info("Invalid regex correctly raised ORA-12726 error.") + + def test_5412(self): + """Test invalid type pattern (numeric instead of string).""" + logger.info("Testing invalid type pattern (numeric instead of string)...") + indexes = list(self.vector_index.list(123)) + assert len(indexes) == 0 + logger.info("Invalid type pattern handled correctly with empty result.") + + # ---------------- + # Stress / Edge cases + # ---------------- + def test_5413(self): + """Test None as pattern input.""" + logger.info("Testing None as pattern input...") + indexes = self.vector_index.list(None) + assert len(list(indexes)) != len(self.indexes) + logger.info("None pattern handled correctly.") + + def test_5414(self): + """Test empty string as pattern input.""" + logger.info("Testing empty string as pattern input...") + indexes = self.vector_index.list("") + assert len(list(indexes)) != len(self.indexes) + logger.info("Empty string pattern handled correctly.") + + def test_5415(self): + """Test whitespace-only pattern.""" + logger.info("Testing whitespace-only pattern...") + indexes = self.vector_index.list(" ") + assert len(list(indexes)) == 0 + logger.info("Whitespace pattern correctly returned no matches.") + + def test_5416(self): + """Test numeric string pattern yields no matches.""" + logger.info("Testing numeric pattern that should yield no matches...") + indexes = list(self.vector_index.list("test123")) + assert len(indexes) == 0 + logger.info("Numeric pattern correctly returned no matches.") + + def test_5417(self): + """Test pattern with special characters '$'.""" + logger.info("Testing pattern with special characters '$'...") + indexes = self.vector_index.list("test_vector_index1$") + assert len(list(indexes)) == 1 + logger.info("Special character pattern matched correctly.") + + def test_5418(self): + """Test extremely long regex pattern expecting ORA-12733 error.""" + logger.info("Testing extremely long regex pattern expecting ORA-12733 error...") + pattern = "^" + "a" * 1000 + "$" + with pytest.raises(oracledb.DatabaseError) as cm: + list(self.vector_index.list(pattern)) + assert "ORA-12733" in str(cm.value) + logger.info("Long regex correctly raised ORA-12733 error.") + + def test_5419(self): + """Test case-insensitive match for prefix.""" + logger.info("Testing case-insensitive match for prefix '^TEST'...") + indexes = self.vector_index.list("^TEST") + assert len(list(indexes)) == 7 + logger.info("Case-insensitive match returned expected count.") diff --git a/tests/vector_index/test_5500_enable_disable_index.py b/tests/vector_index/test_5500_enable_disable_index.py new file mode 100644 index 0000000..c1702e2 --- /dev/null +++ b/tests/vector_index/test_5500_enable_disable_index.py @@ -0,0 +1,337 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2025, Oracle and/or its affiliates. +# +# Licensed under the Universal Permissive License v 1.0 as shown at +# http://oss.oracle.com/licenses/upl. +# ----------------------------------------------------------------------------- + +import logging +import pytest +import select_ai +import test_env +import oracledb +import time +from select_ai import VectorIndex + +# Set up global logger (one per module) +logger = logging.getLogger("TestEnableDisableVectorIndex") + +@pytest.fixture(scope="class", autouse=True) +def setup_logging(): + logging.basicConfig( + format="%(asctime)s %(levelname)s %(name)s %(message)s", + level=logging.INFO + ) + +@pytest.fixture(scope="class") +def enabledisable_params(request): + params = { + "user": test_env.get_test_user(), + "password": test_env.get_test_password(), + "dsn": test_env.get_connect_string(), + "use_wallet": test_env.get_use_wallet(), + "user_ocid": test_env.get_user_ocid(), + "tenancy_ocid": test_env.get_tenancy_ocid(), + "private_key": test_env.get_private_key(), + "fingerprint": test_env.get_fingerprint(), + "cred_username": test_env.get_cred_username(), + "cred_password": test_env.get_cred_password(), + "oci_compartment_id": test_env.get_compartment_id(), + "embedding_location": test_env.get_embedding_location(), + } + request.cls.enabledisable_params = params + +@pytest.fixture(scope="class", autouse=True) +def setup_and_teardown(request, enabledisable_params): + logger.info("=== Setting up TestEnableDisableVectorIndex class ===") + p = request.cls.enabledisable_params + test_env.create_connection(use_wallet=p["use_wallet"]) + assert select_ai.is_connected(), "Connection to DB failed" + logger.info("Fetching credential secrets and OCI configuration...") + + # table setup + with select_ai.cursor() as cursor: + cursor.execute("begin execute immediate 'drop table test_items purge'; exception when others then null; end;") + cursor.execute("create table test_items (id number primary key, name varchar2(50))") + cursor.execute("insert into test_items values (1, 'Alpha')") + cursor.execute("insert into test_items values (2, 'Beta')") + cursor.execute("commit") + + # test resources + request.cls.create_credential() + request.cls.profile = request.cls.create_profile() + logger.info("Setup complete.") + + # Start with clean vector index + vi_attrs = select_ai.OracleVectorIndexAttributes( + location=p["embedding_location"], + object_storage_credential_name="OBJSTORE_CRED" + ) + request.cls.vector_index_attributes = vi_attrs + request.cls.index_name = "test_vector_index" + vector_index = select_ai.VectorIndex( + index_name=request.cls.index_name, + attributes=vi_attrs, + description="Test vector index", + profile=request.cls.profile + ) + vector_index.create(replace=True) + created_indexes = [idx.index_name for idx in VectorIndex.list()] + assert request.cls.index_name.upper() in created_indexes, f"VectorIndex {request.cls.index_name} was not created" + + yield + + logger.info("=== Tearing down TestEnableDisableVectorIndex class ===") + try: + vector_index = VectorIndex(index_name=request.cls.index_name) + vector_index.delete(force=True) + except Exception as e: + logger.info(f"Warning: drop vector index failed: {e}") + request.cls.delete_profile(request.cls.profile) + request.cls.delete_credential() + logger.info("Disconnecting from DB...") + try: + select_ai.disconnect() + except Exception as e: + logger.warning(f"Warning: disconnect failed ({e})") + +@pytest.fixture(autouse=True) +def log_test_name(request): + logger.info(f"--- Starting test: {request.function.__name__} ---") + yield + logger.info(f"--- Finished test: {request.function.__name__} ---") + +@pytest.mark.usefixtures("enabledisable_params", "setup_and_teardown") +class TestEnableDisableVectorIndex: + @classmethod + def get_native_cred_param(cls, cred_name=None) -> dict: + logger.info(f"Preparing native credential params for: {cred_name}") + p = cls.enabledisable_params + return dict( + credential_name = cred_name, + user_ocid = p["user_ocid"], + tenancy_ocid = p["tenancy_ocid"], + private_key = p["private_key"], + fingerprint = p["fingerprint"] + ) + @classmethod + def get_cred_param(cls, cred_name=None) -> dict: + logger.info(f"Preparing basic credential params for: {cred_name}") + p = cls.enabledisable_params + return dict( + credential_name = cred_name, + username = p["cred_username"], + password = p["cred_password"] + ) + @classmethod + def create_credential(cls, genai_cred="GENAI_CRED", objstore_cred="OBJSTORE_CRED"): + logger.info(f"Creating credentials: {genai_cred}, {objstore_cred}") + genai_credential = cls.get_native_cred_param(genai_cred) + objstore_credential = cls.get_cred_param(objstore_cred) + try: + logger.info(f"Creating GenAI credential: {genai_cred}") + select_ai.create_credential(credential=genai_credential, replace=True) + logger.info("GenAI credential created.") + except Exception as e: + logger.error(f"create_credential() raised {e} unexpectedly.") + raise AssertionError(f"create_credential() raised {e} unexpectedly.") + try: + logger.info(f"Creating ObjectStore credential: {objstore_cred}") + select_ai.create_credential(credential=objstore_credential, replace=True) + logger.info("ObjectStore credential created.") + except Exception as e: + logger.error(f"create_credential() raised {e} unexpectedly.") + raise AssertionError(f"create_credential() raised {e} unexpectedly.") + @classmethod + def create_profile(cls, profile_name="vector_ai_profile"): + logger.info(f"Creating Profile: {profile_name}") + p = cls.enabledisable_params + provider = select_ai.OCIGenAIProvider( + oci_compartment_id=p["oci_compartment_id"], + oci_apiformat="GENERIC" + ) + profile_attributes = select_ai.ProfileAttributes( + credential_name="GENAI_CRED", + provider=provider + ) + profile = select_ai.Profile( + profile_name=profile_name, + attributes=profile_attributes, + description="OCI GENAI Profile", + replace=True + ) + logger.info(f"Profile '{profile_name}' created successfully.") + return profile + @classmethod + def delete_profile(cls, profile): + logger.info("Deleting profile...") + try: + profile.delete() + logger.info(f"Profile '{profile.profile_name}' deleted successfully.") + except Exception as e: + logger.error(f"profile.delete() raised {e} unexpectedly.") + raise AssertionError(f"profile.delete() raised {e} unexpectedly.") + @classmethod + def delete_credential(cls): + logger.info("Deleting credentials...") + try: + select_ai.delete_credential("GENAI_CRED", force=True) + logger.info("Deleted credential 'GENAI_CRED'") + except Exception as e: + logger.error(f"delete_credential() raised {e} unexpectedly.") + raise AssertionError(f"delete_credential() raised {e} unexpectedly.") + try: + select_ai.delete_credential("OBJSTORE_CRED", force=True) + logger.info("Deleted credential 'OBJSTORE_CRED'") + except Exception as e: + logger.error(f"delete_credential() raised {e} unexpectedly.") + raise AssertionError(f"delete_credential() raised {e} unexpectedly.") + def setup_method(self, method): + logger.info(f"SetUp for {method.__name__}") + self.objstore_cred = "OBJSTORE_CRED" + self.vecidx = select_ai.VectorIndex() + self.vector_index = list(self.vecidx.list(index_name_pattern=".*"))[0] + logger.info(self.vector_index.index_name) + try: + self.vector_index.enable() + time.sleep(1) + except oracledb.DatabaseError as e: + if "ORA-20000" not in str(e): + raise + def teardown_method(self, method): + logger.info(f"TearDown for {method.__name__}") + + def wait_for_status_table(self, cursor, status_table, retries=5, delay=2): + for _ in range(retries): + try: + cursor.execute(f"SELECT COUNT(*) FROM {status_table}") + return cursor.fetchone() + except oracledb.DatabaseError as e: + if "ORA-00942" in str(e): + time.sleep(delay) + continue + raise + return None + + def wait_for_pipeline_entry(self, cursor, pipeline_name, retries=5, delay=2): + for _ in range(retries): + cursor.execute( + "SELECT status_table FROM user_cloud_pipelines WHERE pipeline_name = :1", + [pipeline_name] + ) + row = cursor.fetchone() + if row and row[0]: + return row[0] + time.sleep(delay) + return None + + def test_5501(self): + """Disabling and enabling the vector index.""" + logger.info(f"Disabling vector index: {self.index_name}") + self.vector_index.disable() + logger.info(f"Enabling vector index: {self.index_name}") + self.vector_index.enable() + logger.info(f"Vector index enabled successfully") + + def test_5502(self): + """Disable same vector index twice (should be harmless).""" + logger.info(f"First disable of vector index: {self.index_name}") + self.vector_index.disable() + logger.info(f"Attempting second disable of vector index: {self.index_name}") + self.vector_index.disable() + + def test_5503(self): + """Enable same vector index twice (should be harmless).""" + logger.info(f"Enabling vector index: {self.index_name}") + self.vector_index.enable() + self.vector_index.enable() + + def test_5504(self): + """Ensure queries work after enabling the vector index.""" + logger.info("Querying test_items table after enabling vector index") + with select_ai.cursor() as cursor: + cursor.execute("select count(*) from test_items") + row_count, = cursor.fetchone() + logger.info(f"Number of rows in test_items: {row_count}") + df = self.profile.run_sql(prompt="How many rows in test_items") + logger.info(f"run_sql returned: {df}") + assert len(df) > 0, "run_sql should return rows when index is enabled" + + def test_5505(self): + """Ensure queries fail after disabling the vector index.""" + logger.info(f"Disabling vector index: {self.index_name} to test query blocking") + self.vector_index.disable() + logger.info(f"Running query should raise DatabaseError") + with pytest.raises(oracledb.DatabaseError) as cm: + self.profile.run_sql(prompt="Show all rows from test_items") + logger.info(f"Expected database error confirmed: {cm.value}") + + def test_5506(self): + """Disabling a nonexistent index raises error.""" + logger.info("Disabling nonexistent index to test error handling") + invalid_index = VectorIndex(index_name="does_not_exist") + with pytest.raises(oracledb.DatabaseError) as cm: + invalid_index.disable() + logger.info(f"Expected database error confirmed: {cm.value}") + + def test_5507(self): + """Enabling a nonexistent index raises error.""" + logger.info("Enabling nonexistent index to test error handling") + invalid_index = VectorIndex(index_name="does_not_exist") + with pytest.raises(oracledb.DatabaseError) as cm: + invalid_index.enable() + logger.info(f"Expected database error confirmed: {cm.value}") + + def test_5508(self): + """Disabling after delete raises error; vector index recreated.""" + logger.info(f"Deleting vector index: {self.index_name}") + self.vector_index.delete(force=True) + logger.info(f"Attempting to disable deleted index") + with pytest.raises(oracledb.DatabaseError): + self.vector_index.disable() + logger.info(f"Recreating vector index for subsequent tests") + vector_index = select_ai.VectorIndex( + index_name=self.index_name, + attributes=self.vector_index_attributes, + description="Test vector index", + profile=self.profile + ) + vector_index.create(replace=True) + logger.info(f"Vector index recreated successfully") + + def test_5509(self): + """Pipeline inactive after disabling the vector index.""" + logger.info(f"Disabling vector index: {self.index_name} to check pipeline inactivity") + self.vector_index.disable() + pipeline_name = f"{self.index_name.upper()}$VECPIPELINE" + with select_ai.cursor() as cursor: + cursor.execute( + "SELECT status_table FROM user_cloud_pipelines WHERE pipeline_name = :1", + [pipeline_name] + ) + row = cursor.fetchone() + if row is None: + logger.info(f"Pipeline is inactive (no entry in user_cloud_pipelines)") + assert True + return + status_table = row[0] + logger.info(f"Status table found: {status_table}, querying should fail") + with pytest.raises(oracledb.DatabaseError): + cursor.execute(f"SELECT * FROM {status_table} FETCH FIRST 1 ROWS ONLY") + + def test_5510(self): + """Pipeline active after enabling the vector index.""" + pipeline_name = f"{self.index_name.upper()}$VECPIPELINE" + logger.info(f"Checking pipeline activity after enabling vector index") + with select_ai.cursor() as cursor: + cursor.execute( + "SELECT status_table FROM user_cloud_pipelines WHERE pipeline_name = :pipeline_name", + {"pipeline_name": pipeline_name} + ) + (status_table,) = cursor.fetchone() + logger.info(f"Status table found: {status_table}") + assert status_table is not None, f"No pipeline entry found for {pipeline_name}" + count_row = self.wait_for_status_table(cursor, status_table) + assert count_row is not None, f"No result returned from status_table {status_table}" + assert count_row[0] >= 0, "Pipeline table should be accessible when enabled" + logger.info(f"Pipeline is active and accessible")