remdb 0.2.6__py3-none-any.whl → 0.3.118__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.

Files changed (104) hide show
  1. rem/__init__.py +129 -2
  2. rem/agentic/README.md +76 -0
  3. rem/agentic/__init__.py +15 -0
  4. rem/agentic/agents/__init__.py +16 -2
  5. rem/agentic/agents/sse_simulator.py +500 -0
  6. rem/agentic/context.py +28 -22
  7. rem/agentic/llm_provider_models.py +301 -0
  8. rem/agentic/mcp/tool_wrapper.py +29 -3
  9. rem/agentic/otel/setup.py +92 -4
  10. rem/agentic/providers/phoenix.py +32 -43
  11. rem/agentic/providers/pydantic_ai.py +168 -24
  12. rem/agentic/schema.py +358 -21
  13. rem/agentic/tools/rem_tools.py +3 -3
  14. rem/api/README.md +238 -1
  15. rem/api/deps.py +255 -0
  16. rem/api/main.py +154 -37
  17. rem/api/mcp_router/resources.py +1 -1
  18. rem/api/mcp_router/server.py +26 -5
  19. rem/api/mcp_router/tools.py +454 -7
  20. rem/api/middleware/tracking.py +172 -0
  21. rem/api/routers/admin.py +494 -0
  22. rem/api/routers/auth.py +124 -0
  23. rem/api/routers/chat/completions.py +152 -16
  24. rem/api/routers/chat/models.py +7 -3
  25. rem/api/routers/chat/sse_events.py +526 -0
  26. rem/api/routers/chat/streaming.py +608 -45
  27. rem/api/routers/dev.py +81 -0
  28. rem/api/routers/feedback.py +148 -0
  29. rem/api/routers/messages.py +473 -0
  30. rem/api/routers/models.py +78 -0
  31. rem/api/routers/query.py +360 -0
  32. rem/api/routers/shared_sessions.py +406 -0
  33. rem/auth/middleware.py +126 -27
  34. rem/cli/commands/README.md +237 -64
  35. rem/cli/commands/ask.py +15 -11
  36. rem/cli/commands/cluster.py +1300 -0
  37. rem/cli/commands/configure.py +170 -97
  38. rem/cli/commands/db.py +396 -139
  39. rem/cli/commands/experiments.py +278 -96
  40. rem/cli/commands/process.py +22 -15
  41. rem/cli/commands/scaffold.py +47 -0
  42. rem/cli/commands/schema.py +97 -50
  43. rem/cli/main.py +37 -6
  44. rem/config.py +2 -2
  45. rem/models/core/core_model.py +7 -1
  46. rem/models/core/rem_query.py +5 -2
  47. rem/models/entities/__init__.py +21 -0
  48. rem/models/entities/domain_resource.py +38 -0
  49. rem/models/entities/feedback.py +123 -0
  50. rem/models/entities/message.py +30 -1
  51. rem/models/entities/session.py +83 -0
  52. rem/models/entities/shared_session.py +180 -0
  53. rem/models/entities/user.py +10 -3
  54. rem/registry.py +373 -0
  55. rem/schemas/agents/rem.yaml +7 -3
  56. rem/services/content/providers.py +94 -140
  57. rem/services/content/service.py +115 -24
  58. rem/services/dreaming/affinity_service.py +2 -16
  59. rem/services/dreaming/moment_service.py +2 -15
  60. rem/services/embeddings/api.py +24 -17
  61. rem/services/embeddings/worker.py +16 -16
  62. rem/services/phoenix/EXPERIMENT_DESIGN.md +3 -3
  63. rem/services/phoenix/client.py +252 -19
  64. rem/services/postgres/README.md +159 -15
  65. rem/services/postgres/__init__.py +2 -1
  66. rem/services/postgres/diff_service.py +531 -0
  67. rem/services/postgres/pydantic_to_sqlalchemy.py +427 -129
  68. rem/services/postgres/repository.py +132 -0
  69. rem/services/postgres/schema_generator.py +291 -9
  70. rem/services/postgres/service.py +6 -6
  71. rem/services/rate_limit.py +113 -0
  72. rem/services/rem/README.md +14 -0
  73. rem/services/rem/parser.py +44 -9
  74. rem/services/rem/service.py +36 -2
  75. rem/services/session/compression.py +17 -1
  76. rem/services/session/reload.py +1 -1
  77. rem/services/user_service.py +98 -0
  78. rem/settings.py +169 -22
  79. rem/sql/background_indexes.sql +21 -16
  80. rem/sql/migrations/001_install.sql +387 -54
  81. rem/sql/migrations/002_install_models.sql +2320 -393
  82. rem/sql/migrations/003_optional_extensions.sql +326 -0
  83. rem/sql/migrations/004_cache_system.sql +548 -0
  84. rem/utils/__init__.py +18 -0
  85. rem/utils/constants.py +97 -0
  86. rem/utils/date_utils.py +228 -0
  87. rem/utils/embeddings.py +17 -4
  88. rem/utils/files.py +167 -0
  89. rem/utils/mime_types.py +158 -0
  90. rem/utils/model_helpers.py +156 -1
  91. rem/utils/schema_loader.py +284 -21
  92. rem/utils/sql_paths.py +146 -0
  93. rem/utils/sql_types.py +3 -1
  94. rem/utils/vision.py +9 -14
  95. rem/workers/README.md +14 -14
  96. rem/workers/__init__.py +2 -1
  97. rem/workers/db_maintainer.py +74 -0
  98. rem/workers/unlogged_maintainer.py +463 -0
  99. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/METADATA +598 -171
  100. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/RECORD +102 -73
  101. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/WHEEL +1 -1
  102. rem/sql/002_install_models.sql +0 -1068
  103. rem/sql/install_models.sql +0 -1038
  104. {remdb-0.2.6.dist-info → remdb-0.3.118.dist-info}/entry_points.txt +0 -0
