File size: 8,248 Bytes
979853c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""
Unit tests for PostgreSQL safe index name generation.

This module tests the _safe_index_name helper function which prevents
PostgreSQL's silent 63-byte identifier truncation from causing index
lookup failures.
"""

import pytest

# Mark all tests as offline (no external dependencies)
pytestmark = pytest.mark.offline


class TestSafeIndexName:
    """Test suite for _safe_index_name function."""

    def test_short_name_unchanged(self):
        """Short index names should remain unchanged."""
        from lightrag.kg.postgres_impl import _safe_index_name

        # Short table name - should return unchanged
        result = _safe_index_name("lightrag_vdb_entity", "hnsw_cosine")
        assert result == "idx_lightrag_vdb_entity_hnsw_cosine"
        assert len(result.encode("utf-8")) <= 63

    def test_long_name_gets_hashed(self):
        """Long table names exceeding 63 bytes should get hashed."""
        from lightrag.kg.postgres_impl import _safe_index_name

        # Long table name that would exceed 63 bytes
        long_table_name = "LIGHTRAG_VDB_ENTITY_text_embedding_3_large_3072d"
        result = _safe_index_name(long_table_name, "hnsw_cosine")

        # Should be within 63 bytes
        assert len(result.encode("utf-8")) <= 63

        # Should start with idx_ prefix
        assert result.startswith("idx_")

        # Should contain the suffix
        assert result.endswith("_hnsw_cosine")

        # Should NOT be the naive concatenation (which would be truncated)
        naive_name = f"idx_{long_table_name.lower()}_hnsw_cosine"
        assert result != naive_name

    def test_deterministic_output(self):
        """Same input should always produce same output (deterministic)."""
        from lightrag.kg.postgres_impl import _safe_index_name

        table_name = "LIGHTRAG_VDB_CHUNKS_text_embedding_3_large_3072d"
        suffix = "hnsw_cosine"

        result1 = _safe_index_name(table_name, suffix)
        result2 = _safe_index_name(table_name, suffix)

        assert result1 == result2

    def test_different_suffixes_different_results(self):
        """Different suffixes should produce different index names."""
        from lightrag.kg.postgres_impl import _safe_index_name

        table_name = "LIGHTRAG_VDB_ENTITY_text_embedding_3_large_3072d"

        result1 = _safe_index_name(table_name, "hnsw_cosine")
        result2 = _safe_index_name(table_name, "ivfflat_cosine")

        assert result1 != result2

    def test_case_insensitive(self):
        """Table names should be normalized to lowercase."""
        from lightrag.kg.postgres_impl import _safe_index_name

        result_upper = _safe_index_name("LIGHTRAG_VDB_ENTITY", "hnsw_cosine")
        result_lower = _safe_index_name("lightrag_vdb_entity", "hnsw_cosine")

        assert result_upper == result_lower

    def test_boundary_case_exactly_63_bytes(self):
        """Test boundary case where name is exactly at 63-byte limit."""
        from lightrag.kg.postgres_impl import _safe_index_name

        # Create a table name that results in exactly 63 bytes
        # idx_ (4) + table_name + _ (1) + suffix = 63
        # So table_name + suffix = 58

        # Test a name that's just under the limit (should remain unchanged)
        short_suffix = "id"
        # idx_ (4) + 56 chars + _ (1) + id (2) = 63
        table_56 = "a" * 56
        result = _safe_index_name(table_56, short_suffix)
        expected = f"idx_{table_56}_{short_suffix}"
        assert result == expected
        assert len(result.encode("utf-8")) == 63

    def test_unicode_handling(self):
        """Unicode characters should be properly handled (bytes, not chars)."""
        from lightrag.kg.postgres_impl import _safe_index_name

        # Unicode characters can take more bytes than visible chars
        # Chinese characters are 3 bytes each in UTF-8
        table_name = "lightrag_测试_table"  # Contains Chinese chars
        result = _safe_index_name(table_name, "hnsw_cosine")

        # Should always be within 63 bytes
        assert len(result.encode("utf-8")) <= 63

    def test_real_world_model_names(self):
        """Test with real-world embedding model names that cause issues."""
        from lightrag.kg.postgres_impl import _safe_index_name

        # These are actual model names that have caused issues
        test_cases = [
            ("LIGHTRAG_VDB_CHUNKS_text_embedding_3_large_3072d", "hnsw_cosine"),
            ("LIGHTRAG_VDB_ENTITY_text_embedding_3_large_3072d", "hnsw_cosine"),
            ("LIGHTRAG_VDB_RELATION_text_embedding_3_large_3072d", "hnsw_cosine"),
            (
                "LIGHTRAG_VDB_ENTITY_bge_m3_1024d",
                "hnsw_cosine",
            ),  # Shorter model name
            (
                "LIGHTRAG_VDB_CHUNKS_nomic_embed_text_v1_768d",
                "ivfflat_cosine",
            ),  # Different index type
        ]

        for table_name, suffix in test_cases:
            result = _safe_index_name(table_name, suffix)

            # Critical: must be within PostgreSQL's 63-byte limit
            assert (
                len(result.encode("utf-8")) <= 63
            ), f"Index name too long: {result} for table {table_name}"

            # Must have consistent format
            assert result.startswith("idx_"), f"Missing idx_ prefix: {result}"
            assert result.endswith(f"_{suffix}"), f"Missing suffix {suffix}: {result}"

    def test_hash_uniqueness_for_similar_tables(self):
        """Similar but different table names should produce different hashes."""
        from lightrag.kg.postgres_impl import _safe_index_name

        # These tables have similar names but should have different hashes
        tables = [
            "LIGHTRAG_VDB_CHUNKS_model_a_1024d",
            "LIGHTRAG_VDB_CHUNKS_model_b_1024d",
            "LIGHTRAG_VDB_ENTITY_model_a_1024d",
        ]

        results = [_safe_index_name(t, "hnsw_cosine") for t in tables]

        # All results should be unique
        assert len(set(results)) == len(results), "Hash collision detected!"


class TestIndexNameIntegration:
    """Integration-style tests for index name usage patterns."""

    def test_pg_indexes_lookup_compatibility(self):
        """
        Test that the generated index name will work with pg_indexes lookup.

        This is the core problem: PostgreSQL stores the truncated name,
        but we were looking up the untruncated name. Our fix ensures we
        always use a name that fits within 63 bytes.
        """
        from lightrag.kg.postgres_impl import _safe_index_name

        table_name = "LIGHTRAG_VDB_CHUNKS_text_embedding_3_large_3072d"
        suffix = "hnsw_cosine"

        # Generate the index name
        index_name = _safe_index_name(table_name, suffix)

        # Simulate what PostgreSQL would store (truncate at 63 bytes)
        stored_name = index_name.encode("utf-8")[:63].decode("utf-8", errors="ignore")

        # The key fix: our generated name should equal the stored name
        # because it's already within the 63-byte limit
        assert (
            index_name == stored_name
        ), "Index name would be truncated by PostgreSQL, causing lookup failures!"

    def test_backward_compatibility_short_names(self):
        """
        Ensure backward compatibility with existing short index names.

        For tables that have existing indexes with short names (pre-model-suffix era),
        the function should not change their names.
        """
        from lightrag.kg.postgres_impl import _safe_index_name

        # Legacy table names without model suffix
        legacy_tables = [
            "LIGHTRAG_VDB_ENTITY",
            "LIGHTRAG_VDB_RELATION",
            "LIGHTRAG_VDB_CHUNKS",
        ]

        for table in legacy_tables:
            for suffix in ["hnsw_cosine", "ivfflat_cosine", "id"]:
                result = _safe_index_name(table, suffix)
                expected = f"idx_{table.lower()}_{suffix}"

                # Short names should remain unchanged for backward compatibility
                if len(expected.encode("utf-8")) <= 63:
                    assert (
                        result == expected
                    ), f"Short name changed unexpectedly: {result} != {expected}"