From 4b50c3929ace1faaa52a9ff62605df9de9c1f3e3 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 10:32:24 +0200 Subject: [PATCH 01/12] feat: add support for postgres schema selection Add support for selecting a PostgreSQL schema instead of always using 'public'. The schema is extracted from the connection URL's options parameter (search_path), following PostgreSQL's native libpq format. Changes: - Add _parse_schema_from_url() to extract schema from connection URL - Thread schema parameter through all extraction methods with 'public' default - Add pg_namespace JOINs for correct cross-schema disambiguation - Add schema input field in DatabaseModal (PostgreSQL only) - Add comprehensive unit tests for URL schema parsing - Update documentation with custom schema configuration guide Based on PR #373 by sirudog with the following fixes: - Fix pg_namespace JOIN order in extract_columns_info to prevent duplicate rows when same-named tables exist across schemas - Fix regex to require '=' separator (prevents mis-capture edge cases) - Improve $user handling to loop through all schemas instead of only checking first two positions - Fix pylint line-too-long in test file Co-authored-by: sirudog <1550561+sirudog@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/wordlist.txt | 1 + README.md | 1 + api/loaders/postgres_loader.py | 95 ++++++++++++++++----- app/src/components/modals/DatabaseModal.tsx | 28 ++++++ docs/postgres_loader.md | 54 +++++++++++- tests/test_postgres_loader.py | 89 +++++++++++++++++++ 6 files changed, 246 insertions(+), 22 deletions(-) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 8c21b746..929ab0f9 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -8,6 +8,7 @@ schemas psycopg html PostgreSQLLoader +PostgresLoader api postgres postgresql diff --git a/README.md b/README.md index b07c0935..6d0f1a49 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,7 @@ with requests.post(url, headers=headers, json={"chat": ["Count orders last week" continue obj = json.loads(part) print('STREAM:', obj) +``` Notes & tips - Graph IDs are namespaced per-user. When calling the API directly use the plain graph id (the server will namespace by the authenticated user). For uploaded files the `database` field determines the saved graph id. diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index be0be497..9fdff140 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -5,6 +5,7 @@ import decimal import logging from typing import AsyncGenerator, Dict, Any, List, Tuple +from urllib.parse import urlparse, parse_qs, unquote import psycopg2 from psycopg2 import sql @@ -96,6 +97,47 @@ def _serialize_value(value): return None return value + @staticmethod + def _parse_schema_from_url(connection_url: str) -> str: + """ + Parse the search_path from the connection URL's options parameter. + + The options parameter follows PostgreSQL's libpq format: + postgresql://user:pass@host:port/db?options=-csearch_path%3Dschema_name + + Args: + connection_url: PostgreSQL connection URL + + Returns: + The first schema from search_path, or 'public' if not specified + """ + try: + parsed = urlparse(connection_url) + query_params = parse_qs(parsed.query) + + options = query_params.get('options', []) + if not options: + return 'public' + + options_str = unquote(options[0]) + + # Parse -c search_path=value from options + # Format can be: -csearch_path=schema or -c search_path=schema + match = re.search(r'-c\s*search_path\s*=\s*([^\s]+)', options_str, re.IGNORECASE) + if match: + search_path = match.group(1) + schemas = search_path.split(',') + for s in schemas: + s = s.strip().strip('"\'') + if s and s != '$user': + return s + return 'public' + + return 'public' + + except Exception: # pylint: disable=broad-exception-caught + return 'public' + @staticmethod async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, str], None]: """ @@ -103,12 +145,17 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s Args: connection_url: PostgreSQL connection URL in format: - postgresql://username:password@host:port/database + postgresql://username:password@host:port/database + Optionally with schema via options parameter: + postgresql://...?options=-csearch_path%3Dschema_name Returns: Tuple[bool, str]: Success status and message """ try: + # Parse schema from connection URL (defaults to 'public') + schema = PostgresLoader._parse_schema_from_url(connection_url) + # Connect to PostgreSQL database conn = psycopg2.connect(connection_url) cursor = conn.cursor() @@ -120,11 +167,11 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s # Get all table information yield True, "Extracting table information..." - entities = PostgresLoader.extract_tables_info(cursor) + entities = PostgresLoader.extract_tables_info(cursor, schema) yield True, "Extracting relationship information..." # Get all relationship information - relationships = PostgresLoader.extract_relationships(cursor) + relationships = PostgresLoader.extract_relationships(cursor, schema) # Close database connection cursor.close() @@ -146,19 +193,20 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s yield False, "Failed to load PostgreSQL database schema" @staticmethod - def extract_tables_info(cursor) -> Dict[str, Any]: + def extract_tables_info(cursor, schema: str = 'public') -> Dict[str, Any]: """ Extract table and column information from PostgreSQL database. Args: cursor: Database cursor + schema: Database schema to extract tables from (default: 'public') Returns: Dict containing table information """ entities = {} - # Get all tables in public schema + # Get all tables in the specified schema cursor.execute(""" SELECT table_name, table_comment FROM information_schema.tables t @@ -166,13 +214,14 @@ def extract_tables_info(cursor) -> Dict[str, Any]: SELECT schemaname, tablename, description as table_comment FROM pg_tables pt JOIN pg_class pc ON pc.relname = pt.tablename + JOIN pg_namespace pn ON pn.oid = pc.relnamespace AND pn.nspname = pt.schemaname JOIN pg_description pd ON pd.objoid = pc.oid AND pd.objsubid = 0 - WHERE pt.schemaname = 'public' + WHERE pt.schemaname = %s ) tc ON tc.tablename = t.table_name - WHERE t.table_schema = 'public' + WHERE t.table_schema = %s AND t.table_type = 'BASE TABLE' ORDER BY t.table_name; - """) + """, (schema, schema)) tables = cursor.fetchall() @@ -180,10 +229,10 @@ def extract_tables_info(cursor) -> Dict[str, Any]: table_name = table_name.strip() # Get column information for this table - columns_info = PostgresLoader.extract_columns_info(cursor, table_name) + columns_info = PostgresLoader.extract_columns_info(cursor, table_name, schema) # Get foreign keys for this table - foreign_keys = PostgresLoader.extract_foreign_keys(cursor, table_name) + foreign_keys = PostgresLoader.extract_foreign_keys(cursor, table_name, schema) # Generate table description table_description = table_comment if table_comment else f"Table: {table_name}" @@ -201,13 +250,14 @@ def extract_tables_info(cursor) -> Dict[str, Any]: return entities @staticmethod - def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: + def extract_columns_info(cursor, table_name: str, schema: str = 'public') -> Dict[str, Any]: """ Extract column information for a specific table. Args: cursor: Database cursor table_name: Name of the table + schema: Database schema (default: 'public') Returns: Dict containing column information @@ -231,6 +281,7 @@ def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name WHERE tc.table_name = %s + AND tc.table_schema = %s AND tc.constraint_type = 'PRIMARY KEY' ) pk ON pk.column_name = c.column_name LEFT JOIN ( @@ -239,15 +290,17 @@ def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name WHERE tc.table_name = %s + AND tc.table_schema = %s AND tc.constraint_type = 'FOREIGN KEY' ) fk ON fk.column_name = c.column_name - LEFT JOIN pg_class pc ON pc.relname = c.table_name + LEFT JOIN pg_namespace pn ON pn.nspname = c.table_schema + LEFT JOIN pg_class pc ON pc.relname = c.table_name AND pc.relnamespace = pn.oid LEFT JOIN pg_attribute pa ON pa.attrelid = pc.oid AND pa.attname = c.column_name LEFT JOIN pg_description pgd ON pgd.objoid = pc.oid AND pgd.objsubid = pa.attnum WHERE c.table_name = %s - AND c.table_schema = 'public' + AND c.table_schema = %s ORDER BY c.ordinal_position; - """, (table_name, table_name, table_name)) + """, (table_name, schema, table_name, schema, table_name, schema)) columns = cursor.fetchall() columns_info = {} @@ -289,13 +342,14 @@ def extract_columns_info(cursor, table_name: str) -> Dict[str, Any]: return columns_info @staticmethod - def extract_foreign_keys(cursor, table_name: str) -> List[Dict[str, str]]: + def extract_foreign_keys(cursor, table_name: str, schema: str = 'public') -> List[Dict[str, str]]: """ Extract foreign key information for a specific table. Args: cursor: Database cursor table_name: Name of the table + schema: Database schema (default: 'public') Returns: List of foreign key dictionaries @@ -315,8 +369,8 @@ def extract_foreign_keys(cursor, table_name: str) -> List[Dict[str, str]]: AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = %s - AND tc.table_schema = 'public'; - """, (table_name,)) + AND tc.table_schema = %s; + """, (table_name, schema)) foreign_keys = [] for constraint_name, column_name, foreign_table, foreign_column in cursor.fetchall(): @@ -330,12 +384,13 @@ def extract_foreign_keys(cursor, table_name: str) -> List[Dict[str, str]]: return foreign_keys @staticmethod - def extract_relationships(cursor) -> Dict[str, List[Dict[str, str]]]: + def extract_relationships(cursor, schema: str = 'public') -> Dict[str, List[Dict[str, str]]]: """ Extract all relationship information from the database. Args: cursor: Database cursor + schema: Database schema (default: 'public') Returns: Dict containing relationship information @@ -355,9 +410,9 @@ def extract_relationships(cursor) -> Dict[str, List[Dict[str, str]]]: ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema WHERE tc.constraint_type = 'FOREIGN KEY' - AND tc.table_schema = 'public' + AND tc.table_schema = %s ORDER BY tc.table_name, tc.constraint_name; - """) + """, (schema,)) relationships = {} for (table_name, constraint_name, column_name, diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx index 35feae26..a0ac49d1 100644 --- a/app/src/components/modals/DatabaseModal.tsx +++ b/app/src/components/modals/DatabaseModal.tsx @@ -29,6 +29,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { const [database, setDatabase] = useState(""); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [schema, setSchema] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const [connectionSteps, setConnectionSteps] = useState([]); const { refreshGraphs } = useDatabase(); @@ -94,6 +95,12 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { const builtUrl = new URL(`${protocol}://${host}:${port}/${database}`); builtUrl.username = username; builtUrl.password = password; + + // Append schema option for PostgreSQL if provided + if (selectedDatabase === 'postgresql' && schema.trim()) { + builtUrl.searchParams.set('options', `-csearch_path=${schema.trim()}`); + } + dbUrl = builtUrl.toString(); } @@ -177,6 +184,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { setDatabase(""); setUsername(""); setPassword(""); + setSchema(""); setConnectionSteps([]); }, 1000); } else { @@ -384,6 +392,26 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { className="bg-muted border-border focus-visible:ring-purple-500" /> + + {/* Schema field - PostgreSQL only */} + {selectedDatabase === 'postgresql' && ( +
+ + setSchema(e.target.value)} + className="bg-muted border-border" + /> +

+ Leave empty to use the default 'public' schema +

