remdb 0.3.114__py3-none-any.whl → 0.3.127__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of remdb might be problematic. Click here for more details.
- rem/agentic/agents/sse_simulator.py +2 -0
- rem/agentic/context.py +23 -3
- rem/agentic/mcp/tool_wrapper.py +29 -3
- rem/agentic/otel/setup.py +1 -0
- rem/agentic/providers/pydantic_ai.py +26 -2
- rem/api/main.py +4 -1
- rem/api/mcp_router/server.py +9 -3
- rem/api/mcp_router/tools.py +324 -2
- rem/api/routers/admin.py +218 -1
- rem/api/routers/chat/completions.py +250 -4
- rem/api/routers/chat/models.py +81 -7
- rem/api/routers/chat/otel_utils.py +33 -0
- rem/api/routers/chat/sse_events.py +17 -1
- rem/api/routers/chat/streaming.py +35 -1
- rem/api/routers/feedback.py +134 -14
- rem/api/routers/query.py +6 -3
- rem/cli/commands/README.md +42 -0
- rem/cli/commands/cluster.py +617 -168
- rem/cli/commands/configure.py +1 -3
- rem/cli/commands/db.py +66 -22
- rem/cli/commands/experiments.py +242 -26
- rem/cli/commands/schema.py +6 -5
- rem/config.py +8 -1
- rem/services/phoenix/client.py +59 -18
- rem/services/postgres/diff_service.py +108 -3
- rem/services/postgres/schema_generator.py +205 -4
- rem/services/session/compression.py +7 -0
- rem/settings.py +150 -18
- rem/sql/migrations/001_install.sql +156 -0
- rem/sql/migrations/002_install_models.sql +1864 -1
- rem/sql/migrations/004_cache_system.sql +548 -0
- rem/utils/__init__.py +18 -0
- rem/utils/schema_loader.py +94 -3
- rem/utils/sql_paths.py +146 -0
- rem/workers/__init__.py +3 -1
- rem/workers/db_listener.py +579 -0
- rem/workers/unlogged_maintainer.py +463 -0
- {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/METADATA +213 -177
- {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/RECORD +41 -36
- {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/WHEEL +0 -0
- {remdb-0.3.114.dist-info → remdb-0.3.127.dist-info}/entry_points.txt +0 -0
rem/settings.py
CHANGED
|
@@ -33,14 +33,15 @@ Example .env file:
|
|
|
33
33
|
AUTH__OIDC_CLIENT_ID=your-client-id
|
|
34
34
|
AUTH__SESSION_SECRET=your-secret-key
|
|
35
35
|
|
|
36
|
-
# OpenTelemetry (disabled by default)
|
|
36
|
+
# OpenTelemetry (disabled by default - enable via env var when collector available)
|
|
37
|
+
# Standard OTLP collector ports: 4317 (gRPC), 4318 (HTTP)
|
|
37
38
|
OTEL__ENABLED=false
|
|
38
39
|
OTEL__SERVICE_NAME=rem-api
|
|
39
|
-
OTEL__COLLECTOR_ENDPOINT=http://localhost:
|
|
40
|
-
OTEL__PROTOCOL=
|
|
40
|
+
OTEL__COLLECTOR_ENDPOINT=http://localhost:4317
|
|
41
|
+
OTEL__PROTOCOL=grpc
|
|
41
42
|
|
|
42
|
-
# Arize Phoenix (
|
|
43
|
-
PHOENIX__ENABLED=
|
|
43
|
+
# Arize Phoenix (enabled by default - can be disabled via env var)
|
|
44
|
+
PHOENIX__ENABLED=true
|
|
44
45
|
PHOENIX__COLLECTOR_ENDPOINT=http://localhost:6006/v1/traces
|
|
45
46
|
PHOENIX__PROJECT_NAME=rem
|
|
46
47
|
|
|
@@ -241,6 +242,11 @@ class OTELSettings(BaseSettings):
|
|
|
241
242
|
description="Export timeout in milliseconds",
|
|
242
243
|
)
|
|
243
244
|
|
|
245
|
+
insecure: bool = Field(
|
|
246
|
+
default=True,
|
|
247
|
+
description="Use insecure (non-TLS) gRPC connection (default: True for local dev)",
|
|
248
|
+
)
|
|
249
|
+
|
|
244
250
|
|
|
245
251
|
class PhoenixSettings(BaseSettings):
|
|
246
252
|
"""
|
|
@@ -267,8 +273,8 @@ class PhoenixSettings(BaseSettings):
|
|
|
267
273
|
)
|
|
268
274
|
|
|
269
275
|
enabled: bool = Field(
|
|
270
|
-
default=
|
|
271
|
-
description="Enable Phoenix integration (
|
|
276
|
+
default=True,
|
|
277
|
+
description="Enable Phoenix integration (enabled by default)",
|
|
272
278
|
)
|
|
273
279
|
|
|
274
280
|
base_url: str = Field(
|
|
@@ -1051,10 +1057,26 @@ class ModelsSettings(BaseSettings):
|
|
|
1051
1057
|
|
|
1052
1058
|
@property
|
|
1053
1059
|
def module_list(self) -> list[str]:
|
|
1054
|
-
"""
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1060
|
+
"""
|
|
1061
|
+
Get modules as a list, filtering empty strings.
|
|
1062
|
+
|
|
1063
|
+
Auto-detects ./models folder if it exists and is importable.
|
|
1064
|
+
"""
|
|
1065
|
+
modules = []
|
|
1066
|
+
if self.import_modules:
|
|
1067
|
+
modules = [m.strip() for m in self.import_modules.split(";") if m.strip()]
|
|
1068
|
+
|
|
1069
|
+
# Auto-detect ./models if it exists and is a Python package (convention over configuration)
|
|
1070
|
+
from pathlib import Path
|
|
1071
|
+
|
|
1072
|
+
models_path = Path("./models")
|
|
1073
|
+
if models_path.exists() and models_path.is_dir():
|
|
1074
|
+
# Check if it's a Python package (has __init__.py)
|
|
1075
|
+
if (models_path / "__init__.py").exists():
|
|
1076
|
+
if "models" not in modules:
|
|
1077
|
+
modules.insert(0, "models")
|
|
1078
|
+
|
|
1079
|
+
return modules
|
|
1058
1080
|
|
|
1059
1081
|
|
|
1060
1082
|
class SchemaSettings(BaseSettings):
|
|
@@ -1240,6 +1262,110 @@ class GitSettings(BaseSettings):
|
|
|
1240
1262
|
)
|
|
1241
1263
|
|
|
1242
1264
|
|
|
1265
|
+
class DBListenerSettings(BaseSettings):
|
|
1266
|
+
"""
|
|
1267
|
+
PostgreSQL LISTEN/NOTIFY database listener settings.
|
|
1268
|
+
|
|
1269
|
+
The DB Listener is a lightweight worker that subscribes to PostgreSQL
|
|
1270
|
+
NOTIFY events and dispatches them to external systems (SQS, REST, custom).
|
|
1271
|
+
|
|
1272
|
+
Architecture:
|
|
1273
|
+
- Single-replica deployment (to avoid duplicate processing)
|
|
1274
|
+
- Dedicated connection for LISTEN (not from connection pool)
|
|
1275
|
+
- Automatic reconnection with exponential backoff
|
|
1276
|
+
- Graceful shutdown on SIGTERM
|
|
1277
|
+
|
|
1278
|
+
Use Cases:
|
|
1279
|
+
- Sync data changes to external systems (Phoenix, webhooks)
|
|
1280
|
+
- Trigger async jobs without polling
|
|
1281
|
+
- Event-driven architectures with PostgreSQL as event source
|
|
1282
|
+
|
|
1283
|
+
Example PostgreSQL trigger:
|
|
1284
|
+
CREATE OR REPLACE FUNCTION notify_feedback_insert()
|
|
1285
|
+
RETURNS TRIGGER AS $$
|
|
1286
|
+
BEGIN
|
|
1287
|
+
PERFORM pg_notify('feedback_sync', json_build_object(
|
|
1288
|
+
'id', NEW.id,
|
|
1289
|
+
'table', 'feedbacks',
|
|
1290
|
+
'action', 'insert'
|
|
1291
|
+
)::text);
|
|
1292
|
+
RETURN NEW;
|
|
1293
|
+
END;
|
|
1294
|
+
$$ LANGUAGE plpgsql;
|
|
1295
|
+
|
|
1296
|
+
Environment variables:
|
|
1297
|
+
DB_LISTENER__ENABLED - Enable the listener worker (default: false)
|
|
1298
|
+
DB_LISTENER__CHANNELS - Comma-separated PostgreSQL channels to listen on
|
|
1299
|
+
DB_LISTENER__HANDLER_TYPE - Handler type: 'sqs', 'rest', or 'custom'
|
|
1300
|
+
DB_LISTENER__SQS_QUEUE_URL - SQS queue URL (for handler_type=sqs)
|
|
1301
|
+
DB_LISTENER__REST_ENDPOINT - REST endpoint URL (for handler_type=rest)
|
|
1302
|
+
DB_LISTENER__RECONNECT_DELAY - Initial reconnect delay in seconds
|
|
1303
|
+
DB_LISTENER__MAX_RECONNECT_DELAY - Maximum reconnect delay in seconds
|
|
1304
|
+
|
|
1305
|
+
References:
|
|
1306
|
+
- PostgreSQL NOTIFY: https://www.postgresql.org/docs/current/sql-notify.html
|
|
1307
|
+
- Brandur's Notifier: https://brandur.org/notifier
|
|
1308
|
+
"""
|
|
1309
|
+
|
|
1310
|
+
model_config = SettingsConfigDict(
|
|
1311
|
+
env_prefix="DB_LISTENER__",
|
|
1312
|
+
env_file=".env",
|
|
1313
|
+
env_file_encoding="utf-8",
|
|
1314
|
+
extra="ignore",
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
enabled: bool = Field(
|
|
1318
|
+
default=False,
|
|
1319
|
+
description="Enable the DB Listener worker (disabled by default)",
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
channels: str = Field(
|
|
1323
|
+
default="",
|
|
1324
|
+
description=(
|
|
1325
|
+
"Comma-separated list of PostgreSQL channels to LISTEN on. "
|
|
1326
|
+
"Example: 'feedback_sync,entity_update,user_events'"
|
|
1327
|
+
),
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
handler_type: str = Field(
|
|
1331
|
+
default="rest",
|
|
1332
|
+
description=(
|
|
1333
|
+
"Handler type for dispatching notifications. Options: "
|
|
1334
|
+
"'sqs' (publish to SQS), 'rest' (POST to endpoint), 'custom' (Python handlers)"
|
|
1335
|
+
),
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
sqs_queue_url: str = Field(
|
|
1339
|
+
default="",
|
|
1340
|
+
description="SQS queue URL for handler_type='sqs'",
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
rest_endpoint: str = Field(
|
|
1344
|
+
default="http://localhost:8000/api/v1/internal/events",
|
|
1345
|
+
description=(
|
|
1346
|
+
"REST endpoint URL for handler_type='rest'. "
|
|
1347
|
+
"Receives POST with {channel, payload, source} JSON body."
|
|
1348
|
+
),
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
reconnect_delay: float = Field(
|
|
1352
|
+
default=1.0,
|
|
1353
|
+
description="Initial delay (seconds) between reconnection attempts",
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
max_reconnect_delay: float = Field(
|
|
1357
|
+
default=60.0,
|
|
1358
|
+
description="Maximum delay (seconds) between reconnection attempts (exponential backoff cap)",
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
@property
|
|
1362
|
+
def channel_list(self) -> list[str]:
|
|
1363
|
+
"""Get channels as a list, filtering empty strings."""
|
|
1364
|
+
if not self.channels:
|
|
1365
|
+
return []
|
|
1366
|
+
return [c.strip() for c in self.channels.split(",") if c.strip()]
|
|
1367
|
+
|
|
1368
|
+
|
|
1243
1369
|
class TestSettings(BaseSettings):
|
|
1244
1370
|
"""
|
|
1245
1371
|
Test environment settings.
|
|
@@ -1334,11 +1460,6 @@ class Settings(BaseSettings):
|
|
|
1334
1460
|
description="Root path for reverse proxy (e.g., /rem for ALB routing)",
|
|
1335
1461
|
)
|
|
1336
1462
|
|
|
1337
|
-
sql_dir: str = Field(
|
|
1338
|
-
default="src/rem/sql",
|
|
1339
|
-
description="Directory for SQL files and migrations",
|
|
1340
|
-
)
|
|
1341
|
-
|
|
1342
1463
|
# Nested settings groups
|
|
1343
1464
|
api: APISettings = Field(default_factory=APISettings)
|
|
1344
1465
|
chat: ChatSettings = Field(default_factory=ChatSettings)
|
|
@@ -1354,16 +1475,27 @@ class Settings(BaseSettings):
|
|
|
1354
1475
|
s3: S3Settings = Field(default_factory=S3Settings)
|
|
1355
1476
|
git: GitSettings = Field(default_factory=GitSettings)
|
|
1356
1477
|
sqs: SQSSettings = Field(default_factory=SQSSettings)
|
|
1478
|
+
db_listener: DBListenerSettings = Field(default_factory=DBListenerSettings)
|
|
1357
1479
|
chunking: ChunkingSettings = Field(default_factory=ChunkingSettings)
|
|
1358
1480
|
content: ContentSettings = Field(default_factory=ContentSettings)
|
|
1359
1481
|
schema_search: SchemaSettings = Field(default_factory=SchemaSettings)
|
|
1360
1482
|
test: TestSettings = Field(default_factory=TestSettings)
|
|
1361
1483
|
|
|
1362
1484
|
|
|
1485
|
+
# Auto-load .env file from current directory if it exists
|
|
1486
|
+
# This happens BEFORE config file loading, so .env takes precedence
|
|
1487
|
+
from pathlib import Path
|
|
1488
|
+
from dotenv import load_dotenv
|
|
1489
|
+
|
|
1490
|
+
_dotenv_path = Path(".env")
|
|
1491
|
+
if _dotenv_path.exists():
|
|
1492
|
+
load_dotenv(_dotenv_path, override=False) # Don't override existing env vars
|
|
1493
|
+
logger.debug(f"Loaded environment from {_dotenv_path.resolve()}")
|
|
1494
|
+
|
|
1363
1495
|
# Load configuration from ~/.rem/config.yaml before initializing settings
|
|
1364
1496
|
# This allows user configuration to be merged with environment variables
|
|
1365
|
-
# Set
|
|
1366
|
-
if not os.getenv("
|
|
1497
|
+
# Set REM_SKIP_CONFIG=1 to disable (useful for development with .env)
|
|
1498
|
+
if not os.getenv("REM_SKIP_CONFIG", "").lower() in ("true", "1", "yes"):
|
|
1367
1499
|
try:
|
|
1368
1500
|
from rem.config import load_config, merge_config_to_env
|
|
1369
1501
|
|
|
@@ -604,6 +604,162 @@ CREATE INDEX IF NOT EXISTS idx_rate_limits_expires ON rate_limits (expires_at);
|
|
|
604
604
|
COMMENT ON TABLE rate_limits IS
|
|
605
605
|
'UNLOGGED rate limiting table. Counts may be lost on crash (acceptable for rate limiting).';
|
|
606
606
|
|
|
607
|
+
-- ============================================================================
|
|
608
|
+
-- SHARED SESSIONS HELPER FUNCTIONS
|
|
609
|
+
-- ============================================================================
|
|
610
|
+
-- Note: The shared_sessions TABLE is created by 002_install_models.sql (auto-generated)
|
|
611
|
+
-- These functions provide aggregate queries for the session sharing workflow.
|
|
612
|
+
|
|
613
|
+
-- Count distinct users sharing sessions with the current user
|
|
614
|
+
CREATE OR REPLACE FUNCTION fn_count_shared_with_me(
|
|
615
|
+
p_tenant_id VARCHAR(100),
|
|
616
|
+
p_user_id VARCHAR(256)
|
|
617
|
+
)
|
|
618
|
+
RETURNS BIGINT AS $$
|
|
619
|
+
BEGIN
|
|
620
|
+
RETURN (
|
|
621
|
+
SELECT COUNT(DISTINCT owner_user_id)
|
|
622
|
+
FROM shared_sessions
|
|
623
|
+
WHERE tenant_id = p_tenant_id
|
|
624
|
+
AND shared_with_user_id = p_user_id
|
|
625
|
+
AND deleted_at IS NULL
|
|
626
|
+
);
|
|
627
|
+
END;
|
|
628
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
629
|
+
|
|
630
|
+
COMMENT ON FUNCTION fn_count_shared_with_me IS
|
|
631
|
+
'Count distinct users sharing sessions with the specified user.';
|
|
632
|
+
|
|
633
|
+
-- Get aggregated summary of users sharing sessions with current user
|
|
634
|
+
CREATE OR REPLACE FUNCTION fn_get_shared_with_me(
|
|
635
|
+
p_tenant_id VARCHAR(100),
|
|
636
|
+
p_user_id VARCHAR(256),
|
|
637
|
+
p_limit INTEGER DEFAULT 50,
|
|
638
|
+
p_offset INTEGER DEFAULT 0
|
|
639
|
+
)
|
|
640
|
+
RETURNS TABLE(
|
|
641
|
+
user_id VARCHAR(256),
|
|
642
|
+
name VARCHAR(256),
|
|
643
|
+
email VARCHAR(256),
|
|
644
|
+
session_count BIGINT,
|
|
645
|
+
message_count BIGINT,
|
|
646
|
+
first_message_at TIMESTAMP,
|
|
647
|
+
last_message_at TIMESTAMP
|
|
648
|
+
) AS $$
|
|
649
|
+
BEGIN
|
|
650
|
+
RETURN QUERY
|
|
651
|
+
SELECT
|
|
652
|
+
ss.owner_user_id AS user_id,
|
|
653
|
+
COALESCE(u.name, ss.owner_user_id) AS name,
|
|
654
|
+
u.email AS email,
|
|
655
|
+
COUNT(DISTINCT ss.session_id)::BIGINT AS session_count,
|
|
656
|
+
COALESCE(SUM(msg_counts.msg_count), 0)::BIGINT AS message_count,
|
|
657
|
+
MIN(msg_counts.first_msg)::TIMESTAMP AS first_message_at,
|
|
658
|
+
MAX(msg_counts.last_msg)::TIMESTAMP AS last_message_at
|
|
659
|
+
FROM shared_sessions ss
|
|
660
|
+
LEFT JOIN users u ON u.user_id = ss.owner_user_id AND u.tenant_id = ss.tenant_id
|
|
661
|
+
LEFT JOIN (
|
|
662
|
+
SELECT
|
|
663
|
+
m.session_id,
|
|
664
|
+
m.user_id,
|
|
665
|
+
COUNT(*)::BIGINT AS msg_count,
|
|
666
|
+
MIN(m.created_at) AS first_msg,
|
|
667
|
+
MAX(m.created_at) AS last_msg
|
|
668
|
+
FROM messages m
|
|
669
|
+
WHERE m.tenant_id = p_tenant_id
|
|
670
|
+
AND m.deleted_at IS NULL
|
|
671
|
+
GROUP BY m.session_id, m.user_id
|
|
672
|
+
) msg_counts ON msg_counts.session_id = ss.session_id AND msg_counts.user_id = ss.owner_user_id
|
|
673
|
+
WHERE ss.tenant_id = p_tenant_id
|
|
674
|
+
AND ss.shared_with_user_id = p_user_id
|
|
675
|
+
AND ss.deleted_at IS NULL
|
|
676
|
+
GROUP BY ss.owner_user_id, u.name, u.email
|
|
677
|
+
ORDER BY MAX(msg_counts.last_msg) DESC NULLS LAST
|
|
678
|
+
LIMIT p_limit
|
|
679
|
+
OFFSET p_offset;
|
|
680
|
+
END;
|
|
681
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
682
|
+
|
|
683
|
+
COMMENT ON FUNCTION fn_get_shared_with_me IS
|
|
684
|
+
'Get aggregated summary of users sharing sessions with the specified user.';
|
|
685
|
+
|
|
686
|
+
-- Count messages in sessions shared by a specific user
|
|
687
|
+
CREATE OR REPLACE FUNCTION fn_count_shared_messages(
|
|
688
|
+
p_tenant_id VARCHAR(100),
|
|
689
|
+
p_recipient_user_id VARCHAR(256),
|
|
690
|
+
p_owner_user_id VARCHAR(256)
|
|
691
|
+
)
|
|
692
|
+
RETURNS BIGINT AS $$
|
|
693
|
+
BEGIN
|
|
694
|
+
RETURN (
|
|
695
|
+
SELECT COUNT(*)
|
|
696
|
+
FROM messages m
|
|
697
|
+
WHERE m.tenant_id = p_tenant_id
|
|
698
|
+
AND m.deleted_at IS NULL
|
|
699
|
+
AND m.session_id IN (
|
|
700
|
+
SELECT ss.session_id
|
|
701
|
+
FROM shared_sessions ss
|
|
702
|
+
WHERE ss.tenant_id = p_tenant_id
|
|
703
|
+
AND ss.owner_user_id = p_owner_user_id
|
|
704
|
+
AND ss.shared_with_user_id = p_recipient_user_id
|
|
705
|
+
AND ss.deleted_at IS NULL
|
|
706
|
+
)
|
|
707
|
+
);
|
|
708
|
+
END;
|
|
709
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
710
|
+
|
|
711
|
+
COMMENT ON FUNCTION fn_count_shared_messages IS
|
|
712
|
+
'Count messages in sessions shared by a specific user with the recipient.';
|
|
713
|
+
|
|
714
|
+
-- Get messages from sessions shared by a specific user
|
|
715
|
+
CREATE OR REPLACE FUNCTION fn_get_shared_messages(
|
|
716
|
+
p_tenant_id VARCHAR(100),
|
|
717
|
+
p_recipient_user_id VARCHAR(256),
|
|
718
|
+
p_owner_user_id VARCHAR(256),
|
|
719
|
+
p_limit INTEGER DEFAULT 50,
|
|
720
|
+
p_offset INTEGER DEFAULT 0
|
|
721
|
+
)
|
|
722
|
+
RETURNS TABLE(
|
|
723
|
+
id UUID,
|
|
724
|
+
content TEXT,
|
|
725
|
+
message_type VARCHAR(256),
|
|
726
|
+
session_id VARCHAR(256),
|
|
727
|
+
model VARCHAR(256),
|
|
728
|
+
token_count INTEGER,
|
|
729
|
+
created_at TIMESTAMP,
|
|
730
|
+
metadata JSONB
|
|
731
|
+
) AS $$
|
|
732
|
+
BEGIN
|
|
733
|
+
RETURN QUERY
|
|
734
|
+
SELECT
|
|
735
|
+
m.id,
|
|
736
|
+
m.content,
|
|
737
|
+
m.message_type,
|
|
738
|
+
m.session_id,
|
|
739
|
+
m.model,
|
|
740
|
+
m.token_count,
|
|
741
|
+
m.created_at,
|
|
742
|
+
m.metadata
|
|
743
|
+
FROM messages m
|
|
744
|
+
WHERE m.tenant_id = p_tenant_id
|
|
745
|
+
AND m.deleted_at IS NULL
|
|
746
|
+
AND m.session_id IN (
|
|
747
|
+
SELECT ss.session_id
|
|
748
|
+
FROM shared_sessions ss
|
|
749
|
+
WHERE ss.tenant_id = p_tenant_id
|
|
750
|
+
AND ss.owner_user_id = p_owner_user_id
|
|
751
|
+
AND ss.shared_with_user_id = p_recipient_user_id
|
|
752
|
+
AND ss.deleted_at IS NULL
|
|
753
|
+
)
|
|
754
|
+
ORDER BY m.created_at DESC
|
|
755
|
+
LIMIT p_limit
|
|
756
|
+
OFFSET p_offset;
|
|
757
|
+
END;
|
|
758
|
+
$$ LANGUAGE plpgsql STABLE;
|
|
759
|
+
|
|
760
|
+
COMMENT ON FUNCTION fn_get_shared_messages IS
|
|
761
|
+
'Get messages from sessions shared by a specific user with the recipient.';
|
|
762
|
+
|
|
607
763
|
-- ============================================================================
|
|
608
764
|
-- RECORD INSTALLATION
|
|
609
765
|
-- ============================================================================
|