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.
Files changed (87) hide show
  1. sqlseed_web/AGENTS.md +106 -0
  2. sqlseed_web/__init__.py +24 -0
  3. sqlseed_web/__main__.py +8 -0
  4. sqlseed_web/_application.py +205 -0
  5. sqlseed_web/ai_settings.py +290 -0
  6. sqlseed_web/api.py +1102 -0
  7. sqlseed_web/app.py +26 -0
  8. sqlseed_web/managed_worker.py +184 -0
  9. sqlseed_web/operation_errors.py +42 -0
  10. sqlseed_web/plugin_environment.py +231 -0
  11. sqlseed_web/plugin_management.py +322 -0
  12. sqlseed_web/plugin_process.py +131 -0
  13. sqlseed_web/runtime_lifecycle.py +130 -0
  14. sqlseed_web/runtime_session.py +97 -0
  15. sqlseed_web/settings_environment.py +368 -0
  16. sqlseed_web/sqlite_target.py +86 -0
  17. sqlseed_web/state.py +348 -0
  18. sqlseed_web/static/AGENTS.md +180 -0
  19. sqlseed_web/static/ai.css +57 -0
  20. sqlseed_web/static/configs.css +50 -0
  21. sqlseed_web/static/date-picker.css +230 -0
  22. sqlseed_web/static/disclosure.css +149 -0
  23. sqlseed_web/static/graph-clarity.css +103 -0
  24. sqlseed_web/static/index.html +29 -0
  25. sqlseed_web/static/js/api.js +228 -0
  26. sqlseed_web/static/js/app.js +97 -0
  27. sqlseed_web/static/js/dropdown.js +432 -0
  28. sqlseed_web/static/js/filepicker.js +203 -0
  29. sqlseed_web/static/js/genform.js +1066 -0
  30. sqlseed_web/static/js/labels.js +195 -0
  31. sqlseed_web/static/js/pages/browse.js +209 -0
  32. sqlseed_web/static/js/pages/configs.js +424 -0
  33. sqlseed_web/static/js/pages/connect.js +332 -0
  34. sqlseed_web/static/js/pages/heal.js +395 -0
  35. sqlseed_web/static/js/pages/meta.js +110 -0
  36. sqlseed_web/static/js/pages/runs.js +293 -0
  37. sqlseed_web/static/js/pages/settings.js +942 -0
  38. sqlseed_web/static/js/pages/wizard.js +751 -0
  39. sqlseed_web/static/js/pages/workbench.js +3123 -0
  40. sqlseed_web/static/js/tree.js +126 -0
  41. sqlseed_web/static/js/workbench/ai-eligibility.js +33 -0
  42. sqlseed_web/static/js/workbench/ai-handoff.js +31 -0
  43. sqlseed_web/static/js/workbench/ai-stream.js +116 -0
  44. sqlseed_web/static/js/workbench/ai.js +888 -0
  45. sqlseed_web/static/js/workbench/connection.js +508 -0
  46. sqlseed_web/static/js/workbench/date-picker.js +445 -0
  47. sqlseed_web/static/js/workbench/dependency-view.js +119 -0
  48. sqlseed_web/static/js/workbench/editor.js +1236 -0
  49. sqlseed_web/static/js/workbench/focus.js +11 -0
  50. sqlseed_web/static/js/workbench/graph-layout.js +332 -0
  51. sqlseed_web/static/js/workbench/graph.js +970 -0
  52. sqlseed_web/static/js/workbench/guidance.js +29 -0
  53. sqlseed_web/static/js/workbench/model.js +124 -0
  54. sqlseed_web/static/js/workbench/plugin-management.js +512 -0
  55. sqlseed_web/static/js/workbench/preview-scroll-layout.js +94 -0
  56. sqlseed_web/static/js/workbench/preview.js +572 -0
  57. sqlseed_web/static/js/workbench/provider-guide.js +33 -0
  58. sqlseed_web/static/js/workbench/recovery.js +28 -0
  59. sqlseed_web/static/js/workbench/scroll-lock.js +26 -0
  60. sqlseed_web/static/js/workbench/session.js +174 -0
  61. sqlseed_web/static/js/workbench/table-data.js +186 -0
  62. sqlseed_web/static/js/workbench/ui.js +262 -0
  63. sqlseed_web/static/navigation.css +92 -0
  64. sqlseed_web/static/preview.css +29 -0
  65. sqlseed_web/static/runs.css +53 -0
  66. sqlseed_web/static/scrollbars.css +42 -0
  67. sqlseed_web/static/settings.css +108 -0
  68. sqlseed_web/static/style.css +3382 -0
  69. sqlseed_web/static/table-data.css +27 -0
  70. sqlseed_web/static/workbench.css +509 -0
  71. sqlseed_web/supervised_plugins.py +173 -0
  72. sqlseed_web/supervisor.py +238 -0
  73. sqlseed_web/workbench.py +381 -0
  74. sqlseed_web/workbench_ai.py +887 -0
  75. sqlseed_web/workbench_ai_relations.py +285 -0
  76. sqlseed_web/workbench_ai_stream.py +172 -0
  77. sqlseed_web/workbench_data.py +163 -0
  78. sqlseed_web/workbench_execution.py +199 -0
  79. sqlseed_web/workbench_runtime.py +1218 -0
  80. sqlseed_web/workbench_schema.py +277 -0
  81. sqlseed_web/workbench_store.py +458 -0
  82. sqlseed_web/worker_control.py +192 -0
  83. sqlseed_web-0.2.4.dist-info/METADATA +105 -0
  84. sqlseed_web-0.2.4.dist-info/RECORD +87 -0
  85. sqlseed_web-0.2.4.dist-info/WHEEL +4 -0
  86. sqlseed_web-0.2.4.dist-info/entry_points.txt +2 -0
  87. 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