+
+ )} )} diff --git a/docs/postgres_loader.md b/docs/postgres_loader.md index e200c188..181ca498 100644 --- a/docs/postgres_loader.md +++ b/docs/postgres_loader.md @@ -5,6 +5,7 @@ This loader connects to a PostgreSQL database and extracts the complete schema i ## Features - **Complete Schema Extraction**: Retrieves all tables, columns, data types, constraints, and relationships +- **Custom Schema Support**: Configure which PostgreSQL schema to extract using the `search_path` option - **Foreign Key Relationships**: Automatically discovers and maps foreign key relationships between tables - **Column Metadata**: Extracts column comments, default values, nullability, and key types - **Batch Processing**: Efficiently processes large schemas with progress tracking @@ -86,6 +87,34 @@ postgresql://[username[:password]@][host[:port]][/database][?options] - `postgresql://user:pass@example.com:5432/production_db` - `postgresql://postgres@127.0.0.1/testdb` +### Custom Schema Configuration + +By default, the loader extracts tables from the `public` schema. To use a different schema, add the `search_path` option to your connection URL using PostgreSQL's standard `options` parameter. +More info [here](https://www.postgresql.org/docs/18/runtime-config-client.html#GUC-SEARCH-PATH). + +**Format:** +``` +postgresql://user:pass@host:port/database?options=-csearch_path%3Dschema_name +``` + +**Examples:** +- Extract from `sales` schema + `postgresql://postgres:password@localhost:5432/mydb?options=-csearch_path=sales` +- Extract from `dbo` schema + `postgresql://user:pass@host:5432/enterprise_db?options=-csearch_path=dbo` +- Extract from `inventory` schema + `postgresql://admin:secret@192.168.1.100:5432/warehouse?options=-csearch_path%3Dinventory` + +**Notes:** +- The `%3D` is the URL-encoded form of `=` +- If `search_path` is not specified, the loader defaults to `public` +- The schema must exist and the user must have `USAGE` permission on it +- This follows PostgreSQL's native `search_path` configuration option + +**Using the UI:** + +When connecting via the QueryWeaver UI with "Manual Entry" mode for PostgreSQL, you can specify the schema in the optional "Schema" field. Leave it empty to use the default `public` schema. + ### Integration with Graph Database {% capture python_1 %} @@ -218,7 +247,7 @@ PostgreSQL schema loaded successfully. Found 15 tables. ## Limitations - Currently only supports PostgreSQL databases -- Extracts schema from the 'public' schema only +- Extracts schema from one schema at a time (defaults to 'public', configurable via `search_path`) - Requires read permissions on information_schema and pg_* system tables - Large schemas may take time to process due to embedding generation @@ -228,8 +257,29 @@ PostgreSQL schema loaded successfully. Found 15 tables. 1. **Connection Failed**: Verify the connection URL format and database credentials 2. **Permission Denied**: Ensure the database user has read access to system tables -3. **Schema Not Found**: Check that tables exist in the 'public' schema +3. **No Tables Found**: +- Check that tables exist in the target schema +- If using a custom schema, verify the `search_path` option is correctly formatted +- Ensure the schema name is spelled correctly (schema names are case-sensitive) 4. **Graph Database Error**: Verify that the graph database is running and accessible +5. **Schema Permission Error**: Ensure the database user has `USAGE` permission on the target schema: + ```sql + GRANT USAGE ON SCHEMA my_schema TO my_user; + GRANT SELECT ON ALL TABLES IN SCHEMA my_schema TO my_user; + ``` + +### Verifying Schema Configuration + +To verify which schema will be used, you can test the search_path parsing: + +{% capture python_2 %} +from api.loaders.postgres_loader import PostgresLoader + +# Test URL parsing +url = "postgresql://user:pass@localhost:5432/mydatabase?options=-c search_path=my_schema" +schema = PostgresLoader._parse_schema_from_url(url) +print(f"Schema to be used: {schema}") # Output: my_schema +{% endcapture %} ### Debug Mode diff --git a/tests/test_postgres_loader.py b/tests/test_postgres_loader.py index c8ec2e91..cb3bea41 100644 --- a/tests/test_postgres_loader.py +++ b/tests/test_postgres_loader.py @@ -150,6 +150,95 @@ def test_extract_relationships(self): self.assertEqual(user_rel["target_column"], "id") +class TestParseSchemaFromUrl(unittest.TestCase): # pylint: disable=protected-access + """Test cases for _parse_schema_from_url helper method""" + + def test_no_options_returns_public(self): + """Test that URL without options parameter returns 'public'""" + url = "postgresql://user:pass@localhost:5432/mydb" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_empty_options_returns_public(self): + """Test that URL with empty options returns 'public'""" + url = "postgresql://user:pass@localhost:5432/mydb?" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_options_without_search_path_returns_public(self): + """Test that options without search_path returns 'public'""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csynchronous_commit%3Dlocal" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_search_path_simple_schema(self): + """Test parsing a simple single schema from search_path""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3Dmy_schema" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + + def test_search_path_with_space(self): + """Test parsing search_path with space after -c""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-c%20search_path%3Dmy_schema" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + + def test_search_path_multiple_schemas_returns_first(self): + """Test that multiple schemas returns the first one""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3Dschema1,schema2,schema3" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'schema1') + + def test_search_path_dollar_user_first_returns_second(self): + """Test that $user as first schema returns the second schema""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user,my_schema" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + + def test_search_path_dollar_user_only_returns_public(self): + """Test that $user alone returns 'public'""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_search_path_quoted_schema(self): + """Test parsing quoted schema name""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%22my_schema%22" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + + def test_search_path_case_insensitive(self): + """Test that search_path matching is case insensitive""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-cSEARCH_PATH%3Dmy_schema" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + + def test_invalid_url_returns_public(self): + """Test that invalid URL returns 'public'""" + url = "not-a-valid-url" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_empty_url_returns_public(self): + """Test that empty URL returns 'public'""" + url = "" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_multiple_options_with_search_path(self): + """Test parsing search_path when multiple options are present""" + url = ("postgresql://user:pass@localhost:5432/mydb" + "?options=-csynchronous_commit%3Dlocal%20-csearch_path%3Dmy_schema") + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + + def test_search_path_with_equals_sign(self): + """Test parsing search_path with = sign format""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-c%20search_path=custom_schema" + result = PostgresLoader._parse_schema_from_url(url) + self.assertEqual(result, 'custom_schema') + + def run_tests(): """Run all tests""" print("Running PostgreSQL Loader Tests") From f44647ff2d4853737665628323e3f083be1ec8a2 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 10:43:13 +0200 Subject: [PATCH 02/12] fix: make parse_schema_from_url public to fix CI pylint Rename _parse_schema_from_url to parse_schema_from_url since the method is already documented for external use and tested directly. This eliminates W0212 (protected-access) warnings that cause CI pylint to fail with exit code 4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/loaders/postgres_loader.py | 4 ++-- docs/postgres_loader.md | 2 +- tests/test_postgres_loader.py | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index 9fdff140..82b8feab 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -98,7 +98,7 @@ def _serialize_value(value): return value @staticmethod - def _parse_schema_from_url(connection_url: str) -> str: + def parse_schema_from_url(connection_url: str) -> str: """ Parse the search_path from the connection URL's options parameter. @@ -154,7 +154,7 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s """ try: # Parse schema from connection URL (defaults to 'public') - schema = PostgresLoader._parse_schema_from_url(connection_url) + schema = PostgresLoader.parse_schema_from_url(connection_url) # Connect to PostgreSQL database conn = psycopg2.connect(connection_url) diff --git a/docs/postgres_loader.md b/docs/postgres_loader.md index 181ca498..e4dac9d5 100644 --- a/docs/postgres_loader.md +++ b/docs/postgres_loader.md @@ -277,7 +277,7 @@ from api.loaders.postgres_loader import PostgresLoader # Test URL parsing url = "postgresql://user:pass@localhost:5432/mydatabase?options=-c search_path=my_schema" -schema = PostgresLoader._parse_schema_from_url(url) +schema = PostgresLoader.parse_schema_from_url(url) print(f"Schema to be used: {schema}") # Output: my_schema {% endcapture %} diff --git a/tests/test_postgres_loader.py b/tests/test_postgres_loader.py index cb3bea41..ea80e0b2 100644 --- a/tests/test_postgres_loader.py +++ b/tests/test_postgres_loader.py @@ -150,92 +150,92 @@ def test_extract_relationships(self): self.assertEqual(user_rel["target_column"], "id") -class TestParseSchemaFromUrl(unittest.TestCase): # pylint: disable=protected-access - """Test cases for _parse_schema_from_url helper method""" +class TestParseSchemaFromUrl(unittest.TestCase): + """Test cases for parse_schema_from_url helper method""" def test_no_options_returns_public(self): """Test that URL without options parameter returns 'public'""" url = "postgresql://user:pass@localhost:5432/mydb" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'public') def test_empty_options_returns_public(self): """Test that URL with empty options returns 'public'""" url = "postgresql://user:pass@localhost:5432/mydb?" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'public') def test_options_without_search_path_returns_public(self): """Test that options without search_path returns 'public'""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csynchronous_commit%3Dlocal" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'public') def test_search_path_simple_schema(self): """Test parsing a simple single schema from search_path""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3Dmy_schema" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') def test_search_path_with_space(self): """Test parsing search_path with space after -c""" url = "postgresql://user:pass@localhost:5432/mydb?options=-c%20search_path%3Dmy_schema" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') def test_search_path_multiple_schemas_returns_first(self): """Test that multiple schemas returns the first one""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3Dschema1,schema2,schema3" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'schema1') def test_search_path_dollar_user_first_returns_second(self): """Test that $user as first schema returns the second schema""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user,my_schema" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') def test_search_path_dollar_user_only_returns_public(self): """Test that $user alone returns 'public'""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'public') def test_search_path_quoted_schema(self): """Test parsing quoted schema name""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%22my_schema%22" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') def test_search_path_case_insensitive(self): """Test that search_path matching is case insensitive""" url = "postgresql://user:pass@localhost:5432/mydb?options=-cSEARCH_PATH%3Dmy_schema" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') def test_invalid_url_returns_public(self): """Test that invalid URL returns 'public'""" url = "not-a-valid-url" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'public') def test_empty_url_returns_public(self): """Test that empty URL returns 'public'""" url = "" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'public') def test_multiple_options_with_search_path(self): """Test parsing search_path when multiple options are present""" url = ("postgresql://user:pass@localhost:5432/mydb" "?options=-csynchronous_commit%3Dlocal%20-csearch_path%3Dmy_schema") - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') def test_search_path_with_equals_sign(self): """Test parsing search_path with = sign format""" url = "postgresql://user:pass@localhost:5432/mydb?options=-c%20search_path=custom_schema" - result = PostgresLoader._parse_schema_from_url(url) + result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'custom_schema') From e21496a0964dca47194d212f827331bac8c19a84 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:02:17 +0200 Subject: [PATCH 03/12] fix: address review comments on PR #475 - Add constraint_schema qualifier to key_column_usage JOINs in extract_columns_info to prevent cross-schema constraint name collisions - Sanitize schema input in DatabaseModal to strip non-identifier characters before building the URL options - Add edge case tests: empty tokens, blank quoted tokens, repeated $user entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/loaders/postgres_loader.py | 2 + app/src/components/modals/DatabaseModal.tsx | 5 +- .../queryweaver_client.egg-info/PKG-INFO | 10 + .../queryweaver_client.egg-info/SOURCES.txt | 10 + .../dependency_links.txt | 1 + .../queryweaver_client.egg-info/requires.txt | 4 + .../queryweaver_client.egg-info/top_level.txt | 1 + clients/ts/dist/client.d.ts.map | 1 + clients/ts/dist/client.js | 229 ++++++++++++++++++ clients/ts/dist/client.js.map | 1 + clients/ts/dist/index.d.ts.map | 1 + clients/ts/dist/index.js | 8 + clients/ts/dist/index.js.map | 1 + clients/ts/dist/types.d.ts.map | 1 + clients/ts/dist/types.js | 13 + clients/ts/dist/types.js.map | 1 + tests/test_postgres_loader.py | 24 ++ uv.lock | 94 ++----- 18 files changed, 338 insertions(+), 69 deletions(-) create mode 100644 clients/python/queryweaver_client.egg-info/PKG-INFO create mode 100644 clients/python/queryweaver_client.egg-info/SOURCES.txt create mode 100644 clients/python/queryweaver_client.egg-info/dependency_links.txt create mode 100644 clients/python/queryweaver_client.egg-info/requires.txt create mode 100644 clients/python/queryweaver_client.egg-info/top_level.txt create mode 100644 clients/ts/dist/client.d.ts.map create mode 100644 clients/ts/dist/client.js create mode 100644 clients/ts/dist/client.js.map create mode 100644 clients/ts/dist/index.d.ts.map create mode 100644 clients/ts/dist/index.js create mode 100644 clients/ts/dist/index.js.map create mode 100644 clients/ts/dist/types.d.ts.map create mode 100644 clients/ts/dist/types.js create mode 100644 clients/ts/dist/types.js.map diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index 82b8feab..f583ea59 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -280,6 +280,7 @@ def extract_columns_info(cursor, table_name: str, schema: str = 'public') -> Dic FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name + AND tc.constraint_schema = ku.constraint_schema WHERE tc.table_name = %s AND tc.table_schema = %s AND tc.constraint_type = 'PRIMARY KEY' @@ -289,6 +290,7 @@ def extract_columns_info(cursor, table_name: str, schema: str = 'public') -> Dic FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name + AND tc.constraint_schema = ku.constraint_schema WHERE tc.table_name = %s AND tc.table_schema = %s AND tc.constraint_type = 'FOREIGN KEY' diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx index a0ac49d1..e48c28e3 100644 --- a/app/src/components/modals/DatabaseModal.tsx +++ b/app/src/components/modals/DatabaseModal.tsx @@ -98,7 +98,10 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { // Append schema option for PostgreSQL if provided if (selectedDatabase === 'postgresql' && schema.trim()) { - builtUrl.searchParams.set('options', `-csearch_path=${schema.trim()}`); + const sanitized = schema.trim().replace(/[^a-zA-Z0-9_]/g, ''); + if (sanitized) { + builtUrl.searchParams.set('options', `-csearch_path=${sanitized}`); + } } dbUrl = builtUrl.toString(); diff --git a/clients/python/queryweaver_client.egg-info/PKG-INFO b/clients/python/queryweaver_client.egg-info/PKG-INFO new file mode 100644 index 00000000..9fa17d64 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 2.4 +Name: queryweaver-client +Version: 0.1.0 +Summary: Lightweight Python client for QueryWeaver server +Requires-Dist: requests +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: summary diff --git a/clients/python/queryweaver_client.egg-info/SOURCES.txt b/clients/python/queryweaver_client.egg-info/SOURCES.txt new file mode 100644 index 00000000..35578792 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +README.md +setup.py +queryweaver_client/__init__.py +queryweaver_client/client.py +queryweaver_client.egg-info/PKG-INFO +queryweaver_client.egg-info/SOURCES.txt +queryweaver_client.egg-info/dependency_links.txt +queryweaver_client.egg-info/requires.txt +queryweaver_client.egg-info/top_level.txt +tests/test_client_basic.py \ No newline at end of file diff --git a/clients/python/queryweaver_client.egg-info/dependency_links.txt b/clients/python/queryweaver_client.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/clients/python/queryweaver_client.egg-info/requires.txt b/clients/python/queryweaver_client.egg-info/requires.txt new file mode 100644 index 00000000..0c147cb1 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/requires.txt @@ -0,0 +1,4 @@ +requests + +[dev] +pytest diff --git a/clients/python/queryweaver_client.egg-info/top_level.txt b/clients/python/queryweaver_client.egg-info/top_level.txt new file mode 100644 index 00000000..4a077b5c --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/top_level.txt @@ -0,0 +1 @@ +queryweaver_client diff --git a/clients/ts/dist/client.d.ts.map b/clients/ts/dist/client.d.ts.map new file mode 100644 index 00000000..49b20ebd --- /dev/null +++ b/clients/ts/dist/client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAY,MAAM,SAAS,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAyB;IAE/C;;;;;;;OAOG;gBACS,OAAO,GAAE,wBAA6B;IAelD,OAAO,CAAC,GAAG;YAIG,cAAc;IAiB5B;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAWtC;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWzD;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAW1D;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAY1D;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,GAAE,OAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAwBpG;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAY9E;;;OAGG;YACW,wBAAwB;CA0DvC"} \ No newline at end of file diff --git a/clients/ts/dist/client.js b/clients/ts/dist/client.js new file mode 100644 index 00000000..51764221 --- /dev/null +++ b/clients/ts/dist/client.js @@ -0,0 +1,229 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryWeaverClient = void 0; +const types_1 = require("./types"); +const MESSAGE_DELIMITER = '|||FALKORDB_MESSAGE_BOUNDARY|||'; +/** + * QueryWeaver HTTP API client. + * + * Simple wrapper around the REST API exposed by a running QueryWeaver server. + * Supports API token authentication (Bearer) and session cookie usage. + * + * @example + * ```typescript + * // With API token authentication + * const client = new QueryWeaverClient({ + * baseUrl: 'http://localhost:5000', + * apiToken: 'your-api-token' + * }); + * + * // With session cookies (handled automatically by fetch) + * const client = new QueryWeaverClient({ + * baseUrl: 'http://localhost:5000' + * }); + * ``` + */ +class QueryWeaverClient { + /** + * Create a new QueryWeaver client. + * + * @param options - Client configuration options + * @param options.baseUrl - The base URL of the QueryWeaver server (default: 'http://localhost:5000') + * @param options.apiToken - Optional API token for Bearer authentication + * @param options.timeout - Request timeout in milliseconds (default: 30000) + */ + constructor(options = {}) { + this.baseUrl = (options.baseUrl || 'http://localhost:5000').replace(/\/$/, ''); + this.apiToken = options.apiToken; + this.timeout = options.timeout || 30000; + this.defaultHeaders = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }; + if (this.apiToken) { + this.defaultHeaders['Authorization'] = `Bearer ${this.apiToken}`; + } + } + url(path) { + return `${this.baseUrl}${path}`; + } + async raiseForStatus(response) { + if (!response.ok) { + let errorMessage; + let responseData; + try { + responseData = await response.json(); + const data = responseData; + errorMessage = data.error || data.message || `HTTP ${response.status}`; + } + catch { + errorMessage = await response.text() || `HTTP ${response.status}`; + } + throw new types_1.APIError(errorMessage, response.status, responseData); + } + } + /** + * List available user schemas/databases. + * + * @returns A list of database/schema names. + */ + async listSchemas() { + const response = await fetch(this.url('/graphs'), { + method: 'GET', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Get schema nodes/edges for a specific graph. + */ + async getSchema(graphId) { + const response = await fetch(this.url(`/graphs/${graphId}/data`), { + method: 'GET', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Delete a schema. + */ + async deleteSchema(graphId) { + const response = await fetch(this.url(`/graphs/${graphId}`), { + method: 'DELETE', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Refresh schema. + */ + async refreshSchema(graphId) { + const response = await fetch(this.url(`/graphs/${graphId}/refresh`), { + method: 'POST', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Connect to a database and return final result. + */ + async connectDatabase(dbUrl) { + const response = await fetch(this.url('/database'), { + method: 'POST', + headers: this.defaultHeaders, + body: JSON.stringify({ url: dbUrl }), + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return this.consumeStreamingResponse(response); + } + /** + * Run a natural language query and return final result. + * + * @param graphId - The database/schema ID to query + * @param chatData - The chat data containing the query messages + * @param autoConfirm - If true, automatically confirm destructive operations. + * If false (default), returns confirmation request for user approval. + * @returns The query result or confirmation request + */ + async query(graphId, chatData, autoConfirm = false) { + const response = await fetch(this.url(`/graphs/${graphId}`), { + method: 'POST', + headers: this.defaultHeaders, + body: JSON.stringify(chatData), + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + const result = await this.consumeStreamingResponse(response); + // If autoConfirm is enabled and result requires confirmation, automatically confirm + if (autoConfirm && result.type === 'destructive_confirmation') { + const confirmData = { + sql_query: result.sql_query, + confirmation: 'YES', + chat: chatData.messages, + }; + return this.confirm(graphId, confirmData); + } + return result; + } + /** + * Confirm destructive operation and return result. + */ + async confirm(graphId, confirmData) { + const response = await fetch(this.url(`/graphs/${graphId}/confirm`), { + method: 'POST', + headers: this.defaultHeaders, + body: JSON.stringify(confirmData), + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return this.consumeStreamingResponse(response); + } + /** + * Consume a streaming response and return the final result. + * Properly buffers chunks and splits on the exact MESSAGE_DELIMITER. + */ + async consumeStreamingResponse(response) { + const events = []; + if (!response.body) { + return {}; + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + try { + let done = false; + while (!done) { + const { done: chunkDone, value } = await reader.read(); + done = chunkDone; + if (done) + break; + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + // Split by the delimiter and process complete messages + const parts = buffer.split(MESSAGE_DELIMITER); + // Keep the last part in the buffer (it may be incomplete) + buffer = parts[parts.length - 1]; + // Process all complete messages + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i].trim(); + if (!part) + continue; + try { + const event = JSON.parse(part); + events.push(event); + } + catch { + // If not valid JSON, store as raw text + events.push({ raw: part }); + } + } + } + // Process any remaining data in the buffer + if (buffer.trim()) { + try { + const event = JSON.parse(buffer.trim()); + events.push(event); + } + catch { + events.push({ raw: buffer.trim() }); + } + } + } + finally { + reader.releaseLock(); + } + // Return the last event which typically contains the final result + return events.length > 0 ? events[events.length - 1] : {}; + } +} +exports.QueryWeaverClient = QueryWeaverClient; +//# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/clients/ts/dist/client.js.map b/clients/ts/dist/client.js.map new file mode 100644 index 00000000..d6b993d1 --- /dev/null +++ b/clients/ts/dist/client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/clients/ts/dist/index.d.ts.map b/clients/ts/dist/index.d.ts.map new file mode 100644 index 00000000..6fc0ce9a --- /dev/null +++ b/clients/ts/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"} \ No newline at end of file diff --git a/clients/ts/dist/index.js b/clients/ts/dist/index.js new file mode 100644 index 00000000..051eb1f7 --- /dev/null +++ b/clients/ts/dist/index.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APIError = exports.QueryWeaverClient = void 0; +var client_1 = require("./client"); +Object.defineProperty(exports, "QueryWeaverClient", { enumerable: true, get: function () { return client_1.QueryWeaverClient; } }); +var types_1 = require("./types"); +Object.defineProperty(exports, "APIError", { enumerable: true, get: function () { return types_1.APIError; } }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/clients/ts/dist/index.js.map b/clients/ts/dist/index.js.map new file mode 100644 index 00000000..429fc316 --- /dev/null +++ b/clients/ts/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAC1B,iCAAmC;AAA1B,iGAAA,QAAQ,OAAA"} \ No newline at end of file diff --git a/clients/ts/dist/types.d.ts.map b/clients/ts/dist/types.d.ts.map new file mode 100644 index 00000000..43c9d2ca --- /dev/null +++ b/clients/ts/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,QAAS,SAAQ,KAAK;IAGxB,MAAM,EAAE,MAAM;IACd,QAAQ,CAAC,EAAE,OAAO;gBAFzB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,OAAO,YAAA;CAK5B"} \ No newline at end of file diff --git a/clients/ts/dist/types.js b/clients/ts/dist/types.js new file mode 100644 index 00000000..c2504967 --- /dev/null +++ b/clients/ts/dist/types.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APIError = void 0; +class APIError extends Error { + constructor(message, status, response) { + super(message); + this.status = status; + this.response = response; + this.name = 'APIError'; + } +} +exports.APIError = APIError; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/clients/ts/dist/types.js.map b/clients/ts/dist/types.js.map new file mode 100644 index 00000000..bb2a5c03 --- /dev/null +++ b/clients/ts/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAyBA,MAAa,QAAS,SAAQ,KAAK;IACjC,YACE,OAAe,EACR,MAAc,EACd,QAAkB;QAEzB,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAU;QAGzB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AATD,4BASC"} \ No newline at end of file diff --git a/tests/test_postgres_loader.py b/tests/test_postgres_loader.py index ea80e0b2..3486c752 100644 --- a/tests/test_postgres_loader.py +++ b/tests/test_postgres_loader.py @@ -238,6 +238,30 @@ def test_search_path_with_equals_sign(self): result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'custom_schema') + def test_search_path_empty_tokens_returns_public(self): + """Test that empty schema tokens (e.g. search_path=,) fallback to public""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D," + result = PostgresLoader.parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_search_path_blank_quoted_tokens_returns_public(self): + """Test that blank quoted tokens fallback to public""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%22%22,%22%22" + result = PostgresLoader.parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_search_path_repeated_dollar_user_returns_public(self): + """Test that repeated $user entries with no real schema returns public""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user,%24user" + result = PostgresLoader.parse_schema_from_url(url) + self.assertEqual(result, 'public') + + def test_search_path_repeated_dollar_user_with_schema(self): + """Test that repeated $user entries followed by a real schema returns that schema""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user,%24user,my_schema" + result = PostgresLoader.parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + def run_tests(): """Run all tests""" diff --git a/uv.lock b/uv.lock index 558e69ba..7a9ba97a 100644 --- a/uv.lock +++ b/uv.lock @@ -518,15 +518,15 @@ wheels = [ [[package]] name = "falkordb" -version = "1.2.2" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-dateutil" }, { name = "redis" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/9f/d02f8282490b525ff6a05024ce23e2f519d3fc3dbd6cb2f004ab1bae4920/falkordb-1.2.2.tar.gz", hash = "sha256:3fc150d3262b971916a0f2c15f419f7fe521a94fe3a672b6f174288d885fd239", size = 30179, upload-time = "2025-12-07T13:29:53.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/52/5495fcba8e21c269a605092a7c4a8b33ceecae283dbfe76fc53f7f5b50ab/falkordb-1.6.0.tar.gz", hash = "sha256:5c307d973f3fc3987a18478ebd5882f7e842d4225463a8ef5e026970ebfba8c6", size = 98157, upload-time = "2026-02-21T06:36:19.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/56/5e79a1a3ed0afa4c8e32a99edb862f1d2ab66b7dc65a35fd638ea157f33d/falkordb-1.2.2-py3-none-any.whl", hash = "sha256:cc1fcccaa1148be9e3d22d85af03d027f19b398e0f38d04424f3757a02744976", size = 36046, upload-time = "2025-12-07T13:29:52.686Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8b/59ec60885abd3b6b2b3a1e5917627c3cae656b4cff7f847c5217ec3dc952/falkordb-1.6.0-py3-none-any.whl", hash = "sha256:0f190e9d6104595fd51ece4f1e7b5d49d62cfee346d94151d7986a138fd90d89", size = 37378, upload-time = "2026-02-21T06:36:17.769Z" }, ] [[package]] @@ -785,47 +785,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, ] -[[package]] -name = "grpcio" -version = "1.78.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/de/de568532d9907552700f80dcec38219d8d298ad9e71f5e0a095abaf2761e/grpcio-1.78.1.tar.gz", hash = "sha256:27c625532d33ace45d57e775edf1982e183ff8641c72e4e91ef7ba667a149d72", size = 12835760, upload-time = "2026-02-20T01:16:10.869Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/ed/d2eb9d27fded1a76b2a80eb9aa8b12101da7e41ce2bac0ad3651e88a14ae/grpcio-1.78.1-cp312-cp312-linux_armv7l.whl", hash = "sha256:41e4605c923e0e9a84a2718e4948a53a530172bfaf1a6d1ded16ef9c5849fca2", size = 5913389, upload-time = "2026-02-20T01:13:49.005Z" }, - { url = "https://files.pythonhosted.org/packages/69/1b/40034e9ab010eeb3fa41ec61d8398c6dbf7062f3872c866b8f72700e2522/grpcio-1.78.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:39da1680d260c0c619c3b5fa2dc47480ca24d5704c7a548098bca7de7f5dd17f", size = 11811839, upload-time = "2026-02-20T01:13:51.839Z" }, - { url = "https://files.pythonhosted.org/packages/b4/69/fe16ef2979ea62b8aceb3a3f1e7a8bbb8b717ae2a44b5899d5d426073273/grpcio-1.78.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b5d5881d72a09b8336a8f874784a8eeffacde44a7bc1a148bce5a0243a265ef0", size = 6475805, upload-time = "2026-02-20T01:13:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5b/1e/069e0a9062167db18446917d7c00ae2e91029f96078a072bedc30aaaa8c3/grpcio-1.78.1-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:888ceb7821acd925b1c90f0cdceaed1386e69cfe25e496e0771f6c35a156132f", size = 7169955, upload-time = "2026-02-20T01:13:59.553Z" }, - { url = "https://files.pythonhosted.org/packages/38/fc/44a57e2bb4a755e309ee4e9ed2b85c9af93450b6d3118de7e69410ee05fa/grpcio-1.78.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8942bdfc143b467c264b048862090c4ba9a0223c52ae28c9ae97754361372e42", size = 6690767, upload-time = "2026-02-20T01:14:02.31Z" }, - { url = "https://files.pythonhosted.org/packages/b8/87/21e16345d4c75046d453916166bc72a3309a382c8e97381ec4b8c1a54729/grpcio-1.78.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:716a544969660ed609164aff27b2effd3ff84e54ac81aa4ce77b1607ca917d22", size = 7266846, upload-time = "2026-02-20T01:14:12.974Z" }, - { url = "https://files.pythonhosted.org/packages/11/df/d6261983f9ca9ef4d69893765007a9a3211b91d9faf85a2591063df381c7/grpcio-1.78.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d50329b081c223d444751076bb5b389d4f06c2b32d51b31a1e98172e6cecfb9", size = 8253522, upload-time = "2026-02-20T01:14:17.407Z" }, - { url = "https://files.pythonhosted.org/packages/de/7c/4f96a0ff113c5d853a27084d7590cd53fdb05169b596ea9f5f27f17e021e/grpcio-1.78.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e836778c13ff70edada16567e8da0c431e8818eaae85b80d11c1ba5782eccbb", size = 7698070, upload-time = "2026-02-20T01:14:20.032Z" }, - { url = "https://files.pythonhosted.org/packages/17/3c/7b55c0b5af88fbeb3d0c13e25492d3ace41ac9dbd0f5f8f6c0fb613b6706/grpcio-1.78.1-cp312-cp312-win32.whl", hash = "sha256:07eb016ea7444a22bef465cce045512756956433f54450aeaa0b443b8563b9ca", size = 4066474, upload-time = "2026-02-20T01:14:22.602Z" }, - { url = "https://files.pythonhosted.org/packages/5d/17/388c12d298901b0acf10b612b650692bfed60e541672b1d8965acbf2d722/grpcio-1.78.1-cp312-cp312-win_amd64.whl", hash = "sha256:02b82dcd2fa580f5e82b4cf62ecde1b3c7cc9ba27b946421200706a6e5acaf85", size = 4797537, upload-time = "2026-02-20T01:14:25.444Z" }, - { url = "https://files.pythonhosted.org/packages/df/72/754754639cfd16ad04619e1435a518124b2d858e5752225376f9285d4c51/grpcio-1.78.1-cp313-cp313-linux_armv7l.whl", hash = "sha256:2b7ad2981550ce999e25ce3f10c8863f718a352a2fd655068d29ea3fd37b4907", size = 5919437, upload-time = "2026-02-20T01:14:29.403Z" }, - { url = "https://files.pythonhosted.org/packages/5c/84/6267d1266f8bc335d3a8b7ccf981be7de41e3ed8bd3a49e57e588212b437/grpcio-1.78.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:409bfe22220889b9906739910a0ee4c197a967c21b8dd14b4b06dd477f8819ce", size = 11803701, upload-time = "2026-02-20T01:14:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/f3/56/c9098e8b920a54261cd605bbb040de0cde1ca4406102db0aa2c0b11d1fb4/grpcio-1.78.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:34b6cb16f4b67eeb5206250dc5b4d5e8e3db939535e58efc330e4c61341554bd", size = 6479416, upload-time = "2026-02-20T01:14:35.926Z" }, - { url = "https://files.pythonhosted.org/packages/86/cf/5d52024371ee62658b7ed72480200524087528844ec1b65265bbcd31c974/grpcio-1.78.1-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:39d21fd30d38a5afb93f0e2e71e2ec2bd894605fb75d41d5a40060c2f98f8d11", size = 7174087, upload-time = "2026-02-20T01:14:39.98Z" }, - { url = "https://files.pythonhosted.org/packages/31/e6/5e59551afad4279e27335a6d60813b8aa3ae7b14fb62cea1d329a459c118/grpcio-1.78.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09fbd4bcaadb6d8604ed1504b0bdf7ac18e48467e83a9d930a70a7fefa27e862", size = 6692881, upload-time = "2026-02-20T01:14:42.466Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/940062de2d14013c02f51b079eb717964d67d46f5d44f22038975c9d9576/grpcio-1.78.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:db681513a1bdd879c0b24a5a6a70398da5eaaba0e077a306410dc6008426847a", size = 7269092, upload-time = "2026-02-20T01:14:45.826Z" }, - { url = "https://files.pythonhosted.org/packages/09/87/9db657a4b5f3b15560ec591db950bc75a1a2f9e07832578d7e2b23d1a7bd/grpcio-1.78.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f81816faa426da461e9a597a178832a351d6f1078102590a4b32c77d251b71eb", size = 8252037, upload-time = "2026-02-20T01:14:48.57Z" }, - { url = "https://files.pythonhosted.org/packages/e2/37/b980e0265479ec65e26b6e300a39ceac33ecb3f762c2861d4bac990317cf/grpcio-1.78.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffbb760df1cd49e0989f9826b2fd48930700db6846ac171eaff404f3cfbe5c28", size = 7695243, upload-time = "2026-02-20T01:14:51.376Z" }, - { url = "https://files.pythonhosted.org/packages/98/46/5fc42c100ab702fa1ea41a75c890c563c3f96432b4a287d5a6369654f323/grpcio-1.78.1-cp313-cp313-win32.whl", hash = "sha256:1a56bf3ee99af5cf32d469de91bf5de79bdac2e18082b495fc1063ea33f4f2d0", size = 4065329, upload-time = "2026-02-20T01:14:53.952Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/806d60bb6611dfc16cf463d982bd92bd8b6bd5f87dfac66b0a44dfe20995/grpcio-1.78.1-cp313-cp313-win_amd64.whl", hash = "sha256:8991c2add0d8505178ff6c3ae54bd9386279e712be82fa3733c54067aae9eda1", size = 4797637, upload-time = "2026-02-20T01:14:57.276Z" }, - { url = "https://files.pythonhosted.org/packages/96/3a/2d2ec4d2ce2eb9d6a2b862630a0d9d4ff4239ecf1474ecff21442a78612a/grpcio-1.78.1-cp314-cp314-linux_armv7l.whl", hash = "sha256:d101fe49b1e0fb4a7aa36ed0c3821a0f67a5956ef572745452d2cd790d723a3f", size = 5920256, upload-time = "2026-02-20T01:15:00.23Z" }, - { url = "https://files.pythonhosted.org/packages/9c/92/dccb7d087a1220ed358753945230c1ddeeed13684b954cb09db6758f1271/grpcio-1.78.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:5ce1855e8cfc217cdf6bcfe0cf046d7cf81ddcc3e6894d6cfd075f87a2d8f460", size = 11813749, upload-time = "2026-02-20T01:15:03.312Z" }, - { url = "https://files.pythonhosted.org/packages/ef/47/c20e87f87986da9998f30f14776ce27e61f02482a3a030ffe265089342c6/grpcio-1.78.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd26048d066b51f39fe9206e2bcc2cea869a5e5b2d13c8d523f4179193047ebd", size = 6488739, upload-time = "2026-02-20T01:15:14.349Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c2/088bd96e255133d7d87c3eed0d598350d16cde1041bdbe2bb065967aaf91/grpcio-1.78.1-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4b8d7fda614cf2af0f73bbb042f3b7fee2ecd4aea69ec98dbd903590a1083529", size = 7173096, upload-time = "2026-02-20T01:15:17.687Z" }, - { url = "https://files.pythonhosted.org/packages/60/ce/168db121073a03355ce3552b3b1f790b5ded62deffd7d98c5f642b9d3d81/grpcio-1.78.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:656a5bd142caeb8b1efe1fe0b4434ecc7781f44c97cfc7927f6608627cf178c0", size = 6693861, upload-time = "2026-02-20T01:15:20.911Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d0/90b30ec2d9425215dd56922d85a90babbe6ee7e8256ba77d866b9c0d3aba/grpcio-1.78.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:99550e344482e3c21950c034f74668fccf8a546d50c1ecb4f717543bbdc071ba", size = 7278083, upload-time = "2026-02-20T01:15:23.698Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fb/73f9ba0b082bcd385d46205095fd9c917754685885b28fce3741e9f54529/grpcio-1.78.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8f27683ca68359bd3f0eb4925824d71e538f84338b3ae337ead2ae43977d7541", size = 8252546, upload-time = "2026-02-20T01:15:26.517Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/6a89ea3cb5db6c3d9ed029b0396c49f64328c0cf5d2630ffeed25711920a/grpcio-1.78.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a40515b69ac50792f9b8ead260f194ba2bb3285375b6c40c7ff938f14c3df17d", size = 7696289, upload-time = "2026-02-20T01:15:29.718Z" }, - { url = "https://files.pythonhosted.org/packages/3d/05/63a7495048499ef437b4933d32e59b7f737bd5368ad6fb2479e2bd83bf2c/grpcio-1.78.1-cp314-cp314-win32.whl", hash = "sha256:2c473b54ef1618f4fb85e82ff4994de18143b74efc088b91b5a935a3a45042ba", size = 4142186, upload-time = "2026-02-20T01:15:32.786Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ce/adfe7e5f701d503be7778291757452e3fab6b19acf51917c79f5d1cf7f8a/grpcio-1.78.1-cp314-cp314-win_amd64.whl", hash = "sha256:e2a6b33d1050dce2c6f563c5caf7f7cbeebf7fba8cde37ffe3803d50526900d1", size = 4932000, upload-time = "2026-02-20T01:15:36.127Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -1161,13 +1120,12 @@ wheels = [ [[package]] name = "litellm" -version = "1.80.17" +version = "1.82.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, { name = "click" }, { name = "fastuuid" }, - { name = "grpcio" }, { name = "httpx" }, { name = "importlib-metadata" }, { name = "jinja2" }, @@ -1178,9 +1136,9 @@ dependencies = [ { name = "tiktoken" }, { name = "tokenizers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/3d/e066b752d1a9f234e9cb34bb443487e6bbc1bde546bbe1f2a0d18a3d4659/litellm-1.80.17.tar.gz", hash = "sha256:514ae407e488bccbe0c33a280ed88495d6f079c1dbfe745eb374d898c94b0ac3", size = 13413478, upload-time = "2026-01-17T02:08:03.668Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/00/49bb5c28e0dea0f5086229a2a08d5fdc6c8dc0d8e2acb2a2d1f7dd9f4b70/litellm-1.82.0.tar.gz", hash = "sha256:d388f52447daccbcaafa19a3e68d17b75f1374b5bf2cde680d65e1cd86e50d22", size = 16800355, upload-time = "2026-03-01T02:35:30.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/32/6673e1250c6a5fe8b5ebb9b6ad039f4c7ce5b66cb6f94056bdef582fd1af/litellm-1.80.17-py3-none-any.whl", hash = "sha256:52b23a21910a16820e6ea4b982dc81d27c993c1054ce148c56b251e9b89d89df", size = 11708291, upload-time = "2026-01-17T02:08:00.562Z" }, + { url = "https://files.pythonhosted.org/packages/28/89/eb28bfcf97d6b045c400e72eb047c381594467048c237dbb6c227764084c/litellm-1.82.0-py3-none-any.whl", hash = "sha256:5496b5d4532cccdc7a095c21cbac4042f7662021c57bc1d17be4e39838929e80", size = 14911978, upload-time = "2026-03-01T02:35:26.844Z" }, ] [[package]] @@ -1555,21 +1513,21 @@ wheels = [ [[package]] name = "playwright" -version = "1.57.0" +version = "1.58.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/b6/e17543cea8290ae4dced10be21d5a43c360096aa2cce0aa7039e60c50df3/playwright-1.57.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:9351c1ac3dfd9b3820fe7fc4340d96c0d3736bb68097b9b7a69bd45d25e9370c", size = 41985039, upload-time = "2025-12-09T08:06:18.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/04/ef95b67e1ff59c080b2effd1a9a96984d6953f667c91dfe9d77c838fc956/playwright-1.57.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a4a9d65027bce48eeba842408bcc1421502dfd7e41e28d207e94260fa93ca67e", size = 40775575, upload-time = "2025-12-09T08:06:22.105Z" }, - { url = "https://files.pythonhosted.org/packages/60/bd/5563850322a663956c927eefcf1457d12917e8f118c214410e815f2147d1/playwright-1.57.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:99104771abc4eafee48f47dac2369e0015516dc1ce8c409807d2dd440828b9a4", size = 41985042, upload-time = "2025-12-09T08:06:25.357Z" }, - { url = "https://files.pythonhosted.org/packages/56/61/3a803cb5ae0321715bfd5247ea871d25b32c8f372aeb70550a90c5f586df/playwright-1.57.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:284ed5a706b7c389a06caa431b2f0ba9ac4130113c3a779767dda758c2497bb1", size = 45975252, upload-time = "2025-12-09T08:06:29.186Z" }, - { url = "https://files.pythonhosted.org/packages/83/d7/b72eb59dfbea0013a7f9731878df8c670f5f35318cedb010c8a30292c118/playwright-1.57.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a1bae6c0a07839cdeaddbc0756b3b2b85e476c07945f64ece08f1f956a86f1", size = 45706917, upload-time = "2025-12-09T08:06:32.549Z" }, - { url = "https://files.pythonhosted.org/packages/e4/09/3fc9ebd7c95ee54ba6a68d5c0bc23e449f7235f4603fc60534a364934c16/playwright-1.57.0-py3-none-win32.whl", hash = "sha256:1dd93b265688da46e91ecb0606d36f777f8eadcf7fbef12f6426b20bf0c9137c", size = 36553860, upload-time = "2025-12-09T08:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/58/d4/dcdfd2a33096aeda6ca0d15584800443dd2be64becca8f315634044b135b/playwright-1.57.0-py3-none-win_amd64.whl", hash = "sha256:6caefb08ed2c6f29d33b8088d05d09376946e49a73be19271c8cd5384b82b14c", size = 36553864, upload-time = "2025-12-09T08:06:38.915Z" }, - { url = "https://files.pythonhosted.org/packages/6a/60/fe31d7e6b8907789dcb0584f88be741ba388413e4fbce35f1eba4e3073de/playwright-1.57.0-py3-none-win_arm64.whl", hash = "sha256:5f065f5a133dbc15e6e7c71e7bc04f258195755b1c32a432b792e28338c8335e", size = 32837940, upload-time = "2025-12-09T08:06:42.268Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/9c6061d5703267f1baae6a4647bfd1862e386fbfdb97d889f6f6ae9e3f64/playwright-1.58.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:96e3204aac292ee639edbfdef6298b4be2ea0a55a16b7068df91adac077cc606", size = 42251098, upload-time = "2026-01-30T15:09:24.028Z" }, + { url = "https://files.pythonhosted.org/packages/e0/40/59d34a756e02f8c670f0fee987d46f7ee53d05447d43cd114ca015cb168c/playwright-1.58.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:70c763694739d28df71ed578b9c8202bb83e8fe8fb9268c04dd13afe36301f71", size = 41039625, upload-time = "2026-01-30T15:09:27.558Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ee/3ce6209c9c74a650aac9028c621f357a34ea5cd4d950700f8e2c4b7fe2c4/playwright-1.58.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:185e0132578733d02802dfddfbbc35f42be23a45ff49ccae5081f25952238117", size = 42251098, upload-time = "2026-01-30T15:09:30.461Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/009958cbf23fac551a940d34e3206e6c7eed2b8c940d0c3afd1feb0b0589/playwright-1.58.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:c95568ba1eda83812598c1dc9be60b4406dffd60b149bc1536180ad108723d6b", size = 46235268, upload-time = "2026-01-30T15:09:33.787Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a6/0e66ad04b6d3440dae73efb39540c5685c5fc95b17c8b29340b62abbd952/playwright-1.58.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f9999948f1ab541d98812de25e3a8c410776aa516d948807140aff797b4bffa", size = 45964214, upload-time = "2026-01-30T15:09:36.751Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/236e60ab9f6d62ed0fd32150d61f1f494cefbf02304c0061e78ed80c1c32/playwright-1.58.0-py3-none-win32.whl", hash = "sha256:1e03be090e75a0fabbdaeab65ce17c308c425d879fa48bb1d7986f96bfad0b99", size = 36815998, upload-time = "2026-01-30T15:09:39.627Z" }, + { url = "https://files.pythonhosted.org/packages/41/f8/5ec599c5e59d2f2f336a05b4f318e733077cd5044f24adb6f86900c3e6a7/playwright-1.58.0-py3-none-win_amd64.whl", hash = "sha256:a2bf639d0ce33b3ba38de777e08697b0d8f3dc07ab6802e4ac53fb65e3907af8", size = 36816005, upload-time = "2026-01-30T15:09:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c4/cc0229fea55c87d6c9c67fe44a21e2cd28d1d558a5478ed4d617e9fb0c93/playwright-1.58.0-py3-none-win_arm64.whl", hash = "sha256:32ffe5c303901a13a0ecab91d1c3f74baf73b84f4bedbb6b935f5bc11cc98e1b", size = 33085919, upload-time = "2026-01-30T15:09:45.71Z" }, ] [[package]] @@ -2145,24 +2103,24 @@ dev = [ [package.metadata] requires-dist = [ { name = "authlib", specifier = "~=1.6.4" }, - { name = "falkordb", specifier = "~=1.2.2" }, + { name = "falkordb", specifier = "~=1.6.0" }, { name = "fastapi", specifier = "~=0.135.1" }, { name = "fastmcp", specifier = ">=2.13.1" }, { name = "graphiti-core", specifier = ">=0.28.1" }, { name = "itsdangerous", specifier = "~=2.2.0" }, { name = "jinja2", specifier = "~=3.1.4" }, { name = "jsonschema", specifier = "~=4.26.0" }, - { name = "litellm", specifier = "~=1.80.9" }, + { name = "litellm", specifier = "~=1.82.0" }, { name = "psycopg2-binary", specifier = "~=2.9.11" }, { name = "pymysql", specifier = "~=1.1.0" }, { name = "python-multipart", specifier = "~=0.0.10" }, - { name = "tqdm", specifier = "~=4.67.1" }, - { name = "uvicorn", specifier = "~=0.40.0" }, + { name = "tqdm", specifier = "~=4.67.3" }, + { name = "uvicorn", specifier = "~=0.41.0" }, ] [package.metadata.requires-dev] dev = [ - { name = "playwright", specifier = "~=1.57.0" }, + { name = "playwright", specifier = "~=1.58.0" }, { name = "pylint", specifier = "~=4.0.3" }, { name = "pytest", specifier = "~=8.4.2" }, { name = "pytest-asyncio", specifier = "~=1.2.0" }, @@ -2171,11 +2129,11 @@ dev = [ [[package]] name = "redis" -version = "6.4.0" +version = "7.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/82/4d1a5279f6c1251d3d2a603a798a1137c657de9b12cfc1fba4858232c4d2/redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034", size = 4928081, upload-time = "2026-03-06T18:18:16.287Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" }, + { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, ] [[package]] @@ -2639,15 +2597,15 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.40.0" +version = "0.41.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" }, ] [[package]] From 7cb758c62ad6066f0c96e2e61c8ccdb02d6f6584 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:02:27 +0200 Subject: [PATCH 04/12] chore: remove accidentally committed build artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../queryweaver_client.egg-info/PKG-INFO | 10 - .../queryweaver_client.egg-info/SOURCES.txt | 10 - .../dependency_links.txt | 1 - .../queryweaver_client.egg-info/requires.txt | 4 - .../queryweaver_client.egg-info/top_level.txt | 1 - clients/ts/dist/client.d.ts.map | 1 - clients/ts/dist/client.js | 229 ------------------ clients/ts/dist/client.js.map | 1 - clients/ts/dist/index.d.ts.map | 1 - clients/ts/dist/index.js | 8 - clients/ts/dist/index.js.map | 1 - clients/ts/dist/types.d.ts.map | 1 - clients/ts/dist/types.js | 13 - clients/ts/dist/types.js.map | 1 - 14 files changed, 282 deletions(-) delete mode 100644 clients/python/queryweaver_client.egg-info/PKG-INFO delete mode 100644 clients/python/queryweaver_client.egg-info/SOURCES.txt delete mode 100644 clients/python/queryweaver_client.egg-info/dependency_links.txt delete mode 100644 clients/python/queryweaver_client.egg-info/requires.txt delete mode 100644 clients/python/queryweaver_client.egg-info/top_level.txt delete mode 100644 clients/ts/dist/client.d.ts.map delete mode 100644 clients/ts/dist/client.js delete mode 100644 clients/ts/dist/client.js.map delete mode 100644 clients/ts/dist/index.d.ts.map delete mode 100644 clients/ts/dist/index.js delete mode 100644 clients/ts/dist/index.js.map delete mode 100644 clients/ts/dist/types.d.ts.map delete mode 100644 clients/ts/dist/types.js delete mode 100644 clients/ts/dist/types.js.map diff --git a/clients/python/queryweaver_client.egg-info/PKG-INFO b/clients/python/queryweaver_client.egg-info/PKG-INFO deleted file mode 100644 index 9fa17d64..00000000 --- a/clients/python/queryweaver_client.egg-info/PKG-INFO +++ /dev/null @@ -1,10 +0,0 @@ -Metadata-Version: 2.4 -Name: queryweaver-client -Version: 0.1.0 -Summary: Lightweight Python client for QueryWeaver server -Requires-Dist: requests -Provides-Extra: dev -Requires-Dist: pytest; extra == "dev" -Dynamic: provides-extra -Dynamic: requires-dist -Dynamic: summary diff --git a/clients/python/queryweaver_client.egg-info/SOURCES.txt b/clients/python/queryweaver_client.egg-info/SOURCES.txt deleted file mode 100644 index 35578792..00000000 --- a/clients/python/queryweaver_client.egg-info/SOURCES.txt +++ /dev/null @@ -1,10 +0,0 @@ -README.md -setup.py -queryweaver_client/__init__.py -queryweaver_client/client.py -queryweaver_client.egg-info/PKG-INFO -queryweaver_client.egg-info/SOURCES.txt -queryweaver_client.egg-info/dependency_links.txt -queryweaver_client.egg-info/requires.txt -queryweaver_client.egg-info/top_level.txt -tests/test_client_basic.py \ No newline at end of file diff --git a/clients/python/queryweaver_client.egg-info/dependency_links.txt b/clients/python/queryweaver_client.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/clients/python/queryweaver_client.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/clients/python/queryweaver_client.egg-info/requires.txt b/clients/python/queryweaver_client.egg-info/requires.txt deleted file mode 100644 index 0c147cb1..00000000 --- a/clients/python/queryweaver_client.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests - -[dev] -pytest diff --git a/clients/python/queryweaver_client.egg-info/top_level.txt b/clients/python/queryweaver_client.egg-info/top_level.txt deleted file mode 100644 index 4a077b5c..00000000 --- a/clients/python/queryweaver_client.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -queryweaver_client diff --git a/clients/ts/dist/client.d.ts.map b/clients/ts/dist/client.d.ts.map deleted file mode 100644 index 49b20ebd..00000000 --- a/clients/ts/dist/client.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAY,MAAM,SAAS,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAyB;IAE/C;;;;;;;OAOG;gBACS,OAAO,GAAE,wBAA6B;IAelD,OAAO,CAAC,GAAG;YAIG,cAAc;IAiB5B;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAWtC;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWzD;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAW1D;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAY1D;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,GAAE,OAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAwBpG;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAY9E;;;OAGG;YACW,wBAAwB;CA0DvC"} \ No newline at end of file diff --git a/clients/ts/dist/client.js b/clients/ts/dist/client.js deleted file mode 100644 index 51764221..00000000 --- a/clients/ts/dist/client.js +++ /dev/null @@ -1,229 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryWeaverClient = void 0; -const types_1 = require("./types"); -const MESSAGE_DELIMITER = '|||FALKORDB_MESSAGE_BOUNDARY|||'; -/** - * QueryWeaver HTTP API client. - * - * Simple wrapper around the REST API exposed by a running QueryWeaver server. - * Supports API token authentication (Bearer) and session cookie usage. - * - * @example - * ```typescript - * // With API token authentication - * const client = new QueryWeaverClient({ - * baseUrl: 'http://localhost:5000', - * apiToken: 'your-api-token' - * }); - * - * // With session cookies (handled automatically by fetch) - * const client = new QueryWeaverClient({ - * baseUrl: 'http://localhost:5000' - * }); - * ``` - */ -class QueryWeaverClient { - /** - * Create a new QueryWeaver client. - * - * @param options - Client configuration options - * @param options.baseUrl - The base URL of the QueryWeaver server (default: 'http://localhost:5000') - * @param options.apiToken - Optional API token for Bearer authentication - * @param options.timeout - Request timeout in milliseconds (default: 30000) - */ - constructor(options = {}) { - this.baseUrl = (options.baseUrl || 'http://localhost:5000').replace(/\/$/, ''); - this.apiToken = options.apiToken; - this.timeout = options.timeout || 30000; - this.defaultHeaders = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }; - if (this.apiToken) { - this.defaultHeaders['Authorization'] = `Bearer ${this.apiToken}`; - } - } - url(path) { - return `${this.baseUrl}${path}`; - } - async raiseForStatus(response) { - if (!response.ok) { - let errorMessage; - let responseData; - try { - responseData = await response.json(); - const data = responseData; - errorMessage = data.error || data.message || `HTTP ${response.status}`; - } - catch { - errorMessage = await response.text() || `HTTP ${response.status}`; - } - throw new types_1.APIError(errorMessage, response.status, responseData); - } - } - /** - * List available user schemas/databases. - * - * @returns A list of database/schema names. - */ - async listSchemas() { - const response = await fetch(this.url('/graphs'), { - method: 'GET', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Get schema nodes/edges for a specific graph. - */ - async getSchema(graphId) { - const response = await fetch(this.url(`/graphs/${graphId}/data`), { - method: 'GET', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Delete a schema. - */ - async deleteSchema(graphId) { - const response = await fetch(this.url(`/graphs/${graphId}`), { - method: 'DELETE', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Refresh schema. - */ - async refreshSchema(graphId) { - const response = await fetch(this.url(`/graphs/${graphId}/refresh`), { - method: 'POST', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Connect to a database and return final result. - */ - async connectDatabase(dbUrl) { - const response = await fetch(this.url('/database'), { - method: 'POST', - headers: this.defaultHeaders, - body: JSON.stringify({ url: dbUrl }), - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return this.consumeStreamingResponse(response); - } - /** - * Run a natural language query and return final result. - * - * @param graphId - The database/schema ID to query - * @param chatData - The chat data containing the query messages - * @param autoConfirm - If true, automatically confirm destructive operations. - * If false (default), returns confirmation request for user approval. - * @returns The query result or confirmation request - */ - async query(graphId, chatData, autoConfirm = false) { - const response = await fetch(this.url(`/graphs/${graphId}`), { - method: 'POST', - headers: this.defaultHeaders, - body: JSON.stringify(chatData), - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - const result = await this.consumeStreamingResponse(response); - // If autoConfirm is enabled and result requires confirmation, automatically confirm - if (autoConfirm && result.type === 'destructive_confirmation') { - const confirmData = { - sql_query: result.sql_query, - confirmation: 'YES', - chat: chatData.messages, - }; - return this.confirm(graphId, confirmData); - } - return result; - } - /** - * Confirm destructive operation and return result. - */ - async confirm(graphId, confirmData) { - const response = await fetch(this.url(`/graphs/${graphId}/confirm`), { - method: 'POST', - headers: this.defaultHeaders, - body: JSON.stringify(confirmData), - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return this.consumeStreamingResponse(response); - } - /** - * Consume a streaming response and return the final result. - * Properly buffers chunks and splits on the exact MESSAGE_DELIMITER. - */ - async consumeStreamingResponse(response) { - const events = []; - if (!response.body) { - return {}; - } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - try { - let done = false; - while (!done) { - const { done: chunkDone, value } = await reader.read(); - done = chunkDone; - if (done) - break; - const chunk = decoder.decode(value, { stream: true }); - buffer += chunk; - // Split by the delimiter and process complete messages - const parts = buffer.split(MESSAGE_DELIMITER); - // Keep the last part in the buffer (it may be incomplete) - buffer = parts[parts.length - 1]; - // Process all complete messages - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i].trim(); - if (!part) - continue; - try { - const event = JSON.parse(part); - events.push(event); - } - catch { - // If not valid JSON, store as raw text - events.push({ raw: part }); - } - } - } - // Process any remaining data in the buffer - if (buffer.trim()) { - try { - const event = JSON.parse(buffer.trim()); - events.push(event); - } - catch { - events.push({ raw: buffer.trim() }); - } - } - } - finally { - reader.releaseLock(); - } - // Return the last event which typically contains the final result - return events.length > 0 ? events[events.length - 1] : {}; - } -} -exports.QueryWeaverClient = QueryWeaverClient; -//# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/clients/ts/dist/client.js.map b/clients/ts/dist/client.js.map deleted file mode 100644 index d6b993d1..00000000 --- a/clients/ts/dist/client.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/clients/ts/dist/index.d.ts.map b/clients/ts/dist/index.d.ts.map deleted file mode 100644 index 6fc0ce9a..00000000 --- a/clients/ts/dist/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"} \ No newline at end of file diff --git a/clients/ts/dist/index.js b/clients/ts/dist/index.js deleted file mode 100644 index 051eb1f7..00000000 --- a/clients/ts/dist/index.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APIError = exports.QueryWeaverClient = void 0; -var client_1 = require("./client"); -Object.defineProperty(exports, "QueryWeaverClient", { enumerable: true, get: function () { return client_1.QueryWeaverClient; } }); -var types_1 = require("./types"); -Object.defineProperty(exports, "APIError", { enumerable: true, get: function () { return types_1.APIError; } }); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/clients/ts/dist/index.js.map b/clients/ts/dist/index.js.map deleted file mode 100644 index 429fc316..00000000 --- a/clients/ts/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAC1B,iCAAmC;AAA1B,iGAAA,QAAQ,OAAA"} \ No newline at end of file diff --git a/clients/ts/dist/types.d.ts.map b/clients/ts/dist/types.d.ts.map deleted file mode 100644 index 43c9d2ca..00000000 --- a/clients/ts/dist/types.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,QAAS,SAAQ,KAAK;IAGxB,MAAM,EAAE,MAAM;IACd,QAAQ,CAAC,EAAE,OAAO;gBAFzB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,OAAO,YAAA;CAK5B"} \ No newline at end of file diff --git a/clients/ts/dist/types.js b/clients/ts/dist/types.js deleted file mode 100644 index c2504967..00000000 --- a/clients/ts/dist/types.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APIError = void 0; -class APIError extends Error { - constructor(message, status, response) { - super(message); - this.status = status; - this.response = response; - this.name = 'APIError'; - } -} -exports.APIError = APIError; -//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/clients/ts/dist/types.js.map b/clients/ts/dist/types.js.map deleted file mode 100644 index bb2a5c03..00000000 --- a/clients/ts/dist/types.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAyBA,MAAa,QAAS,SAAQ,KAAK;IACjC,YACE,OAAe,EACR,MAAc,EACd,QAAkB;QAEzB,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAU;QAGzB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AATD,4BASC"} \ No newline at end of file From 9bc3dee8d96aaced1cda81b169933c68dc529508 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:06:17 +0200 Subject: [PATCH 05/12] fix: address copilot reviewer comments on PR #475 - Fix regex to capture search_path values with spaces after commas (e.g. $user, public) by matching up to next -c option or EOL - Set session search_path explicitly after connecting so sample queries resolve to the correct schema - Use versionless PostgreSQL docs link (/docs/current/) - Clarify case-sensitivity note for schema names in troubleshooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/loaders/postgres_loader.py | 9 +- .../queryweaver_client.egg-info/PKG-INFO | 10 + .../queryweaver_client.egg-info/SOURCES.txt | 10 + .../dependency_links.txt | 1 + .../queryweaver_client.egg-info/requires.txt | 4 + .../queryweaver_client.egg-info/top_level.txt | 1 + clients/ts/dist/client.d.ts.map | 1 + clients/ts/dist/client.js | 229 ++++++++++++++++++ clients/ts/dist/client.js.map | 1 + clients/ts/dist/index.d.ts.map | 1 + clients/ts/dist/index.js | 8 + clients/ts/dist/index.js.map | 1 + clients/ts/dist/types.d.ts.map | 1 + clients/ts/dist/types.js | 13 + clients/ts/dist/types.js.map | 1 + docs/postgres_loader.md | 4 +- tests/test_postgres_loader.py | 6 + 17 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 clients/python/queryweaver_client.egg-info/PKG-INFO create mode 100644 clients/python/queryweaver_client.egg-info/SOURCES.txt create mode 100644 clients/python/queryweaver_client.egg-info/dependency_links.txt create mode 100644 clients/python/queryweaver_client.egg-info/requires.txt create mode 100644 clients/python/queryweaver_client.egg-info/top_level.txt create mode 100644 clients/ts/dist/client.d.ts.map create mode 100644 clients/ts/dist/client.js create mode 100644 clients/ts/dist/client.js.map create mode 100644 clients/ts/dist/index.d.ts.map create mode 100644 clients/ts/dist/index.js create mode 100644 clients/ts/dist/index.js.map create mode 100644 clients/ts/dist/types.d.ts.map create mode 100644 clients/ts/dist/types.js create mode 100644 clients/ts/dist/types.js.map diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index f583ea59..831ceb68 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -123,7 +123,8 @@ def parse_schema_from_url(connection_url: str) -> str: # Parse -c search_path=value from options # Format can be: -csearch_path=schema or -c search_path=schema - match = re.search(r'-c\s*search_path\s*=\s*([^\s]+)', options_str, re.IGNORECASE) + # Capture up to the next "-c" option (or end of string) so values like "$user, public" work. + match = re.search(r'-c\s*search_path\s*=\s*(.+?)(?=\s+-c|\s*$)', options_str, re.IGNORECASE) if match: search_path = match.group(1) schemas = search_path.split(',') @@ -160,6 +161,12 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s conn = psycopg2.connect(connection_url) cursor = conn.cursor() + # Set the session search_path to the parsed schema so unqualified + # table references (e.g. in sample queries) resolve correctly. + cursor.execute( + sql.SQL("SET search_path TO {}").format(sql.Identifier(schema)) + ) + # Extract database name from connection URL db_name = connection_url.split('/')[-1] if '?' in db_name: diff --git a/clients/python/queryweaver_client.egg-info/PKG-INFO b/clients/python/queryweaver_client.egg-info/PKG-INFO new file mode 100644 index 00000000..9fa17d64 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 2.4 +Name: queryweaver-client +Version: 0.1.0 +Summary: Lightweight Python client for QueryWeaver server +Requires-Dist: requests +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" +Dynamic: provides-extra +Dynamic: requires-dist +Dynamic: summary diff --git a/clients/python/queryweaver_client.egg-info/SOURCES.txt b/clients/python/queryweaver_client.egg-info/SOURCES.txt new file mode 100644 index 00000000..35578792 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/SOURCES.txt @@ -0,0 +1,10 @@ +README.md +setup.py +queryweaver_client/__init__.py +queryweaver_client/client.py +queryweaver_client.egg-info/PKG-INFO +queryweaver_client.egg-info/SOURCES.txt +queryweaver_client.egg-info/dependency_links.txt +queryweaver_client.egg-info/requires.txt +queryweaver_client.egg-info/top_level.txt +tests/test_client_basic.py \ No newline at end of file diff --git a/clients/python/queryweaver_client.egg-info/dependency_links.txt b/clients/python/queryweaver_client.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/clients/python/queryweaver_client.egg-info/requires.txt b/clients/python/queryweaver_client.egg-info/requires.txt new file mode 100644 index 00000000..0c147cb1 --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/requires.txt @@ -0,0 +1,4 @@ +requests + +[dev] +pytest diff --git a/clients/python/queryweaver_client.egg-info/top_level.txt b/clients/python/queryweaver_client.egg-info/top_level.txt new file mode 100644 index 00000000..4a077b5c --- /dev/null +++ b/clients/python/queryweaver_client.egg-info/top_level.txt @@ -0,0 +1 @@ +queryweaver_client diff --git a/clients/ts/dist/client.d.ts.map b/clients/ts/dist/client.d.ts.map new file mode 100644 index 00000000..49b20ebd --- /dev/null +++ b/clients/ts/dist/client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAY,MAAM,SAAS,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAyB;IAE/C;;;;;;;OAOG;gBACS,OAAO,GAAE,wBAA6B;IAelD,OAAO,CAAC,GAAG;YAIG,cAAc;IAiB5B;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAWtC;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWzD;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAW1D;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAY1D;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,GAAE,OAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAwBpG;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAY9E;;;OAGG;YACW,wBAAwB;CA0DvC"} \ No newline at end of file diff --git a/clients/ts/dist/client.js b/clients/ts/dist/client.js new file mode 100644 index 00000000..51764221 --- /dev/null +++ b/clients/ts/dist/client.js @@ -0,0 +1,229 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryWeaverClient = void 0; +const types_1 = require("./types"); +const MESSAGE_DELIMITER = '|||FALKORDB_MESSAGE_BOUNDARY|||'; +/** + * QueryWeaver HTTP API client. + * + * Simple wrapper around the REST API exposed by a running QueryWeaver server. + * Supports API token authentication (Bearer) and session cookie usage. + * + * @example + * ```typescript + * // With API token authentication + * const client = new QueryWeaverClient({ + * baseUrl: 'http://localhost:5000', + * apiToken: 'your-api-token' + * }); + * + * // With session cookies (handled automatically by fetch) + * const client = new QueryWeaverClient({ + * baseUrl: 'http://localhost:5000' + * }); + * ``` + */ +class QueryWeaverClient { + /** + * Create a new QueryWeaver client. + * + * @param options - Client configuration options + * @param options.baseUrl - The base URL of the QueryWeaver server (default: 'http://localhost:5000') + * @param options.apiToken - Optional API token for Bearer authentication + * @param options.timeout - Request timeout in milliseconds (default: 30000) + */ + constructor(options = {}) { + this.baseUrl = (options.baseUrl || 'http://localhost:5000').replace(/\/$/, ''); + this.apiToken = options.apiToken; + this.timeout = options.timeout || 30000; + this.defaultHeaders = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }; + if (this.apiToken) { + this.defaultHeaders['Authorization'] = `Bearer ${this.apiToken}`; + } + } + url(path) { + return `${this.baseUrl}${path}`; + } + async raiseForStatus(response) { + if (!response.ok) { + let errorMessage; + let responseData; + try { + responseData = await response.json(); + const data = responseData; + errorMessage = data.error || data.message || `HTTP ${response.status}`; + } + catch { + errorMessage = await response.text() || `HTTP ${response.status}`; + } + throw new types_1.APIError(errorMessage, response.status, responseData); + } + } + /** + * List available user schemas/databases. + * + * @returns A list of database/schema names. + */ + async listSchemas() { + const response = await fetch(this.url('/graphs'), { + method: 'GET', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Get schema nodes/edges for a specific graph. + */ + async getSchema(graphId) { + const response = await fetch(this.url(`/graphs/${graphId}/data`), { + method: 'GET', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Delete a schema. + */ + async deleteSchema(graphId) { + const response = await fetch(this.url(`/graphs/${graphId}`), { + method: 'DELETE', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Refresh schema. + */ + async refreshSchema(graphId) { + const response = await fetch(this.url(`/graphs/${graphId}/refresh`), { + method: 'POST', + headers: this.defaultHeaders, + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return response.json(); + } + /** + * Connect to a database and return final result. + */ + async connectDatabase(dbUrl) { + const response = await fetch(this.url('/database'), { + method: 'POST', + headers: this.defaultHeaders, + body: JSON.stringify({ url: dbUrl }), + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return this.consumeStreamingResponse(response); + } + /** + * Run a natural language query and return final result. + * + * @param graphId - The database/schema ID to query + * @param chatData - The chat data containing the query messages + * @param autoConfirm - If true, automatically confirm destructive operations. + * If false (default), returns confirmation request for user approval. + * @returns The query result or confirmation request + */ + async query(graphId, chatData, autoConfirm = false) { + const response = await fetch(this.url(`/graphs/${graphId}`), { + method: 'POST', + headers: this.defaultHeaders, + body: JSON.stringify(chatData), + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + const result = await this.consumeStreamingResponse(response); + // If autoConfirm is enabled and result requires confirmation, automatically confirm + if (autoConfirm && result.type === 'destructive_confirmation') { + const confirmData = { + sql_query: result.sql_query, + confirmation: 'YES', + chat: chatData.messages, + }; + return this.confirm(graphId, confirmData); + } + return result; + } + /** + * Confirm destructive operation and return result. + */ + async confirm(graphId, confirmData) { + const response = await fetch(this.url(`/graphs/${graphId}/confirm`), { + method: 'POST', + headers: this.defaultHeaders, + body: JSON.stringify(confirmData), + signal: AbortSignal.timeout(this.timeout), + }); + await this.raiseForStatus(response); + return this.consumeStreamingResponse(response); + } + /** + * Consume a streaming response and return the final result. + * Properly buffers chunks and splits on the exact MESSAGE_DELIMITER. + */ + async consumeStreamingResponse(response) { + const events = []; + if (!response.body) { + return {}; + } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + try { + let done = false; + while (!done) { + const { done: chunkDone, value } = await reader.read(); + done = chunkDone; + if (done) + break; + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + // Split by the delimiter and process complete messages + const parts = buffer.split(MESSAGE_DELIMITER); + // Keep the last part in the buffer (it may be incomplete) + buffer = parts[parts.length - 1]; + // Process all complete messages + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i].trim(); + if (!part) + continue; + try { + const event = JSON.parse(part); + events.push(event); + } + catch { + // If not valid JSON, store as raw text + events.push({ raw: part }); + } + } + } + // Process any remaining data in the buffer + if (buffer.trim()) { + try { + const event = JSON.parse(buffer.trim()); + events.push(event); + } + catch { + events.push({ raw: buffer.trim() }); + } + } + } + finally { + reader.releaseLock(); + } + // Return the last event which typically contains the final result + return events.length > 0 ? events[events.length - 1] : {}; + } +} +exports.QueryWeaverClient = QueryWeaverClient; +//# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/clients/ts/dist/client.js.map b/clients/ts/dist/client.js.map new file mode 100644 index 00000000..d6b993d1 --- /dev/null +++ b/clients/ts/dist/client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/clients/ts/dist/index.d.ts.map b/clients/ts/dist/index.d.ts.map new file mode 100644 index 00000000..6fc0ce9a --- /dev/null +++ b/clients/ts/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"} \ No newline at end of file diff --git a/clients/ts/dist/index.js b/clients/ts/dist/index.js new file mode 100644 index 00000000..051eb1f7 --- /dev/null +++ b/clients/ts/dist/index.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APIError = exports.QueryWeaverClient = void 0; +var client_1 = require("./client"); +Object.defineProperty(exports, "QueryWeaverClient", { enumerable: true, get: function () { return client_1.QueryWeaverClient; } }); +var types_1 = require("./types"); +Object.defineProperty(exports, "APIError", { enumerable: true, get: function () { return types_1.APIError; } }); +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/clients/ts/dist/index.js.map b/clients/ts/dist/index.js.map new file mode 100644 index 00000000..429fc316 --- /dev/null +++ b/clients/ts/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAC1B,iCAAmC;AAA1B,iGAAA,QAAQ,OAAA"} \ No newline at end of file diff --git a/clients/ts/dist/types.d.ts.map b/clients/ts/dist/types.d.ts.map new file mode 100644 index 00000000..43c9d2ca --- /dev/null +++ b/clients/ts/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,QAAS,SAAQ,KAAK;IAGxB,MAAM,EAAE,MAAM;IACd,QAAQ,CAAC,EAAE,OAAO;gBAFzB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,OAAO,YAAA;CAK5B"} \ No newline at end of file diff --git a/clients/ts/dist/types.js b/clients/ts/dist/types.js new file mode 100644 index 00000000..c2504967 --- /dev/null +++ b/clients/ts/dist/types.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.APIError = void 0; +class APIError extends Error { + constructor(message, status, response) { + super(message); + this.status = status; + this.response = response; + this.name = 'APIError'; + } +} +exports.APIError = APIError; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/clients/ts/dist/types.js.map b/clients/ts/dist/types.js.map new file mode 100644 index 00000000..bb2a5c03 --- /dev/null +++ b/clients/ts/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAyBA,MAAa,QAAS,SAAQ,KAAK;IACjC,YACE,OAAe,EACR,MAAc,EACd,QAAkB;QAEzB,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAU;QAGzB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AATD,4BASC"} \ No newline at end of file diff --git a/docs/postgres_loader.md b/docs/postgres_loader.md index e4dac9d5..d6484b19 100644 --- a/docs/postgres_loader.md +++ b/docs/postgres_loader.md @@ -90,7 +90,7 @@ postgresql://[username[:password]@][host[:port]][/database][?options] ### Custom Schema Configuration By default, the loader extracts tables from the `public` schema. To use a different schema, add the `search_path` option to your connection URL using PostgreSQL's standard `options` parameter. -More info [here](https://www.postgresql.org/docs/18/runtime-config-client.html#GUC-SEARCH-PATH). +More info [here](https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-SEARCH-PATH). **Format:** ``` @@ -260,7 +260,7 @@ PostgreSQL schema loaded successfully. Found 15 tables. 3. **No Tables Found**: - Check that tables exist in the target schema - If using a custom schema, verify the `search_path` option is correctly formatted -- Ensure the schema name is spelled correctly (schema names are case-sensitive) +- Ensure the schema name is spelled correctly (schema names are case-sensitive when created or referenced with double quotes; unquoted identifiers are folded to lower-case) 4. **Graph Database Error**: Verify that the graph database is running and accessible 5. **Schema Permission Error**: Ensure the database user has `USAGE` permission on the target schema: ```sql diff --git a/tests/test_postgres_loader.py b/tests/test_postgres_loader.py index 3486c752..bdd0947c 100644 --- a/tests/test_postgres_loader.py +++ b/tests/test_postgres_loader.py @@ -195,6 +195,12 @@ def test_search_path_dollar_user_first_returns_second(self): result = PostgresLoader.parse_schema_from_url(url) self.assertEqual(result, 'my_schema') + def test_search_path_dollar_user_space_after_comma(self): + """Test that $user, my_schema (space after comma) returns my_schema""" + url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user,%20my_schema" + result = PostgresLoader.parse_schema_from_url(url) + self.assertEqual(result, 'my_schema') + def test_search_path_dollar_user_only_returns_public(self): """Test that $user alone returns 'public'""" url = "postgresql://user:pass@localhost:5432/mydb?options=-csearch_path%3D%24user" From 3075e95ca7f68c120a7ab1ecf202b638f9a06392 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:06:26 +0200 Subject: [PATCH 06/12] chore: gitignore build artifacts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 5 +- .../queryweaver_client.egg-info/PKG-INFO | 10 - .../queryweaver_client.egg-info/SOURCES.txt | 10 - .../dependency_links.txt | 1 - .../queryweaver_client.egg-info/requires.txt | 4 - .../queryweaver_client.egg-info/top_level.txt | 1 - clients/ts/dist/client.d.ts.map | 1 - clients/ts/dist/client.js | 229 ------------------ clients/ts/dist/client.js.map | 1 - clients/ts/dist/index.d.ts.map | 1 - clients/ts/dist/index.js | 8 - clients/ts/dist/index.js.map | 1 - clients/ts/dist/types.d.ts.map | 1 - clients/ts/dist/types.js | 13 - clients/ts/dist/types.js.map | 1 - 15 files changed, 4 insertions(+), 283 deletions(-) delete mode 100644 clients/python/queryweaver_client.egg-info/PKG-INFO delete mode 100644 clients/python/queryweaver_client.egg-info/SOURCES.txt delete mode 100644 clients/python/queryweaver_client.egg-info/dependency_links.txt delete mode 100644 clients/python/queryweaver_client.egg-info/requires.txt delete mode 100644 clients/python/queryweaver_client.egg-info/top_level.txt delete mode 100644 clients/ts/dist/client.d.ts.map delete mode 100644 clients/ts/dist/client.js delete mode 100644 clients/ts/dist/client.js.map delete mode 100644 clients/ts/dist/index.d.ts.map delete mode 100644 clients/ts/dist/index.js delete mode 100644 clients/ts/dist/index.js.map delete mode 100644 clients/ts/dist/types.d.ts.map delete mode 100644 clients/ts/dist/types.js delete mode 100644 clients/ts/dist/types.js.map diff --git a/.gitignore b/.gitignore index a9e199cd..3bf6a813 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ demo_tokens.py /blob-report/ /playwright/.cache/ /playwright/.auth/ -e2e/.auth/ \ No newline at end of file +e2e/.auth/ +# Build artifacts +clients/python/queryweaver_client.egg-info/ +clients/ts/dist/ diff --git a/clients/python/queryweaver_client.egg-info/PKG-INFO b/clients/python/queryweaver_client.egg-info/PKG-INFO deleted file mode 100644 index 9fa17d64..00000000 --- a/clients/python/queryweaver_client.egg-info/PKG-INFO +++ /dev/null @@ -1,10 +0,0 @@ -Metadata-Version: 2.4 -Name: queryweaver-client -Version: 0.1.0 -Summary: Lightweight Python client for QueryWeaver server -Requires-Dist: requests -Provides-Extra: dev -Requires-Dist: pytest; extra == "dev" -Dynamic: provides-extra -Dynamic: requires-dist -Dynamic: summary diff --git a/clients/python/queryweaver_client.egg-info/SOURCES.txt b/clients/python/queryweaver_client.egg-info/SOURCES.txt deleted file mode 100644 index 35578792..00000000 --- a/clients/python/queryweaver_client.egg-info/SOURCES.txt +++ /dev/null @@ -1,10 +0,0 @@ -README.md -setup.py -queryweaver_client/__init__.py -queryweaver_client/client.py -queryweaver_client.egg-info/PKG-INFO -queryweaver_client.egg-info/SOURCES.txt -queryweaver_client.egg-info/dependency_links.txt -queryweaver_client.egg-info/requires.txt -queryweaver_client.egg-info/top_level.txt -tests/test_client_basic.py \ No newline at end of file diff --git a/clients/python/queryweaver_client.egg-info/dependency_links.txt b/clients/python/queryweaver_client.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891..00000000 --- a/clients/python/queryweaver_client.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/clients/python/queryweaver_client.egg-info/requires.txt b/clients/python/queryweaver_client.egg-info/requires.txt deleted file mode 100644 index 0c147cb1..00000000 --- a/clients/python/queryweaver_client.egg-info/requires.txt +++ /dev/null @@ -1,4 +0,0 @@ -requests - -[dev] -pytest diff --git a/clients/python/queryweaver_client.egg-info/top_level.txt b/clients/python/queryweaver_client.egg-info/top_level.txt deleted file mode 100644 index 4a077b5c..00000000 --- a/clients/python/queryweaver_client.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -queryweaver_client diff --git a/clients/ts/dist/client.d.ts.map b/clients/ts/dist/client.d.ts.map deleted file mode 100644 index 49b20ebd..00000000 --- a/clients/ts/dist/client.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAY,MAAM,SAAS,CAAC;AAIjG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAyB;IAE/C;;;;;;;OAOG;gBACS,OAAO,GAAE,wBAA6B;IAelD,OAAO,CAAC,GAAG;YAIG,cAAc;IAiB5B;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAWtC;;OAEG;IACG,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWtD;;OAEG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWzD;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAW1D;;OAEG;IACG,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAY1D;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAW,GAAE,OAAe,GAAG,OAAO,CAAC,WAAW,CAAC;IAwBpG;;OAEG;IACG,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC;IAY9E;;;OAGG;YACW,wBAAwB;CA0DvC"} \ No newline at end of file diff --git a/clients/ts/dist/client.js b/clients/ts/dist/client.js deleted file mode 100644 index 51764221..00000000 --- a/clients/ts/dist/client.js +++ /dev/null @@ -1,229 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryWeaverClient = void 0; -const types_1 = require("./types"); -const MESSAGE_DELIMITER = '|||FALKORDB_MESSAGE_BOUNDARY|||'; -/** - * QueryWeaver HTTP API client. - * - * Simple wrapper around the REST API exposed by a running QueryWeaver server. - * Supports API token authentication (Bearer) and session cookie usage. - * - * @example - * ```typescript - * // With API token authentication - * const client = new QueryWeaverClient({ - * baseUrl: 'http://localhost:5000', - * apiToken: 'your-api-token' - * }); - * - * // With session cookies (handled automatically by fetch) - * const client = new QueryWeaverClient({ - * baseUrl: 'http://localhost:5000' - * }); - * ``` - */ -class QueryWeaverClient { - /** - * Create a new QueryWeaver client. - * - * @param options - Client configuration options - * @param options.baseUrl - The base URL of the QueryWeaver server (default: 'http://localhost:5000') - * @param options.apiToken - Optional API token for Bearer authentication - * @param options.timeout - Request timeout in milliseconds (default: 30000) - */ - constructor(options = {}) { - this.baseUrl = (options.baseUrl || 'http://localhost:5000').replace(/\/$/, ''); - this.apiToken = options.apiToken; - this.timeout = options.timeout || 30000; - this.defaultHeaders = { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }; - if (this.apiToken) { - this.defaultHeaders['Authorization'] = `Bearer ${this.apiToken}`; - } - } - url(path) { - return `${this.baseUrl}${path}`; - } - async raiseForStatus(response) { - if (!response.ok) { - let errorMessage; - let responseData; - try { - responseData = await response.json(); - const data = responseData; - errorMessage = data.error || data.message || `HTTP ${response.status}`; - } - catch { - errorMessage = await response.text() || `HTTP ${response.status}`; - } - throw new types_1.APIError(errorMessage, response.status, responseData); - } - } - /** - * List available user schemas/databases. - * - * @returns A list of database/schema names. - */ - async listSchemas() { - const response = await fetch(this.url('/graphs'), { - method: 'GET', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Get schema nodes/edges for a specific graph. - */ - async getSchema(graphId) { - const response = await fetch(this.url(`/graphs/${graphId}/data`), { - method: 'GET', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Delete a schema. - */ - async deleteSchema(graphId) { - const response = await fetch(this.url(`/graphs/${graphId}`), { - method: 'DELETE', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Refresh schema. - */ - async refreshSchema(graphId) { - const response = await fetch(this.url(`/graphs/${graphId}/refresh`), { - method: 'POST', - headers: this.defaultHeaders, - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return response.json(); - } - /** - * Connect to a database and return final result. - */ - async connectDatabase(dbUrl) { - const response = await fetch(this.url('/database'), { - method: 'POST', - headers: this.defaultHeaders, - body: JSON.stringify({ url: dbUrl }), - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return this.consumeStreamingResponse(response); - } - /** - * Run a natural language query and return final result. - * - * @param graphId - The database/schema ID to query - * @param chatData - The chat data containing the query messages - * @param autoConfirm - If true, automatically confirm destructive operations. - * If false (default), returns confirmation request for user approval. - * @returns The query result or confirmation request - */ - async query(graphId, chatData, autoConfirm = false) { - const response = await fetch(this.url(`/graphs/${graphId}`), { - method: 'POST', - headers: this.defaultHeaders, - body: JSON.stringify(chatData), - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - const result = await this.consumeStreamingResponse(response); - // If autoConfirm is enabled and result requires confirmation, automatically confirm - if (autoConfirm && result.type === 'destructive_confirmation') { - const confirmData = { - sql_query: result.sql_query, - confirmation: 'YES', - chat: chatData.messages, - }; - return this.confirm(graphId, confirmData); - } - return result; - } - /** - * Confirm destructive operation and return result. - */ - async confirm(graphId, confirmData) { - const response = await fetch(this.url(`/graphs/${graphId}/confirm`), { - method: 'POST', - headers: this.defaultHeaders, - body: JSON.stringify(confirmData), - signal: AbortSignal.timeout(this.timeout), - }); - await this.raiseForStatus(response); - return this.consumeStreamingResponse(response); - } - /** - * Consume a streaming response and return the final result. - * Properly buffers chunks and splits on the exact MESSAGE_DELIMITER. - */ - async consumeStreamingResponse(response) { - const events = []; - if (!response.body) { - return {}; - } - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - try { - let done = false; - while (!done) { - const { done: chunkDone, value } = await reader.read(); - done = chunkDone; - if (done) - break; - const chunk = decoder.decode(value, { stream: true }); - buffer += chunk; - // Split by the delimiter and process complete messages - const parts = buffer.split(MESSAGE_DELIMITER); - // Keep the last part in the buffer (it may be incomplete) - buffer = parts[parts.length - 1]; - // Process all complete messages - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i].trim(); - if (!part) - continue; - try { - const event = JSON.parse(part); - events.push(event); - } - catch { - // If not valid JSON, store as raw text - events.push({ raw: part }); - } - } - } - // Process any remaining data in the buffer - if (buffer.trim()) { - try { - const event = JSON.parse(buffer.trim()); - events.push(event); - } - catch { - events.push({ raw: buffer.trim() }); - } - } - } - finally { - reader.releaseLock(); - } - // Return the last event which typically contains the final result - return events.length > 0 ? events[events.length - 1] : {}; - } -} -exports.QueryWeaverClient = QueryWeaverClient; -//# sourceMappingURL=client.js.map \ No newline at end of file diff --git a/clients/ts/dist/client.js.map b/clients/ts/dist/client.js.map deleted file mode 100644 index d6b993d1..00000000 --- a/clients/ts/dist/client.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":";;;AAAA,mCAAiG;AAEjG,MAAM,iBAAiB,GAAG,iCAAiC,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAa,iBAAiB;IAM5B;;;;;;;OAOG;IACH,YAAY,UAAoC,EAAE;QAChD,IAAI,CAAC,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,IAAI,uBAAuB,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC/E,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;QACjC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,KAAK,CAAC;QAExC,IAAI,CAAC,cAAc,GAAG;YACpB,QAAQ,EAAE,kBAAkB;YAC5B,cAAc,EAAE,kBAAkB;SACnC,CAAC;QAEF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,GAAG,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;QACnE,CAAC;IACH,CAAC;IAEO,GAAG,CAAC,IAAY;QACtB,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC;IAClC,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,QAAkB;QAC7C,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,IAAI,YAAoB,CAAC;YACzB,IAAI,YAAqB,CAAC;YAE1B,IAAI,CAAC;gBACH,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBACrC,MAAM,IAAI,GAAG,YAAuC,CAAC;gBACrD,YAAY,GAAI,IAAI,CAAC,KAAgB,IAAK,IAAI,CAAC,OAAkB,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;YACjG,CAAC;YAAC,MAAM,CAAC;gBACP,YAAY,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,IAAI,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpE,CAAC;YAED,MAAM,IAAI,gBAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QAClE,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,WAAW;QACf,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE;YAChD,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,SAAS,CAAC,OAAe;QAC7B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,OAAO,OAAO,CAAC,EAAE;YAChE,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY,CAAC,OAAe;QAChC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,OAAO,EAAE,CAAC,EAAE;YAC3D,MAAM,EAAE,QAAQ;YAChB,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,aAAa,CAAC,OAAe;QACjC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,OAAO,UAAU,CAAC,EAAE;YACnE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,eAAe,CAAC,KAAa;QACjC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE;YAClD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;YACpC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,KAAK,CAAC,OAAe,EAAE,QAAkB,EAAE,cAAuB,KAAK;QAC3E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,OAAO,EAAE,CAAC,EAAE;YAC3D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;YAC9B,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC,CAAC;QAE7D,oFAAoF;QACpF,IAAI,WAAW,IAAI,MAAM,CAAC,IAAI,KAAK,0BAA0B,EAAE,CAAC;YAC9D,MAAM,WAAW,GAAgB;gBAC/B,SAAS,EAAE,MAAM,CAAC,SAAmB;gBACrC,YAAY,EAAE,KAAK;gBACnB,IAAI,EAAE,QAAQ,CAAC,QAAQ;aACxB,CAAC;YACF,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;QAC5C,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,WAAwB;QACrD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,OAAO,UAAU,CAAC,EAAE;YACnE,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,IAAI,CAAC,cAAc;YAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;YACjC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC;IAED;;;OAGG;IACK,KAAK,CAAC,wBAAwB,CAAC,QAAkB;QACvD,MAAM,MAAM,GAAkB,EAAE,CAAC;QAEjC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;QAClC,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,IAAI,CAAC;YACH,IAAI,IAAI,GAAG,KAAK,CAAC;YACjB,OAAO,CAAC,IAAI,EAAE,CAAC;gBACb,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBACvD,IAAI,GAAG,SAAS,CAAC;gBACjB,IAAI,IAAI;oBAAE,MAAM;gBAEhB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;gBACtD,MAAM,IAAI,KAAK,CAAC;gBAEhB,uDAAuD;gBACvD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;gBAE9C,0DAA0D;gBAC1D,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAEjC,gCAAgC;gBAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;oBAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBAC7B,IAAI,CAAC,IAAI;wBAAE,SAAS;oBAEpB,IAAI,CAAC;wBACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;wBAC/B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;oBACrB,CAAC;oBAAC,MAAM,CAAC;wBACP,uCAAuC;wBACvC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC;oBAC7B,CAAC;gBACH,CAAC;YACH,CAAC;YAED,2CAA2C;YAC3C,IAAI,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;gBAClB,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;oBACxC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACrB,CAAC;gBAAC,MAAM,CAAC;oBACP,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,WAAW,EAAE,CAAC;QACvB,CAAC;QAED,kEAAkE;QAClE,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,CAAC;CACF;AAzOD,8CAyOC"} \ No newline at end of file diff --git a/clients/ts/dist/index.d.ts.map b/clients/ts/dist/index.d.ts.map deleted file mode 100644 index 6fc0ce9a..00000000 --- a/clients/ts/dist/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,WAAW,EAAE,wBAAwB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"} \ No newline at end of file diff --git a/clients/ts/dist/index.js b/clients/ts/dist/index.js deleted file mode 100644 index 051eb1f7..00000000 --- a/clients/ts/dist/index.js +++ /dev/null @@ -1,8 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APIError = exports.QueryWeaverClient = void 0; -var client_1 = require("./client"); -Object.defineProperty(exports, "QueryWeaverClient", { enumerable: true, get: function () { return client_1.QueryWeaverClient; } }); -var types_1 = require("./types"); -Object.defineProperty(exports, "APIError", { enumerable: true, get: function () { return types_1.APIError; } }); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/clients/ts/dist/index.js.map b/clients/ts/dist/index.js.map deleted file mode 100644 index 429fc316..00000000 --- a/clients/ts/dist/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,mCAA6C;AAApC,2GAAA,iBAAiB,OAAA;AAC1B,iCAAmC;AAA1B,iGAAA,QAAQ,OAAA"} \ No newline at end of file diff --git a/clients/ts/dist/types.d.ts.map b/clients/ts/dist/types.d.ts.map deleted file mode 100644 index 43c9d2ca..00000000 --- a/clients/ts/dist/types.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,QAAQ,CAAC;IACtC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,WAAW,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,QAAS,SAAQ,KAAK;IAGxB,MAAM,EAAE,MAAM;IACd,QAAQ,CAAC,EAAE,OAAO;gBAFzB,OAAO,EAAE,MAAM,EACR,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,OAAO,YAAA;CAK5B"} \ No newline at end of file diff --git a/clients/ts/dist/types.js b/clients/ts/dist/types.js deleted file mode 100644 index c2504967..00000000 --- a/clients/ts/dist/types.js +++ /dev/null @@ -1,13 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.APIError = void 0; -class APIError extends Error { - constructor(message, status, response) { - super(message); - this.status = status; - this.response = response; - this.name = 'APIError'; - } -} -exports.APIError = APIError; -//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/clients/ts/dist/types.js.map b/clients/ts/dist/types.js.map deleted file mode 100644 index bb2a5c03..00000000 --- a/clients/ts/dist/types.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;;AAyBA,MAAa,QAAS,SAAQ,KAAK;IACjC,YACE,OAAe,EACR,MAAc,EACd,QAAkB;QAEzB,KAAK,CAAC,OAAO,CAAC,CAAC;QAHR,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAU;QAGzB,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AATD,4BASC"} \ No newline at end of file From 5e31f3cd280971a2dff0489ab17ddb9cf325d686 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:27:12 +0200 Subject: [PATCH 07/12] fix: replace ReDoS-vulnerable regex in parse_schema_from_url Replace (.+?)(?=\s+-c|\s*$) with [^\s,]+(?:\s*,\s*[^\s,]+)* to eliminate polynomial backtracking flagged by CodeQL. The new pattern uses unambiguous character classes with no overlapping quantifiers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/loaders/postgres_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index 831ceb68..cdb3fae1 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -123,8 +123,8 @@ def parse_schema_from_url(connection_url: str) -> str: # Parse -c search_path=value from options # Format can be: -csearch_path=schema or -c search_path=schema - # Capture up to the next "-c" option (or end of string) so values like "$user, public" work. - match = re.search(r'-c\s*search_path\s*=\s*(.+?)(?=\s+-c|\s*$)', options_str, re.IGNORECASE) + # Match comma-separated schema tokens (supports spaces after commas). + match = re.search(r'-c\s*search_path\s*=\s*([^\s,]+(?:\s*,\s*[^\s,]+)*)', options_str, re.IGNORECASE) if match: search_path = match.group(1) schemas = search_path.split(',') From a12fa8843420e7fb131e58ccd65e522b07a5ba0f Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:52:49 +0200 Subject: [PATCH 08/12] fix: validate schema input instead of silent sanitization, fix doc URL encoding - DatabaseModal: Show validation error for invalid schema characters instead of silently stripping them. Throw error on submit if invalid chars present. - docs: URL-encode the example URL to prevent copy/paste connection failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- app/src/components/modals/DatabaseModal.tsx | 30 +++++++++++++++------ docs/postgres_loader.md | 2 +- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/app/src/components/modals/DatabaseModal.tsx b/app/src/components/modals/DatabaseModal.tsx index e48c28e3..32590e63 100644 --- a/app/src/components/modals/DatabaseModal.tsx +++ b/app/src/components/modals/DatabaseModal.tsx @@ -30,6 +30,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [schema, setSchema] = useState(""); + const [schemaError, setSchemaError] = useState(""); const [isConnecting, setIsConnecting] = useState(false); const [connectionSteps, setConnectionSteps] = useState([]); const { refreshGraphs } = useDatabase(); @@ -98,10 +99,10 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { // Append schema option for PostgreSQL if provided if (selectedDatabase === 'postgresql' && schema.trim()) { - const sanitized = schema.trim().replace(/[^a-zA-Z0-9_]/g, ''); - if (sanitized) { - builtUrl.searchParams.set('options', `-csearch_path=${sanitized}`); + if (/[^a-zA-Z0-9_]/.test(schema.trim())) { + throw new Error('Schema name can only contain letters, digits, and underscores'); } + builtUrl.searchParams.set('options', `-csearch_path=${schema.trim()}`); } dbUrl = builtUrl.toString(); @@ -188,6 +189,7 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { setUsername(""); setPassword(""); setSchema(""); + setSchemaError(""); setConnectionSteps([]); }, 1000); } else { @@ -407,12 +409,24 @@ const DatabaseModal = ({ open, onOpenChange }: DatabaseModalProps) => { data-testid="schema-input" placeholder="public" value={schema} - onChange={(e) => setSchema(e.target.value)} - className="bg-muted border-border" + onChange={(e) => { + const val = e.target.value; + setSchema(val); + if (val && /[^a-zA-Z0-9_]/.test(val)) { + setSchemaError('Schema name can only contain letters, digits, and underscores'); + } else { + setSchemaError(''); + } + }} + className={`bg-muted border-border ${schemaError ? 'border-red-500' : ''}`} /> -

