coderag-ai 0.3.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.
- coderag/__init__.py +12 -0
- coderag/analyzers/__init__.py +0 -0
- coderag/analyzers/base.py +36 -0
- coderag/analyzers/flake8_adapter.py +43 -0
- coderag/analyzers/pylint_adapter.py +50 -0
- coderag/analyzers/workflow.py +107 -0
- coderag/api/__init__.py +0 -0
- coderag/api/app.py +351 -0
- coderag/api/dashboard.py +237 -0
- coderag/api/schemas.py +181 -0
- coderag/cli/__init__.py +0 -0
- coderag/cli/main.py +359 -0
- coderag/context/__init__.py +0 -0
- coderag/context/builder.py +206 -0
- coderag/context/package.py +100 -0
- coderag/core/__init__.py +0 -0
- coderag/core/config.py +100 -0
- coderag/core/logging.py +58 -0
- coderag/core/text.py +80 -0
- coderag/core/tokens.py +66 -0
- coderag/db/__init__.py +0 -0
- coderag/db/base.py +58 -0
- coderag/db/migrations/env.py +77 -0
- coderag/db/migrations/script.py.mako +26 -0
- coderag/db/migrations/versions/.gitkeep +0 -0
- coderag/db/migrations/versions/9a973932b94d_measure_baseline_tokens.py +41 -0
- coderag/db/migrations/versions/c8a40feeda41_initial_schema.py +227 -0
- coderag/db/models.py +275 -0
- coderag/embeddings/__init__.py +0 -0
- coderag/embeddings/base.py +53 -0
- coderag/embeddings/hashing.py +43 -0
- coderag/embeddings/pipeline.py +87 -0
- coderag/embeddings/registry.py +38 -0
- coderag/embeddings/sentence_transformer.py +47 -0
- coderag/evaluation/__init__.py +0 -0
- coderag/evaluation/datasets.py +36 -0
- coderag/evaluation/harness.py +246 -0
- coderag/git/__init__.py +0 -0
- coderag/git/repo.py +139 -0
- coderag/indexing/__init__.py +0 -0
- coderag/indexing/ignore.py +101 -0
- coderag/indexing/indexer.py +445 -0
- coderag/llm/__init__.py +0 -0
- coderag/llm/anthropic.py +117 -0
- coderag/llm/base.py +60 -0
- coderag/llm/null.py +16 -0
- coderag/llm/registry.py +17 -0
- coderag/localdb.py +100 -0
- coderag/mcp_server.py +209 -0
- coderag/parsing/__init__.py +0 -0
- coderag/parsing/base.py +76 -0
- coderag/parsing/python.py +294 -0
- coderag/parsing/registry.py +40 -0
- coderag/retrieval/__init__.py +0 -0
- coderag/retrieval/base.py +60 -0
- coderag/retrieval/engine.py +176 -0
- coderag/retrieval/fusion.py +41 -0
- coderag/retrieval/graph.py +139 -0
- coderag/retrieval/lexical.py +57 -0
- coderag/retrieval/reranking.py +99 -0
- coderag/retrieval/semantic.py +53 -0
- coderag/retrieval/symbol.py +91 -0
- coderag/security/__init__.py +0 -0
- coderag/security/authz.py +35 -0
- coderag/security/secrets.py +110 -0
- coderag/service.py +160 -0
- coderag/telemetry.py +72 -0
- coderag_ai-0.3.0.dist-info/METADATA +364 -0
- coderag_ai-0.3.0.dist-info/RECORD +73 -0
- coderag_ai-0.3.0.dist-info/WHEEL +4 -0
- coderag_ai-0.3.0.dist-info/entry_points.txt +3 -0
- coderag_ai-0.3.0.dist-info/licenses/LICENSE +202 -0
- coderag_ai-0.3.0.dist-info/licenses/NOTICE +5 -0
coderag/core/config.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Central configuration.
|
|
2
|
+
|
|
3
|
+
All tunable behaviour (ranking weights, budgets, model names, provider
|
|
4
|
+
selection) lives here so there are no magic constants scattered through the
|
|
5
|
+
code. Values are read from the environment / a ``.env`` file.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings(BaseSettings):
|
|
17
|
+
model_config = SettingsConfigDict(
|
|
18
|
+
env_prefix="CODERAG_",
|
|
19
|
+
env_file=".env",
|
|
20
|
+
env_file_encoding="utf-8",
|
|
21
|
+
extra="ignore",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# ---- Database -------------------------------------------------------
|
|
25
|
+
# A libpq URL. In dev/test the pgserver fixture overrides this.
|
|
26
|
+
database_url: str = Field(
|
|
27
|
+
default="postgresql+psycopg://coderag:coderag@localhost:5432/coderag"
|
|
28
|
+
)
|
|
29
|
+
db_echo: bool = False
|
|
30
|
+
|
|
31
|
+
# Default repository name used when a request doesn't name one (handy for the
|
|
32
|
+
# MCP server / CLI when several repositories are indexed).
|
|
33
|
+
default_repository: str | None = None
|
|
34
|
+
|
|
35
|
+
# ---- Embeddings -----------------------------------------------------
|
|
36
|
+
# "hashing" -> deterministic, dependency-free (default; offline, great for tests)
|
|
37
|
+
# "sentence_transformer" -> local SentenceTransformers model (needs `embeddings` extra)
|
|
38
|
+
embedding_provider: str = "hashing"
|
|
39
|
+
embedding_model: str = "sentence-transformers/all-MiniLM-L6-v2"
|
|
40
|
+
embedding_device: str = "cpu"
|
|
41
|
+
embedding_batch_size: int = 32
|
|
42
|
+
# Dimension of the hashing embedder (the ST provider reports its own).
|
|
43
|
+
embedding_dimension: int = 384
|
|
44
|
+
|
|
45
|
+
# ---- Token counting -------------------------------------------------
|
|
46
|
+
# "heuristic" (offline, deterministic) or "tiktoken" (needs `tokens` extra).
|
|
47
|
+
token_counter: str = "heuristic"
|
|
48
|
+
tiktoken_encoding: str = "cl100k_base"
|
|
49
|
+
|
|
50
|
+
# ---- Retrieval / fusion --------------------------------------------
|
|
51
|
+
# Per-source candidate limits (how many each retriever returns).
|
|
52
|
+
symbol_limit: int = 10
|
|
53
|
+
lexical_limit: int = 30
|
|
54
|
+
semantic_limit: int = 30
|
|
55
|
+
# Reciprocal Rank Fusion constant and per-source weights.
|
|
56
|
+
rrf_k: int = 60
|
|
57
|
+
weight_symbol: float = 2.0
|
|
58
|
+
weight_lexical: float = 1.0
|
|
59
|
+
weight_semantic: float = 1.0
|
|
60
|
+
weight_graph: float = 0.7
|
|
61
|
+
# Max merged candidates carried into ranking.
|
|
62
|
+
max_candidates: int = 60
|
|
63
|
+
|
|
64
|
+
# ---- Graph expansion (bounded) -------------------------------------
|
|
65
|
+
graph_enabled: bool = True
|
|
66
|
+
graph_max_depth: int = 1
|
|
67
|
+
graph_max_candidates: int = 20
|
|
68
|
+
graph_max_tokens: int = 6000
|
|
69
|
+
|
|
70
|
+
# ---- Reranking (optional) ------------------------------------------
|
|
71
|
+
reranker_enabled: bool = False
|
|
72
|
+
reranker_model: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"
|
|
73
|
+
|
|
74
|
+
# ---- Context builder ------------------------------------------------
|
|
75
|
+
max_context_tokens: int = 12000
|
|
76
|
+
# Reserve tokens for the scaffolding/instructions of the prompt.
|
|
77
|
+
context_overhead_tokens: int = 600
|
|
78
|
+
|
|
79
|
+
# ---- LLM provider ---------------------------------------------------
|
|
80
|
+
llm_provider: str = "anthropic" # "anthropic" | "null"
|
|
81
|
+
anthropic_api_key: str | None = None
|
|
82
|
+
anthropic_base_url: str = "https://api.anthropic.com"
|
|
83
|
+
anthropic_model: str = "claude-sonnet-5"
|
|
84
|
+
anthropic_version: str = "2023-06-01"
|
|
85
|
+
llm_max_output_tokens: int = 1024
|
|
86
|
+
llm_timeout_seconds: float = 60.0
|
|
87
|
+
|
|
88
|
+
# ---- Analyzers (Phase 10) ------------------------------------------
|
|
89
|
+
max_fix_attempts: int = 2
|
|
90
|
+
|
|
91
|
+
# ---- Security -------------------------------------------------------
|
|
92
|
+
# Extra path fragments to ignore during indexing (comma-separated env).
|
|
93
|
+
extra_ignore: list[str] = Field(default_factory=list)
|
|
94
|
+
# If True, structured logs may include short code snippets (never full files).
|
|
95
|
+
log_code_snippets: bool = False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@lru_cache
|
|
99
|
+
def get_settings() -> Settings:
|
|
100
|
+
return Settings()
|
coderag/core/logging.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Structured logging.
|
|
2
|
+
|
|
3
|
+
Security rule (SECURITY.md): we NEVER log full source code, full prompts,
|
|
4
|
+
secrets, or credentials. Log events carry identifiers (symbol id, path,
|
|
5
|
+
counts) — not payloads. Helpers here make the safe path the easy path.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _DynamicStderrFactory:
|
|
17
|
+
"""Create a PrintLogger bound to the *current* sys.stderr on each call.
|
|
18
|
+
|
|
19
|
+
Resolving the stream dynamically (with caching disabled) avoids writing to a
|
|
20
|
+
stale/closed stream — important under pytest, which swaps sys.stderr per test.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __call__(self, *args: object) -> structlog.PrintLogger:
|
|
24
|
+
return structlog.PrintLogger(file=sys.stderr)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def configure_logging(level: str = "INFO") -> None:
|
|
28
|
+
structlog.configure(
|
|
29
|
+
processors=[
|
|
30
|
+
structlog.contextvars.merge_contextvars,
|
|
31
|
+
structlog.processors.add_log_level,
|
|
32
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
33
|
+
structlog.processors.StackInfoRenderer(),
|
|
34
|
+
structlog.processors.format_exc_info,
|
|
35
|
+
structlog.dev.ConsoleRenderer(colors=False),
|
|
36
|
+
],
|
|
37
|
+
wrapper_class=structlog.make_filtering_bound_logger(
|
|
38
|
+
getattr(logging, level.upper(), logging.INFO)
|
|
39
|
+
),
|
|
40
|
+
logger_factory=_DynamicStderrFactory(),
|
|
41
|
+
cache_logger_on_first_use=False,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
|
|
46
|
+
return structlog.get_logger(name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def preview(text: str, limit: int = 80) -> str:
|
|
50
|
+
"""Return a short, safe single-line preview of possibly-sensitive text.
|
|
51
|
+
|
|
52
|
+
Use this instead of logging raw code/queries. Truncates and collapses
|
|
53
|
+
whitespace so full payloads never reach the logs.
|
|
54
|
+
"""
|
|
55
|
+
flat = " ".join(text.split())
|
|
56
|
+
if len(flat) <= limit:
|
|
57
|
+
return flat
|
|
58
|
+
return flat[:limit] + "…"
|
coderag/core/text.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Identifier tokenization shared by parsing and lexical retrieval.
|
|
2
|
+
|
|
3
|
+
Full-text search over code works far better if identifiers are also indexed as
|
|
4
|
+
their component words: ``retry_payment`` -> ``retry_payment retry payment`` and
|
|
5
|
+
``PaymentService`` -> ``PaymentService payment service``. The same expansion is
|
|
6
|
+
applied to queries so ``retry payment`` matches ``retry_payment``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
_IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
14
|
+
_CAMEL_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z0-9]+|[A-Z]+|[0-9]+")
|
|
15
|
+
|
|
16
|
+
# Common language keywords we don't want dominating the search document.
|
|
17
|
+
_STOP = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"self", "cls", "def", "class", "return", "import", "from", "as", "if",
|
|
20
|
+
"else", "elif", "for", "while", "try", "except", "finally", "with",
|
|
21
|
+
"pass", "raise", "none", "true", "false", "and", "or", "not", "in", "is",
|
|
22
|
+
"lambda", "yield", "await", "async", "global", "nonlocal", "del", "assert",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def split_identifier(identifier: str) -> list[str]:
|
|
28
|
+
"""Return the identifier plus its snake/camel component words (lowercased)."""
|
|
29
|
+
out: list[str] = [identifier]
|
|
30
|
+
for part in identifier.split("_"):
|
|
31
|
+
for w in _CAMEL_RE.findall(part):
|
|
32
|
+
if w:
|
|
33
|
+
out.append(w.lower())
|
|
34
|
+
# de-dup preserving order
|
|
35
|
+
seen: set[str] = set()
|
|
36
|
+
result: list[str] = []
|
|
37
|
+
for t in out:
|
|
38
|
+
tl = t.lower()
|
|
39
|
+
if tl and tl not in seen:
|
|
40
|
+
seen.add(tl)
|
|
41
|
+
result.append(t if t == identifier else tl)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def extract_identifiers(code: str, limit: int = 200) -> list[str]:
|
|
46
|
+
"""Unique identifiers appearing in a code snippet (order-preserving, capped)."""
|
|
47
|
+
seen: set[str] = set()
|
|
48
|
+
out: list[str] = []
|
|
49
|
+
for m in _IDENT_RE.finditer(code):
|
|
50
|
+
tok = m.group(0)
|
|
51
|
+
if tok in seen or tok.lower() in _STOP:
|
|
52
|
+
continue
|
|
53
|
+
seen.add(tok)
|
|
54
|
+
out.append(tok)
|
|
55
|
+
if len(out) >= limit:
|
|
56
|
+
break
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_search_terms(*groups: list[str], limit: int = 300) -> list[str]:
|
|
61
|
+
"""Merge identifier groups into a de-duplicated, word-expanded term list."""
|
|
62
|
+
seen: set[str] = set()
|
|
63
|
+
out: list[str] = []
|
|
64
|
+
for group in groups:
|
|
65
|
+
for ident in group:
|
|
66
|
+
for term in split_identifier(ident):
|
|
67
|
+
key = term.lower()
|
|
68
|
+
if key in seen:
|
|
69
|
+
continue
|
|
70
|
+
seen.add(key)
|
|
71
|
+
out.append(term)
|
|
72
|
+
if len(out) >= limit:
|
|
73
|
+
return out
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def expand_query_terms(query: str) -> list[str]:
|
|
78
|
+
"""Tokenize a free-text query into word-expanded search terms."""
|
|
79
|
+
idents = _IDENT_RE.findall(query)
|
|
80
|
+
return build_search_terms(idents)
|
coderag/core/tokens.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Token counting.
|
|
2
|
+
|
|
3
|
+
Token accounting is a primary product feature, so counting must be:
|
|
4
|
+
* deterministic and offline by default (HeuristicTokenCounter), and
|
|
5
|
+
* pluggable (TiktokenCounter) for closer estimates.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: these are *estimates* used for budgeting before we call the LLM.
|
|
8
|
+
The authoritative token counts always come back from the LLM provider's usage
|
|
9
|
+
report and are stored in ``llm_requests``. We never present an estimate as the
|
|
10
|
+
real billed number.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Protocol
|
|
16
|
+
|
|
17
|
+
from coderag.core.config import Settings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenCounter(Protocol):
|
|
21
|
+
def count(self, text: str) -> int: ...
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def name(self) -> str: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HeuristicTokenCounter:
|
|
28
|
+
"""Offline estimator.
|
|
29
|
+
|
|
30
|
+
Uses a blend of a per-character ratio and a whitespace token count, which
|
|
31
|
+
tracks BPE tokenisation of source code reasonably well without any network
|
|
32
|
+
access or model download.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name = "heuristic"
|
|
36
|
+
|
|
37
|
+
def count(self, text: str) -> int:
|
|
38
|
+
if not text:
|
|
39
|
+
return 0
|
|
40
|
+
char_estimate = len(text) / 4.0
|
|
41
|
+
word_estimate = len(text.split()) * 1.3
|
|
42
|
+
return max(1, round((char_estimate + word_estimate) / 2))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TiktokenCounter:
|
|
46
|
+
"""Closer estimate using tiktoken (optional dependency, may need network)."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, encoding: str = "cl100k_base") -> None:
|
|
49
|
+
import tiktoken # noqa: PLC0415 (lazy: optional dependency)
|
|
50
|
+
|
|
51
|
+
self._enc = tiktoken.get_encoding(encoding)
|
|
52
|
+
self.name = f"tiktoken:{encoding}"
|
|
53
|
+
|
|
54
|
+
def count(self, text: str) -> int:
|
|
55
|
+
if not text:
|
|
56
|
+
return 0
|
|
57
|
+
return len(self._enc.encode(text, disallowed_special=()))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_token_counter(settings: Settings) -> TokenCounter:
|
|
61
|
+
if settings.token_counter == "tiktoken":
|
|
62
|
+
try:
|
|
63
|
+
return TiktokenCounter(settings.tiktoken_encoding)
|
|
64
|
+
except Exception: # pragma: no cover - graceful offline fallback
|
|
65
|
+
return HeuristicTokenCounter()
|
|
66
|
+
return HeuristicTokenCounter()
|
coderag/db/__init__.py
ADDED
|
File without changes
|
coderag/db/base.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Engine / session management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import Engine, create_engine
|
|
9
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
10
|
+
|
|
11
|
+
from coderag.core.config import Settings, get_settings
|
|
12
|
+
|
|
13
|
+
_engine: Engine | None = None
|
|
14
|
+
_SessionFactory: sessionmaker[Session] | None = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def make_engine(settings: Settings | None = None) -> Engine:
|
|
18
|
+
settings = settings or get_settings()
|
|
19
|
+
return create_engine(
|
|
20
|
+
settings.database_url,
|
|
21
|
+
echo=settings.db_echo,
|
|
22
|
+
pool_pre_ping=True,
|
|
23
|
+
future=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_engine() -> Engine:
|
|
28
|
+
global _engine
|
|
29
|
+
if _engine is None:
|
|
30
|
+
_engine = make_engine()
|
|
31
|
+
return _engine
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_session_factory() -> sessionmaker[Session]:
|
|
35
|
+
global _SessionFactory
|
|
36
|
+
if _SessionFactory is None:
|
|
37
|
+
_SessionFactory = sessionmaker(bind=get_engine(), expire_on_commit=False, future=True)
|
|
38
|
+
return _SessionFactory
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def configure_engine(engine: Engine) -> None:
|
|
42
|
+
"""Install a specific engine (used by tests / the pgserver fixture)."""
|
|
43
|
+
global _engine, _SessionFactory
|
|
44
|
+
_engine = engine
|
|
45
|
+
_SessionFactory = sessionmaker(bind=engine, expire_on_commit=False, future=True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@contextmanager
|
|
49
|
+
def session_scope() -> Iterator[Session]:
|
|
50
|
+
session = get_session_factory()()
|
|
51
|
+
try:
|
|
52
|
+
yield session
|
|
53
|
+
session.commit()
|
|
54
|
+
except Exception:
|
|
55
|
+
session.rollback()
|
|
56
|
+
raise
|
|
57
|
+
finally:
|
|
58
|
+
session.close()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Alembic environment.
|
|
2
|
+
|
|
3
|
+
The database URL comes from CODERAG settings / the environment (never hardcoded),
|
|
4
|
+
and the pgvector extension is created before any migration runs so that the
|
|
5
|
+
``vector`` type resolves.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from alembic import context
|
|
11
|
+
from sqlalchemy import text
|
|
12
|
+
|
|
13
|
+
from coderag.core.config import get_settings
|
|
14
|
+
from coderag.db.base import make_engine
|
|
15
|
+
from coderag.db.models import Base
|
|
16
|
+
|
|
17
|
+
config = context.config
|
|
18
|
+
target_metadata = Base.metadata
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def render_item(type_, obj, autogen_context):
|
|
22
|
+
"""Ensure pgvector's Vector type renders with its import in migrations."""
|
|
23
|
+
import pgvector.sqlalchemy
|
|
24
|
+
|
|
25
|
+
if type_ == "type" and isinstance(obj, pgvector.sqlalchemy.Vector):
|
|
26
|
+
autogen_context.imports.add("import pgvector.sqlalchemy")
|
|
27
|
+
dim = getattr(obj, "dim", None)
|
|
28
|
+
return f"pgvector.sqlalchemy.Vector({dim if dim else ''})"
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _url() -> str:
|
|
33
|
+
# Allow an explicit override (used by tests), else fall back to settings.
|
|
34
|
+
return config.get_main_option("sqlalchemy.url") or get_settings().database_url
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_migrations_offline() -> None:
|
|
38
|
+
context.configure(
|
|
39
|
+
url=_url(),
|
|
40
|
+
target_metadata=target_metadata,
|
|
41
|
+
literal_binds=True,
|
|
42
|
+
dialect_opts={"paramstyle": "named"},
|
|
43
|
+
compare_type=True,
|
|
44
|
+
render_item=render_item,
|
|
45
|
+
)
|
|
46
|
+
with context.begin_transaction():
|
|
47
|
+
context.run_migrations()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run_migrations_online() -> None:
|
|
51
|
+
engine = make_engine_from_url(_url())
|
|
52
|
+
with engine.connect() as connection:
|
|
53
|
+
connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
|
54
|
+
connection.commit()
|
|
55
|
+
context.configure(
|
|
56
|
+
connection=connection,
|
|
57
|
+
target_metadata=target_metadata,
|
|
58
|
+
compare_type=True,
|
|
59
|
+
render_item=render_item,
|
|
60
|
+
)
|
|
61
|
+
with context.begin_transaction():
|
|
62
|
+
context.run_migrations()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def make_engine_from_url(url: str):
|
|
66
|
+
from sqlalchemy import create_engine
|
|
67
|
+
|
|
68
|
+
return create_engine(url, future=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# Keep import used even if make_engine is unused directly.
|
|
72
|
+
_ = make_engine
|
|
73
|
+
|
|
74
|
+
if context.is_offline_mode():
|
|
75
|
+
run_migrations_offline()
|
|
76
|
+
else:
|
|
77
|
+
run_migrations_online()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""${message}
|
|
2
|
+
|
|
3
|
+
Revision ID: ${up_revision}
|
|
4
|
+
Revises: ${down_revision | comma,n}
|
|
5
|
+
Create Date: ${create_date}
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
${imports if imports else ""}
|
|
14
|
+
|
|
15
|
+
revision: str = ${repr(up_revision)}
|
|
16
|
+
down_revision: str | None = ${repr(down_revision)}
|
|
17
|
+
branch_labels: str | Sequence[str] | None = ${repr(branch_labels)}
|
|
18
|
+
depends_on: str | Sequence[str] | None = ${repr(depends_on)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
${upgrades if upgrades else "pass"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def downgrade() -> None:
|
|
26
|
+
${downgrades if downgrades else "pass"}
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""measure baseline tokens
|
|
2
|
+
|
|
3
|
+
Revision ID: 9a973932b94d
|
|
4
|
+
Revises: c8a40feeda41
|
|
5
|
+
Create Date: 2026-09-01 23:20:53.372206
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
11
|
+
from alembic import op
|
|
12
|
+
import sqlalchemy as sa
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
revision: str = '9a973932b94d'
|
|
16
|
+
down_revision: str | None = 'c8a40feeda41'
|
|
17
|
+
branch_labels: str | Sequence[str] | None = None
|
|
18
|
+
depends_on: str | Sequence[str] | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def upgrade() -> None:
|
|
22
|
+
# server_default backfills existing rows (these are NOT NULL); the default is
|
|
23
|
+
# then dropped so the ORM default governs new inserts. Without this, upgrading
|
|
24
|
+
# a database that already has queries/source_files rows would fail.
|
|
25
|
+
op.add_column('queries', sa.Column('baseline_tokens', sa.Integer(),
|
|
26
|
+
nullable=False, server_default='0'))
|
|
27
|
+
op.add_column('queries', sa.Column('baseline_files', sa.Integer(),
|
|
28
|
+
nullable=False, server_default='0'))
|
|
29
|
+
op.add_column('source_files', sa.Column('token_count', sa.Integer(),
|
|
30
|
+
nullable=False, server_default='0'))
|
|
31
|
+
op.alter_column('queries', 'baseline_tokens', server_default=None)
|
|
32
|
+
op.alter_column('queries', 'baseline_files', server_default=None)
|
|
33
|
+
op.alter_column('source_files', 'token_count', server_default=None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def downgrade() -> None:
|
|
37
|
+
# ### commands auto generated by Alembic - please adjust! ###
|
|
38
|
+
op.drop_column('source_files', 'token_count')
|
|
39
|
+
op.drop_column('queries', 'baseline_files')
|
|
40
|
+
op.drop_column('queries', 'baseline_tokens')
|
|
41
|
+
# ### end Alembic commands ###
|