sqlseed-web 0.2.4__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.
- sqlseed_web/AGENTS.md +106 -0
- sqlseed_web/__init__.py +24 -0
- sqlseed_web/__main__.py +8 -0
- sqlseed_web/_application.py +205 -0
- sqlseed_web/ai_settings.py +290 -0
- sqlseed_web/api.py +1102 -0
- sqlseed_web/app.py +26 -0
- sqlseed_web/managed_worker.py +184 -0
- sqlseed_web/operation_errors.py +42 -0
- sqlseed_web/plugin_environment.py +231 -0
- sqlseed_web/plugin_management.py +322 -0
- sqlseed_web/plugin_process.py +131 -0
- sqlseed_web/runtime_lifecycle.py +130 -0
- sqlseed_web/runtime_session.py +97 -0
- sqlseed_web/settings_environment.py +368 -0
- sqlseed_web/sqlite_target.py +86 -0
- sqlseed_web/state.py +348 -0
- sqlseed_web/static/AGENTS.md +180 -0
- sqlseed_web/static/ai.css +57 -0
- sqlseed_web/static/configs.css +50 -0
- sqlseed_web/static/date-picker.css +230 -0
- sqlseed_web/static/disclosure.css +149 -0
- sqlseed_web/static/graph-clarity.css +103 -0
- sqlseed_web/static/index.html +29 -0
- sqlseed_web/static/js/api.js +228 -0
- sqlseed_web/static/js/app.js +97 -0
- sqlseed_web/static/js/dropdown.js +432 -0
- sqlseed_web/static/js/filepicker.js +203 -0
- sqlseed_web/static/js/genform.js +1066 -0
- sqlseed_web/static/js/labels.js +195 -0
- sqlseed_web/static/js/pages/browse.js +209 -0
- sqlseed_web/static/js/pages/configs.js +424 -0
- sqlseed_web/static/js/pages/connect.js +332 -0
- sqlseed_web/static/js/pages/heal.js +395 -0
- sqlseed_web/static/js/pages/meta.js +110 -0
- sqlseed_web/static/js/pages/runs.js +293 -0
- sqlseed_web/static/js/pages/settings.js +942 -0
- sqlseed_web/static/js/pages/wizard.js +751 -0
- sqlseed_web/static/js/pages/workbench.js +3123 -0
- sqlseed_web/static/js/tree.js +126 -0
- sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
- sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
- sqlseed_web/static/js/workbench/ai-stream.js +116 -0
- sqlseed_web/static/js/workbench/ai.js +888 -0
- sqlseed_web/static/js/workbench/connection.js +508 -0
- sqlseed_web/static/js/workbench/date-picker.js +445 -0
- sqlseed_web/static/js/workbench/dependency-view.js +119 -0
- sqlseed_web/static/js/workbench/editor.js +1236 -0
- sqlseed_web/static/js/workbench/focus.js +11 -0
- sqlseed_web/static/js/workbench/graph-layout.js +332 -0
- sqlseed_web/static/js/workbench/graph.js +970 -0
- sqlseed_web/static/js/workbench/guidance.js +29 -0
- sqlseed_web/static/js/workbench/model.js +124 -0
- sqlseed_web/static/js/workbench/plugin-management.js +512 -0
- sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
- sqlseed_web/static/js/workbench/preview.js +572 -0
- sqlseed_web/static/js/workbench/provider-guide.js +33 -0
- sqlseed_web/static/js/workbench/recovery.js +28 -0
- sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
- sqlseed_web/static/js/workbench/session.js +174 -0
- sqlseed_web/static/js/workbench/table-data.js +186 -0
- sqlseed_web/static/js/workbench/ui.js +262 -0
- sqlseed_web/static/navigation.css +92 -0
- sqlseed_web/static/preview.css +29 -0
- sqlseed_web/static/runs.css +53 -0
- sqlseed_web/static/scrollbars.css +42 -0
- sqlseed_web/static/settings.css +108 -0
- sqlseed_web/static/style.css +3382 -0
- sqlseed_web/static/table-data.css +27 -0
- sqlseed_web/static/workbench.css +509 -0
- sqlseed_web/supervised_plugins.py +173 -0
- sqlseed_web/supervisor.py +238 -0
- sqlseed_web/workbench.py +381 -0
- sqlseed_web/workbench_ai.py +887 -0
- sqlseed_web/workbench_ai_relations.py +285 -0
- sqlseed_web/workbench_ai_stream.py +172 -0
- sqlseed_web/workbench_data.py +163 -0
- sqlseed_web/workbench_execution.py +199 -0
- sqlseed_web/workbench_runtime.py +1218 -0
- sqlseed_web/workbench_schema.py +277 -0
- sqlseed_web/workbench_store.py +458 -0
- sqlseed_web/worker_control.py +192 -0
- sqlseed_web-0.2.4.dist-info/METADATA +105 -0
- sqlseed_web-0.2.4.dist-info/RECORD +87 -0
- sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
- sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
- sqlseed_web-0.2.4.dist-info/licenses/LICENSE +679 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Explicit, read-only replacement planning; execution options are not core config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from sqlseed.config.models import GeneratorConfig
|
|
11
|
+
|
|
12
|
+
from sqlseed_web.state import Connection
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_execution(value: Any = None) -> dict[str, Any]:
|
|
16
|
+
"""Keep destructive options explicit and reject misspelled/unknown flags."""
|
|
17
|
+
if value is None:
|
|
18
|
+
value = {}
|
|
19
|
+
if not isinstance(value, dict) or set(value) - {"mode", "reset_identity"}:
|
|
20
|
+
raise ValueError("执行选项只支持 mode 与 reset_identity")
|
|
21
|
+
mode = value.get("mode", "append")
|
|
22
|
+
reset = value.get("reset_identity", False)
|
|
23
|
+
if mode not in ("append", "replace_selected") or not isinstance(reset, bool):
|
|
24
|
+
raise ValueError("执行方式必须为 append 或 replace_selected,重置 ID 必须为布尔值")
|
|
25
|
+
if reset and mode != "replace_selected":
|
|
26
|
+
raise ValueError("只有清空所选表时才能重置 ID")
|
|
27
|
+
return {"mode": mode, "reset_identity": reset}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _plan_issue(code: str, message: str, *, table: str = "", severity: str = "error") -> dict[str, Any]:
|
|
31
|
+
return {"code": code, "message": message, "table": table, "severity": severity}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _enrichment_issues(config: GeneratorConfig) -> list[dict[str, Any]]:
|
|
35
|
+
issues: list[dict[str, Any]] = []
|
|
36
|
+
for table_config in config.tables:
|
|
37
|
+
if table_config.enrich:
|
|
38
|
+
issues.append(
|
|
39
|
+
_plan_issue(
|
|
40
|
+
"replacement_enrich_not_supported",
|
|
41
|
+
f"{table_config.name} 启用了从现有数据增强规则,清空会移除其来源;请关闭增强或使用追加。",
|
|
42
|
+
table=table_config.name,
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
return issues
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _incoming_replacement_issues(
|
|
49
|
+
name: str, table: dict[str, Any], selected: set[str], sqlite: bool
|
|
50
|
+
) -> list[dict[str, Any]]:
|
|
51
|
+
issues: list[dict[str, Any]] = []
|
|
52
|
+
for fk in table["foreign_keys"]:
|
|
53
|
+
if fk["ref_schema"] not in (None, "main") and sqlite:
|
|
54
|
+
continue
|
|
55
|
+
if fk["ref_table"] not in selected:
|
|
56
|
+
continue
|
|
57
|
+
if name not in selected:
|
|
58
|
+
issues.append(
|
|
59
|
+
_plan_issue(
|
|
60
|
+
"external_incoming_fk",
|
|
61
|
+
f"未选表 {name} 引用 {fk['ref_table']},清空会影响所选范围外的数据;请一并选择相关表或使用追加。",
|
|
62
|
+
table=name,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
elif name == fk["ref_table"] and not fk["nullable"]:
|
|
66
|
+
issues.append(
|
|
67
|
+
_plan_issue(
|
|
68
|
+
"self_reference_after_clear",
|
|
69
|
+
f"{name} 的自引用字段不可空,清空后没有可用首条来源,不能安全重建。",
|
|
70
|
+
table=name,
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
return issues
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _table_replacement_issues(
|
|
77
|
+
tables: dict[str, Any], selected: set[str], sqlite: bool, execution: dict[str, Any]
|
|
78
|
+
) -> list[dict[str, Any]]:
|
|
79
|
+
issues: list[dict[str, Any]] = []
|
|
80
|
+
for name, table in tables.items():
|
|
81
|
+
issues.extend(_incoming_replacement_issues(name, table, selected, sqlite))
|
|
82
|
+
if (
|
|
83
|
+
name in selected
|
|
84
|
+
and not execution["reset_identity"]
|
|
85
|
+
and any(column.get("is_rowid_alias") and not column["is_autoincrement"] for column in table["columns"])
|
|
86
|
+
):
|
|
87
|
+
issues.append(
|
|
88
|
+
_plan_issue(
|
|
89
|
+
"rowid_restarts_on_clear",
|
|
90
|
+
f"{name} 使用普通 SQLite INTEGER 主键;清空后数据库可能自然从 1 分配,与重置 AUTOINCREMENT 选项无关。",
|
|
91
|
+
table=name,
|
|
92
|
+
severity="warning",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return issues
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _association_replacement_issues(
|
|
99
|
+
config: GeneratorConfig, selected: set[str], tables: dict[str, Any]
|
|
100
|
+
) -> list[dict[str, Any]]:
|
|
101
|
+
issues: list[dict[str, Any]] = []
|
|
102
|
+
for association in config.associations:
|
|
103
|
+
if association.source_table in selected:
|
|
104
|
+
for name in association.target_tables:
|
|
105
|
+
if name in tables and name not in selected:
|
|
106
|
+
issues.append(
|
|
107
|
+
_plan_issue(
|
|
108
|
+
"external_incoming_association",
|
|
109
|
+
f"未选表 {name} 通过配置关联引用 {association.source_table},请一并选择或使用追加。",
|
|
110
|
+
table=name,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
return issues
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _sqlite_replacement_issues(
|
|
117
|
+
conn: Connection, tables: dict[str, Any], order: list[str], selected: set[str]
|
|
118
|
+
) -> list[dict[str, Any]]:
|
|
119
|
+
issues: list[dict[str, Any]] = []
|
|
120
|
+
# Trigger side effects cannot be bounded by FK metadata or safely
|
|
121
|
+
# inferred by parsing arbitrary SQL. No trigger-text allowlist.
|
|
122
|
+
for trigger in conn.orchestrator.query("SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'"):
|
|
123
|
+
if trigger["tbl_name"] in selected:
|
|
124
|
+
issues.append(
|
|
125
|
+
_plan_issue(
|
|
126
|
+
"replacement_trigger_not_supported",
|
|
127
|
+
f"{trigger['tbl_name']} 存在触发器 {trigger['name']},暂不能保证重建操作仅影响所选表;请使用追加。",
|
|
128
|
+
table=trigger["tbl_name"],
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
for name in order:
|
|
132
|
+
if tables[name]["row_count"] and any(
|
|
133
|
+
fk["table"] == name and str(fk["on_delete"]).upper() == "RESTRICT"
|
|
134
|
+
for fk in conn.orchestrator.query("SELECT * FROM pragma_foreign_key_list(?)", (name,))
|
|
135
|
+
):
|
|
136
|
+
issues.append(
|
|
137
|
+
_plan_issue(
|
|
138
|
+
"self_reference_delete_restrict",
|
|
139
|
+
f"{name} 存在自引用 ON DELETE RESTRICT,不能保证整表删除顺序;请使用追加。",
|
|
140
|
+
table=name,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
return issues
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_execution_plan(
|
|
147
|
+
conn: Connection,
|
|
148
|
+
config: GeneratorConfig,
|
|
149
|
+
schema: dict[str, Any],
|
|
150
|
+
order: list[str],
|
|
151
|
+
execution: dict[str, Any],
|
|
152
|
+
config_hash: str,
|
|
153
|
+
) -> dict[str, Any]:
|
|
154
|
+
"""Describe physical delete scope and gate unverified database behavior."""
|
|
155
|
+
replacing = execution["mode"] == "replace_selected"
|
|
156
|
+
sqlite = schema["dialect"] == "sqlite"
|
|
157
|
+
selected = set(order)
|
|
158
|
+
tables = {table["name"]: table for table in schema["tables"]}
|
|
159
|
+
reset_supported = sqlite and any(column["is_autoincrement"] for name in order for column in tables[name]["columns"])
|
|
160
|
+
issues: list[dict[str, Any]] = []
|
|
161
|
+
|
|
162
|
+
if replacing:
|
|
163
|
+
if not sqlite:
|
|
164
|
+
issues.append(
|
|
165
|
+
_plan_issue(
|
|
166
|
+
"replacement_not_supported", "当前仅验证了 SQLite 的整次事务清空与回滚;此连接暂只支持追加生成。"
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
issues.extend(_enrichment_issues(config))
|
|
170
|
+
issues.extend(_table_replacement_issues(tables, selected, sqlite, execution))
|
|
171
|
+
issues.extend(_association_replacement_issues(config, selected, tables))
|
|
172
|
+
if sqlite:
|
|
173
|
+
issues.extend(_sqlite_replacement_issues(conn, tables, order, selected))
|
|
174
|
+
if execution["reset_identity"] and not reset_supported:
|
|
175
|
+
issues.append(
|
|
176
|
+
_plan_issue(
|
|
177
|
+
"identity_reset_not_supported", "所选表没有可重置的 SQLite AUTOINCREMENT 序列,请关闭重置 ID。"
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
plan: dict[str, Any] = {
|
|
181
|
+
"ok": not any(item["severity"] == "error" for item in issues),
|
|
182
|
+
"issues": issues,
|
|
183
|
+
"mode": execution["mode"],
|
|
184
|
+
"execution": execution,
|
|
185
|
+
"delete_order": list(reversed(order)) if replacing else [],
|
|
186
|
+
"clear_tables": [{"name": name, "row_count": tables[name]["row_count"]} for name in reversed(order)]
|
|
187
|
+
if replacing
|
|
188
|
+
else [],
|
|
189
|
+
"reset_identity_supported": reset_supported,
|
|
190
|
+
"atomic": replacing and sqlite,
|
|
191
|
+
}
|
|
192
|
+
binding = {
|
|
193
|
+
"plan": plan,
|
|
194
|
+
"config_hash": config_hash,
|
|
195
|
+
"schema_hash": schema["schema_hash"],
|
|
196
|
+
"target_key": schema["target_key"],
|
|
197
|
+
}
|
|
198
|
+
plan["plan_hash"] = hashlib.sha256(json.dumps(binding, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
199
|
+
return plan
|