- Leave empty to use the default 'public' schema -

+ {schemaError ? ( +

{schemaError}

+ ) : ( +

+ Leave empty to use the default 'public' schema +

+ )} )} diff --git a/docs/postgres_loader.md b/docs/postgres_loader.md index d6484b19..1f3ee838 100644 --- a/docs/postgres_loader.md +++ b/docs/postgres_loader.md @@ -276,7 +276,7 @@ To verify which schema will be used, you can test the search_path parsing: from api.loaders.postgres_loader import PostgresLoader # Test URL parsing -url = "postgresql://user:pass@localhost:5432/mydatabase?options=-c search_path=my_schema" +url = "postgresql://user:pass@localhost:5432/mydatabase?options=-csearch_path%3Dmy_schema" schema = PostgresLoader.parse_schema_from_url(url) print(f"Schema to be used: {schema}") # Output: my_schema {% endcapture %} From 6e33932851c06b82edf475661f86dadc7f7b0df1 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Mon, 9 Mar 2026 11:57:47 +0200 Subject: [PATCH 09/12] fix: revert doc URL to readable form to fix spellcheck The URL-encoded form (-csearch_path%3Dmy_schema) inside the Liquid capture block triggers spellcheck failures ('csearch', 'Dmy'). Reverted to readable form since Python's urlparse handles both formats fine. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/postgres_loader.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/postgres_loader.md b/docs/postgres_loader.md index 1f3ee838..d6484b19 100644 --- a/docs/postgres_loader.md +++ b/docs/postgres_loader.md @@ -276,7 +276,7 @@ To verify which schema will be used, you can test the search_path parsing: from api.loaders.postgres_loader import PostgresLoader # Test URL parsing -url = "postgresql://user:pass@localhost:5432/mydatabase?options=-csearch_path%3Dmy_schema" +url = "postgresql://user:pass@localhost:5432/mydatabase?options=-c search_path=my_schema" schema = PostgresLoader.parse_schema_from_url(url) print(f"Schema to be used: {schema}") # Output: my_schema {% endcapture %} From 4d7914ba057c67144bc897eeaa01ccf3c52af382 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 10 Mar 2026 15:45:54 +0200 Subject: [PATCH 10/12] fix: add missing tech terms to spellcheck wordlist Add terms from AGENTS.md/CLAUDE.md (added in staging merge) to the spellcheck wordlist: config, docstring, dotenv, ESLint, HSTS, init, Middleware, monorepo, PRs, pylint, pytest, Radix, Zod, and error class names. Also fix DockerHub capitalization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/wordlist.txt | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index 929ab0f9..b266a0db 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -78,6 +78,7 @@ LLM Ollama OpenAI OpenAI's +DockerHub Dockerhub FDE github @@ -99,4 +100,21 @@ Sanitization JOINs subqueries subquery -TTL \ No newline at end of file +TTL + +config +docstring +dotenv +ESLint +GraphNotFoundError +HSTS +init +InternalError +InvalidArgumentError +Middleware +monorepo +PRs +pylint +pytest +Radix +Zod \ No newline at end of file From 4987dec228d7a10cb6e16f03e2c33be8d8bc695e Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 10 Mar 2026 15:57:45 +0200 Subject: [PATCH 11/12] fix: ensure DB connection cleanup on error and add cursor type hints - Wrap psycopg2 connection/cursor in try/finally so they are always closed, even when extract_tables_info or extract_relationships raises - Set conn/cursor to None after explicit close to avoid double-close in the finally block - Add Any type hints to cursor parameters on extract_tables_info, extract_columns_info, extract_foreign_keys, extract_relationships, and _execute_sample_query Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/loaders/postgres_loader.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/api/loaders/postgres_loader.py b/api/loaders/postgres_loader.py index cdb3fae1..9d58f4e2 100644 --- a/api/loaders/postgres_loader.py +++ b/api/loaders/postgres_loader.py @@ -53,7 +53,7 @@ class PostgresLoader(BaseLoader): @staticmethod def _execute_sample_query( - cursor, table_name: str, col_name: str, sample_size: int = 3 + cursor: Any, table_name: str, col_name: str, sample_size: int = 3 ) -> List[Any]: """ Execute query to get random sample values for a column. @@ -153,6 +153,8 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s Returns: Tuple[bool, str]: Success status and message """ + conn = None + cursor = None try: # Parse schema from connection URL (defaults to 'public') schema = PostgresLoader.parse_schema_from_url(connection_url) @@ -180,9 +182,11 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s # Get all relationship information relationships = PostgresLoader.extract_relationships(cursor, schema) - # Close database connection + # Close database connection before graph loading cursor.close() + cursor = None conn.close() + conn = None yield True, "Loading data into graph..." # Load data into graph @@ -198,9 +202,14 @@ async def load(prefix: str, connection_url: str) -> AsyncGenerator[tuple[bool, s except Exception as e: # pylint: disable=broad-exception-caught logging.error("Error loading PostgreSQL schema: %s", e) yield False, "Failed to load PostgreSQL database schema" + finally: + if cursor is not None: + cursor.close() + if conn is not None: + conn.close() @staticmethod - def extract_tables_info(cursor, schema: str = 'public') -> Dict[str, Any]: + def extract_tables_info(cursor: Any, schema: str = 'public') -> Dict[str, Any]: """ Extract table and column information from PostgreSQL database. @@ -257,7 +266,7 @@ def extract_tables_info(cursor, schema: str = 'public') -> Dict[str, Any]: return entities @staticmethod - def extract_columns_info(cursor, table_name: str, schema: str = 'public') -> Dict[str, Any]: + def extract_columns_info(cursor: Any, table_name: str, schema: str = 'public') -> Dict[str, Any]: """ Extract column information for a specific table. @@ -351,7 +360,7 @@ def extract_columns_info(cursor, table_name: str, schema: str = 'public') -> Dic return columns_info @staticmethod - def extract_foreign_keys(cursor, table_name: str, schema: str = 'public') -> List[Dict[str, str]]: + def extract_foreign_keys(cursor: Any, table_name: str, schema: str = 'public') -> List[Dict[str, str]]: """ Extract foreign key information for a specific table. @@ -393,7 +402,7 @@ def extract_foreign_keys(cursor, table_name: str, schema: str = 'public') -> Lis return foreign_keys @staticmethod - def extract_relationships(cursor, schema: str = 'public') -> Dict[str, List[Dict[str, str]]]: + def extract_relationships(cursor: Any, schema: str = 'public') -> Dict[str, List[Dict[str, str]]]: """ Extract all relationship information from the database. From 04f572860ecbbd9576237684f0b843e5009343e3 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Tue, 10 Mar 2026 16:12:20 +0200 Subject: [PATCH 12/12] fix: increase timeout for multi-step E2E chat tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark three tests that perform multiple LLM round-trips with test.slow() to triple their timeout (60s → 180s), preventing spurious CI failures when LLM responses are slow: - multiple sequential queries maintain conversation history - switching databases clears chat history - duplicate record shows user-friendly error message Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- e2e/tests/chat.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e/tests/chat.spec.ts b/e2e/tests/chat.spec.ts index cf10442a..bbc78ecb 100644 --- a/e2e/tests/chat.spec.ts +++ b/e2e/tests/chat.spec.ts @@ -105,6 +105,7 @@ test.describe('Chat Feature Tests', () => { }); test('multiple sequential queries maintain conversation history', async () => { + test.slow(); // Two sequential LLM round-trips need extra time in CI const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen(); @@ -171,6 +172,7 @@ test.describe('Chat Feature Tests', () => { }); test('switching databases clears chat history', async () => { + test.slow(); // Two database connections plus LLM round-trip need extra time in CI // Connect two databases via API const { postgres: postgresUrl } = getTestDatabases(); @@ -266,6 +268,7 @@ test.describe('Chat Feature Tests', () => { }); test('duplicate record shows user-friendly error message', async () => { + test.slow(); // Two LLM round-trips with confirmation dialogs need extra time in CI const homePage = await browser.createNewPage(HomePage, getBaseUrl(), 'e2e/.auth/user.json'); await browser.setPageToFullScreen();