Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
ci: the migrate secret must be the session-mode pooler, not the direct URI#508
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
5f318a359c2a05148c8887d31c5dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| """Read-only drift report between backend/db/migrations/ and a live database. | ||
| Answers the question you must answer before applying a backlog of migrations to | ||
| an environment that has been touched outside the repo (#317): **is the ledger | ||
| merely behind, or is it lying?** | ||
| SUPABASE_DB_URL=... python scripts/migration_drift_report.py | ||
| Writes nothing. Runs no DDL. Safe against production. | ||
| Four sections: | ||
| PENDING — on disk, not in schema_migrations. | ||
| ORPHANS — in schema_migrations, not on disk. Means the environment ran SQL | ||
| that never existed in this repo (dashboard editor, ad-hoc script). | ||
| A filename collision here is the dangerous shape: staging having | ||
| recorded `0032_retire_summer_2026.sql` while the repo's own | ||
| `0032_rooms_missing_columns.sql` is pending means two different | ||
| migrations share a number. | ||
| ALREADY — objects a PENDING migration would create that ALREADY EXIST. | ||
| Every row here is the ledger lying: the schema moved without | ||
| being recorded, so "pending" overstates what will actually run. | ||
| Migrations written with IF NOT EXISTS will no-op safely; ones | ||
| without it will fail the whole run. | ||
| BLOCKERS — live rows that already violate a UNIQUE index a PENDING migration | ||
| would create. IF NOT EXISTS cannot save this one: it is a DATA | ||
| conflict, invisible to a schema-only diff, and it is what turns a | ||
| clean-looking backlog into a half-applied run. | ||
| The object list is parsed from the migration SQL itself (CREATE TABLE / ADD | ||
| COLUMN / CREATE INDEX), so it stays correct as migrations are added — nothing | ||
| to keep in sync by hand. | ||
| Exit codes: 2 no SUPABASE_DB_URL, 1 drift found (no ledger, orphans, a | ||
| non-idempotent collision, or a data blocker), 0 clean. Nonzero means "do not | ||
| apply on top of this" — so the report can gate CI directly rather than being | ||
| re-implemented inline, which is how the workflow preflight and this script | ||
| drifted apart in the first place. | ||
| """ | ||
| from __future__ import annotations | ||
| import os | ||
| import pathlib | ||
| import re | ||
| import sys | ||
| import psycopg | ||
| MIGRATIONS = pathlib.Path(__file__).resolve().parent.parent / "db" / "migrations" | ||
| RE_TABLE = re.compile(r"create\s+table\s+(?:if\s+not\s+exists\s+)?([a-z0-9_.]+)", re.I) | ||
| RE_COLUMN = re.compile( | ||
| r"alter\s+table\s+([a-z0-9_.]+)\s+add\s+column\s+(?:if\s+not\s+exists\s+)?([a-z0-9_]+)", | ||
| re.I, | ||
| ) | ||
| RE_INDEX = re.compile(r"create\s+(?:unique\s+)?index\s+(?:if\s+not\s+exists\s+)?([a-z0-9_]+)", re.I) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bashset -euo pipefail
rg -n -i --glob '*.sql' \
'create\s+(unique\s+)?index\s+concurrently\b' \
backend/db/migrationsRepository: SaplingLearn/Sapling Length of output: 158 🏁 Script executed: #!/bin/bashset -euo pipefail
# Inspect the regex usage and migration files of interest without executing repo code.
wc -l backend/scripts/migration_drift_report.py
sed -n '1,120p' backend/scripts/migration_drift_report.py
printf'\nSQL migration files creating indexes:\n'
rg -n -i --glob '*.sql''create\s+(?:unique\s+)?index\b' backend/db/migrations | head -50Repository: SaplingLearn/Sapling Length of output: 10931 🌐 Web query:
💡 Result: In PostgreSQL, the CONCURRENTLY option is used with the CREATE INDEX command to build an index without locking out concurrent writes (inserts, updates, or deletes) to the table [1][2]. The syntax grammar for creating an index concurrently is [1][2]: CREATE [ UNIQUE ] INDEX CONCURRENTLY [ [ IF NOT EXISTS ] name ] ON [ ONLY ] table_name [ USING method ] ( { column_name | ( expression) } [ COLLATE collation ] [ opclass [ ( opclass_parameter = value [,... ]) ] ] [ ASC | DESC ] [ NULLS { FIRST | LAST } ] [,...]) [ INCLUDE ( column_name [,...]) ] [ NULLS [ NOT ] DISTINCT ] [ WITH ( storage_parameter [= value] [,... ]) ] [ TABLESPACE tablespace_name ] [ WHERE predicate ] Key considerations for this command include: 1. Operational Impact: While it avoids blocking writes, the command requires two scans of the table and must wait for existing transactions that could modify or use the index to terminate [1][2]. 2. Performance: It requires more total work and takes significantly longer to complete than a standard index build [1][2]. It also imposes extra CPU and I/O load on the database [1][2]. 3. Failure Recovery: If the build fails (e.g., due to a deadlock or a duplicate key violation), the index is left in an invalid state. The recommended recovery method is to drop the invalid index and retry the CREATE INDEX CONCURRENTLY command [2]. Citations:
🌐 Web query:
💡 Result: The syntax CREATE INDEX CONCURRENTLY IF NOT EXISTS is supported in PostgreSQL version 10 and all subsequent versions [1]. The IF NOT EXISTS clause was introduced for the CREATE INDEX command in PostgreSQL 10 to allow index creation without throwing an error if an index with the same name already exists [1]. When using this syntax, the index name is mandatory [2][1]. The CONCURRENTLY option allows the index to be built without locking out concurrent writes to the table [2][3]. As of PostgreSQL 10 and later, the general grammar for this command is: CREATE [ UNIQUE ] INDEX [ CONCURRENTLY ] [ [ IF NOT EXISTS ] name ] ON table_name [ USING method ] ( { column_name | ( expression) } [... ]) [... ] [2][1] Citations:
Support
🤖 Prompt for AI Agents | ||
| # A UNIQUE index is the one thing in a pending migration that IF NOT EXISTS | ||
| # cannot make safe: it still fails if the live data already violates it. That | ||
| # is a DATA problem, invisible to a schema-only diff, and it is what turns a | ||
| # clean-looking backlog into a half-applied run. Parsed so the check follows | ||
| # whatever migrations are actually pending. | ||
| RE_UNIQUE_INDEX = re.compile( | ||
| r"create\s+unique\s+index\s+(?:if\s+not\s+exists\s+)?([a-z0-9_]+)\s+" | ||
| r"on\s+([a-z0-9_.]+)\s*\(([^)]*)\)(?:\s*where\s+([^;]+))?", | ||
| re.I | re.S, | ||
Comment on lines
+63
to
+66
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bashset -euo pipefail
# Inspect pending migration syntax that the current regular expression can skip.
rg -n -i --glob '*.sql' \
'create\s+unique\s+index|create\s+unique\s+index\s+concurrently|using\s+\w+|nulls\s+(not\s+)?distinct' \
backend/db/migrationsRepository: SaplingLearn/Sapling Length of output: 2285 🏁 Script executed: #!/bin/bashset -euo pipefail
echo"== script outline =="
ast-grep outline backend/scripts/migration_drift_report.py ||trueecho"== relevant ranges =="
sed -n '1,240p' backend/scripts/migration_drift_report.py
echo"== migration samples =="forfin backend/db/migrations/0039_rag_vector_store.sql \
backend/db/migrations/0018_documents_request_id.sql \
backend/db/migrations/0020_academics_split.sql \
backend/db/migrations/0021_gradebook.sql \
backend/db/migrations/0022_unique_nulls.sql 2>/dev/null \
backend/db/migrations/0027_gradescope.sql \
backend/db/migrations/0036_offering_null_section_unique.sql;do
[ -f"$f" ] ||continueecho"--- $f ---"
sed -n '1,90p'"$f"doneRepository: SaplingLearn/Sapling Length of output: 7436 🏁 Script executed: #!/bin/bashset -euo pipefail
python3 - <<'PY'import reRE_UNIQUE_INDEX = re.compile( r"create\s+unique\s+index\s+(?:if\s+not\s+exists\s+)?([a-z0-9_]+)\s+" r"on\s+([a-z0-9_.]+)\s*\(([^)]*)\)(?:\s*where\s+([^;]+))?", re.I | re.S,)cases = { "plain": 'CREATE UNIQUE INDEX IF NOT EXISTS documents_request_id_user_unique ON documents USING btree (request_id, user_id)', "concurrently": 'CREATE UNIQUE INDEX idx_unique ON users USING btree (email) CONCURRENTLY', "quoted_index_table_columns": 'CREATE UNIQUE INDEX "My Index" ON "My Schema"."My Table" ("Column One", column_two)', "nulls_not_distinct": 'CREATE UNIQUE INDEX idx_u ON users (email) NULLS NOT DISTINCT', "nulls_distinct": 'CREATE UNIQUE INDEX idx_u ON users (email NULLS DISTINCT)', "partial": 'CREATE UNIQUE INDEX idx_partial ON users (email) WHERE (active)', "with_index_params": 'CREATE UNIQUE INDEX idx_ivfflat ON courses USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)',}for label, sql in cases.items(): m = RE_UNIQUE_INDEX.search(sql) print(f"{label}: matched={bool(m)}") if m: print(" groups =", m.groups()) else: print(" sql =", sql)# Extract existing migrations that call this parser's supported formfrom pathlib import Pathfor path in sorted(Path("backend/db/migrations").glob("*.sql")): text = path.read_text(errors="ignore") for m in RE_UNIQUE_INDEX.finditer(text): print(f"EXISTS {path}:{m.start()}: idx={m.group(1)} table={m.group(2)} cols={m.group(3)[:80]} where={m.group(4)[:40] if m.group(4) else None}")PYforfin \
backend/db/migrations/0039_rag_vector_store.sql \
backend/db/migrations/0018_documents_request_id.sql \
backend/db/migrations/0020_academics_split.sql \
backend/db/migrations/0021_gradebook.sql \
backend/db/migrations/0023_graph_integrity.sql \
backend/db/migrations/0036_offering_null_section_unique.sql;do
[ -f"$f" ] ||continueecho"--- $f ---"
sed -n '1,90p'"$f"doneRepository: SaplingLearn/Sapling Length of output: 18440 Cover the full
🤖 Prompt for AI Agents | ||
| ) | ||
| def bare(name: str) -> str: | ||
| return name.split(".")[-1] | ||
Comment on lines
+70
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bashset -euo pipefail
echo"## locate file"
git ls-files | grep -F 'backend/scripts/migration_drift_report.py'||trueecho"## outline"
ast-grep outline backend/scripts/migration_drift_report.py ||trueecho"## relevant lines"
wc -l backend/scripts/migration_drift_report.py
sed -n '1,180p' backend/scripts/migration_drift_report.py | cat -n
echo"## references to bare/schema_migrations/public/auth"
rg -n "bare|schema_migrations|\.migrate|search_path|current_schema|public|auth|table_schema|schemaname|obj_schema" backend/scripts/migration_drift_report.py backend/db ||trueRepository: SaplingLearn/Sapling Length of output: 19317 🏁 Script executed: #!/bin/bashset -euo pipefail
python3 - <<'PY'import refrom pathlib import Pathp = Path('backend/scripts/migration_drift_report.py')if not p.exists(): print('missing backend/scripts/migration_drift_report.py') raise SystemExittext = p.read_text()print('## parsed sections')for start, name in [(48, 'bare'), (61, 'report_catalog_operations'), (98, 'probe_ledger')]: lines = text.splitlines() print(f'\n--- {name} at line {start+1} ---') limit = 160 print('\n'.join(f'{i+1}: {line}' for i in range(max(0, start-limit), min(len(lines), start+limit+limit+20))))print('\n## schema/index name extraction occurrences')for m in re.finditer(r'\b[bare]\s*\(', text): print('bare call around line', text.count('\n',0,m.start('bare')+1)+1)print('bare imports:', bool(re.search(r'from.*import.*bare|import.*utils|sys.path', text)))for m in re.finditer(r'\.split\("[.]\"', text[:200]): print('split found at', text.count('\n',0,m.start())+1, 'context=', text[max(0,m.start()-80):m.end()+80])print('\n## catalog filter fields')for m in re.finditer(r'(SELECT .*?FROM .*?(\n.*?){0,8}.*?WHERE)', text, re.S): if 'catalog' in text[:m.start()] or 'migration_drift' in text[:m.start()]: passprint('index usage around ',"n/a")PYRepository: SaplingLearn/Sapling Length of output: 354 Keep schema-qualified identifiers in the drift checks.
Use the parsed schema from qualified names. For unqualified names, apply the same schema rules used by 🤖 Prompt for AI Agents | ||
| def main() -> int: | ||
| url = os.getenv("SUPABASE_DB_URL") | ||
| if not url: | ||
| print("SUPABASE_DB_URL is not set", file=sys.stderr) | ||
| return 2 | ||
| files = sorted(p.name for p in MIGRATIONS.glob("*.sql")) | ||
| conn = psycopg.connect(url) | ||
| has_ledger = conn.execute( | ||
| "SELECT count(*) FROM information_schema.tables WHERE table_name = %s", | ||
| ("schema_migrations",), | ||
| ).fetchone()[0] | ||
| if not has_ledger: | ||
| print("NO LEDGER — this database has never been migrated by db/migrate.py.") | ||
| print("Reconcile with `python -m db.migrate --baseline` against a schema you") | ||
| print("have verified is current, rather than applying.") | ||
| return 1 | ||
| recorded = { | ||
| r[0] for r in conn.execute("SELECT filename FROM schema_migrations").fetchall() | ||
| } | ||
| pending = [f for f in files if f not in recorded] | ||
| orphans = sorted(recorded - set(files)) | ||
| print(f"on disk {len(files)} | recorded {len(recorded)} | pending {len(pending)}\n") | ||
| print("PENDING") | ||
| for p in pending: | ||
| print(f" {p}") | ||
| if not pending: | ||
| print(" none") | ||
| print("\nORPHANS (recorded here, absent from the repo)") | ||
| for o in orphans: | ||
| clash = [f for f in files if f.split("_")[0] == o.split("_")[0]] | ||
| note = f" <-- NUMBER COLLIDES WITH {clash[0]}" if clash else "" | ||
| print(f" {o}{note}") | ||
| if not orphans: | ||
| print(" none") | ||
| # Which objects the pending migrations would create, that already exist. | ||
| print("\nALREADY EXISTS (pending migration, object already present)") | ||
| found_any = False | ||
| unsafe_already = False | ||
| for name in pending: | ||
| sql = (MIGRATIONS / name).read_text() | ||
| hits: list[str] = [] | ||
| for tbl in {bare(t) for t in RE_TABLE.findall(sql)}: | ||
| n = conn.execute( | ||
| "SELECT count(*) FROM information_schema.tables WHERE table_name = %s", | ||
| (tbl,), | ||
| ).fetchone()[0] | ||
| if n: | ||
| hits.append(f"table {tbl}") | ||
| for tbl, col in {(bare(t), c) for t, c in RE_COLUMN.findall(sql)}: | ||
| n = conn.execute( | ||
| "SELECT count(*) FROM information_schema.columns " | ||
| "WHERE table_name = %s AND column_name = %s", | ||
| (tbl, col), | ||
| ).fetchone()[0] | ||
| if n: | ||
| hits.append(f"column {tbl}.{col}") | ||
| for idx in set(RE_INDEX.findall(sql)): | ||
| n = conn.execute( | ||
| "SELECT count(*) FROM pg_indexes WHERE indexname = %s", (idx,) | ||
| ).fetchone()[0] | ||
| if n: | ||
| hits.append(f"index {idx}") | ||
| if hits: | ||
| found_any = True | ||
| idempotent = "if not exists" in sql.lower() | ||
| if not idempotent: | ||
| unsafe_already = True | ||
| flag = "safe: uses IF NOT EXISTS" if idempotent else "!! NO IF NOT EXISTS — would fail" | ||
| print(f" {name} ({flag})") | ||
Comment on lines
+147
to
+153
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Report idempotence for each conflicting operation.
Track the idempotence modifier with each extracted table, column, or index. Render the safety result for each detected conflict. 🤖 Prompt for AI Agents | ||
| for h in sorted(hits): | ||
| print(f" {h}") | ||
| if not found_any: | ||
| print(" none — the ledger is behind, not lying") | ||
| # Data blockers: a pending UNIQUE index that live rows already violate. | ||
| print("\nDATA BLOCKERS (pending UNIQUE index vs rows already present)") | ||
| blocked = False | ||
| for name in pending: | ||
| sql = (MIGRATIONS / name).read_text() | ||
| for idx, table, cols, pred in RE_UNIQUE_INDEX.findall(sql): | ||
| cols_sql = ", ".join(c.strip() for c in cols.split(",")) | ||
| where = f" WHERE {pred.strip()}" if pred else "" | ||
| try: | ||
| dupes = conn.execute( | ||
| f"SELECT {cols_sql}, count(*) FROM {bare(table)}{where} " | ||
| f"GROUP BY {cols_sql} HAVING count(*) > 1 LIMIT 5" | ||
| ).fetchall() | ||
Comment on lines
+168
to
+171
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bashset -euo pipefail
echo"Files matching migration_drift_report.py:"
fd -a 'migration_drift_report.py'.||true
file="$(fd 'migration_drift_report.py'.| head -n 1)"if [ -n"${file:-}" ];thenechoecho"Line count:"
wc -l "$file"echoecho"Outline:"
ast-grep outline "$file"||trueechoecho"Relevant section lines 120-180:"
sed -n '120,180p'"$file"| nl -ba -v120
fiechoecho"Search for duplicate/unique report symbols:"
rg -n "dupes|count\\(\\*\\)|IS NOT NULL|NULLS NOT DISTINCT|UNIQUE""${file:-backend/scripts/migration_drift_report.py}"||trueechoecho"Search for SQL parser definitions around duplicate checks:"
rg -n "def .*duplicate|class .*Duplicate|parse|definition|constrain" backend -S ||trueRepository: SaplingLearn/Sapling Length of output: 850 🏁 Script executed: #!/bin/bashset -euo pipefail
file="backend/scripts/migration_drift_report.py"echo"Line count:"
wc -l "$file"echoecho"Relevant lines 1-175 with Python-line-number-style numbering:"
python3 - <<'PY'from pathlib import Pathp = Path("backend/scripts/migration_drift_report.py")for i, line in enumerate(p.read_text().splitlines(), 1): print(f"{i:4}: {line}")PYechoecho"Search relevant terms in file:"
rg -n "dupes|count\\(\\*\\)|IS NOT NULL|NULLS NOT DISTINCT|UNIQUE|unique|where|cols_sql|KEYS|RE_KEY""$file" -S ||trueechoecho"Read-only behavioral probe for PostgreSQL UNIQUE duplicate semantics from parser docs snippet if available, plus local duplicate logic shape:"
python3 - <<'PY'from pathlib import Pathimport rep = Path("backend/scripts/migration_drift_report.py")src = p.read_text()print("has null_not_distinct_term:", "NULLS NOT DISTINCT" in src)print("has null check before duplicate group:", "IS NOT NULL" in src and "GROUP BY" in src)for name, regex in [ ("RE_KEY", r"(?<=RE_KEY = re\.compile\()('|\")(.*)\\1"), ("RE_UNIQUE_INDEX", r"(?<=RE_UNIQUE_INDEX = re\.compile\()('|\")(.*)\\1"),]: m = re.search(regex, src, re.S) print(f"{name}: {m.group(2) if m else None}")PYRepository: SaplingLearn/Sapling Length of output: 9653 🌐 Web query:
💡 Result: In PostgreSQL, the handling of NULL values in unique indexes and unique constraints is controlled by the NULLS DISTINCT or NULLS NOT DISTINCT clauses [1][2]. By default, PostgreSQL treats NULL values as distinct [1][3]. This means that a unique index or constraint will allow multiple rows to contain a NULL value in the indexed column(s), as they are not considered equal to each other [1][2]. To change this behavior so that NULL values are treated as equivalent (and thus a unique constraint will reject multiple NULLs), you can specify the NULLS NOT DISTINCT clause when creating the index or the constraint [1][4][2]. Example usage for an index: CREATE UNIQUE INDEX name ON table (column) NULLS NOT DISTINCT; Example usage for a table constraint: CREATE TABLE products ( product_no integer UNIQUE NULLS NOT DISTINCT,...); Alternatively, you can explicitly specify the default behavior by using NULLS DISTINCT [2][5]. As noted in the PostgreSQL documentation, because the SQL standard leaves null treatment in unique constraints as implementation-defined, it is important to be aware of this setting when writing portable applications [2][5]. Citations:
🌐 Web query:
💡 Result: In PostgreSQL, the NULLS NOT DISTINCT clause is used with unique indexes to define how NULL values are handled regarding uniqueness constraints [1][2]. By default, PostgreSQL treats NULL values as distinct, meaning a unique index (or unique constraint) allows multiple rows to contain a NULL value in the indexed column(s) [1][2]. When you specify NULLS NOT DISTINCT, the index treats NULL values as equal to one another [1][3]. Consequently, if a table has this type of unique index, it will reject any attempt to insert or update a row if another row already exists with a NULL value in that same column (or combination of columns) [1][4]. Syntax: CREATE UNIQUE INDEX index_name ON table_name (column_name) NULLS NOT DISTINCT; This clause can be applied to both manually created unique indexes and unique constraints [1][2]. It is important to note that this feature is applicable to B-tree indexes [1][4]. Additionally, you can combine this with a WHERE predicate to create a partial unique index that treats NULLs as non-distinct only for the subset of rows that satisfy the predicate [2][5]. Citations:
Exclude NULLs from unique-index duplicate checks unless the index declares The report checks 🤖 Prompt for AI Agents | ||
| except Exception as exc: # table may not exist yet — that's fine | ||
| conn.rollback() | ||
| print(f" {name}: {idx} — could not check ({type(exc).__name__})") | ||
| continue | ||
| if dupes: | ||
| blocked = True | ||
| print(f" {name}: {idx} WOULD FAIL — duplicate rows exist:") | ||
| for d in dupes: | ||
| print(f" {d}") | ||
Comment on lines
+176
to
+180
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Do not print raw duplicate key values.
As per coding guidelines, “Protect sensitive columns with the encryption helpers … at read boundaries.” 🤖 Prompt for AI AgentsSource: Coding guidelines | ||
| if not blocked: | ||
| print(" none") | ||
| # Exit nonzero on anything that makes "apply the backlog" unsafe, so this | ||
| # can gate CI as-is. PENDING alone is not drift — that is the normal state | ||
| # of an environment that is merely behind. | ||
| problems = [] | ||
| if orphans: | ||
| problems.append(f"{len(orphans)} orphan(s) recorded but absent from the repo") | ||
| if unsafe_already: | ||
| problems.append("an object already exists for a migration without IF NOT EXISTS") | ||
| if blocked: | ||
| problems.append("live rows already violate a pending UNIQUE index") | ||
| if problems: | ||
| print("\nDRIFT — do not apply on top of this:") | ||
| for p in problems: | ||
| print(f" {p}") | ||
| return 1 | ||
| return 0 | ||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: SaplingLearn/Sapling
Length of output: 4274
🏁 Script executed:
Repository: SaplingLearn/Sapling
Length of output: 16548
Add an approved read-only catalog access boundary.
backend/scripts/migration_drift_report.pyopens a directpsycopgconnection at line 60. The allowedbackend/db/migrate.pyexception is for raw DDL, not this read-only catalog-check script. Route this through a catalog helper/API that remains underdb/connection.py::table()or document a narrow read-only exception before merge.🤖 Prompt for AI Agents
Source: Coding guidelines