|
| 1 | +# The MIT License (MIT) |
| 2 | +# Copyright (c) Microsoft Corporation. All rights reserved. |
| 3 | + |
| 4 | +import os |
| 5 | +import unittest |
| 6 | +import uuid |
| 7 | + |
| 8 | +import pytest |
| 9 | + |
| 10 | +import test_config |
| 11 | +from azure.cosmos import CosmosClient, PartitionKey |
| 12 | +from azure.cosmos._global_secondary_index import GlobalSecondaryIndexDefinition |
| 13 | + |
| 14 | + |
| 15 | +@pytest.mark.cosmosGSI |
| 16 | +class TestGlobalSecondaryIndexLive(unittest.TestCase): |
| 17 | + """Live tests for Global Secondary Index (GSI) container operations. |
| 18 | +
|
| 19 | + These tests require a Cosmos DB account with GSI support enabled. |
| 20 | + The account endpoint and key are sourced from Key Vault secrets: |
| 21 | + - gsi-pipeline-uri -> GSI_ACCOUNT_HOST |
| 22 | + - gsi-pipeline-key -> GSI_ACCOUNT_KEY |
| 23 | + """ |
| 24 | + client: CosmosClient = None |
| 25 | + host = os.getenv('GSI_ACCOUNT_HOST', test_config.TestConfig.host) |
| 26 | + masterKey = os.getenv('GSI_ACCOUNT_KEY', test_config.TestConfig.masterKey) |
| 27 | + connectionPolicy = test_config.TestConfig.connectionPolicy |
| 28 | + |
| 29 | + @classmethod |
| 30 | + def setUpClass(cls): |
| 31 | + if (cls.masterKey == '[YOUR_KEY_HERE]' or |
| 32 | + cls.host == '[YOUR_ENDPOINT_HERE]'): |
| 33 | + raise Exception( |
| 34 | + "You must specify your Azure Cosmos account values for " |
| 35 | + "'masterKey' and 'host' at the top of this class to run the " |
| 36 | + "tests.") |
| 37 | + cls.client = CosmosClient(cls.host, cls.masterKey) |
| 38 | + cls.test_db = cls.client.create_database(str(uuid.uuid4())) |
| 39 | + |
| 40 | + @classmethod |
| 41 | + def tearDownClass(cls): |
| 42 | + if cls.test_db: |
| 43 | + cls.client.delete_database(cls.test_db.id) |
| 44 | + |
| 45 | + def test_create_gsi_container(self): |
| 46 | + """Test creating a GSI container derived from a source container.""" |
| 47 | + # Create source container |
| 48 | + source_container = self.test_db.create_container( |
| 49 | + id="source-container-" + str(uuid.uuid4())[:8], |
| 50 | + partition_key=PartitionKey(path="/id") |
| 51 | + ) |
| 52 | + |
| 53 | + # Create GSI container using GlobalSecondaryIndexDefinition |
| 54 | + gsi_definition = GlobalSecondaryIndexDefinition( |
| 55 | + source_container_id=source_container.id, |
| 56 | + definition="SELECT c.id, c.email, c.name FROM c" |
| 57 | + ) |
| 58 | + gsi_container = self.test_db.create_container( |
| 59 | + id="gsi-container-" + str(uuid.uuid4())[:8], |
| 60 | + partition_key=PartitionKey(path="/id"), |
| 61 | + global_secondary_index_definition=gsi_definition |
| 62 | + ) |
| 63 | + |
| 64 | + # Read back the container properties and verify GSI definition is present |
| 65 | + properties = gsi_container.read() |
| 66 | + self.assertIn("globalSecondaryIndexDefinition", properties) |
| 67 | + gsi_props = properties["globalSecondaryIndexDefinition"] |
| 68 | + self.assertEqual(gsi_props["sourceCollectionId"], source_container.id) |
| 69 | + self.assertEqual(gsi_props["definition"], "SELECT c.id, c.email, c.name FROM c") |
| 70 | + self.assertIn("status", gsi_props) |
| 71 | + |
| 72 | + # Clean up - delete GSI container first, then source |
| 73 | + self.test_db.delete_container(gsi_container.id) |
| 74 | + self.test_db.delete_container(source_container.id) |
| 75 | + |
| 76 | + def test_create_gsi_container_with_dict(self): |
| 77 | + """Test creating a GSI container using a raw dict instead of the class.""" |
| 78 | + # Create source container |
| 79 | + source_container = self.test_db.create_container( |
| 80 | + id="source-dict-" + str(uuid.uuid4())[:8], |
| 81 | + partition_key=PartitionKey(path="/id") |
| 82 | + ) |
| 83 | + |
| 84 | + # Create GSI container using a dict |
| 85 | + gsi_dict = { |
| 86 | + "sourceCollectionId": source_container.id, |
| 87 | + "definition": "SELECT c.id, c.category FROM c" |
| 88 | + } |
| 89 | + gsi_container = self.test_db.create_container( |
| 90 | + id="gsi-dict-" + str(uuid.uuid4())[:8], |
| 91 | + partition_key=PartitionKey(path="/id"), |
| 92 | + global_secondary_index_definition=gsi_dict |
| 93 | + ) |
| 94 | + |
| 95 | + # Verify |
| 96 | + properties = gsi_container.read() |
| 97 | + self.assertIn("globalSecondaryIndexDefinition", properties) |
| 98 | + |
| 99 | + # Clean up |
| 100 | + self.test_db.delete_container(gsi_container.id) |
| 101 | + self.test_db.delete_container(source_container.id) |
| 102 | + |
| 103 | + def test_create_gsi_container_if_not_exists(self): |
| 104 | + """Test create_container_if_not_exists with GSI definition.""" |
| 105 | + # Create source container |
| 106 | + source_container = self.test_db.create_container( |
| 107 | + id="source-ifne-" + str(uuid.uuid4())[:8], |
| 108 | + partition_key=PartitionKey(path="/id") |
| 109 | + ) |
| 110 | + |
| 111 | + gsi_definition = GlobalSecondaryIndexDefinition( |
| 112 | + source_container_id=source_container.id, |
| 113 | + definition="SELECT c.id, c.timestamp FROM c" |
| 114 | + ) |
| 115 | + container_id = "gsi-ifne-" + str(uuid.uuid4())[:8] |
| 116 | + |
| 117 | + # First call creates |
| 118 | + gsi_container = self.test_db.create_container_if_not_exists( |
| 119 | + id=container_id, |
| 120 | + partition_key=PartitionKey(path="/id"), |
| 121 | + global_secondary_index_definition=gsi_definition |
| 122 | + ) |
| 123 | + self.assertEqual(gsi_container.id, container_id) |
| 124 | + |
| 125 | + # Second call returns existing |
| 126 | + gsi_container_again = self.test_db.create_container_if_not_exists( |
| 127 | + id=container_id, |
| 128 | + partition_key=PartitionKey(path="/id"), |
| 129 | + global_secondary_index_definition=gsi_definition |
| 130 | + ) |
| 131 | + self.assertEqual(gsi_container_again.id, container_id) |
| 132 | + |
| 133 | + # Clean up |
| 134 | + self.test_db.delete_container(gsi_container.id) |
| 135 | + self.test_db.delete_container(source_container.id) |
| 136 | + |
| 137 | + |
| 138 | +if __name__ == "__main__": |
| 139 | + unittest.main() |
0 commit comments