Skip to content

Commit a15fcbd

Browse files
authored
Merge pull request lightspeed-core#964 from tisnik/lcore-1150-schema-namespace-used-by-conversation-cache
LCORE-1150: Schema namespace used by conversation cache
2 parents b24cdaf + 63c8415 commit a15fcbd

2 files changed

Lines changed: 126 additions & 5 deletions

File tree

src/cache/postgres_cache.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import psycopg2
5+
from psycopg2.extensions import AsIs
56

67
from cache.cache import Cache
78
from cache.cache_error import CacheError
@@ -38,6 +39,10 @@ class PostgresCache(Cache):
3839
```
3940
"""
4041

42+
CREATE_SCHEMA = """
43+
CREATE SCHEMA IF NOT EXISTS %s;
44+
"""
45+
4146
CREATE_CACHE_TABLE = """
4247
CREATE TABLE IF NOT EXISTS cache (
4348
user_id text NOT NULL,
@@ -133,6 +138,18 @@ def connect(self) -> None:
133138
# even if PostgreSQL is not alive
134139
self.connection = None
135140
config = self.postgres_config
141+
namespace = "public"
142+
if config.namespace is not None:
143+
namespace = config.namespace
144+
# LCORE-1158 need to be implemented too!
145+
# validate namespace contains only valid PostgreSQL identifier characters
146+
if not namespace.replace("_", "").replace("$", "").isalnum():
147+
raise ValueError(f"Invalid namespace: {namespace}")
148+
if len(namespace) > 63:
149+
raise ValueError(
150+
f"Invalid namespace: {namespace}. "
151+
"Maximum length is 63 characters."
152+
)
136153
try:
137154
self.connection = psycopg2.connect(
138155
host=config.host,
@@ -143,8 +160,9 @@ def connect(self) -> None:
143160
sslmode=config.ssl_mode,
144161
sslrootcert=config.ca_cert_path,
145162
gssencmode=config.gss_encmode,
163+
options=f"-c search_path={namespace}",
146164
)
147-
self.initialize_cache()
165+
self.initialize_cache(namespace)
148166
except Exception as e:
149167
if self.connection is not None:
150168
self.connection.close()
@@ -166,8 +184,16 @@ def connected(self) -> bool:
166184
logger.error("Disconnected from storage: %s", e)
167185
return False
168186

169-
def initialize_cache(self) -> None:
170-
"""Initialize cache - clean it up etc."""
187+
def initialize_cache(self, namespace: str) -> None:
188+
"""Initialize cache and create schema if needed.
189+
190+
Parameters:
191+
namespace: PostgreSQL schema namespace to use for cache tables.
192+
If not "public", the schema will be created if it doesn't exist.
193+
194+
Raises:
195+
CacheError: If the cache is disconnected.
196+
"""
171197
if self.connection is None:
172198
logger.error("Cache is disconnected")
173199
raise CacheError("Initialize_cache: cache is disconnected")
@@ -177,6 +203,10 @@ def initialize_cache(self) -> None:
177203
# and it should not interfere with other statements
178204
cursor = self.connection.cursor()
179205

206+
logger.info("Initializing schema")
207+
if namespace != "public":
208+
cursor.execute(PostgresCache.CREATE_SCHEMA, (AsIs(namespace),))
209+
180210
logger.info("Initializing table for cache")
181211
cursor.execute(PostgresCache.CREATE_CACHE_TABLE)
182212

tests/unit/cache/test_postgres_cache.py

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,56 @@ def postgres_cache_config() -> PostgreSQLDatabaseConfiguration:
109109
)
110110

111111

112+
@pytest.fixture(scope="module", name="postgres_cache_config_fixture_wrong_namespace")
113+
def postgres_cache_config_wrong_namespace() -> PostgreSQLDatabaseConfiguration:
114+
"""Fixture with invalid namespace containing spaces for validation testing.
115+
116+
Create a PostgreSQLDatabaseConfiguration with an invalid namespace ("foo bar baz")
117+
to verify that the PostgresCache constructor properly rejects namespaces with spaces.
118+
119+
Returns:
120+
PostgreSQLDatabaseConfiguration: A configuration object with host,
121+
port, db, user, SecretStr password, and an invalid namespace containing
122+
spaces. Values are placeholders and not intended for real database
123+
connections.
124+
"""
125+
# can be any configuration, becuase tests won't really try to
126+
# connect to database
127+
return PostgreSQLDatabaseConfiguration(
128+
host="localhost",
129+
port=1234,
130+
db="database",
131+
user="user",
132+
password=SecretStr("password"),
133+
namespace="foo bar baz",
134+
)
135+
136+
137+
@pytest.fixture(scope="module", name="postgres_cache_config_fixture_too_long_namespace")
138+
def postgres_cache_config_too_long_namespace() -> PostgreSQLDatabaseConfiguration:
139+
"""Fixture with namespace exceeding PostgreSQL's 63-character limit.
140+
141+
Create a PostgreSQLDatabaseConfiguration with an overly long namespace
142+
to verify that the PostgresCache constructor enforces the maximum length constraint.
143+
144+
Returns:
145+
PostgreSQLDatabaseConfiguration: A configuration object with host,
146+
port, db, user, SecretStr password, and a namespace exceeding 63
147+
characters. Values are placeholders and not intended for real database
148+
connections.
149+
"""
150+
# can be any configuration, becuase tests won't really try to
151+
# connect to database
152+
return PostgreSQLDatabaseConfiguration(
153+
host="localhost",
154+
port=1234,
155+
db="database",
156+
user="user",
157+
password=SecretStr("password"),
158+
namespace="too long namespace that is longer than allowed 63 characters limit",
159+
)
160+
161+
112162
def test_cache_initialization(
113163
postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration,
114164
mocker: MockerFixture,
@@ -220,7 +270,7 @@ def test_initialize_cache_when_connected(
220270
mocker.patch("psycopg2.connect")
221271
cache = PostgresCache(postgres_cache_config_fixture)
222272
# should not fail
223-
cache.initialize_cache()
273+
cache.initialize_cache("public")
224274

225275

226276
def test_initialize_cache_when_disconnected(
@@ -234,7 +284,7 @@ def test_initialize_cache_when_disconnected(
234284
cache.connection = None
235285

236286
with pytest.raises(CacheError, match="cache is disconnected"):
237-
cache.initialize_cache()
287+
cache.initialize_cache("public")
238288

239289

240290
def test_ready_method(
@@ -619,3 +669,44 @@ def test_insert_and_get_without_referenced_documents(
619669
assert len(retrieved_entries) == 1
620670
assert retrieved_entries[0] == entry_without_docs
621671
assert retrieved_entries[0].referenced_documents is None
672+
673+
674+
def test_initialize_cache_with_custom_namespace(
675+
postgres_cache_config_fixture: PostgreSQLDatabaseConfiguration,
676+
mocker: MockerFixture,
677+
) -> None:
678+
"""Test the initialize_cache() with a custom namespace."""
679+
mock_connect = mocker.patch("psycopg2.connect")
680+
cache = PostgresCache(postgres_cache_config_fixture)
681+
682+
mock_connection = mock_connect.return_value
683+
mock_cursor = mock_connection.cursor.return_value
684+
685+
# should not fail and should execute CREATE SCHEMA
686+
cache.initialize_cache("custom_schema")
687+
688+
# Verify CREATE SCHEMA was called for non-public namespace
689+
create_schema_calls = [
690+
call
691+
for call in mock_cursor.execute.call_args_list
692+
if "CREATE SCHEMA" in str(call)
693+
]
694+
assert len(create_schema_calls) > 0
695+
696+
697+
def test_connect_to_cache_with_improper_namespace(
698+
postgres_cache_config_fixture_wrong_namespace: PostgreSQLDatabaseConfiguration,
699+
) -> None:
700+
"""Test that PostgresCache constructor raises ValueError for invalid namespace."""
701+
# should fail due to invalid namespace containing spaces
702+
with pytest.raises(ValueError, match="Invalid namespace: foo bar baz"):
703+
PostgresCache(postgres_cache_config_fixture_wrong_namespace)
704+
705+
706+
def test_connect_to_cache_with_too_long_namespace(
707+
postgres_cache_config_fixture_too_long_namespace: PostgreSQLDatabaseConfiguration,
708+
) -> None:
709+
"""Test that PostgresCache constructor raises ValueError for invalid namespace."""
710+
# should fail due to invalid namespace containing spaces
711+
with pytest.raises(ValueError, match="Invalid namespace: too long namespace"):
712+
PostgresCache(postgres_cache_config_fixture_too_long_namespace)

0 commit comments

Comments
 (0)