@@ -218,6 +218,7 @@ COMMENT ON FUNCTION rebuild_kv_store() IS
218
218
  -- Returns structured columns extracted from entity records
219
219
  -- Parameters: entity_key, tenant_id (for backward compat), user_id (actual filter)
220
220
  -- Note: tenant_id parameter exists for backward compatibility but is ignored
221
+ -- Note: Includes user-owned AND public (NULL user_id) resources
221
222
  CREATE OR REPLACE FUNCTION rem_lookup(
222
223
  p_entity_key VARCHAR(255),
223
224
  p_tenant_id VARCHAR(100),
@@ -230,13 +231,17 @@ RETURNS TABLE(
230
231
  DECLARE
231
232
  entity_table VARCHAR(100);
232
233
  query_sql TEXT;
234
+ effective_user_id VARCHAR(100);
233
235
  BEGIN
234
- -- Use p_user_id for filtering (p_tenant_id ignored for backward compat)
236
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
237
+
235
238
  -- First lookup in KV store to get entity_type (table name)
239
+ -- Include user-owned AND public (NULL user_id) entries
236
240
  SELECT kv.entity_type INTO entity_table
237
241
  FROM kv_store kv
238
- WHERE kv.user_id = p_user_id
239
- AND kv.entity_key = p_entity_key;
242
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
243
+ AND kv.entity_key = p_entity_key
244
+ LIMIT 1;
240
245
 
241
246
  -- If not found, return empty
242
247
  IF entity_table IS NULL THEN
@@ -250,73 +255,63 @@ BEGIN
250
255
  %L::VARCHAR(100) AS entity_type,
251
256
  row_to_json(t)::jsonb AS data
252
257
  FROM %I t
253
- WHERE t.user_id = $1
258
+ WHERE (t.user_id = $1 OR t.user_id IS NULL)
254
259
  AND t.name = $2
255
260
  AND t.deleted_at IS NULL
256
261
  ', entity_table, entity_table);
257
262
 
258
- RETURN QUERY EXECUTE query_sql USING p_user_id, p_entity_key;
263
+ RETURN QUERY EXECUTE query_sql USING effective_user_id, p_entity_key;
259
264
  END;
260
265
  $$ LANGUAGE plpgsql STABLE;
261
266
 
262
267
  COMMENT ON FUNCTION rem_lookup IS
263
- 'REM LOOKUP query: O(1) entity lookup by natural key. Returns raw entity data as JSONB for LLM consumption. tenant_id parameter exists for backward compatibility but filtering uses user_id.';
268
+ 'REM LOOKUP: O(1) entity lookup. Returns user-owned AND public (NULL user_id) entities.';
264
269
 
265
270
  -- REM FETCH: Fetch full entity records from multiple tables
266
271
  -- Takes JSONB mapping of {table_name: [entity_keys]}, fetches all records
267
272
  -- Returns complete entity records as JSONB (not just KV store metadata)
273
+ -- Note: Includes user-owned AND public (NULL user_id) resources
268
274
  CREATE OR REPLACE FUNCTION rem_fetch(
269
275
  p_entities_by_table JSONB,
270
276
  p_user_id VARCHAR(100)
271
277
  )
272
278
  RETURNS TABLE(
273
- entity_key TEXT,
274
- entity_type TEXT,
279
+ entity_key VARCHAR(255),
280
+ entity_type VARCHAR(100),
275
281
  entity_record JSONB
276
282
  ) AS $$
277
283
  DECLARE
278
284
  table_name TEXT;
279
- entity_keys TEXT[];
285
+ entity_keys JSONB;
280
286
  query_sql TEXT;
281
- result_record RECORD;
282
287
  BEGIN
283
- -- Iterate over each table in the JSONB object
284
- FOR table_name IN SELECT jsonb_object_keys(p_entities_by_table)
288
+ -- For each table in the input JSONB
289
+ FOR table_name, entity_keys IN SELECT * FROM jsonb_each(p_entities_by_table)
285
290
  LOOP
286
- -- Extract array of keys for this table
287
- entity_keys := ARRAY(
288
- SELECT jsonb_array_elements_text(p_entities_by_table->table_name)
289
- );
290
-
291
- -- Build dynamic query for this table
291
+ -- Dynamic query to fetch records from the table
292
+ -- Include user-owned AND public (NULL user_id)
292
293
  query_sql := format('
293
294
  SELECT
294
- t.name AS entity_key,
295
- %L AS entity_type,
295
+ t.name::VARCHAR(255) AS entity_key,
296
+ %L::VARCHAR(100) AS entity_type,
296
297
  row_to_json(t)::jsonb AS entity_record
297
298
  FROM %I t
298
- WHERE t.user_id = $1
299
- AND t.name = ANY($2)
299
+ WHERE t.name = ANY(SELECT jsonb_array_elements_text($1))
300
+ AND (t.user_id = $2 OR t.user_id IS NULL)
300
301
  AND t.deleted_at IS NULL
301
302
  ', table_name, table_name);
302
303
 
303
- -- Execute and return rows for this table
304
- FOR result_record IN EXECUTE query_sql USING p_user_id, entity_keys
305
- LOOP
306
- entity_key := result_record.entity_key;
307
- entity_type := result_record.entity_type;
308
- entity_record := result_record.entity_record;
309
- RETURN NEXT;
310
- END LOOP;
304
+ RETURN QUERY EXECUTE query_sql USING entity_keys, p_user_id;
311
305
  END LOOP;
312
306
  END;
313
307
  $$ LANGUAGE plpgsql STABLE;
314
308
 
315
309
  COMMENT ON FUNCTION rem_fetch IS
316
- 'REM FETCH: Fetch full entity records (all columns as JSONB) from multiple tables. Takes JSONB mapping {table_name: [keys]}, fetches all records, returns unified result set. Use for hydrating LOOKUP, FUZZY, SEARCH, and TRAVERSE results.';
310
+ 'REM FETCH: Batch fetch entities. Returns user-owned AND public (NULL user_id) entities.';
317
311
 
318
312
  -- REM FUZZY: Fuzzy text search using pg_trgm similarity
319
313
  -- Returns raw entity data as JSONB for LLM consumption
314
+ -- Note: Includes user-owned AND public (NULL user_id) resources
320
315
  CREATE OR REPLACE FUNCTION rem_fuzzy(
321
316
  p_query TEXT,
322
317
  p_tenant_id VARCHAR(100),
@@ -333,16 +328,18 @@ DECLARE
333
328
  kv_matches RECORD;
334
329
  entities_by_table JSONB := '{}'::jsonb;
335
330
  table_keys JSONB;
331
+ effective_user_id VARCHAR(100);
336
332
  BEGIN
337
- -- First, find matching keys in KV store with similarity scores
338
- -- Group by table to prepare for batch fetch
333
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
334
+
335
+ -- Find matching keys in KV store (user-owned AND public)
339
336
  FOR kv_matches IN
340
337
  SELECT
341
338
  kv.entity_key,
342
339
  kv.entity_type,
343
340
  similarity(kv.entity_key, p_query) AS sim_score
344
341
  FROM kv_store kv
345
- WHERE kv.user_id = COALESCE(p_user_id, p_tenant_id)
342
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
346
343
  AND kv.entity_key % p_query -- Trigram similarity operator
347
344
  AND similarity(kv.entity_key, p_query) >= p_threshold
348
345
  ORDER BY sim_score DESC
@@ -365,29 +362,155 @@ BEGIN
365
362
  END IF;
366
363
  END LOOP;
367
364
 
368
- -- Fetch full records using rem_fetch helper
369
- -- Return raw entity data as JSONB for LLM consumption
370
- -- Use p_user_id (not p_tenant_id) for actual filtering
365
+ -- Fetch full records using rem_fetch (which now supports NULL user_id)
371
366
  RETURN QUERY
372
367
  SELECT
373
368
  f.entity_type::VARCHAR(100),
374
369
  similarity(f.entity_key, p_query) AS similarity_score,
375
370
  f.entity_record AS data
376
- FROM rem_fetch(entities_by_table, COALESCE(p_user_id, p_tenant_id)) f
371
+ FROM rem_fetch(entities_by_table, effective_user_id) f
377
372
  ORDER BY similarity_score DESC;
378
373
  END;
379
374
  $$ LANGUAGE plpgsql STABLE;
380
375
 
381
376
  COMMENT ON FUNCTION rem_fuzzy IS
382
- 'REM FUZZY query: Fuzzy text search using pg_trgm. Returns raw entity data as JSONB for LLM consumption. tenant_id parameter exists for backward compatibility but filtering uses user_id.';
377
+ 'REM FUZZY: Fuzzy text search. Returns user-owned AND public (NULL user_id) entities.';
378
+
379
+ -- ============================================================================
380
+ -- REM TRAVERSE (Graph Traversal)
381
+ -- ============================================================================
382
+
383
+ -- REM TRAVERSE: Recursive graph traversal following edges
384
+ -- Explores graph_edges starting from entity_key up to max_depth
385
+ -- Uses cached kv_store.graph_edges for fast traversal (no polymorphic view!)
386
+ -- When keys_only=false, automatically fetches full entity records
387
+ -- Note: Includes user-owned AND public (NULL user_id) resources
388
+ CREATE OR REPLACE FUNCTION rem_traverse(
389
+ p_entity_key VARCHAR(255),
390
+ p_tenant_id VARCHAR(100), -- Backward compat parameter (not used for filtering)
391
+ p_user_id VARCHAR(100),
392
+ p_max_depth INTEGER DEFAULT 1,
393
+ p_rel_type VARCHAR(100) DEFAULT NULL,
394
+ p_keys_only BOOLEAN DEFAULT FALSE
395
+ )
396
+ RETURNS TABLE(
397
+ depth INTEGER,
398
+ entity_key VARCHAR(255),
399
+ entity_type VARCHAR(100),
400
+ entity_id UUID,
401
+ rel_type VARCHAR(100),
402
+ rel_weight REAL,
403
+ path TEXT[],
404
+ entity_record JSONB
405
+ ) AS $$
406
+ DECLARE
407
+ graph_keys RECORD;
408
+ entities_by_table JSONB := '{}'::jsonb;
409
+ table_keys JSONB;
410
+ effective_user_id VARCHAR(100);
411
+ BEGIN
412
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
413
+
414
+ FOR graph_keys IN
415
+ WITH RECURSIVE graph_traversal AS (
416
+ -- Base case: Find starting entity (user-owned OR public)
417
+ SELECT
418
+ 0 AS depth,
419
+ kv.entity_key,
420
+ kv.entity_type,
421
+ kv.entity_id,
422
+ NULL::VARCHAR(100) AS rel_type,
423
+ NULL::REAL AS rel_weight,
424
+ ARRAY[kv.entity_key]::TEXT[] AS path
425
+ FROM kv_store kv
426
+ WHERE (kv.user_id = effective_user_id OR kv.user_id IS NULL)
427
+ AND kv.entity_key = p_entity_key
428
+
429
+ UNION ALL
430
+
431
+ -- Recursive case: Follow outbound edges
432
+ SELECT
433
+ gt.depth + 1,
434
+ target_kv.entity_key,
435
+ target_kv.entity_type,
436
+ target_kv.entity_id,
437
+ (edge->>'rel_type')::VARCHAR(100) AS rel_type,
438
+ COALESCE((edge->>'weight')::REAL, 1.0) AS rel_weight,
439
+ gt.path || target_kv.entity_key AS path
440
+ FROM graph_traversal gt
441
+ JOIN kv_store source_kv ON source_kv.entity_key = gt.entity_key
442
+ AND (source_kv.user_id = effective_user_id OR source_kv.user_id IS NULL)
443
+ CROSS JOIN LATERAL jsonb_array_elements(COALESCE(source_kv.graph_edges, '[]'::jsonb)) AS edge
444
+ JOIN kv_store target_kv ON target_kv.entity_key = (edge->>'dst')::VARCHAR(255)
445
+ AND (target_kv.user_id = effective_user_id OR target_kv.user_id IS NULL)
446
+ WHERE gt.depth < p_max_depth
447
+ AND (p_rel_type IS NULL OR (edge->>'rel_type')::VARCHAR(100) = p_rel_type)
448
+ AND NOT (target_kv.entity_key = ANY(gt.path))
449
+ )
450
+ SELECT DISTINCT ON (entity_key)
451
+ gt.depth,
452
+ gt.entity_key,
453
+ gt.entity_type,
454
+ gt.entity_id,
455
+ gt.rel_type,
456
+ gt.rel_weight,
457
+ gt.path
458
+ FROM graph_traversal gt
459
+ WHERE gt.depth > 0
460
+ ORDER BY gt.entity_key, gt.depth
461
+ LOOP
462
+ IF p_keys_only THEN
463
+ depth := graph_keys.depth;
464
+ entity_key := graph_keys.entity_key;
465
+ entity_type := graph_keys.entity_type;
466
+ entity_id := graph_keys.entity_id;
467
+ rel_type := graph_keys.rel_type;
468
+ rel_weight := graph_keys.rel_weight;
469
+ path := graph_keys.path;
470
+ entity_record := NULL;
471
+ RETURN NEXT;
472
+ ELSE
473
+ IF entities_by_table ? graph_keys.entity_type THEN
474
+ table_keys := entities_by_table->graph_keys.entity_type;
475
+ entities_by_table := jsonb_set(
476
+ entities_by_table,
477
+ ARRAY[graph_keys.entity_type],
478
+ table_keys || jsonb_build_array(graph_keys.entity_key)
479
+ );
480
+ ELSE
481
+ entities_by_table := jsonb_set(
482
+ entities_by_table,
483
+ ARRAY[graph_keys.entity_type],
484
+ jsonb_build_array(graph_keys.entity_key)
485
+ );
486
+ END IF;
487
+ END IF;
488
+ END LOOP;
383
489
 
384
- -- REM TRAVERSE: Moved to 002_install_models.sql (after entity tables are created)
385
- -- See 002_install_models.sql for the full rem_traverse function with keys_only parameter
490
+ IF NOT p_keys_only AND entities_by_table != '{}'::jsonb THEN
491
+ RETURN QUERY
492
+ SELECT
493
+ NULL::INTEGER AS depth,
494
+ f.entity_key::VARCHAR(255),
495
+ f.entity_type::VARCHAR(100),
496
+ NULL::UUID AS entity_id,
497
+ NULL::VARCHAR(100) AS rel_type,
498
+ NULL::REAL AS rel_weight,
499
+ NULL::TEXT[] AS path,
500
+ f.entity_record
501
+ FROM rem_fetch(entities_by_table, effective_user_id) f;
502
+ END IF;
503
+ END;
504
+ $$ LANGUAGE plpgsql STABLE;
505
+
506
+ COMMENT ON FUNCTION rem_traverse IS
507
+ 'REM TRAVERSE: Graph traversal. Returns user-owned AND public (NULL user_id) entities.';
386
508
 
387
509
  -- REM SEARCH: Vector similarity search using embeddings
388
510
  -- Joins to embeddings table for semantic search
511
+ -- Note: Includes user-owned AND public (NULL user_id) resources
389
512
  CREATE OR REPLACE FUNCTION rem_search(
390
- p_query_embedding vector(1536),
513
+ p_query_embedding vector,
391
514
  p_table_name VARCHAR(100),
392
515
  p_field_name VARCHAR(100),
393
516
  p_tenant_id VARCHAR(100),
@@ -405,39 +528,38 @@ DECLARE
405
528
  embeddings_table VARCHAR(200);
406
529
  source_table VARCHAR(100);
407
530
  query_sql TEXT;
531
+ effective_user_id VARCHAR(100);
408
532
  BEGIN
409
- -- Construct embeddings table name
410
533
  embeddings_table := 'embeddings_' || p_table_name;
411
534
  source_table := p_table_name;
535
+ effective_user_id := COALESCE(p_user_id, p_tenant_id);
412
536
 
413
- -- Dynamic query to join source table with embeddings table
414
- -- Returns raw entity data as JSONB for LLM consumption
415
- -- Note: Using inner product for OpenAI embeddings (normalized vectors)
416
- -- Inner product <#> returns negative value, so we negate it to get [0, 1]
417
- -- where 1 = perfect match, 0 = orthogonal
537
+ -- Uses cosine distance <=> operator (0-2 range, 0=identical)
538
+ -- Similarity = 1 - distance gives 0-1 range where 1 = most similar
539
+ -- Includes user-owned AND public (NULL user_id) resources
418
540
  query_sql := format('
419
541
  SELECT
420
542
  %L::VARCHAR(100) AS entity_type,
421
- (1.0 - (e.embedding <#> $1) * -1.0)::REAL AS similarity_score,
543
+ (1.0 - (e.embedding <=> $1))::REAL AS similarity_score,
422
544
  row_to_json(t)::jsonb AS data
423
545
  FROM %I t
424
546
  JOIN %I e ON e.entity_id = t.id
425
- WHERE t.user_id = $2
547
+ WHERE (t.user_id = $2 OR t.user_id IS NULL)
426
548
  AND e.field_name = $3
427
549
  AND e.provider = $4
428
- AND (1.0 - (e.embedding <#> $1) * -1.0) >= $5
550
+ AND (1.0 - (e.embedding <=> $1)) >= $5
429
551
  AND t.deleted_at IS NULL
430
- ORDER BY e.embedding <#> $1 DESC
552
+ ORDER BY e.embedding <=> $1
431
553
  LIMIT $6
432
554
  ', source_table, source_table, embeddings_table);
433
555
 
434
556
  RETURN QUERY EXECUTE query_sql
435
- USING p_query_embedding, p_user_id, p_field_name, p_provider, p_min_similarity, p_limit;
557
+ USING p_query_embedding, effective_user_id, p_field_name, p_provider, p_min_similarity, p_limit;
436
558
  END;
437
559
  $$ LANGUAGE plpgsql STABLE;
438
560
 
439
561
  COMMENT ON FUNCTION rem_search IS
440
- 'REM SEARCH query: Vector similarity search using inner product for OpenAI normalized embeddings. Returns raw entity data as JSONB for LLM consumption, scoped to user_id';
562
+ 'REM SEARCH: Vector similarity search. Returns user-owned AND public (NULL user_id) resources.';
441
563
 
442
564
  -- Function to get migration status
443
565
  CREATE OR REPLACE FUNCTION migration_status()
@@ -464,6 +586,180 @@ $$ LANGUAGE plpgsql;
464
586
  COMMENT ON FUNCTION migration_status() IS
465
587
  'Get summary of applied migrations by type';
466
588
 
589
+ -- ============================================================================
590
+ -- RATE LIMITS (UNLOGGED for performance)
591
+ -- ============================================================================
592
+ -- High-performance rate limiting table. Uses UNLOGGED for speed - counts may
593
+ -- be lost on database crash/restart, which is acceptable (fail-open on error).
594
+
595
+ CREATE UNLOGGED TABLE IF NOT EXISTS rate_limits (
596
+ key VARCHAR(512) PRIMARY KEY,
597
+ count INTEGER NOT NULL DEFAULT 1,
598
+ expires_at TIMESTAMP NOT NULL,
599
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
600
+ );
601
+
602
+ CREATE INDEX IF NOT EXISTS idx_rate_limits_expires ON rate_limits (expires_at);
603
+
604
+ COMMENT ON TABLE rate_limits IS
605
+ 'UNLOGGED rate limiting table. Counts may be lost on crash (acceptable for rate limiting).';
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
+
467
763
  -- ============================================================================
468
764
  -- RECORD INSTALLATION
469
765
  -- ============================================================================
@@ -474,6 +770,43 @@ ON CONFLICT (name) DO UPDATE
474
770
  SET applied_at = CURRENT_TIMESTAMP,
475
771
  applied_by = CURRENT_USER;
476
772
 
773
+ -- ============================================================================
774
+ -- GRANTS FOR APPLICATION USER
775
+ -- ============================================================================
776
+ -- Grant permissions to remuser (the application database user)
777
+ -- This ensures the application can run migrations and manage schema
778
+ -- Note: remuser is created by CNPG as the database owner in bootstrap.initdb.owner
779
+
780
+ DO $$
781
+ DECLARE
782
+ app_user TEXT := 'remuser';
783
+ BEGIN
784
+ -- Only grant if the user exists (handles different deployment scenarios)
785
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = app_user) THEN
786
+ -- Grant ownership of migration tracking table so app can record migrations
787
+ EXECUTE format('ALTER TABLE rem_migrations OWNER TO %I', app_user);
788
+ EXECUTE format('ALTER TABLE kv_store OWNER TO %I', app_user);
789
+ EXECUTE format('ALTER TABLE rate_limits OWNER TO %I', app_user);
790
+
791
+ -- Grant usage on schema
792
+ EXECUTE format('GRANT ALL ON SCHEMA public TO %I', app_user);
793
+
794
+ -- Grant privileges on all tables in public schema
795
+ EXECUTE format('GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO %I', app_user);
796
+ EXECUTE format('GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO %I', app_user);
797
+ EXECUTE format('GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO %I', app_user);
798
+
799
+ -- Set default privileges for future objects
800
+ EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO %I', app_user);
801
+ EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO %I', app_user);
802
+ EXECUTE format('ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO %I', app_user);
803
+
804
+ RAISE NOTICE '✓ Granted permissions to application user: %', app_user;
805
+ ELSE
806
+ RAISE NOTICE 'Application user % does not exist, skipping grants', app_user;
807
+ END IF;
808
+ END $$;
809
+
477
810
  -- ============================================================================
478
811
  -- COMPLETION
479
812
  -- ============================================================================