-
-
Notifications
You must be signed in to change notification settings - Fork 40
schema-column guard cannot see a migration in a module-level helper called from _post_init - false violation blocking #2048, and latent for every store whose _post_init delegates #2993
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| ### Fixed | ||
|
|
||
| - `scripts/check_schema_column_migrations.py` now follows one level of same-file | ||
| module-level helper calls from `_post_init` when checking for ALTER TABLE | ||
| migrations. A guarded migration that lives in a module-level coroutine called | ||
| by `_post_init` (the `agent_registry_store.py` pattern) no longer produces a | ||
| false violation. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -129,7 +129,8 @@ class Violation: | |||||||||||||||||||||||||||||||||||||||||||||
| def __str__(self) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||
| fix = ( | ||||||||||||||||||||||||||||||||||||||||||||||
| "add a guarded _post_init coroutine that ALTERs this column " | ||||||||||||||||||||||||||||||||||||||||||||||
| "into place after a PRAGMA table_info check" | ||||||||||||||||||||||||||||||||||||||||||||||
| "into place after a PRAGMA table_info check, either inline " | ||||||||||||||||||||||||||||||||||||||||||||||
| "or via a module-level helper that _post_init calls" | ||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||
| f"{self.path}: table '{self.table}', column '{self.column}' " | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -449,8 +450,30 @@ def _post_init_added_columns(tree: ast.AST) -> set[tuple[str, str]]: | |||||||||||||||||||||||||||||||||||||||||||||
| SQL is read out of the AST's string constants rather than out of stripped | ||||||||||||||||||||||||||||||||||||||||||||||
| source text, so ``#`` inside a SQL literal cannot chop the statement and a | ||||||||||||||||||||||||||||||||||||||||||||||
| triple-quoted SQL literal is not mistaken for a docstring. | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| One level of same-file call indirection is also followed: if ``_post_init`` | ||||||||||||||||||||||||||||||||||||||||||||||
| calls a module-level ``FunctionDef``/``AsyncFunctionDef`` by plain name | ||||||||||||||||||||||||||||||||||||||||||||||
| (e.g. ``await _migration_v1_add_status(self._db)``), that helper's SQL | ||||||||||||||||||||||||||||||||||||||||||||||
| literals are collected too. A visited set prevents cycles. | ||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||
| added: set[tuple[str, str]] = set() | ||||||||||||||||||||||||||||||||||||||||||||||
| module_functions: dict[str, ast.FunctionDef | ast.AsyncFunctionDef] = {} | ||||||||||||||||||||||||||||||||||||||||||||||
| for node in ast.walk(tree): | ||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(node, ast.Module): | ||||||||||||||||||||||||||||||||||||||||||||||
| for item in node.body: | ||||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): | ||||||||||||||||||||||||||||||||||||||||||||||
| module_functions[item.name] = item | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| def _called_names(fn: ast.AST) -> set[str]: | ||||||||||||||||||||||||||||||||||||||||||||||
| names: set[str] = set() | ||||||||||||||||||||||||||||||||||||||||||||||
| for child in ast.walk(fn): | ||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| isinstance(child, ast.Call) | ||||||||||||||||||||||||||||||||||||||||||||||
| and isinstance(child.func, ast.Name) | ||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||
| names.add(child.func.id) | ||||||||||||||||||||||||||||||||||||||||||||||
| return names | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+469
to
+475
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Do not collect calls from nested bodies. Line 469 traverses nested Make Proposed fix def _called_names(fn: ast.AST) -> set[str]:
names: set[str] = set()
- for child in ast.walk(fn):
- if (
- isinstance(child, ast.Call)
- and isinstance(child.func, ast.Name)
- ):
- names.add(child.func.id)
+ def _descend(node: ast.AST) -> None:
+ for child in ast.iter_child_nodes(node):
+ if isinstance(
+ child,
+ (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda),
+ ):
+ continue
+ if isinstance(child, ast.Call) and isinstance(child.func, ast.Name):
+ names.add(child.func.id)
+ _descend(child)
+
+ _descend(fn)
return names📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| for node in ast.walk(tree): | ||||||||||||||||||||||||||||||||||||||||||||||
| if not isinstance(node, ast.ClassDef): | ||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -459,9 +482,23 @@ def _post_init_added_columns(tree: ast.AST) -> set[tuple[str, str]]: | |||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
| if item.name != "_post_init": | ||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
| for literal in _method_sql_literals(item): | ||||||||||||||||||||||||||||||||||||||||||||||
| for m in _ADD_COLUMN_RE.finditer(literal): | ||||||||||||||||||||||||||||||||||||||||||||||
| added.add((m.group(1), m.group(2))) | ||||||||||||||||||||||||||||||||||||||||||||||
| visited: set[str] = set() | ||||||||||||||||||||||||||||||||||||||||||||||
| queue = [item] | ||||||||||||||||||||||||||||||||||||||||||||||
| while queue: | ||||||||||||||||||||||||||||||||||||||||||||||
| fn = queue.pop(0) | ||||||||||||||||||||||||||||||||||||||||||||||
| if fn.name in visited: | ||||||||||||||||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||||||||||||||||
| visited.add(fn.name) | ||||||||||||||||||||||||||||||||||||||||||||||
| for literal in _method_sql_literals(fn): | ||||||||||||||||||||||||||||||||||||||||||||||
| for m in _ADD_COLUMN_RE.finditer(literal): | ||||||||||||||||||||||||||||||||||||||||||||||
| added.add((m.group(1), m.group(2))) | ||||||||||||||||||||||||||||||||||||||||||||||
| for name in _called_names(fn): | ||||||||||||||||||||||||||||||||||||||||||||||
| helper = module_functions.get(name) | ||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||
| helper is not None | ||||||||||||||||||||||||||||||||||||||||||||||
| and helper.name not in visited | ||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||
| queue.append(helper) | ||||||||||||||||||||||||||||||||||||||||||||||
| return added | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
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.
WARNING:
module_functionsonly captures top-level module functions directly inModule.body. Functions defined insideif/try/with/forblocks at module scope are missed, even though_post_initmay legitimately call them. A helper defined inside a conditional block would not be followed, causing a false violation.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.