sqlsaber 0.23.0__py3-none-any.whl → 0.25.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sqlsaber might be problematic. Click here for more details.

@@ -1,13 +1,16 @@
1
1
  """Database schema introspection utilities."""
2
2
 
3
+ import asyncio
3
4
  from abc import ABC, abstractmethod
4
5
  from typing import Any, TypedDict
5
6
 
6
7
  import aiosqlite
8
+ import duckdb
7
9
 
8
10
  from sqlsaber.database.connection import (
9
11
  BaseDatabaseConnection,
10
12
  CSVConnection,
13
+ DuckDBConnection,
11
14
  MySQLConnection,
12
15
  PostgreSQLConnection,
13
16
  SQLiteConnection,
@@ -32,6 +35,15 @@ class ForeignKeyInfo(TypedDict):
32
35
  references: dict[str, str] # {"table": "schema.table", "column": "column_name"}
33
36
 
34
37
 
38
+ class IndexInfo(TypedDict):
39
+ """Type definition for index information."""
40
+
41
+ name: str
42
+ columns: list[str] # ordered
43
+ unique: bool
44
+ type: str | None # btree, gin, FULLTEXT, etc. None if unknown
45
+
46
+
35
47
  class SchemaInfo(TypedDict):
36
48
  """Type definition for schema information."""
37
49
 
@@ -41,6 +53,7 @@ class SchemaInfo(TypedDict):
41
53
  columns: dict[str, ColumnInfo]
42
54
  primary_keys: list[str]
43
55
  foreign_keys: list[ForeignKeyInfo]
56
+ indexes: list[IndexInfo]
44
57
 
45
58
 
46
59
  class BaseSchemaIntrospector(ABC):
@@ -68,6 +81,11 @@ class BaseSchemaIntrospector(ABC):
68
81
  """Get primary keys information for the specific database type."""
69
82
  pass
70
83
 
84
+ @abstractmethod
85
+ async def get_indexes_info(self, connection, tables: list) -> list:
86
+ """Get indexes information for the specific database type."""
87
+ pass
88
+
71
89
  @abstractmethod
72
90
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
73
91
  """Get list of tables with basic information."""
@@ -209,6 +227,43 @@ class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
209
227
  """
210
228
  return await conn.fetch(pk_query)
211
229
 
230
+ async def get_indexes_info(self, connection, tables: list) -> list:
231
+ """Get indexes information for PostgreSQL."""
232
+ if not tables:
233
+ return []
234
+
235
+ pool = await connection.get_pool()
236
+ async with pool.acquire() as conn:
237
+ # Build proper table filters
238
+ idx_table_filters = []
239
+ for table in tables:
240
+ idx_table_filters.append(
241
+ f"(ns.nspname = '{table['table_schema']}' AND t.relname = '{table['table_name']}')"
242
+ )
243
+
244
+ idx_query = f"""
245
+ SELECT
246
+ ns.nspname AS table_schema,
247
+ t.relname AS table_name,
248
+ i.relname AS index_name,
249
+ ix.indisunique AS is_unique,
250
+ am.amname AS index_type,
251
+ array_agg(a.attname ORDER BY ord.ordinality) AS column_names
252
+ FROM pg_class t
253
+ JOIN pg_namespace ns ON ns.oid = t.relnamespace
254
+ JOIN pg_index ix ON ix.indrelid = t.oid
255
+ JOIN pg_class i ON i.oid = ix.indexrelid
256
+ JOIN pg_am am ON am.oid = i.relam
257
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS ord(attnum, ordinality)
258
+ ON TRUE
259
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ord.attnum
260
+ WHERE ns.nspname NOT IN ('pg_catalog', 'information_schema')
261
+ AND ({" OR ".join(idx_table_filters)})
262
+ GROUP BY table_schema, table_name, index_name, is_unique, index_type
263
+ ORDER BY table_schema, table_name, index_name;
264
+ """
265
+ return await conn.fetch(idx_query)
266
+
212
267
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
213
268
  """Get list of tables with basic information for PostgreSQL."""
214
269
  pool = await connection.get_pool()
@@ -379,6 +434,37 @@ class MySQLSchemaIntrospector(BaseSchemaIntrospector):
379
434
  await cursor.execute(pk_query)
380
435
  return await cursor.fetchall()
381
436
 
437
+ async def get_indexes_info(self, connection, tables: list) -> list:
438
+ """Get indexes information for MySQL."""
439
+ if not tables:
440
+ return []
441
+
442
+ pool = await connection.get_pool()
443
+ async with pool.acquire() as conn:
444
+ async with conn.cursor() as cursor:
445
+ # Build proper table filters
446
+ idx_table_filters = []
447
+ for table in tables:
448
+ idx_table_filters.append(
449
+ f"(TABLE_SCHEMA = '{table['table_schema']}' AND TABLE_NAME = '{table['table_name']}')"
450
+ )
451
+
452
+ idx_query = f"""
453
+ SELECT
454
+ TABLE_SCHEMA AS table_schema,
455
+ TABLE_NAME AS table_name,
456
+ INDEX_NAME AS index_name,
457
+ (NON_UNIQUE = 0) AS is_unique,
458
+ INDEX_TYPE AS index_type,
459
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS column_names
460
+ FROM INFORMATION_SCHEMA.STATISTICS
461
+ WHERE ({" OR ".join(idx_table_filters)})
462
+ GROUP BY table_schema, table_name, index_name, is_unique, index_type
463
+ ORDER BY table_schema, table_name, index_name;
464
+ """
465
+ await cursor.execute(idx_query)
466
+ return await cursor.fetchall()
467
+
382
468
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
383
469
  """Get list of tables with basic information for MySQL."""
384
470
  pool = await connection.get_pool()
@@ -531,6 +617,47 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
531
617
 
532
618
  return primary_keys
533
619
 
620
+ async def get_indexes_info(self, connection, tables: list) -> list:
621
+ """Get indexes information for SQLite."""
622
+ if not tables:
623
+ return []
624
+
625
+ indexes = []
626
+ for table in tables:
627
+ table_name = table["table_name"]
628
+
629
+ # Get index list using PRAGMA
630
+ pragma_query = f"PRAGMA index_list({table_name})"
631
+ table_indexes = await self._execute_query(connection, pragma_query)
632
+
633
+ for idx in table_indexes:
634
+ idx_name = idx["name"]
635
+ unique = bool(idx["unique"])
636
+
637
+ # Skip auto-generated primary key indexes
638
+ if idx_name.startswith("sqlite_autoindex_"):
639
+ continue
640
+
641
+ # Get index columns using PRAGMA
642
+ pragma_info_query = f"PRAGMA index_info({idx_name})"
643
+ idx_cols = await self._execute_query(connection, pragma_info_query)
644
+ columns = [
645
+ c["name"] for c in sorted(idx_cols, key=lambda r: r["seqno"])
646
+ ]
647
+
648
+ indexes.append(
649
+ {
650
+ "table_schema": "main",
651
+ "table_name": table_name,
652
+ "index_name": idx_name,
653
+ "is_unique": unique,
654
+ "index_type": None, # SQLite only has B-tree currently
655
+ "column_names": columns,
656
+ }
657
+ )
658
+
659
+ return indexes
660
+
534
661
  async def list_tables_info(self, connection) -> list[dict[str, Any]]:
535
662
  """Get list of tables with basic information for SQLite."""
536
663
  # Get table names without row counts for better performance
@@ -558,6 +685,225 @@ class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
558
685
  ]
559
686
 
560
687
 
688
+ class DuckDBSchemaIntrospector(BaseSchemaIntrospector):
689
+ """DuckDB-specific schema introspection."""
690
+
691
+ async def _execute_query(
692
+ self,
693
+ connection: DuckDBConnection | CSVConnection,
694
+ query: str,
695
+ params: tuple[Any, ...] = (),
696
+ ) -> list[dict[str, Any]]:
697
+ """Run a DuckDB query on a thread and return list of dictionaries."""
698
+
699
+ params_tuple = tuple(params)
700
+
701
+ def fetch_rows(conn: duckdb.DuckDBPyConnection) -> list[dict[str, Any]]:
702
+ cursor = conn.execute(query, params_tuple)
703
+ if cursor.description is None:
704
+ return []
705
+
706
+ columns = [col[0] for col in cursor.description]
707
+ rows = conn.fetchall()
708
+ return [dict(zip(columns, row)) for row in rows]
709
+
710
+ if isinstance(connection, CSVConnection):
711
+ return await connection.execute_query(query, *params_tuple)
712
+
713
+ def run_query() -> list[dict[str, Any]]:
714
+ conn = duckdb.connect(connection.database_path)
715
+ try:
716
+ return fetch_rows(conn)
717
+ finally:
718
+ conn.close()
719
+
720
+ return await asyncio.to_thread(run_query)
721
+
722
+ async def get_tables_info(
723
+ self, connection, table_pattern: str | None = None
724
+ ) -> list[dict[str, Any]]:
725
+ """Get tables information for DuckDB."""
726
+ where_conditions = [
727
+ "table_schema NOT IN ('information_schema', 'pg_catalog', 'duckdb_catalog')"
728
+ ]
729
+ params: list[Any] = []
730
+
731
+ if table_pattern:
732
+ if "." in table_pattern:
733
+ schema_pattern, table_name_pattern = table_pattern.split(".", 1)
734
+ where_conditions.append(
735
+ "(table_schema LIKE ? AND table_name LIKE ?)"
736
+ )
737
+ params.extend([schema_pattern, table_name_pattern])
738
+ else:
739
+ where_conditions.append(
740
+ "(table_name LIKE ? OR table_schema || '.' || table_name LIKE ?)"
741
+ )
742
+ params.extend([table_pattern, table_pattern])
743
+
744
+ query = f"""
745
+ SELECT
746
+ table_schema,
747
+ table_name,
748
+ table_type
749
+ FROM information_schema.tables
750
+ WHERE {" AND ".join(where_conditions)}
751
+ ORDER BY table_schema, table_name;
752
+ """
753
+
754
+ return await self._execute_query(connection, query, tuple(params))
755
+
756
+ async def get_columns_info(self, connection, tables: list) -> list[dict[str, Any]]:
757
+ """Get columns information for DuckDB."""
758
+ if not tables:
759
+ return []
760
+
761
+ table_filters = []
762
+ for table in tables:
763
+ table_filters.append(
764
+ "(table_schema = ? AND table_name = ?)"
765
+ )
766
+
767
+ params: list[Any] = []
768
+ for table in tables:
769
+ params.extend([table["table_schema"], table["table_name"]])
770
+
771
+ query = f"""
772
+ SELECT
773
+ table_schema,
774
+ table_name,
775
+ column_name,
776
+ data_type,
777
+ is_nullable,
778
+ column_default,
779
+ character_maximum_length,
780
+ numeric_precision,
781
+ numeric_scale
782
+ FROM information_schema.columns
783
+ WHERE {" OR ".join(table_filters)}
784
+ ORDER BY table_schema, table_name, ordinal_position;
785
+ """
786
+
787
+ return await self._execute_query(connection, query, tuple(params))
788
+
789
+ async def get_foreign_keys_info(self, connection, tables: list) -> list[dict[str, Any]]:
790
+ """Get foreign keys information for DuckDB."""
791
+ if not tables:
792
+ return []
793
+
794
+ table_filters = []
795
+ params: list[Any] = []
796
+ for table in tables:
797
+ table_filters.append("(kcu.table_schema = ? AND kcu.table_name = ?)")
798
+ params.extend([table["table_schema"], table["table_name"]])
799
+
800
+ query = f"""
801
+ SELECT
802
+ kcu.table_schema,
803
+ kcu.table_name,
804
+ kcu.column_name,
805
+ ccu.table_schema AS foreign_table_schema,
806
+ ccu.table_name AS foreign_table_name,
807
+ ccu.column_name AS foreign_column_name
808
+ FROM information_schema.referential_constraints AS rc
809
+ JOIN information_schema.key_column_usage AS kcu
810
+ ON rc.constraint_schema = kcu.constraint_schema
811
+ AND rc.constraint_name = kcu.constraint_name
812
+ JOIN information_schema.key_column_usage AS ccu
813
+ ON rc.unique_constraint_schema = ccu.constraint_schema
814
+ AND rc.unique_constraint_name = ccu.constraint_name
815
+ AND ccu.ordinal_position = kcu.position_in_unique_constraint
816
+ WHERE {" OR ".join(table_filters)}
817
+ ORDER BY kcu.table_schema, kcu.table_name, kcu.ordinal_position;
818
+ """
819
+
820
+ return await self._execute_query(connection, query, tuple(params))
821
+
822
+ async def get_primary_keys_info(self, connection, tables: list) -> list[dict[str, Any]]:
823
+ """Get primary keys information for DuckDB."""
824
+ if not tables:
825
+ return []
826
+
827
+ table_filters = []
828
+ params: list[Any] = []
829
+ for table in tables:
830
+ table_filters.append("(tc.table_schema = ? AND tc.table_name = ?)")
831
+ params.extend([table["table_schema"], table["table_name"]])
832
+
833
+ query = f"""
834
+ SELECT
835
+ tc.table_schema,
836
+ tc.table_name,
837
+ kcu.column_name
838
+ FROM information_schema.table_constraints AS tc
839
+ JOIN information_schema.key_column_usage AS kcu
840
+ ON tc.constraint_name = kcu.constraint_name
841
+ AND tc.constraint_schema = kcu.constraint_schema
842
+ WHERE tc.constraint_type = 'PRIMARY KEY'
843
+ AND ({" OR ".join(table_filters)})
844
+ ORDER BY tc.table_schema, tc.table_name, kcu.ordinal_position;
845
+ """
846
+
847
+ return await self._execute_query(connection, query, tuple(params))
848
+
849
+ async def get_indexes_info(self, connection, tables: list) -> list[dict[str, Any]]:
850
+ """Get indexes information for DuckDB."""
851
+ if not tables:
852
+ return []
853
+
854
+ indexes: list[dict[str, Any]] = []
855
+ for table in tables:
856
+ schema = table["table_schema"]
857
+ table_name = table["table_name"]
858
+ query = """
859
+ SELECT
860
+ schema_name,
861
+ table_name,
862
+ index_name,
863
+ sql
864
+ FROM duckdb_indexes()
865
+ WHERE schema_name = ? AND table_name = ?;
866
+ """
867
+ rows = await self._execute_query(connection, query, (schema, table_name))
868
+
869
+ for row in rows:
870
+ sql_text = (row.get("sql") or "").strip()
871
+ upper_sql = sql_text.upper()
872
+ unique = "UNIQUE" in upper_sql.split("(")[0]
873
+
874
+ columns: list[str] = []
875
+ if "(" in sql_text and ")" in sql_text:
876
+ column_section = sql_text[sql_text.find("(") + 1 : sql_text.rfind(")")]
877
+ columns = [col.strip().strip('"') for col in column_section.split(",") if col.strip()]
878
+
879
+ indexes.append(
880
+ {
881
+ "table_schema": row.get("schema_name") or schema or "main",
882
+ "table_name": row.get("table_name") or table_name,
883
+ "index_name": row.get("index_name"),
884
+ "is_unique": unique,
885
+ "index_type": None,
886
+ "column_names": columns,
887
+ }
888
+ )
889
+
890
+ return indexes
891
+
892
+ async def list_tables_info(self, connection) -> list[dict[str, Any]]:
893
+ """Get list of tables with basic information for DuckDB."""
894
+ query = """
895
+ SELECT
896
+ table_schema,
897
+ table_name,
898
+ table_type
899
+ FROM information_schema.tables
900
+ WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'duckdb_catalog')
901
+ ORDER BY table_schema, table_name;
902
+ """
903
+
904
+ return await self._execute_query(connection, query)
905
+
906
+
561
907
  class SchemaManager:
562
908
  """Manages database schema introspection."""
563
909
 
@@ -569,8 +915,10 @@ class SchemaManager:
569
915
  self.introspector = PostgreSQLSchemaIntrospector()
570
916
  elif isinstance(db_connection, MySQLConnection):
571
917
  self.introspector = MySQLSchemaIntrospector()
572
- elif isinstance(db_connection, (SQLiteConnection, CSVConnection)):
918
+ elif isinstance(db_connection, SQLiteConnection):
573
919
  self.introspector = SQLiteSchemaIntrospector()
920
+ elif isinstance(db_connection, (DuckDBConnection, CSVConnection)):
921
+ self.introspector = DuckDBSchemaIntrospector()
574
922
  else:
575
923
  raise ValueError(
576
924
  f"Unsupported database connection type: {type(db_connection)}"
@@ -589,12 +937,14 @@ class SchemaManager:
589
937
  columns = await self.introspector.get_columns_info(self.db, tables)
590
938
  foreign_keys = await self.introspector.get_foreign_keys_info(self.db, tables)
591
939
  primary_keys = await self.introspector.get_primary_keys_info(self.db, tables)
940
+ indexes = await self.introspector.get_indexes_info(self.db, tables)
592
941
 
593
942
  # Build schema structure
594
943
  schema_info = self._build_table_structure(tables)
595
944
  self._add_columns_to_schema(schema_info, columns)
596
945
  self._add_primary_keys_to_schema(schema_info, primary_keys)
597
946
  self._add_foreign_keys_to_schema(schema_info, foreign_keys)
947
+ self._add_indexes_to_schema(schema_info, indexes)
598
948
 
599
949
  return schema_info
600
950
 
@@ -613,6 +963,7 @@ class SchemaManager:
613
963
  "columns": {},
614
964
  "primary_keys": [],
615
965
  "foreign_keys": [],
966
+ "indexes": [],
616
967
  }
617
968
  return schema_info
618
969
 
@@ -666,6 +1017,31 @@ class SchemaManager:
666
1017
  }
667
1018
  )
668
1019
 
1020
+ def _add_indexes_to_schema(
1021
+ self, schema_info: dict[str, dict], indexes: list
1022
+ ) -> None:
1023
+ """Add index information to schema."""
1024
+ for idx in indexes:
1025
+ full_name = f"{idx['table_schema']}.{idx['table_name']}"
1026
+ if full_name in schema_info:
1027
+ # Handle different column name formats from different databases
1028
+ if isinstance(idx["column_names"], list):
1029
+ columns = idx["column_names"]
1030
+ else:
1031
+ # MySQL returns comma-separated string
1032
+ columns = (
1033
+ idx["column_names"].split(",") if idx["column_names"] else []
1034
+ )
1035
+
1036
+ schema_info[full_name]["indexes"].append(
1037
+ {
1038
+ "name": idx["index_name"],
1039
+ "columns": columns,
1040
+ "unique": idx["is_unique"],
1041
+ "type": idx.get("index_type"),
1042
+ }
1043
+ )
1044
+
669
1045
  async def list_tables(self) -> dict[str, Any]:
670
1046
  """Get a list of all tables with basic information."""
671
1047
  tables = await self.introspector.list_tables_info(self.db)
@@ -138,6 +138,12 @@ class IntrospectSchemaTool(SQLTool):
138
138
  f"{fk['column']} -> {fk['references']['table']}.{fk['references']['column']}"
139
139
  for fk in table_info["foreign_keys"]
140
140
  ],
141
+ "indexes": [
142
+ f"{idx['name']} ({', '.join(idx['columns'])})"
143
+ + (" UNIQUE" if idx["unique"] else "")
144
+ + (f" [{idx['type']}]" if idx["type"] else "")
145
+ for idx in table_info["indexes"]
146
+ ],
141
147
  }
142
148
 
143
149
  return json.dumps(formatted_info)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlsaber
3
- Version: 0.23.0
3
+ Version: 0.25.0
4
4
  Summary: SQLsaber - Open-source agentic SQL assistant
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.12
@@ -8,10 +8,10 @@ Requires-Dist: aiomysql>=0.2.0
8
8
  Requires-Dist: aiosqlite>=0.21.0
9
9
  Requires-Dist: asyncpg>=0.30.0
10
10
  Requires-Dist: cyclopts>=3.22.1
11
+ Requires-Dist: duckdb>=0.9.2
11
12
  Requires-Dist: fastmcp>=2.9.0
12
13
  Requires-Dist: httpx>=0.28.1
13
14
  Requires-Dist: keyring>=25.6.0
14
- Requires-Dist: pandas>=2.0.0
15
15
  Requires-Dist: platformdirs>=4.0.0
16
16
  Requires-Dist: prompt-toolkit>3.0.51
17
17
  Requires-Dist: pydantic-ai
@@ -58,7 +58,7 @@ Ask your questions in natural language and `sqlsaber` will gather the right cont
58
58
  - 🧠 Memory management
59
59
  - 💬 Interactive REPL mode
60
60
  - 🧵 Conversation threads (store, display, and resume conversations)
61
- - 🗄️ Support for PostgreSQL, SQLite, and MySQL
61
+ - 🗄️ Support for PostgreSQL, MySQL, SQLite, and DuckDB
62
62
  - 🔌 MCP (Model Context Protocol) server support
63
63
  - 🎨 Beautiful formatted output
64
64
 
@@ -170,6 +170,7 @@ saber -d mydb "count all orders"
170
170
 
171
171
  # You can also pass a connection string
172
172
  saber -d "postgresql://user:password@localhost:5432/mydb" "count all orders"
173
+ saber -d "duckdb:///path/to/data.duckdb" "top customers"
173
174
  ```
174
175
 
175
176
  ## Examples
@@ -1,32 +1,32 @@
1
1
  sqlsaber/__init__.py,sha256=HjS8ULtP4MGpnTL7njVY45NKV9Fi4e_yeYuY-hyXWQc,73
2
2
  sqlsaber/__main__.py,sha256=RIHxWeWh2QvLfah-2OkhI5IJxojWfy4fXpMnVEJYvxw,78
3
3
  sqlsaber/agents/__init__.py,sha256=i_MI2eWMQaVzGikKU71FPCmSQxNDKq36Imq1PrYoIPU,130
4
- sqlsaber/agents/base.py,sha256=7zOZTHKxUuU0uMc-NTaCkkBfDnU3jtwbT8_eP1ZtJ2k,2615
4
+ sqlsaber/agents/base.py,sha256=EAuoj3vpWNqksudMd2lL1Fmx68Y91qNX6NyK1RjQ4-g,2679
5
5
  sqlsaber/agents/mcp.py,sha256=GcJTx7YDYH6aaxIADEIxSgcWAdWakUx395JIzVnf17U,768
6
- sqlsaber/agents/pydantic_ai_agent.py,sha256=6RvG2O7G8P6NN9QaRXUodg5Q26QJ4ShGWoTGYbVQ5K4,7065
6
+ sqlsaber/agents/pydantic_ai_agent.py,sha256=qn-DnTGcdUzSEn9xBWwGhgtifYxZ_NEo8XPePnl1StE,7154
7
7
  sqlsaber/cli/__init__.py,sha256=qVSLVJLLJYzoC6aj6y9MFrzZvAwc4_OgxU9DlkQnZ4M,86
8
8
  sqlsaber/cli/auth.py,sha256=jTsRgbmlGPlASSuIKmdjjwfqtKvjfKd_cTYxX0-QqaQ,7400
9
- sqlsaber/cli/commands.py,sha256=mjLG9i1bXf0TEroxkIxq5O7Hhjufz3Ad72cyJz7vE1k,8128
9
+ sqlsaber/cli/commands.py,sha256=NyBDr5qEnCOZrHEMGcEpHLXEWdlzEQW3D61NIrPi2fQ,8727
10
10
  sqlsaber/cli/completers.py,sha256=HsUPjaZweLSeYCWkAcgMl8FylQ1xjWBWYTEL_9F6xfU,6430
11
- sqlsaber/cli/database.py,sha256=JKtHSN-BFzBa14REf0phFVQB7d67m1M5FFaD8N6DdrY,12966
12
- sqlsaber/cli/display.py,sha256=9uXg0GqFXGAwBj1O7-i6aLPQkGq-iURrsKHk6PWTq1E,15025
13
- sqlsaber/cli/interactive.py,sha256=7uM4LoXbhPJr8o5yNjICSzL0uxZkp1psWrVq4G9V0OI,13118
11
+ sqlsaber/cli/database.py,sha256=93etjqiYAfH08jBe_OJpLMNKiu3H81G8O7CMB31MIIc,13424
12
+ sqlsaber/cli/display.py,sha256=XuKiTWUw5k0U0P_f1K7zhDWX5KTO2DQVG0Q0XU9VEhs,16334
13
+ sqlsaber/cli/interactive.py,sha256=lVOtONBeAmZxWdfkvdoVoX4POs_-C1YVs0jPxY9MoZs,13288
14
14
  sqlsaber/cli/memory.py,sha256=OufHFJFwV0_GGn7LvKRTJikkWhV1IwNIUDOxFPHXOaQ,7794
15
15
  sqlsaber/cli/models.py,sha256=ZewtwGQwhd9b-yxBAPKePolvI1qQG-EkmeWAGMqtWNQ,8986
16
16
  sqlsaber/cli/streaming.py,sha256=Eo5CNUgDGY1WYP90jwDA2aY7RefN-TfcStA6NyjUQTY,7076
17
- sqlsaber/cli/threads.py,sha256=HJ6v9wEv1as21B7IJglYs3q6LH7Plv2oheLbM5YEQQA,11549
17
+ sqlsaber/cli/threads.py,sha256=ufDABlqndVJKd5COgSokcFRIKTgsGqXdHV84DVVm7MA,12743
18
18
  sqlsaber/config/__init__.py,sha256=olwC45k8Nc61yK0WmPUk7XHdbsZH9HuUAbwnmKe3IgA,100
19
19
  sqlsaber/config/api_keys.py,sha256=RqWQCko1tY7sES7YOlexgBH5Hd5ne_kGXHdBDNqcV2U,3649
20
20
  sqlsaber/config/auth.py,sha256=b5qB2h1doXyO9Bn8z0CcL8LAR2jF431gGXBGKLgTmtQ,2756
21
- sqlsaber/config/database.py,sha256=c6q3l4EvoBch1ckYHA70hf6L7fSOY-sItnLCpvJiPrA,11357
21
+ sqlsaber/config/database.py,sha256=Yec6_0wdzq-ADblMNnbgvouYCimYOY_DWHT9oweaISc,11449
22
22
  sqlsaber/config/oauth_flow.py,sha256=A3bSXaBLzuAfXV2ZPA94m9NV33c2MyL6M4ii9oEkswQ,10291
23
23
  sqlsaber/config/oauth_tokens.py,sha256=C9z35hyx-PvSAYdC1LNf3rg9_wsEIY56hkEczelbad0,6015
24
24
  sqlsaber/config/providers.py,sha256=JFjeJv1K5Q93zWSlWq3hAvgch1TlgoF0qFa0KJROkKY,2957
25
25
  sqlsaber/config/settings.py,sha256=vgb_RXaM-7DgbxYDmWNw1cSyMqwys4j3qNCvM4bljwI,5586
26
26
  sqlsaber/database/__init__.py,sha256=a_gtKRJnZVO8-fEZI7g3Z8YnGa6Nio-5Y50PgVp07ss,176
27
- sqlsaber/database/connection.py,sha256=1bDPEa6cmdh87gPfhNeBLpOdI0E2_2KlE74q_-4l_jI,18913
28
- sqlsaber/database/resolver.py,sha256=RPXF5EoKzvQDDLmPGNHYd2uG_oNICH8qvUjBp6iXmNY,3348
29
- sqlsaber/database/schema.py,sha256=r12qoN3tdtAXdO22EKlauAe7QwOm8lL2vTMM59XEMMY,26594
27
+ sqlsaber/database/connection.py,sha256=J3U08Qu7NQrmem0jPM5XKIHPmPJE927IiLhN8zA6oLo,19392
28
+ sqlsaber/database/resolver.py,sha256=wSCcn__aCqwIfpt_LCjtW2Zgb8RpG5PlmwwZHli1q_U,3628
29
+ sqlsaber/database/schema.py,sha256=9HXTb5O_nlS2aNDeyv7EXhX7_kN2hs6rbPnJ8fnLyWk,41260
30
30
  sqlsaber/mcp/__init__.py,sha256=COdWq7wauPBp5Ew8tfZItFzbcLDSEkHBJSMhxzy8C9c,112
31
31
  sqlsaber/mcp/mcp.py,sha256=X12oCMZYAtgJ7MNuh5cqz8y3lALrOzkXWcfpuY0Ijxk,3950
32
32
  sqlsaber/memory/__init__.py,sha256=GiWkU6f6YYVV0EvvXDmFWe_CxarmDCql05t70MkTEWs,63
@@ -39,9 +39,9 @@ sqlsaber/tools/base.py,sha256=mHhvAj27BHmckyvuDLCPlAQdzABJyYxd9SJnaYAwwuA,1777
39
39
  sqlsaber/tools/enums.py,sha256=CH32mL-0k9ZA18911xLpNtsgpV6tB85TktMj6uqGz54,411
40
40
  sqlsaber/tools/instructions.py,sha256=X-x8maVkkyi16b6Tl0hcAFgjiYceZaSwyWTfmrvx8U8,9024
41
41
  sqlsaber/tools/registry.py,sha256=HWOQMsNIdL4XZS6TeNUyrL-5KoSDH6PHsWd3X66o-18,3211
42
- sqlsaber/tools/sql_tools.py,sha256=hM6tKqW5MDhFUt6MesoqhTUqIpq_5baIIDoN1MjDCXY,9647
43
- sqlsaber-0.23.0.dist-info/METADATA,sha256=olVI7W6UB-F6oZUqOjTWhJAvYtDbXXO0xoeOVYYmpfM,6178
44
- sqlsaber-0.23.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
- sqlsaber-0.23.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
46
- sqlsaber-0.23.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
47
- sqlsaber-0.23.0.dist-info/RECORD,,
42
+ sqlsaber/tools/sql_tools.py,sha256=j4yRqfKokPFnZ_tEZPrWU5WStDc3Mexo1fWZ8KsmUjQ,9965
43
+ sqlsaber-0.25.0.dist-info/METADATA,sha256=9Q2AsBv4I78FLo8Uezmnv_fCch3jIKgv1gzBBm1cVB4,6243
44
+ sqlsaber-0.25.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ sqlsaber-0.25.0.dist-info/entry_points.txt,sha256=qEbOB7OffXPFgyJc7qEIJlMEX5RN9xdzLmWZa91zCQQ,162
46
+ sqlsaber-0.25.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
47
+ sqlsaber-0.25.0.dist-info/RECORD,,