sqlsaber 0.24.0__py3-none-any.whl → 0.26.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,685 +1,19 @@
1
- """Database schema introspection utilities."""
1
+ """Database schema management."""
2
2
 
3
- from abc import ABC, abstractmethod
4
- from typing import Any, TypedDict
3
+ from typing import Any
5
4
 
6
- import aiosqlite
7
-
8
- from sqlsaber.database.connection import (
5
+ from .base import (
9
6
  BaseDatabaseConnection,
10
- CSVConnection,
11
- MySQLConnection,
12
- PostgreSQLConnection,
13
- SQLiteConnection,
7
+ ColumnInfo,
8
+ ForeignKeyInfo,
9
+ IndexInfo,
10
+ SchemaInfo,
14
11
  )
15
-
16
-
17
- class ColumnInfo(TypedDict):
18
- """Type definition for column information."""
19
-
20
- data_type: str
21
- nullable: bool
22
- default: str | None
23
- max_length: int | None
24
- precision: int | None
25
- scale: int | None
26
-
27
-
28
- class ForeignKeyInfo(TypedDict):
29
- """Type definition for foreign key information."""
30
-
31
- column: str
32
- references: dict[str, str] # {"table": "schema.table", "column": "column_name"}
33
-
34
-
35
- class IndexInfo(TypedDict):
36
- """Type definition for index information."""
37
-
38
- name: str
39
- columns: list[str] # ordered
40
- unique: bool
41
- type: str | None # btree, gin, FULLTEXT, etc. None if unknown
42
-
43
-
44
- class SchemaInfo(TypedDict):
45
- """Type definition for schema information."""
46
-
47
- schema: str
48
- name: str
49
- type: str
50
- columns: dict[str, ColumnInfo]
51
- primary_keys: list[str]
52
- foreign_keys: list[ForeignKeyInfo]
53
- indexes: list[IndexInfo]
54
-
55
-
56
- class BaseSchemaIntrospector(ABC):
57
- """Abstract base class for database-specific schema introspection."""
58
-
59
- @abstractmethod
60
- async def get_tables_info(
61
- self, connection, table_pattern: str | None = None
62
- ) -> dict[str, Any]:
63
- """Get tables information for the specific database type."""
64
- pass
65
-
66
- @abstractmethod
67
- async def get_columns_info(self, connection, tables: list) -> list:
68
- """Get columns information for the specific database type."""
69
- pass
70
-
71
- @abstractmethod
72
- async def get_foreign_keys_info(self, connection, tables: list) -> list:
73
- """Get foreign keys information for the specific database type."""
74
- pass
75
-
76
- @abstractmethod
77
- async def get_primary_keys_info(self, connection, tables: list) -> list:
78
- """Get primary keys information for the specific database type."""
79
- pass
80
-
81
- @abstractmethod
82
- async def get_indexes_info(self, connection, tables: list) -> list:
83
- """Get indexes information for the specific database type."""
84
- pass
85
-
86
- @abstractmethod
87
- async def list_tables_info(self, connection) -> list[dict[str, Any]]:
88
- """Get list of tables with basic information."""
89
- pass
90
-
91
-
92
- class PostgreSQLSchemaIntrospector(BaseSchemaIntrospector):
93
- """PostgreSQL-specific schema introspection."""
94
-
95
- async def get_tables_info(
96
- self, connection, table_pattern: str | None = None
97
- ) -> dict[str, Any]:
98
- """Get tables information for PostgreSQL."""
99
- pool = await connection.get_pool()
100
- async with pool.acquire() as conn:
101
- # Build WHERE clause for filtering
102
- where_conditions = [
103
- "table_schema NOT IN ('pg_catalog', 'information_schema')"
104
- ]
105
- params = []
106
-
107
- if table_pattern:
108
- # Support patterns like 'schema.table' or just 'table'
109
- if "." in table_pattern:
110
- schema_pattern, table_name_pattern = table_pattern.split(".", 1)
111
- where_conditions.append(
112
- "(table_schema LIKE $1 AND table_name LIKE $2)"
113
- )
114
- params.extend([schema_pattern, table_name_pattern])
115
- else:
116
- where_conditions.append(
117
- "(table_name LIKE $1 OR table_schema || '.' || table_name LIKE $1)"
118
- )
119
- params.append(table_pattern)
120
-
121
- # Get tables
122
- tables_query = f"""
123
- SELECT
124
- table_schema,
125
- table_name,
126
- table_type
127
- FROM information_schema.tables
128
- WHERE {" AND ".join(where_conditions)}
129
- ORDER BY table_schema, table_name;
130
- """
131
- return await conn.fetch(tables_query, *params)
132
-
133
- async def get_columns_info(self, connection, tables: list) -> list:
134
- """Get columns information for PostgreSQL."""
135
- if not tables:
136
- return []
137
-
138
- pool = await connection.get_pool()
139
- async with pool.acquire() as conn:
140
- # Build IN clause for the tables we found
141
- table_filters = []
142
- for table in tables:
143
- table_filters.append(
144
- f"(table_schema = '{table['table_schema']}' AND table_name = '{table['table_name']}')"
145
- )
146
-
147
- columns_query = f"""
148
- SELECT
149
- table_schema,
150
- table_name,
151
- column_name,
152
- data_type,
153
- is_nullable,
154
- column_default,
155
- character_maximum_length,
156
- numeric_precision,
157
- numeric_scale
158
- FROM information_schema.columns
159
- WHERE ({" OR ".join(table_filters)})
160
- ORDER BY table_schema, table_name, ordinal_position;
161
- """
162
- return await conn.fetch(columns_query)
163
-
164
- async def get_foreign_keys_info(self, connection, tables: list) -> list:
165
- """Get foreign keys information for PostgreSQL."""
166
- if not tables:
167
- return []
168
-
169
- pool = await connection.get_pool()
170
- async with pool.acquire() as conn:
171
- # Build proper table filters with tc. prefix
172
- fk_table_filters = []
173
- for table in tables:
174
- fk_table_filters.append(
175
- f"(tc.table_schema = '{table['table_schema']}' AND tc.table_name = '{table['table_name']}')"
176
- )
177
-
178
- fk_query = f"""
179
- SELECT
180
- tc.table_schema,
181
- tc.table_name,
182
- kcu.column_name,
183
- ccu.table_schema AS foreign_table_schema,
184
- ccu.table_name AS foreign_table_name,
185
- ccu.column_name AS foreign_column_name
186
- FROM information_schema.table_constraints AS tc
187
- JOIN information_schema.key_column_usage AS kcu
188
- ON tc.constraint_name = kcu.constraint_name
189
- AND tc.table_schema = kcu.table_schema
190
- JOIN information_schema.constraint_column_usage AS ccu
191
- ON ccu.constraint_name = tc.constraint_name
192
- AND ccu.table_schema = tc.table_schema
193
- WHERE tc.constraint_type = 'FOREIGN KEY'
194
- AND ({" OR ".join(fk_table_filters)});
195
- """
196
- return await conn.fetch(fk_query)
197
-
198
- async def get_primary_keys_info(self, connection, tables: list) -> list:
199
- """Get primary keys information for PostgreSQL."""
200
- if not tables:
201
- return []
202
-
203
- pool = await connection.get_pool()
204
- async with pool.acquire() as conn:
205
- # Build proper table filters with tc. prefix
206
- pk_table_filters = []
207
- for table in tables:
208
- pk_table_filters.append(
209
- f"(tc.table_schema = '{table['table_schema']}' AND tc.table_name = '{table['table_name']}')"
210
- )
211
-
212
- pk_query = f"""
213
- SELECT
214
- tc.table_schema,
215
- tc.table_name,
216
- kcu.column_name
217
- FROM information_schema.table_constraints AS tc
218
- JOIN information_schema.key_column_usage AS kcu
219
- ON tc.constraint_name = kcu.constraint_name
220
- AND tc.table_schema = kcu.table_schema
221
- WHERE tc.constraint_type = 'PRIMARY KEY'
222
- AND ({" OR ".join(pk_table_filters)})
223
- ORDER BY tc.table_schema, tc.table_name, kcu.ordinal_position;
224
- """
225
- return await conn.fetch(pk_query)
226
-
227
- async def get_indexes_info(self, connection, tables: list) -> list:
228
- """Get indexes information for PostgreSQL."""
229
- if not tables:
230
- return []
231
-
232
- pool = await connection.get_pool()
233
- async with pool.acquire() as conn:
234
- # Build proper table filters
235
- idx_table_filters = []
236
- for table in tables:
237
- idx_table_filters.append(
238
- f"(ns.nspname = '{table['table_schema']}' AND t.relname = '{table['table_name']}')"
239
- )
240
-
241
- idx_query = f"""
242
- SELECT
243
- ns.nspname AS table_schema,
244
- t.relname AS table_name,
245
- i.relname AS index_name,
246
- ix.indisunique AS is_unique,
247
- am.amname AS index_type,
248
- array_agg(a.attname ORDER BY ord.ordinality) AS column_names
249
- FROM pg_class t
250
- JOIN pg_namespace ns ON ns.oid = t.relnamespace
251
- JOIN pg_index ix ON ix.indrelid = t.oid
252
- JOIN pg_class i ON i.oid = ix.indexrelid
253
- JOIN pg_am am ON am.oid = i.relam
254
- JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS ord(attnum, ordinality)
255
- ON TRUE
256
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ord.attnum
257
- WHERE ns.nspname NOT IN ('pg_catalog', 'information_schema')
258
- AND ({" OR ".join(idx_table_filters)})
259
- GROUP BY table_schema, table_name, index_name, is_unique, index_type
260
- ORDER BY table_schema, table_name, index_name;
261
- """
262
- return await conn.fetch(idx_query)
263
-
264
- async def list_tables_info(self, connection) -> list[dict[str, Any]]:
265
- """Get list of tables with basic information for PostgreSQL."""
266
- pool = await connection.get_pool()
267
- async with pool.acquire() as conn:
268
- # Get tables without row counts for better performance
269
- tables_query = """
270
- SELECT
271
- t.table_schema,
272
- t.table_name,
273
- t.table_type
274
- FROM information_schema.tables t
275
- WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema')
276
- ORDER BY t.table_schema, t.table_name;
277
- """
278
- records = await conn.fetch(tables_query)
279
-
280
- # Convert asyncpg.Record objects to dictionaries
281
- return [
282
- {
283
- "table_schema": record["table_schema"],
284
- "table_name": record["table_name"],
285
- "table_type": record["table_type"],
286
- }
287
- for record in records
288
- ]
289
-
290
-
291
- class MySQLSchemaIntrospector(BaseSchemaIntrospector):
292
- """MySQL-specific schema introspection."""
293
-
294
- async def get_tables_info(
295
- self, connection, table_pattern: str | None = None
296
- ) -> dict[str, Any]:
297
- """Get tables information for MySQL."""
298
- pool = await connection.get_pool()
299
- async with pool.acquire() as conn:
300
- async with conn.cursor() as cursor:
301
- # Build WHERE clause for filtering
302
- where_conditions = [
303
- "table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')"
304
- ]
305
- params = []
306
-
307
- if table_pattern:
308
- # Support patterns like 'schema.table' or just 'table'
309
- if "." in table_pattern:
310
- schema_pattern, table_name_pattern = table_pattern.split(".", 1)
311
- where_conditions.append(
312
- "(table_schema LIKE %s AND table_name LIKE %s)"
313
- )
314
- params.extend([schema_pattern, table_name_pattern])
315
- else:
316
- where_conditions.append(
317
- "(table_name LIKE %s OR CONCAT(table_schema, '.', table_name) LIKE %s)"
318
- )
319
- params.extend([table_pattern, table_pattern])
320
-
321
- # Get tables
322
- tables_query = f"""
323
- SELECT
324
- table_schema,
325
- table_name,
326
- table_type
327
- FROM information_schema.tables
328
- WHERE {" AND ".join(where_conditions)}
329
- ORDER BY table_schema, table_name;
330
- """
331
- await cursor.execute(tables_query, params)
332
- return await cursor.fetchall()
333
-
334
- async def get_columns_info(self, connection, tables: list) -> list:
335
- """Get columns information for MySQL."""
336
- if not tables:
337
- return []
338
-
339
- pool = await connection.get_pool()
340
- async with pool.acquire() as conn:
341
- async with conn.cursor() as cursor:
342
- # Build IN clause for the tables we found
343
- table_filters = []
344
- for table in tables:
345
- table_filters.append(
346
- f"(table_schema = '{table['table_schema']}' AND table_name = '{table['table_name']}')"
347
- )
348
-
349
- columns_query = f"""
350
- SELECT
351
- table_schema,
352
- table_name,
353
- column_name,
354
- data_type,
355
- is_nullable,
356
- column_default,
357
- character_maximum_length,
358
- numeric_precision,
359
- numeric_scale
360
- FROM information_schema.columns
361
- WHERE ({" OR ".join(table_filters)})
362
- ORDER BY table_schema, table_name, ordinal_position;
363
- """
364
- await cursor.execute(columns_query)
365
- return await cursor.fetchall()
366
-
367
- async def get_foreign_keys_info(self, connection, tables: list) -> list:
368
- """Get foreign keys information for MySQL."""
369
- if not tables:
370
- return []
371
-
372
- pool = await connection.get_pool()
373
- async with pool.acquire() as conn:
374
- async with conn.cursor() as cursor:
375
- # Build proper table filters
376
- fk_table_filters = []
377
- for table in tables:
378
- fk_table_filters.append(
379
- f"(tc.table_schema = '{table['table_schema']}' AND tc.table_name = '{table['table_name']}')"
380
- )
381
-
382
- fk_query = f"""
383
- SELECT
384
- tc.table_schema,
385
- tc.table_name,
386
- kcu.column_name,
387
- rc.unique_constraint_schema AS foreign_table_schema,
388
- rc.referenced_table_name AS foreign_table_name,
389
- kcu.referenced_column_name AS foreign_column_name
390
- FROM information_schema.table_constraints AS tc
391
- JOIN information_schema.key_column_usage AS kcu
392
- ON tc.constraint_name = kcu.constraint_name
393
- AND tc.table_schema = kcu.table_schema
394
- JOIN information_schema.referential_constraints AS rc
395
- ON tc.constraint_name = rc.constraint_name
396
- AND tc.table_schema = rc.constraint_schema
397
- WHERE tc.constraint_type = 'FOREIGN KEY'
398
- AND ({" OR ".join(fk_table_filters)});
399
- """
400
- await cursor.execute(fk_query)
401
- return await cursor.fetchall()
402
-
403
- async def get_primary_keys_info(self, connection, tables: list) -> list:
404
- """Get primary keys information for MySQL."""
405
- if not tables:
406
- return []
407
-
408
- pool = await connection.get_pool()
409
- async with pool.acquire() as conn:
410
- async with conn.cursor() as cursor:
411
- # Build proper table filters
412
- pk_table_filters = []
413
- for table in tables:
414
- pk_table_filters.append(
415
- f"(tc.table_schema = '{table['table_schema']}' AND tc.table_name = '{table['table_name']}')"
416
- )
417
-
418
- pk_query = f"""
419
- SELECT
420
- tc.table_schema,
421
- tc.table_name,
422
- kcu.column_name
423
- FROM information_schema.table_constraints AS tc
424
- JOIN information_schema.key_column_usage AS kcu
425
- ON tc.constraint_name = kcu.constraint_name
426
- AND tc.table_schema = kcu.table_schema
427
- WHERE tc.constraint_type = 'PRIMARY KEY'
428
- AND ({" OR ".join(pk_table_filters)})
429
- ORDER BY tc.table_schema, tc.table_name, kcu.ordinal_position;
430
- """
431
- await cursor.execute(pk_query)
432
- return await cursor.fetchall()
433
-
434
- async def get_indexes_info(self, connection, tables: list) -> list:
435
- """Get indexes information for MySQL."""
436
- if not tables:
437
- return []
438
-
439
- pool = await connection.get_pool()
440
- async with pool.acquire() as conn:
441
- async with conn.cursor() as cursor:
442
- # Build proper table filters
443
- idx_table_filters = []
444
- for table in tables:
445
- idx_table_filters.append(
446
- f"(TABLE_SCHEMA = '{table['table_schema']}' AND TABLE_NAME = '{table['table_name']}')"
447
- )
448
-
449
- idx_query = f"""
450
- SELECT
451
- TABLE_SCHEMA AS table_schema,
452
- TABLE_NAME AS table_name,
453
- INDEX_NAME AS index_name,
454
- (NON_UNIQUE = 0) AS is_unique,
455
- INDEX_TYPE AS index_type,
456
- GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) AS column_names
457
- FROM INFORMATION_SCHEMA.STATISTICS
458
- WHERE ({" OR ".join(idx_table_filters)})
459
- GROUP BY table_schema, table_name, index_name, is_unique, index_type
460
- ORDER BY table_schema, table_name, index_name;
461
- """
462
- await cursor.execute(idx_query)
463
- return await cursor.fetchall()
464
-
465
- async def list_tables_info(self, connection) -> list[dict[str, Any]]:
466
- """Get list of tables with basic information for MySQL."""
467
- pool = await connection.get_pool()
468
- async with pool.acquire() as conn:
469
- async with conn.cursor() as cursor:
470
- # Get tables without row counts for better performance
471
- tables_query = """
472
- SELECT
473
- t.table_schema,
474
- t.table_name,
475
- t.table_type
476
- FROM information_schema.tables t
477
- WHERE t.table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
478
- ORDER BY t.table_schema, t.table_name;
479
- """
480
- await cursor.execute(tables_query)
481
- rows = await cursor.fetchall()
482
-
483
- # Convert rows to dictionaries
484
- return [
485
- {
486
- "table_schema": row["table_schema"],
487
- "table_name": row["table_name"],
488
- "table_type": row["table_type"],
489
- }
490
- for row in rows
491
- ]
492
-
493
-
494
- class SQLiteSchemaIntrospector(BaseSchemaIntrospector):
495
- """SQLite-specific schema introspection."""
496
-
497
- async def _execute_query(self, connection, query: str, params=()) -> list:
498
- """Helper method to execute queries on both SQLite and CSV connections."""
499
- # Handle both SQLite and CSV connections
500
- if hasattr(connection, "database_path"):
501
- # Regular SQLite connection
502
- async with aiosqlite.connect(connection.database_path) as conn:
503
- conn.row_factory = aiosqlite.Row
504
- cursor = await conn.execute(query, params)
505
- return await cursor.fetchall()
506
- else:
507
- # CSV connection - use the existing connection
508
- conn = await connection.get_pool()
509
- cursor = await conn.execute(query, params)
510
- return await cursor.fetchall()
511
-
512
- async def get_tables_info(
513
- self, connection, table_pattern: str | None = None
514
- ) -> dict[str, Any]:
515
- """Get tables information for SQLite."""
516
- where_conditions = ["type IN ('table', 'view')", "name NOT LIKE 'sqlite_%'"]
517
- params = ()
518
-
519
- if table_pattern:
520
- where_conditions.append("name LIKE ?")
521
- params = (table_pattern,)
522
-
523
- query = f"""
524
- SELECT
525
- 'main' as table_schema,
526
- name as table_name,
527
- type as table_type
528
- FROM sqlite_master
529
- WHERE {" AND ".join(where_conditions)}
530
- ORDER BY name;
531
- """
532
-
533
- return await self._execute_query(connection, query, params)
534
-
535
- async def get_columns_info(self, connection, tables: list) -> list:
536
- """Get columns information for SQLite."""
537
- if not tables:
538
- return []
539
-
540
- columns = []
541
- for table in tables:
542
- table_name = table["table_name"]
543
-
544
- # Get table info using PRAGMA
545
- pragma_query = f"PRAGMA table_info({table_name})"
546
- table_columns = await self._execute_query(connection, pragma_query)
547
-
548
- for col in table_columns:
549
- columns.append(
550
- {
551
- "table_schema": "main",
552
- "table_name": table_name,
553
- "column_name": col["name"],
554
- "data_type": col["type"],
555
- "is_nullable": "YES" if not col["notnull"] else "NO",
556
- "column_default": col["dflt_value"],
557
- "character_maximum_length": None,
558
- "numeric_precision": None,
559
- "numeric_scale": None,
560
- }
561
- )
562
-
563
- return columns
564
-
565
- async def get_foreign_keys_info(self, connection, tables: list) -> list:
566
- """Get foreign keys information for SQLite."""
567
- if not tables:
568
- return []
569
-
570
- foreign_keys = []
571
- for table in tables:
572
- table_name = table["table_name"]
573
-
574
- # Get foreign key info using PRAGMA
575
- pragma_query = f"PRAGMA foreign_key_list({table_name})"
576
- table_fks = await self._execute_query(connection, pragma_query)
577
-
578
- for fk in table_fks:
579
- foreign_keys.append(
580
- {
581
- "table_schema": "main",
582
- "table_name": table_name,
583
- "column_name": fk["from"],
584
- "foreign_table_schema": "main",
585
- "foreign_table_name": fk["table"],
586
- "foreign_column_name": fk["to"],
587
- }
588
- )
589
-
590
- return foreign_keys
591
-
592
- async def get_primary_keys_info(self, connection, tables: list) -> list:
593
- """Get primary keys information for SQLite."""
594
- if not tables:
595
- return []
596
-
597
- primary_keys = []
598
- for table in tables:
599
- table_name = table["table_name"]
600
-
601
- # Get table info using PRAGMA to find primary keys
602
- pragma_query = f"PRAGMA table_info({table_name})"
603
- table_columns = await self._execute_query(connection, pragma_query)
604
-
605
- for col in table_columns:
606
- if col["pk"]: # Primary key indicator
607
- primary_keys.append(
608
- {
609
- "table_schema": "main",
610
- "table_name": table_name,
611
- "column_name": col["name"],
612
- }
613
- )
614
-
615
- return primary_keys
616
-
617
- async def get_indexes_info(self, connection, tables: list) -> list:
618
- """Get indexes information for SQLite."""
619
- if not tables:
620
- return []
621
-
622
- indexes = []
623
- for table in tables:
624
- table_name = table["table_name"]
625
-
626
- # Get index list using PRAGMA
627
- pragma_query = f"PRAGMA index_list({table_name})"
628
- table_indexes = await self._execute_query(connection, pragma_query)
629
-
630
- for idx in table_indexes:
631
- idx_name = idx["name"]
632
- unique = bool(idx["unique"])
633
-
634
- # Skip auto-generated primary key indexes
635
- if idx_name.startswith("sqlite_autoindex_"):
636
- continue
637
-
638
- # Get index columns using PRAGMA
639
- pragma_info_query = f"PRAGMA index_info({idx_name})"
640
- idx_cols = await self._execute_query(connection, pragma_info_query)
641
- columns = [
642
- c["name"] for c in sorted(idx_cols, key=lambda r: r["seqno"])
643
- ]
644
-
645
- indexes.append(
646
- {
647
- "table_schema": "main",
648
- "table_name": table_name,
649
- "index_name": idx_name,
650
- "is_unique": unique,
651
- "index_type": None, # SQLite only has B-tree currently
652
- "column_names": columns,
653
- }
654
- )
655
-
656
- return indexes
657
-
658
- async def list_tables_info(self, connection) -> list[dict[str, Any]]:
659
- """Get list of tables with basic information for SQLite."""
660
- # Get table names without row counts for better performance
661
- tables_query = """
662
- SELECT
663
- 'main' as table_schema,
664
- name as table_name,
665
- type as table_type
666
- FROM sqlite_master
667
- WHERE type IN ('table', 'view')
668
- AND name NOT LIKE 'sqlite_%'
669
- ORDER BY name;
670
- """
671
-
672
- tables = await self._execute_query(connection, tables_query)
673
-
674
- # Convert to expected format
675
- return [
676
- {
677
- "table_schema": table["table_schema"],
678
- "table_name": table["table_name"],
679
- "table_type": table["table_type"],
680
- }
681
- for table in tables
682
- ]
12
+ from .csv import CSVConnection
13
+ from .duckdb import DuckDBConnection, DuckDBSchemaIntrospector
14
+ from .mysql import MySQLConnection, MySQLSchemaIntrospector
15
+ from .postgresql import PostgreSQLConnection, PostgreSQLSchemaIntrospector
16
+ from .sqlite import SQLiteConnection, SQLiteSchemaIntrospector
683
17
 
684
18
 
685
19
  class SchemaManager:
@@ -693,8 +27,10 @@ class SchemaManager:
693
27
  self.introspector = PostgreSQLSchemaIntrospector()
694
28
  elif isinstance(db_connection, MySQLConnection):
695
29
  self.introspector = MySQLSchemaIntrospector()
696
- elif isinstance(db_connection, (SQLiteConnection, CSVConnection)):
30
+ elif isinstance(db_connection, SQLiteConnection):
697
31
  self.introspector = SQLiteSchemaIntrospector()
32
+ elif isinstance(db_connection, (DuckDBConnection, CSVConnection)):
33
+ self.introspector = DuckDBSchemaIntrospector()
698
34
  else:
699
35
  raise ValueError(
700
36
  f"Unsupported database connection type: {type(db_connection)}"
@@ -741,98 +77,89 @@ class SchemaManager:
741
77
  "foreign_keys": [],
742
78
  "indexes": [],
743
79
  }
80
+
744
81
  return schema_info
745
82
 
746
- def _add_columns_to_schema(
747
- self, schema_info: dict[str, dict], columns: list
748
- ) -> None:
749
- """Add column information to schema."""
83
+ def _add_columns_to_schema(self, schema_info: dict, columns: list) -> None:
84
+ """Add column information to schema structure."""
750
85
  for col in columns:
751
86
  full_name = f"{col['table_schema']}.{col['table_name']}"
752
87
  if full_name in schema_info:
753
- col_info = {
88
+ column_info: ColumnInfo = {
754
89
  "data_type": col["data_type"],
755
- "nullable": col["is_nullable"] == "YES",
756
- "default": col["column_default"],
90
+ "nullable": col.get("is_nullable", "YES") == "YES",
91
+ "default": col.get("column_default"),
92
+ "max_length": col.get("character_maximum_length"),
93
+ "precision": col.get("numeric_precision"),
94
+ "scale": col.get("numeric_scale"),
757
95
  }
758
-
759
- # Add optional attributes
760
- for attr_map in [
761
- ("character_maximum_length", "max_length"),
762
- ("numeric_precision", "precision"),
763
- ("numeric_scale", "scale"),
764
- ]:
765
- if col.get(attr_map[0]):
766
- col_info[attr_map[1]] = col[attr_map[0]]
767
-
768
- schema_info[full_name]["columns"][col["column_name"]] = col_info
96
+ # Add type field for display compatibility
97
+ column_info["type"] = col["data_type"]
98
+ schema_info[full_name]["columns"][col["column_name"]] = column_info
769
99
 
770
100
  def _add_primary_keys_to_schema(
771
- self, schema_info: dict[str, dict], primary_keys: list
101
+ self, schema_info: dict, primary_keys: list
772
102
  ) -> None:
773
- """Add primary key information to schema."""
103
+ """Add primary key information to schema structure."""
774
104
  for pk in primary_keys:
775
105
  full_name = f"{pk['table_schema']}.{pk['table_name']}"
776
106
  if full_name in schema_info:
777
107
  schema_info[full_name]["primary_keys"].append(pk["column_name"])
778
108
 
779
109
  def _add_foreign_keys_to_schema(
780
- self, schema_info: dict[str, dict], foreign_keys: list
110
+ self, schema_info: dict, foreign_keys: list
781
111
  ) -> None:
782
- """Add foreign key information to schema."""
112
+ """Add foreign key information to schema structure."""
783
113
  for fk in foreign_keys:
784
114
  full_name = f"{fk['table_schema']}.{fk['table_name']}"
785
115
  if full_name in schema_info:
786
- schema_info[full_name]["foreign_keys"].append(
787
- {
788
- "column": fk["column_name"],
789
- "references": {
790
- "table": f"{fk['foreign_table_schema']}.{fk['foreign_table_name']}",
791
- "column": fk["foreign_column_name"],
792
- },
793
- }
794
- )
116
+ fk_info: ForeignKeyInfo = {
117
+ "column": fk["column_name"],
118
+ "references": {
119
+ "table": f"{fk['foreign_table_schema']}.{fk['foreign_table_name']}",
120
+ "column": fk["foreign_column_name"],
121
+ },
122
+ }
123
+ schema_info[full_name]["foreign_keys"].append(fk_info)
795
124
 
796
- def _add_indexes_to_schema(
797
- self, schema_info: dict[str, dict], indexes: list
798
- ) -> None:
799
- """Add index information to schema."""
125
+ def _add_indexes_to_schema(self, schema_info: dict, indexes: list) -> None:
126
+ """Add index information to schema structure."""
800
127
  for idx in indexes:
801
128
  full_name = f"{idx['table_schema']}.{idx['table_name']}"
802
129
  if full_name in schema_info:
803
- # Handle different column name formats from different databases
804
- if isinstance(idx["column_names"], list):
130
+ # Handle column names - could be comma-separated string or list
131
+ if isinstance(idx.get("column_names"), str):
132
+ columns = [
133
+ col.strip()
134
+ for col in idx["column_names"].split(",")
135
+ if col.strip()
136
+ ]
137
+ elif isinstance(idx.get("column_names"), list):
805
138
  columns = idx["column_names"]
806
139
  else:
807
- # MySQL returns comma-separated string
808
- columns = (
809
- idx["column_names"].split(",") if idx["column_names"] else []
810
- )
140
+ columns = []
811
141
 
812
- schema_info[full_name]["indexes"].append(
813
- {
814
- "name": idx["index_name"],
815
- "columns": columns,
816
- "unique": idx["is_unique"],
817
- "type": idx.get("index_type"),
818
- }
819
- )
142
+ index_info: IndexInfo = {
143
+ "name": idx["index_name"],
144
+ "columns": columns,
145
+ "unique": bool(idx.get("is_unique", False)),
146
+ "type": idx.get("index_type"),
147
+ }
148
+ schema_info[full_name]["indexes"].append(index_info)
820
149
 
821
150
  async def list_tables(self) -> dict[str, Any]:
822
- """Get a list of all tables with basic information."""
823
- tables = await self.introspector.list_tables_info(self.db)
151
+ """Get list of tables with basic information."""
152
+ tables_list = await self.introspector.list_tables_info(self.db)
824
153
 
825
- # Format the result
826
- result = {"tables": [], "total_tables": len(tables)}
154
+ # Add full_name and name fields for backwards compatibility
155
+ for table in tables_list:
156
+ table["full_name"] = f"{table['table_schema']}.{table['table_name']}"
157
+ table["name"] = table["table_name"]
158
+ table["schema"] = table["table_schema"]
159
+ table["type"] = table["table_type"] # Map table_type to type for display
827
160
 
828
- for table in tables:
829
- result["tables"].append(
830
- {
831
- "schema": table["table_schema"],
832
- "name": table["table_name"],
833
- "full_name": f"{table['table_schema']}.{table['table_name']}",
834
- "type": table["table_type"],
835
- }
836
- )
161
+ return {"tables": tables_list}
837
162
 
838
- return result
163
+ async def close(self):
164
+ """Close database connection."""
165
+ await self.db.close()