sqlseed 0.2.0__tar.gz → 0.2.3__tar.gz
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.
- {sqlseed-0.2.0 → sqlseed-0.2.3}/.gitignore +4 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/PKG-INFO +3 -3
- {sqlseed-0.2.0 → sqlseed-0.2.3}/README.md +2 -2
- {sqlseed-0.2.0 → sqlseed-0.2.3}/README.zh-CN.md +5 -5
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/07-ai-plugin.ipynb +12 -19
- sqlseed-0.2.3/examples/quick_demo.py +148 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/pyproject.toml +2 -1
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/cli/main.py +228 -43
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/config/models.py +5 -1
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/mapper.py +5 -2
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/stream.py +1 -2
- sqlseed-0.2.3/src/sqlseed/plugins/__init__.py +12 -0
- sqlseed-0.2.3/src/sqlseed/plugins/hookspecs.py +119 -0
- sqlseed-0.2.3/src/sqlseed/plugins/manager.py +37 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/CHANGELOG.md +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/CHANGELOG.zh-CN.md +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/LICENSE +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/build_demo_db.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/01-quickstart.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/02-column-mapping.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/03-generators.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/04-database-advanced.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/05-dag-and-constraints.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/06-config-deep-dive.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/08-mcp-server.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/09-plugin-hooks.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/10-cli-reference.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/11-utilities.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/examples/notebooks/12-testing-patterns.ipynb +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/scripts/_create_demo_db.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/scripts/quickstart.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/logger.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/metrics.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/paths.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/progress.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/schema_helpers.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_utils/sql_safe.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/_version.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/cli/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/config/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/config/loader.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/config/snapshot.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/column_dag.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/constraints.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/enrichment.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/expression.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/orchestrator.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/plugin_mediator.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/relation.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/result.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/schema.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/transform.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/core/unique_adjuster.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/_base_adapter.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/_compat.py +2 -2
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/_helpers.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/_protocol.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/optimizer.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/raw_sqlite_adapter.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/database/sqlite_utils_adapter.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/__init__.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/_dispatch.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/_json_helpers.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/_protocol.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/_string_helpers.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/base_provider.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/faker_provider.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/mimesis_provider.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/generators/registry.py +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/src/sqlseed/py.typed +0 -0
- {sqlseed-0.2.0 → sqlseed-0.2.3}/uv.lock +0 -0
|
@@ -33,6 +33,7 @@ ENV/
|
|
|
33
33
|
.idea/
|
|
34
34
|
.trae/
|
|
35
35
|
.claude/
|
|
36
|
+
.gemini/
|
|
36
37
|
.sonarlint/
|
|
37
38
|
*.swp
|
|
38
39
|
*.swo
|
|
@@ -62,6 +63,9 @@ examples/notebooks/batch_config.yaml
|
|
|
62
63
|
*.nbconvert.ipynb
|
|
63
64
|
.ipynb_checkpoints/
|
|
64
65
|
|
|
66
|
+
# AI-generated config outputs
|
|
67
|
+
*_config.yaml
|
|
68
|
+
|
|
65
69
|
# OS
|
|
66
70
|
.DS_Store
|
|
67
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlseed
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Declarative SQLite test data generation toolkit
|
|
5
5
|
Project-URL: Homepage, https://github.com/sunbos/sqlseed
|
|
6
6
|
Project-URL: Documentation, https://github.com/sunbos/sqlseed#readme
|
|
@@ -726,7 +726,7 @@ sqlseed ai-suggest app.db --table projects --output projects.yaml
|
|
|
726
726
|
sqlseed ai-suggest app.db --table projects --output projects.yaml --verify
|
|
727
727
|
|
|
728
728
|
# Specify model (defaults to Gemma 4 26B via Google AI Studio)
|
|
729
|
-
sqlseed ai-suggest app.db --table projects --output projects.yaml --model gemma-4-26b-it
|
|
729
|
+
sqlseed ai-suggest app.db --table projects --output projects.yaml --model gemma-4-26b-a4b-it
|
|
730
730
|
|
|
731
731
|
# Use local LM Studio / Ollama
|
|
732
732
|
sqlseed ai-suggest app.db --table projects --output projects.yaml --backend lm_studio --model google/gemma-4-e4b
|
|
@@ -1079,7 +1079,7 @@ Tests cover all core modules, with path structure mirroring `src/`: `test_core/`
|
|
|
1079
1079
|
| `sqlseed[faker]` | + faker>=30.0 | Faker data engine |
|
|
1080
1080
|
| `sqlseed[mimesis]` | + mimesis>=18.0 | Mimesis data engine (recommended) |
|
|
1081
1081
|
| `sqlseed[docs]` | + mkdocs-material, mkdocstrings | Documentation build |
|
|
1082
|
-
| `sqlseed-ai` | sqlseed, **openai>=1.0
|
|
1082
|
+
| `sqlseed-ai` | sqlseed, **openai>=1.0** | AI plugin (Gemma 4 Native Function Calling), auto-registered via entry-point |
|
|
1083
1083
|
| `mcp-server-sqlseed` | sqlseed, **mcp>=1.0** | MCP server, standalone CLI tool |
|
|
1084
1084
|
| `mcp-server-sqlseed[ai]` | + sqlseed-ai | MCP server with AI support |
|
|
1085
1085
|
|
|
@@ -666,7 +666,7 @@ sqlseed ai-suggest app.db --table projects --output projects.yaml
|
|
|
666
666
|
sqlseed ai-suggest app.db --table projects --output projects.yaml --verify
|
|
667
667
|
|
|
668
668
|
# Specify model (defaults to Gemma 4 26B via Google AI Studio)
|
|
669
|
-
sqlseed ai-suggest app.db --table projects --output projects.yaml --model gemma-4-26b-it
|
|
669
|
+
sqlseed ai-suggest app.db --table projects --output projects.yaml --model gemma-4-26b-a4b-it
|
|
670
670
|
|
|
671
671
|
# Use local LM Studio / Ollama
|
|
672
672
|
sqlseed ai-suggest app.db --table projects --output projects.yaml --backend lm_studio --model google/gemma-4-e4b
|
|
@@ -1019,7 +1019,7 @@ Tests cover all core modules, with path structure mirroring `src/`: `test_core/`
|
|
|
1019
1019
|
| `sqlseed[faker]` | + faker>=30.0 | Faker data engine |
|
|
1020
1020
|
| `sqlseed[mimesis]` | + mimesis>=18.0 | Mimesis data engine (recommended) |
|
|
1021
1021
|
| `sqlseed[docs]` | + mkdocs-material, mkdocstrings | Documentation build |
|
|
1022
|
-
| `sqlseed-ai` | sqlseed, **openai>=1.0
|
|
1022
|
+
| `sqlseed-ai` | sqlseed, **openai>=1.0** | AI plugin (Gemma 4 Native Function Calling), auto-registered via entry-point |
|
|
1023
1023
|
| `mcp-server-sqlseed` | sqlseed, **mcp>=1.0** | MCP server, standalone CLI tool |
|
|
1024
1024
|
| `mcp-server-sqlseed[ai]` | + sqlseed-ai | MCP server with AI support |
|
|
1025
1025
|
|
|
@@ -562,10 +562,10 @@ sqlseed ai-suggest app.db --table projects --output projects.yaml
|
|
|
562
562
|
sqlseed ai-suggest app.db --table projects --output projects.yaml --verify
|
|
563
563
|
|
|
564
564
|
# 指定模型(支持多后端:Google AI Studio、LM Studio、Ollama、OpenAI-compatible)
|
|
565
|
-
sqlseed ai-suggest app.db --table projects -o projects.yaml --model gemma-4-26b-it --backend google_ai_studio
|
|
565
|
+
sqlseed ai-suggest app.db --table projects -o projects.yaml --model gemma-4-26b-a4b-it --backend google_ai_studio
|
|
566
566
|
sqlseed ai-suggest app.db --table projects -o projects.yaml --model gemma-4-31b-it --backend google_ai_studio
|
|
567
567
|
sqlseed ai-suggest app.db --table projects -o projects.yaml --model google/gemma-4-e4b --backend lm_studio
|
|
568
|
-
sqlseed ai-suggest app.db --table projects -o projects.yaml --model gemma-4-
|
|
568
|
+
sqlseed ai-suggest app.db --table projects -o projects.yaml --model gemma-4-e4b-it --backend ollama
|
|
569
569
|
```
|
|
570
570
|
|
|
571
571
|
**Gemma 4 原生函数调用(GEMMA_TOOLS)**:
|
|
@@ -697,8 +697,8 @@ sqlseed ai-suggest app.db -t users -o users.yaml --max-retries 0
|
|
|
697
697
|
sqlseed ai-suggest app.db -t users -o users.yaml --no-cache
|
|
698
698
|
|
|
699
699
|
# ═══ AI 后端选择 ═══
|
|
700
|
-
sqlseed ai-suggest app.db -t users -o users.yaml --backend google_ai_studio --model gemma-4-26b-it
|
|
701
|
-
sqlseed ai-suggest app.db -t users -o users.yaml --backend ollama --model gemma-4-
|
|
700
|
+
sqlseed ai-suggest app.db -t users -o users.yaml --backend google_ai_studio --model gemma-4-26b-a4b-it
|
|
701
|
+
sqlseed ai-suggest app.db -t users -o users.yaml --backend ollama --model gemma-4-e4b-it
|
|
702
702
|
sqlseed ai-suggest app.db -t users -o users.yaml --backend lm_studio --model google/gemma-4-e4b
|
|
703
703
|
sqlseed ai-suggest app.db -t users -o users.yaml --backend openai_compat --model your-model --base-url https://your-api-endpoint
|
|
704
704
|
```
|
|
@@ -827,7 +827,7 @@ mypy src/sqlseed/ # 类型检查
|
|
|
827
827
|
| `sqlseed[faker]` | + faker>=30.0 | Faker 数据引擎 |
|
|
828
828
|
| `sqlseed[mimesis]` | + mimesis>=18.0 | Mimesis 数据引擎(推荐) |
|
|
829
829
|
| `sqlseed[docs]` | + mkdocs-material, mkdocstrings | 文档构建 |
|
|
830
|
-
| `sqlseed-ai` | sqlseed, **openai>=1.0
|
|
830
|
+
| `sqlseed-ai` | sqlseed, **openai>=1.0** | AI 插件,通过 entry-point 自动注册,支持 Gemma 4 GEMMA_TOOLS |
|
|
831
831
|
| `mcp-server-sqlseed` | sqlseed, **mcp>=1.0** | MCP 服务器,独立 CLI 工具 |
|
|
832
832
|
| `mcp-server-sqlseed[ai]` | + sqlseed-ai | MCP 服务器含 AI 支持 |
|
|
833
833
|
|
|
@@ -367,32 +367,25 @@
|
|
|
367
367
|
"name": "stdout",
|
|
368
368
|
"output_type": "stream",
|
|
369
369
|
"text": [
|
|
370
|
-
"
|
|
371
|
-
" 1.
|
|
372
|
-
" 2.
|
|
373
|
-
" 3.
|
|
374
|
-
" 4.
|
|
375
|
-
" 5.
|
|
376
|
-
" 6. minimax/minimax-m2.5:free\n",
|
|
377
|
-
" 7. openai/gpt-oss-120b:free\n",
|
|
378
|
-
" 8. nvidia/nemotron-3-nano-30b-a3b:free\n",
|
|
379
|
-
" 9. google/gemma-4-31b-it:free\n",
|
|
380
|
-
" 10. nvidia/nemotron-nano-9b-v2:free\n",
|
|
381
|
-
" 11. openai/gpt-oss-20b:free\n",
|
|
382
|
-
" 12. google/gemma-4-26b-a4b-it:free\n",
|
|
370
|
+
"Gemma 4 模型优先级 (5 个):\n",
|
|
371
|
+
" 1. gemma-4-31b-it (Gemma 4 31B Dense)\n",
|
|
372
|
+
" 2. gemma-4-26b-a4b-it (Gemma 4 26B A4B MoE (Recommended))\n",
|
|
373
|
+
" 3. gemma-4-12b-it (Gemma 4 12B Unified (Laptop))\n",
|
|
374
|
+
" 4. gemma-4-e4b-it (Gemma 4 E4B (4B Effective, Edge))\n",
|
|
375
|
+
" 5. gemma-4-e2b-it (Gemma 4 E2B (2B Effective, Edge))\n",
|
|
383
376
|
"\n",
|
|
384
|
-
"
|
|
377
|
+
"select_gemma_model() 按优先级尝试, 自动回退\n"
|
|
385
378
|
]
|
|
386
379
|
}
|
|
387
380
|
],
|
|
388
381
|
"source": [
|
|
389
|
-
"from sqlseed_ai._model_selector import
|
|
382
|
+
"from sqlseed_ai._model_selector import _GEMMA_MODEL_PRIORITY\n",
|
|
390
383
|
"\n",
|
|
391
|
-
"print(f'
|
|
392
|
-
"for i, model in enumerate(
|
|
393
|
-
" print(f' {i}. {model}')\n",
|
|
384
|
+
"print(f'Gemma 4 模型优先级 ({len(_GEMMA_MODEL_PRIORITY)} 个):')\n",
|
|
385
|
+
"for i, model in enumerate(_GEMMA_MODEL_PRIORITY, 1):\n",
|
|
386
|
+
" print(f' {i}. {model.value} ({model.display_name})')\n",
|
|
394
387
|
"\n",
|
|
395
|
-
"print('\\
|
|
388
|
+
"print('\\nselect_gemma_model() 按优先级尝试, 自动回退')\n",
|
|
396
389
|
"\n"
|
|
397
390
|
]
|
|
398
391
|
},
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuickStart Demo Script with Real-time Progress Display
|
|
3
|
+
|
|
4
|
+
Features:
|
|
5
|
+
- Creates demo database with 8 tables
|
|
6
|
+
- Fills data with real-time progress bars
|
|
7
|
+
- Shows completion stats and sample data
|
|
8
|
+
- Fast execution (~10 seconds total)
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
python quick_demo.py
|
|
12
|
+
|
|
13
|
+
Perfect for live demonstrations!
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sqlite3
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from build_demo_db import build as build_demo
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, TimeRemainingColumn
|
|
24
|
+
from rich.table import Table
|
|
25
|
+
|
|
26
|
+
from sqlseed import fill
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
console = Console()
|
|
31
|
+
|
|
32
|
+
# Step 1: Create database
|
|
33
|
+
console.print("\n[bold blue]🚀 QuickStart Demo: sqlseed SQLite Test Data Generator[/bold blue]")
|
|
34
|
+
console.print("=" * 60)
|
|
35
|
+
|
|
36
|
+
with Progress(
|
|
37
|
+
SpinnerColumn(),
|
|
38
|
+
TextColumn("[progress.description]{task.description}"),
|
|
39
|
+
console=console,
|
|
40
|
+
transient=False,
|
|
41
|
+
) as progress:
|
|
42
|
+
task = progress.add_task("Creating demo database...", total=None)
|
|
43
|
+
db_path = str(build_demo(Path(__file__).parent / "_demo.db"))
|
|
44
|
+
progress.update(task, completed=1)
|
|
45
|
+
|
|
46
|
+
console.print(f"✅ Database created: [green]{db_path}[/green]")
|
|
47
|
+
|
|
48
|
+
# Step 2: Fill data with progress
|
|
49
|
+
console.print("\n[bold blue]📊 Filling test data with real-time progress:[/bold blue]")
|
|
50
|
+
|
|
51
|
+
fill_stats = []
|
|
52
|
+
|
|
53
|
+
with Progress(
|
|
54
|
+
SpinnerColumn(),
|
|
55
|
+
TextColumn("[progress.description]{task.description}"),
|
|
56
|
+
BarColumn(),
|
|
57
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
58
|
+
TextColumn("{task.completed}/{task.total}"),
|
|
59
|
+
TimeRemainingColumn(),
|
|
60
|
+
TimeElapsedColumn(),
|
|
61
|
+
console=console,
|
|
62
|
+
transient=False,
|
|
63
|
+
refresh_per_second=2,
|
|
64
|
+
) as progress:
|
|
65
|
+
# organizations
|
|
66
|
+
task = progress.add_task("organizations", total=10)
|
|
67
|
+
result = fill(db_path, table="organizations", count=10)
|
|
68
|
+
fill_stats.append(("organizations", result.count, result.elapsed))
|
|
69
|
+
progress.update(task, completed=10)
|
|
70
|
+
|
|
71
|
+
# members
|
|
72
|
+
task = progress.add_task("members", total=100)
|
|
73
|
+
result = fill(db_path, table="members", count=100)
|
|
74
|
+
fill_stats.append(("members", result.count, result.elapsed))
|
|
75
|
+
progress.update(task, completed=100)
|
|
76
|
+
|
|
77
|
+
# projects
|
|
78
|
+
task = progress.add_task("projects", total=30)
|
|
79
|
+
result = fill(db_path, table="projects", count=30)
|
|
80
|
+
fill_stats.append(("projects", result.count, result.elapsed))
|
|
81
|
+
progress.update(task, completed=30)
|
|
82
|
+
|
|
83
|
+
# tasks
|
|
84
|
+
task = progress.add_task("tasks", total=200)
|
|
85
|
+
result = fill(db_path, table="tasks", count=200)
|
|
86
|
+
fill_stats.append(("tasks", result.count, result.elapsed))
|
|
87
|
+
progress.update(task, completed=200)
|
|
88
|
+
|
|
89
|
+
# Step 3: Show summary
|
|
90
|
+
console.print("\n[bold blue]📈 Fill Summary:[/bold blue]")
|
|
91
|
+
table = Table(show_header=True, header_style="bold green")
|
|
92
|
+
table.add_column("Table")
|
|
93
|
+
table.add_column("Rows")
|
|
94
|
+
table.add_column("Time (s)", justify="right")
|
|
95
|
+
|
|
96
|
+
total_rows = 0
|
|
97
|
+
total_time = 0.0
|
|
98
|
+
for table_name, count, elapsed in fill_stats:
|
|
99
|
+
table.add_row(table_name, str(count), f"{elapsed:.2f}")
|
|
100
|
+
total_rows += count
|
|
101
|
+
total_time += elapsed
|
|
102
|
+
|
|
103
|
+
table.add_row("[bold]Total[/bold]", str(total_rows), f"[bold]{total_time:.2f}[/bold]")
|
|
104
|
+
console.print(table)
|
|
105
|
+
|
|
106
|
+
# Step 4: Show sample data
|
|
107
|
+
console.print("\n[bold blue]🔍 Sample Data Preview:[/bold blue]")
|
|
108
|
+
|
|
109
|
+
conn = sqlite3.connect(db_path)
|
|
110
|
+
conn.row_factory = sqlite3.Row
|
|
111
|
+
|
|
112
|
+
# Preview members
|
|
113
|
+
console.print("\n[bold green]members[/bold green] (3 rows):")
|
|
114
|
+
cursor = conn.execute("SELECT member_id, name, email, org_code FROM members LIMIT 3")
|
|
115
|
+
rows = cursor.fetchall()
|
|
116
|
+
member_table = Table()
|
|
117
|
+
for col in ("member_id", "name", "email", "org_code"):
|
|
118
|
+
member_table.add_column(col, style="cyan")
|
|
119
|
+
for row in rows:
|
|
120
|
+
member_table.add_row(*[str(row[c]) for c in ("member_id", "name", "email", "org_code")])
|
|
121
|
+
console.print(member_table)
|
|
122
|
+
|
|
123
|
+
# Preview tasks
|
|
124
|
+
console.print("\n[bold green]tasks[/bold green] (3 rows):")
|
|
125
|
+
cursor = conn.execute("SELECT task_id, project_id, title, priority, status FROM tasks LIMIT 3")
|
|
126
|
+
rows = cursor.fetchall()
|
|
127
|
+
task_table = Table()
|
|
128
|
+
for col in ("task_id", "project_id", "title", "priority", "status"):
|
|
129
|
+
task_table.add_column(col, style="cyan")
|
|
130
|
+
for row in rows:
|
|
131
|
+
task_table.add_row(*[str(row[c]) for c in ("task_id", "project_id", "title", "priority", "status")])
|
|
132
|
+
console.print(task_table)
|
|
133
|
+
|
|
134
|
+
conn.close()
|
|
135
|
+
|
|
136
|
+
# Step 5: Final message
|
|
137
|
+
console.print("\n" + "=" * 60)
|
|
138
|
+
console.print("[bold green]🎉 Demo Complete! [/bold green]")
|
|
139
|
+
console.print("\nKey Features Demonstrated:")
|
|
140
|
+
console.print(" • Auto-schema detection")
|
|
141
|
+
console.print(" • Smart column type matching")
|
|
142
|
+
console.print(" • Foreign key integrity")
|
|
143
|
+
console.print(" • Real-time progress tracking")
|
|
144
|
+
console.print(f"\n[italic]Total time:[/italic] [bold]{total_time:.2f} seconds[/bold]")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|
|
@@ -8,8 +8,9 @@ from typing import Any
|
|
|
8
8
|
import click
|
|
9
9
|
import yaml
|
|
10
10
|
from rich.console import Console
|
|
11
|
-
from rich.
|
|
11
|
+
from rich.live import Live
|
|
12
12
|
from rich.table import Table as RichTable
|
|
13
|
+
from rich.text import Text
|
|
13
14
|
|
|
14
15
|
from sqlseed import fill as api_fill
|
|
15
16
|
from sqlseed import fill_from_config
|
|
@@ -21,17 +22,17 @@ from sqlseed.config.models import GeneratorConfig, ProviderType, TableConfig
|
|
|
21
22
|
from sqlseed.config.snapshot import SnapshotManager
|
|
22
23
|
from sqlseed.core.orchestrator import DataOrchestrator
|
|
23
24
|
|
|
24
|
-
logger = get_logger(__name__)
|
|
25
|
-
|
|
26
25
|
try:
|
|
27
26
|
from sqlseed_ai.analyzer import SchemaAnalyzer
|
|
28
|
-
from sqlseed_ai.config import AIConfig
|
|
29
|
-
from sqlseed_ai.refiner import AiConfigRefiner
|
|
27
|
+
from sqlseed_ai.config import AIBackend, AIConfig
|
|
28
|
+
from sqlseed_ai.refiner import AiConfigRefiner
|
|
30
29
|
|
|
31
30
|
HAS_AI_PLUGIN = True
|
|
32
31
|
except ImportError:
|
|
33
32
|
HAS_AI_PLUGIN = False
|
|
34
33
|
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
35
36
|
|
|
36
37
|
@click.group()
|
|
37
38
|
@click.version_option(version=__version__, prog_name="sqlseed")
|
|
@@ -137,9 +138,6 @@ def fill(**kwargs: Any) -> None:
|
|
|
137
138
|
"--count is required when not using --config. Use -n <number> to specify the number of rows to generate."
|
|
138
139
|
)
|
|
139
140
|
|
|
140
|
-
if not config_path and count is None:
|
|
141
|
-
count = _FILL_DEFAULT_COUNT
|
|
142
|
-
|
|
143
141
|
kwargs["count"] = count
|
|
144
142
|
_execute_fill(kwargs)
|
|
145
143
|
|
|
@@ -355,52 +353,241 @@ def _sanitize_table_config(config_dict: dict[str, Any]) -> None:
|
|
|
355
353
|
col["name"] = re.sub(r"^[:.]+", "", col_name)
|
|
356
354
|
|
|
357
355
|
|
|
358
|
-
|
|
356
|
+
class _StreamingProgressDisplay:
|
|
357
|
+
"""Rich Live display for streaming LLM output with phase indicators."""
|
|
358
|
+
|
|
359
|
+
def __init__(self) -> None:
|
|
360
|
+
self._phase = "connecting"
|
|
361
|
+
self._model = ""
|
|
362
|
+
self._token_count = 0
|
|
363
|
+
self._preview = ""
|
|
364
|
+
self._attempt = 0
|
|
365
|
+
self._max_retries = 0
|
|
366
|
+
self._live: Live | None = None
|
|
367
|
+
|
|
368
|
+
def _render(self) -> Text:
|
|
369
|
+
parts: list[tuple[str, str]] = []
|
|
370
|
+
|
|
371
|
+
# Phase icon and description
|
|
372
|
+
if self._phase == "connecting":
|
|
373
|
+
parts.append(("⏳", "bold yellow"))
|
|
374
|
+
parts.append((f" Connecting to {self._model}...", "yellow"))
|
|
375
|
+
elif self._phase == "streaming":
|
|
376
|
+
parts.append(("⚡", "bold cyan"))
|
|
377
|
+
parts.append((f" Generating ({self._token_count} tokens)", "cyan"))
|
|
378
|
+
if self._preview:
|
|
379
|
+
parts.append((f" {self._preview[-60:]}", "dim"))
|
|
380
|
+
elif self._phase == "parsing":
|
|
381
|
+
parts.append(("📋", "bold blue"))
|
|
382
|
+
parts.append((f" Parsing response ({self._token_count} tokens)...", "blue"))
|
|
383
|
+
elif self._phase == "validating":
|
|
384
|
+
parts.append(("✅", "bold green"))
|
|
385
|
+
parts.append((" Validating configuration...", "green"))
|
|
386
|
+
elif self._phase == "refining":
|
|
387
|
+
parts.append(("🔄", "bold yellow"))
|
|
388
|
+
retry_info = f" (attempt {self._attempt + 1}/{self._max_retries})" if self._max_retries > 0 else ""
|
|
389
|
+
parts.append((f" Self-correction{retry_info}...", "yellow"))
|
|
390
|
+
elif self._phase == "done":
|
|
391
|
+
parts.append(("✅", "bold green"))
|
|
392
|
+
parts.append((f" Done ({self._token_count} tokens)", "green"))
|
|
393
|
+
|
|
394
|
+
text = Text("")
|
|
395
|
+
for content, style in parts:
|
|
396
|
+
text.append(content, style=style)
|
|
397
|
+
return text
|
|
398
|
+
|
|
399
|
+
def start(self) -> None:
|
|
400
|
+
self._live = Live(self._render(), console=Console(), transient=False, refresh_per_second=8)
|
|
401
|
+
self._live.start()
|
|
402
|
+
|
|
403
|
+
def stop(self) -> None:
|
|
404
|
+
if self._live:
|
|
405
|
+
self._live.stop()
|
|
406
|
+
self._live = None
|
|
407
|
+
|
|
408
|
+
def update(self, phase: str, info: dict[str, Any]) -> None:
|
|
409
|
+
self._phase = phase
|
|
410
|
+
if "model" in info:
|
|
411
|
+
self._model = info["model"]
|
|
412
|
+
if "count" in info:
|
|
413
|
+
self._token_count = info["count"]
|
|
414
|
+
if "token" in info:
|
|
415
|
+
# Show a rolling preview of the last few tokens
|
|
416
|
+
self._preview += info["token"]
|
|
417
|
+
if len(self._preview) > 80:
|
|
418
|
+
self._preview = self._preview[-80:]
|
|
419
|
+
if "tokens" in info:
|
|
420
|
+
self._token_count = info["tokens"]
|
|
421
|
+
if "attempt" in info:
|
|
422
|
+
self._attempt = info["attempt"]
|
|
423
|
+
if "max_retries" in info:
|
|
424
|
+
self._max_retries = info["max_retries"]
|
|
425
|
+
if self._live:
|
|
426
|
+
self._live.update(self._render())
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _run_ai_analysis(
|
|
430
|
+
analyzer: Any,
|
|
431
|
+
db_path: str,
|
|
432
|
+
table: str,
|
|
433
|
+
verify: bool,
|
|
434
|
+
max_retries: int,
|
|
435
|
+
no_cache: bool,
|
|
436
|
+
) -> Any:
|
|
437
|
+
config = analyzer._config
|
|
438
|
+
use_streaming = config.should_use_streaming() if config else True
|
|
439
|
+
use_compact = config.should_use_ultra_compact() if config else False
|
|
440
|
+
|
|
441
|
+
# Probe inference speed for local backends and show estimated time
|
|
442
|
+
if config and config.backend in (AIBackend.LM_STUDIO, AIBackend.OLLAMA):
|
|
443
|
+
speed_info = config.probe_inference_speed()
|
|
444
|
+
if speed_info and speed_info.get("is_slow"):
|
|
445
|
+
tps = speed_info["tokens_per_second"]
|
|
446
|
+
est_seconds = int(512 / tps) if tps > 0 else 120
|
|
447
|
+
timeout_display = int(config.resolve_timeout())
|
|
448
|
+
click.echo(
|
|
449
|
+
f"Local inference speed: ~{tps} tok/s. "
|
|
450
|
+
f"Estimated wait: ~{est_seconds}s (timeout: {timeout_display}s). "
|
|
451
|
+
f"Using optimized settings for {config.model}.",
|
|
452
|
+
err=True,
|
|
453
|
+
)
|
|
454
|
+
elif speed_info is None:
|
|
455
|
+
# Probe failed — LM Studio may not be running
|
|
456
|
+
click.echo(
|
|
457
|
+
"Warning: Could not probe local inference speed. "
|
|
458
|
+
"Ensure LM Studio/Ollama is running and a model is loaded.",
|
|
459
|
+
err=True,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if use_streaming:
|
|
463
|
+
display = _StreamingProgressDisplay()
|
|
464
|
+
if verify and max_retries > 0:
|
|
465
|
+
return _handle_ai_verification_streaming(
|
|
466
|
+
analyzer,
|
|
467
|
+
db_path,
|
|
468
|
+
table,
|
|
469
|
+
max_retries,
|
|
470
|
+
no_cache,
|
|
471
|
+
display,
|
|
472
|
+
use_compact=use_compact,
|
|
473
|
+
)
|
|
474
|
+
return _handle_ai_direct(analyzer, db_path, table, use_compact=use_compact, display=display)
|
|
475
|
+
|
|
476
|
+
# Small local models: non-streaming is 5-7x faster (14s vs 75-100s)
|
|
477
|
+
# because streaming TTFT is extremely high on limited GPU hardware.
|
|
478
|
+
if verify and max_retries > 0:
|
|
479
|
+
return _handle_ai_verification_non_streaming(
|
|
480
|
+
analyzer, db_path, table, max_retries, no_cache, use_compact=use_compact
|
|
481
|
+
)
|
|
482
|
+
return _handle_ai_direct(analyzer, db_path, table, use_compact=use_compact)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _handle_ai_direct(
|
|
486
|
+
analyzer: Any,
|
|
487
|
+
db_path: str,
|
|
488
|
+
table: str,
|
|
489
|
+
*,
|
|
490
|
+
use_compact: bool = False,
|
|
491
|
+
display: _StreamingProgressDisplay | None = None,
|
|
492
|
+
) -> Any:
|
|
493
|
+
"""Handle AI direct suggestion with prompt-level fallback.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
analyzer: SchemaAnalyzer instance.
|
|
497
|
+
db_path: Path to the SQLite database.
|
|
498
|
+
table: Table name to analyze.
|
|
499
|
+
use_compact: Whether to force ultra-compact mode.
|
|
500
|
+
display: If provided, use streaming with this progress display.
|
|
501
|
+
"""
|
|
502
|
+
with DataOrchestrator(db_path) as orch:
|
|
503
|
+
schema_ctx = orch.get_schema_context(table)
|
|
504
|
+
|
|
505
|
+
prompt_levels = [(True, True)] if use_compact else [(False, False), (True, False), (True, True)]
|
|
506
|
+
|
|
507
|
+
for compact, ultra in prompt_levels:
|
|
508
|
+
messages = analyzer.build_initial_messages(schema_ctx, compact=compact, ultra_compact=ultra)
|
|
509
|
+
try:
|
|
510
|
+
if display:
|
|
511
|
+
display.start()
|
|
512
|
+
result = analyzer.call_llm_streaming(messages, on_progress=display.update)
|
|
513
|
+
display.stop()
|
|
514
|
+
else:
|
|
515
|
+
mode = "ultra-compact" if ultra else ("compact" if compact else "standard")
|
|
516
|
+
timeout_s = int(analyzer._config.resolve_timeout()) if analyzer._config else 300
|
|
517
|
+
click.echo(f"Analyzing schema & generating AI suggestions ({mode} mode, timeout: {timeout_s}s)...")
|
|
518
|
+
result = analyzer.call_llm(messages)
|
|
519
|
+
if result:
|
|
520
|
+
return result
|
|
521
|
+
click.echo("AI returned empty result, retrying with shorter prompt...", err=True)
|
|
522
|
+
continue
|
|
523
|
+
except (ValueError, RuntimeError, OSError) as e:
|
|
524
|
+
if display:
|
|
525
|
+
display.stop()
|
|
526
|
+
err_msg = str(e).lower()
|
|
527
|
+
if "context" in err_msg and "exceed" in err_msg and not ultra:
|
|
528
|
+
click.echo("Context size exceeded, retrying with shorter prompt...", err=True)
|
|
529
|
+
continue
|
|
530
|
+
click.echo(f"AI suggestion failed: {e}", err=True)
|
|
531
|
+
return None
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _handle_ai_verification_non_streaming(
|
|
536
|
+
analyzer: Any,
|
|
537
|
+
db_path: str,
|
|
538
|
+
table: str,
|
|
539
|
+
max_retries: int,
|
|
540
|
+
no_cache: bool,
|
|
541
|
+
*,
|
|
542
|
+
use_compact: bool = False,
|
|
543
|
+
) -> Any:
|
|
544
|
+
if AiConfigRefiner is None:
|
|
545
|
+
raise ImportError("sqlseed-ai plugin not installed. Install with: pip install sqlseed-ai")
|
|
546
|
+
|
|
359
547
|
refiner = AiConfigRefiner(analyzer, db_path)
|
|
360
548
|
try:
|
|
549
|
+
timeout_s = int(analyzer._config.resolve_timeout()) if analyzer._config else 300
|
|
550
|
+
click.echo(f"Analyzing schema & generating AI suggestions with self-correction (timeout: {timeout_s}s)...")
|
|
361
551
|
return refiner.generate_and_refine(
|
|
362
552
|
table_name=table,
|
|
363
553
|
max_retries=max_retries,
|
|
364
554
|
no_cache=no_cache,
|
|
555
|
+
use_compact=use_compact,
|
|
365
556
|
)
|
|
366
|
-
except
|
|
557
|
+
except (ValueError, RuntimeError, OSError) as e:
|
|
367
558
|
click.echo(f"AI suggestion failed: {e}", err=True)
|
|
368
559
|
return None
|
|
369
560
|
|
|
370
561
|
|
|
371
|
-
def
|
|
372
|
-
with DataOrchestrator(db_path) as orch:
|
|
373
|
-
schema_ctx = orch.get_schema_context(table)
|
|
374
|
-
messages = analyzer.build_initial_messages(schema_ctx)
|
|
375
|
-
try:
|
|
376
|
-
return analyzer.call_llm(messages)
|
|
377
|
-
except (ValueError, RuntimeError) as e:
|
|
378
|
-
click.echo(f"AI suggestion failed: {e}", err=True)
|
|
379
|
-
return None
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
def _run_ai_analysis(
|
|
562
|
+
def _handle_ai_verification_streaming(
|
|
383
563
|
analyzer: Any,
|
|
384
564
|
db_path: str,
|
|
385
565
|
table: str,
|
|
386
|
-
verify: bool,
|
|
387
566
|
max_retries: int,
|
|
388
567
|
no_cache: bool,
|
|
568
|
+
display: _StreamingProgressDisplay,
|
|
569
|
+
*,
|
|
570
|
+
use_compact: bool = False,
|
|
389
571
|
) -> Any:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
572
|
+
if AiConfigRefiner is None:
|
|
573
|
+
raise ImportError("sqlseed-ai plugin not installed. Install with: pip install sqlseed-ai")
|
|
574
|
+
|
|
575
|
+
refiner = AiConfigRefiner(analyzer, db_path)
|
|
576
|
+
try:
|
|
577
|
+
display.start()
|
|
578
|
+
result = refiner.generate_and_refine_streaming(
|
|
579
|
+
table_name=table,
|
|
580
|
+
max_retries=max_retries,
|
|
581
|
+
no_cache=no_cache,
|
|
582
|
+
on_progress=display.update,
|
|
583
|
+
use_compact=use_compact,
|
|
584
|
+
)
|
|
585
|
+
display.stop()
|
|
586
|
+
return result
|
|
587
|
+
except (ValueError, RuntimeError, OSError) as e:
|
|
588
|
+
display.stop()
|
|
589
|
+
click.echo(f"AI suggestion failed: {e}", err=True)
|
|
590
|
+
return None
|
|
404
591
|
|
|
405
592
|
|
|
406
593
|
def _write_ai_output(output: str, db_path: str, result: Any) -> None:
|
|
@@ -444,7 +631,7 @@ def _report_ai_failure() -> None:
|
|
|
444
631
|
@click.argument("db_path")
|
|
445
632
|
@click.option("--table", "-t", required=True, help="Target table name")
|
|
446
633
|
@click.option("--output", "-o", required=True, help="Output YAML file path")
|
|
447
|
-
@click.option("--model", "-m", default=None, help="AI model name (default: auto-select
|
|
634
|
+
@click.option("--model", "-m", default=None, help="AI model name (default: auto-select based on backend)")
|
|
448
635
|
@click.option("--api-key", envvar="SQLSEED_AI_API_KEY", default=None, help="AI API key (env: SQLSEED_AI_API_KEY)")
|
|
449
636
|
@click.option(
|
|
450
637
|
"--base-url",
|
|
@@ -455,7 +642,7 @@ def _report_ai_failure() -> None:
|
|
|
455
642
|
@click.option("--max-retries", default=3, type=int, help="Max refinement retries, 0=disable (default: 3)")
|
|
456
643
|
@click.option("--verify/--no-verify", default=True, help="Enable AI config self-correction (default: verify)")
|
|
457
644
|
@click.option("--no-cache", is_flag=True, help="Skip cached AI configs")
|
|
458
|
-
@click.option("--timeout", default=
|
|
645
|
+
@click.option("--timeout", default=0, type=float, help="API call timeout in seconds (0=auto, default: auto)")
|
|
459
646
|
def ai_suggest(
|
|
460
647
|
db_path: str,
|
|
461
648
|
table: str,
|
|
@@ -490,7 +677,8 @@ def ai_suggest(
|
|
|
490
677
|
click.echo(f"Using AI model: {resolved_model} (via {backend_name})")
|
|
491
678
|
|
|
492
679
|
analyzer = SchemaAnalyzer(config=ai_config)
|
|
493
|
-
|
|
680
|
+
resolved_timeout = ai_config.resolve_timeout()
|
|
681
|
+
total_timeout = resolved_timeout * 2
|
|
494
682
|
|
|
495
683
|
old_handler: Any = None
|
|
496
684
|
if hasattr(signal, "SIGALRM"):
|
|
@@ -507,9 +695,6 @@ def ai_suggest(
|
|
|
507
695
|
if old_handler is not None:
|
|
508
696
|
signal.signal(signal.SIGALRM, old_handler)
|
|
509
697
|
|
|
510
|
-
if ai_config.model != resolved_model:
|
|
511
|
-
click.echo(f"Model fallback: {resolved_model} → {ai_config.model}")
|
|
512
|
-
|
|
513
698
|
if result:
|
|
514
699
|
_write_ai_output(output, db_path, result)
|
|
515
700
|
else:
|
|
@@ -12,7 +12,6 @@ class ProviderType(str, Enum):
|
|
|
12
12
|
FAKER = "faker"
|
|
13
13
|
MIMESIS = "mimesis"
|
|
14
14
|
CUSTOM = "custom"
|
|
15
|
-
AI = "ai"
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class ColumnConstraintsConfig(BaseModel):
|
|
@@ -54,6 +53,11 @@ class ColumnConfig(BaseModel):
|
|
|
54
53
|
# === 约束 ===
|
|
55
54
|
constraints: ColumnConstraintsConfig | None = None
|
|
56
55
|
|
|
56
|
+
# === Native method overrides (from AI suggestions) ===
|
|
57
|
+
faker_method: str | None = None
|
|
58
|
+
mimesis_method: str | None = None
|
|
59
|
+
native_params: dict[str, Any] = Field(default_factory=dict)
|
|
60
|
+
|
|
57
61
|
model_config = {"extra": "ignore"}
|
|
58
62
|
|
|
59
63
|
@model_validator(mode="before")
|
|
@@ -127,7 +127,7 @@ class ColumnMapper:
|
|
|
127
127
|
"comment": {"min_length": 10, "max_length": 200},
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
PATTERN_MATCH_RULES: ClassVar[
|
|
130
|
+
PATTERN_MATCH_RULES: ClassVar[tuple[tuple[str, str, dict[str, Any]], ...]] = (
|
|
131
131
|
(r"^id$", "autoincrement", {}),
|
|
132
132
|
(r".*_id$", "foreign_key_or_integer", {}),
|
|
133
133
|
(
|
|
@@ -159,7 +159,7 @@ class ColumnMapper:
|
|
|
159
159
|
(r".*_address$", "address", {}),
|
|
160
160
|
(r".*_description$|.*_desc$|.*_text$|.*_content$|.*_body$", "text", {"min_length": 50, "max_length": 300}),
|
|
161
161
|
(r".*_title$|.*_subject$|.*_headline$", "sentence", {}),
|
|
162
|
-
|
|
162
|
+
)
|
|
163
163
|
|
|
164
164
|
TYPE_FALLBACK_RULES: ClassVar[dict[str, tuple[str, dict[str, Any]]]] = {
|
|
165
165
|
"INTEGER": ("integer", {"min_value": 0, "max_value": 999999}),
|
|
@@ -229,6 +229,9 @@ class ColumnMapper:
|
|
|
229
229
|
params=user_config.params if hasattr(user_config, "params") else {},
|
|
230
230
|
null_ratio=user_config.null_ratio if hasattr(user_config, "null_ratio") else 0.0,
|
|
231
231
|
provider=provider_val,
|
|
232
|
+
native_faker_method=getattr(user_config, "faker_method", None),
|
|
233
|
+
native_mimesis_method=getattr(user_config, "mimesis_method", None),
|
|
234
|
+
native_params=getattr(user_config, "native_params", None) or None,
|
|
232
235
|
)
|
|
233
236
|
return None
|
|
234
237
|
|
|
@@ -6,8 +6,6 @@ from typing import TYPE_CHECKING, Any
|
|
|
6
6
|
from sqlseed._utils.logger import get_logger
|
|
7
7
|
from sqlseed.generators._protocol import ConfigurationError, GenerationError, UnknownGeneratorError
|
|
8
8
|
|
|
9
|
-
_NATIVE_MISS = object()
|
|
10
|
-
|
|
11
9
|
if TYPE_CHECKING:
|
|
12
10
|
from collections.abc import Iterator
|
|
13
11
|
|
|
@@ -17,6 +15,7 @@ if TYPE_CHECKING:
|
|
|
17
15
|
from sqlseed.core.mapper import GeneratorSpec
|
|
18
16
|
from sqlseed.core.transform import RowTransformFn
|
|
19
17
|
|
|
18
|
+
_NATIVE_MISS = object()
|
|
20
19
|
logger = get_logger(__name__)
|
|
21
20
|
|
|
22
21
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlseed.plugins.hookspecs import PROJECT_NAME, SqlseedHookSpec, hookimpl, hookspec
|
|
4
|
+
from sqlseed.plugins.manager import PluginManager
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"PROJECT_NAME",
|
|
8
|
+
"PluginManager",
|
|
9
|
+
"SqlseedHookSpec",
|
|
10
|
+
"hookimpl",
|
|
11
|
+
"hookspec",
|
|
12
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pluggy
|
|
6
|
+
|
|
7
|
+
hookspec = pluggy.HookspecMarker("sqlseed")
|
|
8
|
+
hookimpl = pluggy.HookimplMarker("sqlseed")
|
|
9
|
+
|
|
10
|
+
PROJECT_NAME = "sqlseed"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SqlseedHookSpec:
|
|
14
|
+
@hookspec
|
|
15
|
+
def sqlseed_register_providers(self, registry: Any) -> None:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@hookspec
|
|
19
|
+
def sqlseed_register_column_mappers(self, mapper: Any) -> None:
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
@hookspec(firstresult=True)
|
|
23
|
+
def sqlseed_ai_analyze_table(
|
|
24
|
+
self,
|
|
25
|
+
table_name: str,
|
|
26
|
+
columns: list[Any],
|
|
27
|
+
indexes: list[dict[str, Any]],
|
|
28
|
+
sample_data: list[dict[str, Any]],
|
|
29
|
+
foreign_keys: list[Any],
|
|
30
|
+
all_table_names: list[str],
|
|
31
|
+
) -> dict[str, Any] | None:
|
|
32
|
+
"""
|
|
33
|
+
[AI Hook] 分析整张表,返回完整的列配置建议。
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@hookspec
|
|
37
|
+
def sqlseed_before_generate(
|
|
38
|
+
self,
|
|
39
|
+
table_name: str,
|
|
40
|
+
count: int,
|
|
41
|
+
config: Any,
|
|
42
|
+
) -> None:
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
@hookspec
|
|
46
|
+
def sqlseed_after_generate(
|
|
47
|
+
self,
|
|
48
|
+
table_name: str,
|
|
49
|
+
count: int,
|
|
50
|
+
elapsed: float,
|
|
51
|
+
) -> None:
|
|
52
|
+
raise NotImplementedError
|
|
53
|
+
|
|
54
|
+
@hookspec
|
|
55
|
+
def sqlseed_transform_row(
|
|
56
|
+
self,
|
|
57
|
+
table_name: str,
|
|
58
|
+
row: dict[str, Any],
|
|
59
|
+
) -> dict[str, Any] | None:
|
|
60
|
+
"""
|
|
61
|
+
Transform/modify each generated row.
|
|
62
|
+
Return modified row, or None to keep unchanged.
|
|
63
|
+
Note: This hook is in the hot path - performance sensitive.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
@hookspec
|
|
67
|
+
def sqlseed_transform_batch(
|
|
68
|
+
self,
|
|
69
|
+
table_name: str,
|
|
70
|
+
batch: list[dict[str, Any]],
|
|
71
|
+
) -> list[dict[str, Any]] | None:
|
|
72
|
+
"""
|
|
73
|
+
Transform/modify a batch of generated data.
|
|
74
|
+
Multiple plugins can chain: each plugin's output feeds into the next.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
@hookspec
|
|
78
|
+
def sqlseed_before_insert(
|
|
79
|
+
self,
|
|
80
|
+
table_name: str,
|
|
81
|
+
batch_number: int,
|
|
82
|
+
batch_size: int,
|
|
83
|
+
) -> None:
|
|
84
|
+
raise NotImplementedError
|
|
85
|
+
|
|
86
|
+
@hookspec
|
|
87
|
+
def sqlseed_after_insert(
|
|
88
|
+
self,
|
|
89
|
+
table_name: str,
|
|
90
|
+
batch_number: int,
|
|
91
|
+
rows_inserted: int,
|
|
92
|
+
) -> None:
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
|
|
95
|
+
@hookspec
|
|
96
|
+
def sqlseed_shared_pool_loaded(
|
|
97
|
+
self,
|
|
98
|
+
table_name: str,
|
|
99
|
+
shared_pool: Any,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Called after a table's generated values are loaded into the shared pool.
|
|
103
|
+
Other plugins can use this to track cross-table associations.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
@hookspec(firstresult=True)
|
|
107
|
+
def sqlseed_pre_generate_templates(
|
|
108
|
+
self,
|
|
109
|
+
table_name: str,
|
|
110
|
+
column_name: str,
|
|
111
|
+
column_type: str,
|
|
112
|
+
count: int,
|
|
113
|
+
sample_data: list[Any],
|
|
114
|
+
) -> list[Any] | None:
|
|
115
|
+
"""
|
|
116
|
+
[AI Hook] Pre-generate candidate value pool for columns that cannot match
|
|
117
|
+
a deterministic generator. Called before DataStream creation.
|
|
118
|
+
Returns a list of template values, or None if the plugin does not handle this column.
|
|
119
|
+
"""
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import pluggy
|
|
6
|
+
|
|
7
|
+
from sqlseed._utils.logger import get_logger
|
|
8
|
+
from sqlseed.plugins.hookspecs import PROJECT_NAME, SqlseedHookSpec
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PluginManager:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self._pm = pluggy.PluginManager(PROJECT_NAME)
|
|
16
|
+
self._pm.add_hookspecs(SqlseedHookSpec)
|
|
17
|
+
|
|
18
|
+
def load_plugins(self) -> None:
|
|
19
|
+
self._pm.load_setuptools_entrypoints(PROJECT_NAME)
|
|
20
|
+
logger.debug("Loaded plugins", plugins=self._pm.get_plugins())
|
|
21
|
+
|
|
22
|
+
def register(self, plugin: Any, name: str | None = None) -> None:
|
|
23
|
+
self._pm.register(plugin, name=name)
|
|
24
|
+
logger.debug("Registered plugin", name=name or str(plugin))
|
|
25
|
+
|
|
26
|
+
def unregister(self, plugin: Any) -> None:
|
|
27
|
+
self._pm.unregister(plugin)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def hook(self) -> Any:
|
|
31
|
+
return self._pm.hook
|
|
32
|
+
|
|
33
|
+
def get_plugins(self) -> set[Any]:
|
|
34
|
+
return self._pm.get_plugins()
|
|
35
|
+
|
|
36
|
+
def is_registered(self, plugin: Any) -> bool:
|
|
37
|
+
return self._pm.is_registered(plugin)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -5,8 +5,6 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from sqlseed._utils.logger import get_logger
|
|
7
7
|
|
|
8
|
-
logger = get_logger(__name__)
|
|
9
|
-
|
|
10
8
|
try:
|
|
11
9
|
import sqlite_utils as _sqlite_utils
|
|
12
10
|
|
|
@@ -16,6 +14,8 @@ except ImportError:
|
|
|
16
14
|
HAS_SQLITE_UTILS = False
|
|
17
15
|
sqlite_utils = None
|
|
18
16
|
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
19
|
__all__ = ["HAS_SQLITE_UTILS", "sqlite_utils"]
|
|
20
20
|
|
|
21
21
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|