gitinstall 1.1.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.
- gitinstall/__init__.py +61 -0
- gitinstall/_sdk.py +541 -0
- gitinstall/academic.py +831 -0
- gitinstall/admin.html +327 -0
- gitinstall/auto_update.py +384 -0
- gitinstall/autopilot.py +349 -0
- gitinstall/badge.py +476 -0
- gitinstall/checkpoint.py +330 -0
- gitinstall/cicd.py +499 -0
- gitinstall/clawhub.html +718 -0
- gitinstall/config_schema.py +353 -0
- gitinstall/db.py +984 -0
- gitinstall/db_backend.py +445 -0
- gitinstall/dep_chain.py +337 -0
- gitinstall/dependency_audit.py +1153 -0
- gitinstall/detector.py +542 -0
- gitinstall/doctor.py +493 -0
- gitinstall/education.py +869 -0
- gitinstall/enterprise.py +802 -0
- gitinstall/error_fixer.py +953 -0
- gitinstall/event_bus.py +251 -0
- gitinstall/executor.py +577 -0
- gitinstall/feature_flags.py +138 -0
- gitinstall/fetcher.py +921 -0
- gitinstall/huggingface.py +922 -0
- gitinstall/hw_detect.py +988 -0
- gitinstall/i18n.py +664 -0
- gitinstall/installer_registry.py +362 -0
- gitinstall/knowledge_base.py +379 -0
- gitinstall/license_check.py +605 -0
- gitinstall/llm.py +569 -0
- gitinstall/log.py +236 -0
- gitinstall/main.py +1408 -0
- gitinstall/mcp_agent.py +841 -0
- gitinstall/mcp_server.py +386 -0
- gitinstall/monorepo.py +810 -0
- gitinstall/multi_source.py +425 -0
- gitinstall/onboard.py +276 -0
- gitinstall/planner.py +222 -0
- gitinstall/planner_helpers.py +323 -0
- gitinstall/planner_known_projects.py +1010 -0
- gitinstall/planner_templates.py +996 -0
- gitinstall/remote_gpu.py +633 -0
- gitinstall/resilience.py +608 -0
- gitinstall/run_tests.py +572 -0
- gitinstall/skills.py +476 -0
- gitinstall/tool_schemas.py +324 -0
- gitinstall/trending.py +279 -0
- gitinstall/uninstaller.py +415 -0
- gitinstall/validate_top100.py +607 -0
- gitinstall/watchdog.py +180 -0
- gitinstall/web.py +1277 -0
- gitinstall/web_ui.html +2277 -0
- gitinstall-1.1.0.dist-info/METADATA +275 -0
- gitinstall-1.1.0.dist-info/RECORD +59 -0
- gitinstall-1.1.0.dist-info/WHEEL +5 -0
- gitinstall-1.1.0.dist-info/entry_points.txt +3 -0
- gitinstall-1.1.0.dist-info/licenses/LICENSE +21 -0
- gitinstall-1.1.0.dist-info/top_level.txt +1 -0
gitinstall/db_backend.py
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"""
|
|
2
|
+
db_backend.py - 数据库抽象后端
|
|
3
|
+
=====================================
|
|
4
|
+
|
|
5
|
+
支持 SQLite(默认)→ PostgreSQL 等后端无缝切换。
|
|
6
|
+
环境变量 GITINSTALL_DB_BACKEND 控制后端类型:
|
|
7
|
+
- "sqlite" (默认) 使用本地文件 ~/.gitinstall/data.db
|
|
8
|
+
- "postgresql" 连接 GITINSTALL_DATABASE_URL
|
|
9
|
+
|
|
10
|
+
面向大众原则:零配置即可用 SQLite,生产部署时切换 PostgreSQL。
|
|
11
|
+
零外部依赖:SQLite 使用 stdlib,PostgreSQL 需安装 psycopg2。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
import sqlite3
|
|
19
|
+
import threading
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any, Optional
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ─────────────────────────────────────────────
|
|
27
|
+
# 抽象后端接口
|
|
28
|
+
# ─────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
class DatabaseBackend(ABC):
|
|
31
|
+
"""数据库后端抽象接口。
|
|
32
|
+
|
|
33
|
+
所有操作通过 execute/executemany/executescript 统一调用。
|
|
34
|
+
使用 '?' 占位符(SQLite 风格),PostgreSQL 后端自动转换为 '%s'。
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def get_connection(self) -> Any:
|
|
39
|
+
"""获取当前线程的数据库连接"""
|
|
40
|
+
|
|
41
|
+
@abstractmethod
|
|
42
|
+
@contextmanager
|
|
43
|
+
def transaction(self):
|
|
44
|
+
"""事务上下文管理器,yield connection"""
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def execute(self, sql: str, params: tuple = ()) -> Any:
|
|
48
|
+
"""执行单条 SQL,返回 cursor"""
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def executemany(self, sql: str, params_list: list[tuple]) -> Any:
|
|
52
|
+
"""批量执行 SQL"""
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def executescript(self, sql: str) -> None:
|
|
56
|
+
"""执行多条 SQL 脚本(DDL 初始化用)"""
|
|
57
|
+
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
|
|
60
|
+
"""执行查询返回单行(dict 或 None)"""
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
|
|
64
|
+
"""执行查询返回所有行(list[dict])"""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
"""关闭连接"""
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def integrity_check(self) -> str:
|
|
72
|
+
"""数据库完整性检查,返回 'ok' 或错误描述"""
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
def table_row_count(self, table: str) -> int:
|
|
76
|
+
"""返回指定表的行数(仅诊断用)"""
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def backend_type(self) -> str:
|
|
81
|
+
"""返回后端类型标识,如 'sqlite' 或 'postgresql'"""
|
|
82
|
+
|
|
83
|
+
def adapt_schema(self, sqlite_schema: str) -> str:
|
|
84
|
+
"""将 SQLite 风格 schema 转换为当前后端语法。
|
|
85
|
+
默认原样返回(SQLite 无需转换)。"""
|
|
86
|
+
return sqlite_schema
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ─────────────────────────────────────────────
|
|
90
|
+
# SQLite 后端实现
|
|
91
|
+
# ─────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
class SQLiteBackend(DatabaseBackend):
|
|
94
|
+
"""线程安全 SQLite 后端,每线程一个连接。"""
|
|
95
|
+
|
|
96
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
97
|
+
if db_path is None:
|
|
98
|
+
db_dir = Path.home() / ".gitinstall"
|
|
99
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
try:
|
|
101
|
+
os.chmod(db_dir, 0o700)
|
|
102
|
+
except OSError:
|
|
103
|
+
pass
|
|
104
|
+
self._db_path = str(db_dir / "data.db")
|
|
105
|
+
else:
|
|
106
|
+
self._db_path = db_path
|
|
107
|
+
self._local = threading.local()
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def backend_type(self) -> str:
|
|
111
|
+
return "sqlite"
|
|
112
|
+
|
|
113
|
+
def get_connection(self) -> sqlite3.Connection:
|
|
114
|
+
conn = getattr(self._local, "conn", None)
|
|
115
|
+
if conn is None:
|
|
116
|
+
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
117
|
+
conn.row_factory = sqlite3.Row
|
|
118
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
119
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
120
|
+
try:
|
|
121
|
+
if os.path.exists(self._db_path):
|
|
122
|
+
os.chmod(self._db_path, 0o600)
|
|
123
|
+
except OSError:
|
|
124
|
+
pass
|
|
125
|
+
self._local.conn = conn
|
|
126
|
+
return conn
|
|
127
|
+
|
|
128
|
+
@contextmanager
|
|
129
|
+
def transaction(self):
|
|
130
|
+
conn = self.get_connection()
|
|
131
|
+
try:
|
|
132
|
+
yield conn
|
|
133
|
+
conn.commit()
|
|
134
|
+
except Exception:
|
|
135
|
+
conn.rollback()
|
|
136
|
+
raise
|
|
137
|
+
|
|
138
|
+
def execute(self, sql: str, params: tuple = ()) -> Any:
|
|
139
|
+
return self.get_connection().execute(sql, params)
|
|
140
|
+
|
|
141
|
+
def executemany(self, sql: str, params_list: list[tuple]) -> Any:
|
|
142
|
+
return self.get_connection().executemany(sql, params_list)
|
|
143
|
+
|
|
144
|
+
def executescript(self, sql: str) -> None:
|
|
145
|
+
self.get_connection().executescript(sql)
|
|
146
|
+
|
|
147
|
+
def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
|
|
148
|
+
row = self.get_connection().execute(sql, params).fetchone()
|
|
149
|
+
return dict(row) if row else None
|
|
150
|
+
|
|
151
|
+
def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
|
|
152
|
+
rows = self.get_connection().execute(sql, params).fetchall()
|
|
153
|
+
return [dict(r) for r in rows]
|
|
154
|
+
|
|
155
|
+
def close(self) -> None:
|
|
156
|
+
conn = getattr(self._local, "conn", None)
|
|
157
|
+
if conn:
|
|
158
|
+
conn.close()
|
|
159
|
+
self._local.conn = None
|
|
160
|
+
|
|
161
|
+
def integrity_check(self) -> str:
|
|
162
|
+
try:
|
|
163
|
+
row = self.get_connection().execute("PRAGMA integrity_check").fetchone()
|
|
164
|
+
return row[0] if row else "unknown"
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return str(e)
|
|
167
|
+
|
|
168
|
+
def table_row_count(self, table: str) -> int:
|
|
169
|
+
# 防注入:仅允许字母、数字、下划线
|
|
170
|
+
import re
|
|
171
|
+
if not re.match(r'^[a-zA-Z_]\w*$', table):
|
|
172
|
+
raise ValueError(f"Invalid table name: {table}")
|
|
173
|
+
row = self.get_connection().execute(
|
|
174
|
+
f"SELECT COUNT(*) FROM {table}"
|
|
175
|
+
).fetchone()
|
|
176
|
+
return row[0] if row else 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ─────────────────────────────────────────────
|
|
180
|
+
# PostgreSQL 后端实现(占位,需要 psycopg2)
|
|
181
|
+
# ─────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
class _PGCursorProxy:
|
|
184
|
+
"""包装 psycopg2 cursor,使 fetchone 返回 dict(兼容 sqlite3.Row)"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, cursor):
|
|
187
|
+
self._cur = cursor
|
|
188
|
+
|
|
189
|
+
def fetchone(self):
|
|
190
|
+
row = self._cur.fetchone()
|
|
191
|
+
return dict(row) if row else None
|
|
192
|
+
|
|
193
|
+
def fetchall(self):
|
|
194
|
+
return [dict(r) for r in self._cur.fetchall()]
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def rowcount(self):
|
|
198
|
+
return self._cur.rowcount
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def lastrowid(self):
|
|
202
|
+
return self._cur.lastrowid
|
|
203
|
+
|
|
204
|
+
def __iter__(self):
|
|
205
|
+
return iter(self._cur)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class _PGConnectionProxy:
|
|
209
|
+
"""包装 psycopg2 connection,提供 sqlite3 兼容 API。
|
|
210
|
+
|
|
211
|
+
使 db.py 中的 conn.execute() / conn.executemany() / conn.commit()
|
|
212
|
+
无需修改即可同时支持 SQLite 和 PostgreSQL。
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, raw_conn, extras_module):
|
|
216
|
+
self._conn = raw_conn
|
|
217
|
+
self._extras = extras_module
|
|
218
|
+
|
|
219
|
+
def execute(self, sql: str, params: tuple = ()) -> _PGCursorProxy:
|
|
220
|
+
sql = _pg_adapt_sql(sql)
|
|
221
|
+
cur = self._conn.cursor(cursor_factory=self._extras.RealDictCursor)
|
|
222
|
+
cur.execute(sql, params)
|
|
223
|
+
return _PGCursorProxy(cur)
|
|
224
|
+
|
|
225
|
+
def executemany(self, sql: str, params_list) -> _PGCursorProxy:
|
|
226
|
+
sql = _pg_adapt_sql(sql)
|
|
227
|
+
cur = self._conn.cursor(cursor_factory=self._extras.RealDictCursor)
|
|
228
|
+
cur.executemany(sql, params_list)
|
|
229
|
+
return _PGCursorProxy(cur)
|
|
230
|
+
|
|
231
|
+
def commit(self):
|
|
232
|
+
self._conn.commit()
|
|
233
|
+
|
|
234
|
+
def rollback(self):
|
|
235
|
+
self._conn.rollback()
|
|
236
|
+
|
|
237
|
+
def close(self):
|
|
238
|
+
self._conn.close()
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def closed(self):
|
|
242
|
+
return self._conn.closed
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _pg_adapt_sql(sql: str) -> str:
|
|
246
|
+
"""将 SQLite 风格 SQL 实时转为 PostgreSQL 兼容语法"""
|
|
247
|
+
# strftime('%s','now') → EXTRACT(EPOCH FROM NOW()) (先转,避免 % 转义干扰)
|
|
248
|
+
sql = re.sub(
|
|
249
|
+
r"strftime\('%s',\s*'now'\)",
|
|
250
|
+
"EXTRACT(EPOCH FROM NOW())",
|
|
251
|
+
sql,
|
|
252
|
+
)
|
|
253
|
+
# date(ts, 'unixepoch', 'localtime') → TO_CHAR(TO_TIMESTAMP(ts), 'YYYY-MM-DD')
|
|
254
|
+
sql = re.sub(
|
|
255
|
+
r"date\((\w+),\s*'unixepoch',\s*'localtime'\)",
|
|
256
|
+
r"TO_CHAR(TO_TIMESTAMP(\1), 'YYYY-MM-DD')",
|
|
257
|
+
sql,
|
|
258
|
+
)
|
|
259
|
+
# 转义已有的 % 为 %%(防止 LIKE 'session:%' 被误解析)
|
|
260
|
+
sql = sql.replace("%", "%%")
|
|
261
|
+
# ? → %s 占位符
|
|
262
|
+
sql = sql.replace("?", "%s")
|
|
263
|
+
# INSERT OR IGNORE → INSERT ... ON CONFLICT DO NOTHING
|
|
264
|
+
has_or_ignore = bool(re.search(r"INSERT\s+OR\s+IGNORE\s+INTO", sql, re.IGNORECASE))
|
|
265
|
+
sql = re.sub(
|
|
266
|
+
r"INSERT\s+OR\s+IGNORE\s+INTO",
|
|
267
|
+
"INSERT INTO",
|
|
268
|
+
sql, flags=re.IGNORECASE,
|
|
269
|
+
)
|
|
270
|
+
if has_or_ignore:
|
|
271
|
+
# 在 VALUES(...) 后追加 ON CONFLICT DO NOTHING
|
|
272
|
+
sql = re.sub(r"(\)\s*)$", r"\1 ON CONFLICT DO NOTHING", sql.rstrip())
|
|
273
|
+
if "ON CONFLICT DO NOTHING" not in sql:
|
|
274
|
+
sql = sql.rstrip(";").rstrip() + " ON CONFLICT DO NOTHING"
|
|
275
|
+
return sql
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class PostgreSQLBackend(DatabaseBackend):
|
|
279
|
+
"""PostgreSQL 后端。需要安装 psycopg2-binary。
|
|
280
|
+
|
|
281
|
+
使用 GITINSTALL_DATABASE_URL 环境变量配置连接字符串,例如:
|
|
282
|
+
postgresql://user:pass@localhost:5432/gitinstall
|
|
283
|
+
|
|
284
|
+
get_connection() 返回 _PGConnectionProxy,提供 sqlite3 兼容 API,
|
|
285
|
+
使 db.py 无需修改即可运行在 PostgreSQL 上。
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
def __init__(self, database_url: Optional[str] = None):
|
|
289
|
+
self._url = database_url or os.getenv("GITINSTALL_DATABASE_URL", "")
|
|
290
|
+
if not self._url:
|
|
291
|
+
raise ValueError(
|
|
292
|
+
"PostgreSQL backend requires GITINSTALL_DATABASE_URL environment variable"
|
|
293
|
+
)
|
|
294
|
+
self._local = threading.local()
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def backend_type(self) -> str:
|
|
298
|
+
return "postgresql"
|
|
299
|
+
|
|
300
|
+
def _import_psycopg2(self):
|
|
301
|
+
try:
|
|
302
|
+
import psycopg2
|
|
303
|
+
import psycopg2.extras
|
|
304
|
+
return psycopg2, psycopg2.extras
|
|
305
|
+
except ImportError:
|
|
306
|
+
raise ImportError(
|
|
307
|
+
"PostgreSQL backend requires psycopg2. "
|
|
308
|
+
"Install with: pip install psycopg2-binary"
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _raw_connection(self):
|
|
312
|
+
"""获取原始 psycopg2 connection(内部用)"""
|
|
313
|
+
raw = getattr(self._local, "raw_conn", None)
|
|
314
|
+
if raw is None or raw.closed:
|
|
315
|
+
psycopg2, _ = self._import_psycopg2()
|
|
316
|
+
raw = psycopg2.connect(self._url)
|
|
317
|
+
raw.autocommit = False
|
|
318
|
+
self._local.raw_conn = raw
|
|
319
|
+
return raw
|
|
320
|
+
|
|
321
|
+
def get_connection(self) -> _PGConnectionProxy:
|
|
322
|
+
"""返回 SQLite 兼容的连接代理"""
|
|
323
|
+
proxy = getattr(self._local, "conn_proxy", None)
|
|
324
|
+
if proxy is None or proxy.closed:
|
|
325
|
+
_, extras = self._import_psycopg2()
|
|
326
|
+
raw = self._raw_connection()
|
|
327
|
+
proxy = _PGConnectionProxy(raw, extras)
|
|
328
|
+
self._local.conn_proxy = proxy
|
|
329
|
+
return proxy
|
|
330
|
+
|
|
331
|
+
@contextmanager
|
|
332
|
+
def transaction(self):
|
|
333
|
+
conn = self.get_connection()
|
|
334
|
+
try:
|
|
335
|
+
yield conn
|
|
336
|
+
conn.commit()
|
|
337
|
+
except Exception:
|
|
338
|
+
conn.rollback()
|
|
339
|
+
raise
|
|
340
|
+
|
|
341
|
+
def _convert_placeholders(self, sql: str) -> str:
|
|
342
|
+
"""将 SQLite 的 ? 占位符转换为 PostgreSQL 的 %s"""
|
|
343
|
+
return sql.replace("?", "%s")
|
|
344
|
+
|
|
345
|
+
def execute(self, sql: str, params: tuple = ()) -> Any:
|
|
346
|
+
proxy = self.get_connection()
|
|
347
|
+
return proxy.execute(sql, params)
|
|
348
|
+
|
|
349
|
+
def executemany(self, sql: str, params_list: list[tuple]) -> Any:
|
|
350
|
+
proxy = self.get_connection()
|
|
351
|
+
return proxy.executemany(sql, params_list)
|
|
352
|
+
|
|
353
|
+
def executescript(self, sql: str) -> None:
|
|
354
|
+
adapted = self.adapt_schema(sql)
|
|
355
|
+
raw = self._raw_connection()
|
|
356
|
+
cur = raw.cursor()
|
|
357
|
+
cur.execute(adapted)
|
|
358
|
+
raw.commit()
|
|
359
|
+
|
|
360
|
+
def fetchone(self, sql: str, params: tuple = ()) -> Optional[dict]:
|
|
361
|
+
proxy = self.get_connection()
|
|
362
|
+
result = proxy.execute(sql, params)
|
|
363
|
+
return result.fetchone()
|
|
364
|
+
|
|
365
|
+
def fetchall(self, sql: str, params: tuple = ()) -> list[dict]:
|
|
366
|
+
proxy = self.get_connection()
|
|
367
|
+
result = proxy.execute(sql, params)
|
|
368
|
+
return result.fetchall()
|
|
369
|
+
|
|
370
|
+
def close(self) -> None:
|
|
371
|
+
raw = getattr(self._local, "raw_conn", None)
|
|
372
|
+
if raw:
|
|
373
|
+
raw.close()
|
|
374
|
+
self._local.raw_conn = None
|
|
375
|
+
self._local.conn_proxy = None
|
|
376
|
+
|
|
377
|
+
def integrity_check(self) -> str:
|
|
378
|
+
try:
|
|
379
|
+
self.execute("SELECT 1")
|
|
380
|
+
return "ok"
|
|
381
|
+
except Exception as e:
|
|
382
|
+
return str(e)
|
|
383
|
+
|
|
384
|
+
def table_row_count(self, table: str) -> int:
|
|
385
|
+
import re
|
|
386
|
+
if not re.match(r'^[a-zA-Z_]\w*$', table):
|
|
387
|
+
raise ValueError(f"Invalid table name: {table}")
|
|
388
|
+
row = self.fetchone(f"SELECT COUNT(*) as cnt FROM {table}")
|
|
389
|
+
return row["cnt"] if row else 0
|
|
390
|
+
|
|
391
|
+
def adapt_schema(self, sqlite_schema: str) -> str:
|
|
392
|
+
"""将 SQLite schema SQL 转为 PostgreSQL 兼容语法"""
|
|
393
|
+
import re
|
|
394
|
+
sql = sqlite_schema
|
|
395
|
+
# INTEGER PRIMARY KEY AUTOINCREMENT → SERIAL PRIMARY KEY
|
|
396
|
+
sql = re.sub(
|
|
397
|
+
r'INTEGER\s+PRIMARY\s+KEY\s+AUTOINCREMENT',
|
|
398
|
+
'SERIAL PRIMARY KEY',
|
|
399
|
+
sql, flags=re.IGNORECASE,
|
|
400
|
+
)
|
|
401
|
+
# REAL DEFAULT (strftime('%s','now')) → DOUBLE PRECISION DEFAULT EXTRACT(EPOCH FROM NOW())
|
|
402
|
+
sql = re.sub(
|
|
403
|
+
r"REAL\s+NOT\s+NULL\s+DEFAULT\s+\(strftime\('%s','now'\)\)",
|
|
404
|
+
"DOUBLE PRECISION NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW())",
|
|
405
|
+
sql, flags=re.IGNORECASE,
|
|
406
|
+
)
|
|
407
|
+
# REAL → DOUBLE PRECISION (remaining)
|
|
408
|
+
sql = re.sub(r'\bREAL\b', 'DOUBLE PRECISION', sql, flags=re.IGNORECASE)
|
|
409
|
+
# INTEGER → INTEGER (compatible, no change needed)
|
|
410
|
+
return sql
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# ─────────────────────────────────────────────
|
|
414
|
+
# 后端工厂
|
|
415
|
+
# ─────────────────────────────────────────────
|
|
416
|
+
|
|
417
|
+
_backend: Optional[DatabaseBackend] = None
|
|
418
|
+
_backend_lock = threading.Lock()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def get_backend() -> DatabaseBackend:
|
|
422
|
+
"""获取当前数据库后端(单例)。
|
|
423
|
+
|
|
424
|
+
通过环境变量 GITINSTALL_DB_BACKEND 选择:
|
|
425
|
+
- "sqlite" (默认) 本地 SQLite
|
|
426
|
+
- "postgresql" PostgreSQL(需要 psycopg2 + GITINSTALL_DATABASE_URL)
|
|
427
|
+
"""
|
|
428
|
+
global _backend
|
|
429
|
+
if _backend is not None:
|
|
430
|
+
return _backend
|
|
431
|
+
with _backend_lock:
|
|
432
|
+
if _backend is not None:
|
|
433
|
+
return _backend
|
|
434
|
+
backend_type = os.getenv("GITINSTALL_DB_BACKEND", "sqlite").lower()
|
|
435
|
+
if backend_type == "postgresql":
|
|
436
|
+
_backend = PostgreSQLBackend()
|
|
437
|
+
else:
|
|
438
|
+
_backend = SQLiteBackend()
|
|
439
|
+
return _backend
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def set_backend(backend: DatabaseBackend) -> None:
|
|
443
|
+
"""替换数据库后端(测试用)"""
|
|
444
|
+
global _backend
|
|
445
|
+
_backend = backend
|