Skip to content

Migrations from TM and docs#56

Open
jeffmaki wants to merge 7 commits into
developfrom
jeff-migrations-from-tm
Open

Migrations from TM and docs#56
jeffmaki wants to merge 7 commits into
developfrom
jeff-migrations-from-tm

Conversation

@jeffmaki

@jeffmaki jeffmaki commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Copy migrations from the old Tasking Manager to this repo's alembic + add docs

Summary

  • Added the complete task-database Alembic migration history, including schema changes, data backfills, organization/team migrations, workspace tables, indexes, constraints, and campaign/task-related tables.
  • Added GeoAlchemy migration helper registration required for spatial migrations.
  • Added python-slugify for migration backfills.
  • Documented deployment architecture, database separation, environment templating, and service responsibilities in README.md.
  • Added .claude to .gitignore.
  • Removed the obsolete initial task-schema migration.

@jeffmaki jeffmaki requested a review from MashB July 10, 2026 16:39

"""

import sqlalchemy as sa
import sqlalchemy as sa
from alembic import op
from geoalchemy2 import Geometry
from sqlalchemy.dialects import postgresql

"""

import sqlalchemy as sa
Comment thread alembic_task/env.py Fixed
@jeffmaki jeffmaki removed the request for review from MashB July 10, 2026 16:41
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jeffmaki, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4f7d4713-afa3-4695-9d7e-7f55af38253f

📥 Commits

Reviewing files that changed from the base of the PR and between c84583b and 94c5f14.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • README.md
  • alembic_task/env.py
  • alembic_task/versions/0eee8c1abd3a_.py
  • pyproject.toml
📝 Walkthrough

Walkthrough

The pull request documents the deployment architecture, updates Alembic geospatial initialization, adds python-slugify, and introduces a broad sequence of task-database schema and data migrations.

Changes

Task database migration chain

Layer / File(s) Summary
Deployment and migration bootstrap
.gitignore, README.md, alembic_task/env.py, pyproject.toml
Documents the deployment topology, ignores .claude, registers GeoAlchemy migration helpers, and adds python-slugify.
Core and workspace schema revisions
alembic_task/versions/*
Adds and updates workspace, imagery, spatial, task, and supporting database tables and constraints.
Task, project, and message data migrations
alembic_task/versions/*
Adds task/project fields, indexes, permissions, geography, message classification, notifications, and counter backfills.
Organization, team, and campaign migrations
alembic_task/versions/*
Adds organization, team, campaign, licensing, partnership, membership, slug, and association migrations.
User, notification, and supporting revisions
alembic_task/versions/*
Updates user profiles, notifications, application keys, editors, triggers, indexes, and miscellaneous schema revisions.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: MashB, sujata-m, susrisha

Poem

A rabbit hops through tables wide,
With Alembic paths all neatly tied.
New schemas bloom, old fields depart,
Slugs and workspaces make a start.
Geo helpers guide the way—
Migration magic grows today!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the changes but too vague to describe the main updates or their scope. Use a more specific title like "Add TM database migrations and deployment documentation".
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (28)
pyproject.toml-38-41 (1)

38-41: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use bound parameters in alembic_task/versions/bfcf4182dcb5_.py. The raw name is still interpolated into SQL, and handle_special_chars() only doubles the first ', so names with additional quotes can still break the statement. Replace the f-string with a parameterized UPDATE.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pyproject.toml` around lines 38 - 41, Update the migration logic in
bfcf4182dcb5_ to replace the interpolated SQL f-string with a parameterized
UPDATE using bound parameters for the organisation name and slug; remove
reliance on handle_special_chars() for SQL escaping and ensure all values are
passed through the database execution parameters.
alembic_task/versions/d2e18f0f34a9_.py-21-27 (1)

21-27: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

project_favorites table is missing a primary key.

This join table has no primary key column or composite primary key constraint. Without uniqueness enforcement, duplicate favorite entries can be inserted, and ORM operations that rely on identity will be unreliable. Consider adding a composite PK on (project_id, user_id) or a surrogate key.

Additionally, both FK columns are nullable=True, which allows rows with no valid references — likely unintended for a favorites association table.

🛡️ Proposed fix: add composite primary key and make FK columns non-null
     op.create_table(
         "project_favorites",
-        sa.Column("project_id", sa.Integer(), nullable=True),
-        sa.Column("user_id", sa.BigInteger(), nullable=True),
+        sa.Column("project_id", sa.Integer(), nullable=False),
+        sa.Column("user_id", sa.BigInteger(), nullable=False),
         sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
         sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+        sa.PrimaryKeyConstraint("project_id", "user_id"),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/d2e18f0f34a9_.py` around lines 21 - 27, Update the
project_favorites table definition in the migration to make project_id and
user_id nullable=False and enforce uniqueness with a composite primary key on
both columns, using a PrimaryKeyConstraint or equivalent constraint in
op.create_table.
alembic_task/versions/068674f06b0f_.py-34-40 (1)

34-40: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Downgrade will fail if expert_mode contains NULLs.

The downgrade sets expert_mode back to nullable=False without first backfilling any NULL values. Since the upgrade made the column nullable, rows with NULL expert_mode may exist at downgrade time, causing ALTER COLUMN ... SET NOT NULL to fail.

🛡️ Proposed fix: backfill NULLs before re-imposing NOT NULL
 def downgrade():
     # ### commands auto-generated by Alembic - please adjust! ###
+    op.execute("UPDATE users SET expert_mode = false WHERE expert_mode IS NULL")
     op.alter_column(
         "users",
         "expert_mode",
         existing_type=sa.BOOLEAN(),
         nullable=False,
         existing_server_default=sa.text("false"),
     )
     op.drop_column("users", "picture_url")
     # ### end Alembic commands ###
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/068674f06b0f_.py` around lines 34 - 40, Before the
downgrade’s op.alter_column call for users.expert_mode, update all NULL
expert_mode values to false using the migration’s database connection, then
apply nullable=False. Keep the existing server default and type settings
unchanged.
alembic_task/versions/2cfa981c70e7_.py-21-42 (1)

21-42: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Upgrade will fail if geometry columns contain NULLs.

Setting centroid and geometry to nullable=False without backfilling existing NULL values will cause ALTER COLUMN ... SET NOT NULL to fail if any rows have NULL geometry data. A data backfill or cleanup step should precede the nullability change.

🛡️ Proposed fix: backfill or remove NULL rows before setting NOT NULL
 def upgrade():
     # ### commands auto-generated by Alembic - please adjust! ###
+    op.execute("DELETE FROM projects WHERE centroid IS NULL OR geometry IS NULL")
     op.alter_column(
         "projects",
         "centroid",

If deleting rows is too aggressive, consider updating with default geometry values instead:

+    op.execute("UPDATE projects SET centroid = ST_SetSRID(ST_MakePoint(0, 0), 4326) WHERE centroid IS NULL")
+    op.execute("UPDATE projects SET geometry = ST_GeomFromEWKT('SRID=4326;MULTIPOLYGON EMPTY') WHERE geometry IS NULL")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/2cfa981c70e7_.py` around lines 21 - 42, Before the
nullability changes in the migration upgrade function, backfill valid geometry
values or remove rows with NULL projects.centroid and projects.geometry,
handling each column consistently and safely. Then retain the op.alter_column
calls to set both columns nullable=False, ensuring the migration succeeds on
existing data.
alembic_task/versions/0a6b82b55983_.py-59-75 (1)

59-75: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Potential TypeError if message.subject or message.message is NULL.

re.search() raises TypeError when passed None. If any existing message has a NULL subject or message field, the migration will crash. Add a guard before regex operations.

🛡️ Proposed fix: guard against NULL fields
-        match = project_task_re.search(message.subject)
+        subject = message.subject or ""
         if match:
             project_id = match.group(1)
             task_id = match.group(2)

-        if validated_notification_re.search(message.subject):
+        if validated_notification_re.search(subject):
             message_type = 4
-        elif invalidated_notification_re.search(message.subject):
+        elif invalidated_notification_re.search(subject):
             message_type = 5
-        elif mention_notification_re.search(message.subject):
+        elif mention_notification_re.search(subject):
             message_type = 3
-        elif welcome_re.search(message.subject):
+        elif welcome_re.search(subject):
             message_type = 1

         # Look for direct messages from project managers
         if message_type is None and project_id is None:
-            match = message_all_re.search(message.message)
+            match = message_all_re.search(message.message or "")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/0a6b82b55983_.py` around lines 59 - 75, Guard nullable
fields before every regex operation in the migration’s message-processing logic:
ensure message.subject and message.message are non-NULL strings before passing
them to project_task_re, validated_notification_re, invalidated_notification_re,
mention_notification_re, welcome_re, or message_all_re. Preserve the existing
classification behavior for populated fields and safely skip matching when
values are NULL.
alembic_task/versions/0a6b82b55983_.py-80-96 (1)

80-96: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Logic bug: messages with message_type but no project_id are silently skipped.

When message_type is set (e.g., welcome messages → type 1) but project_id is None, the code converts project_id to the string "null" (line 85) and then runs select * from projects where id = null (line 91), which always returns empty. This sets project_existence["null"] = False, causing the update to be skipped (line 96). Messages that should have their type classified are left unmodified.

🐛 Proposed fix: skip project existence check when project_id is None
             if project_id not in project_existence:
-                project = conn.execute(
-                    sa.text("select * from projects where id = " + str(project_id))
-                ).first()
-                project_existence[project_id] = project is not None
+                if project_id == "null":
+                    project_existence[project_id] = True
+                else:
+                    project = conn.execute(
+                        sa.text("select * from projects where id = :pid"),
+                        {"pid": int(project_id)},
+                    ).first()
+                    project_existence[project_id] = project is not None

             # Only process messages from projects that still exist
-            if project_existence[project_id]:
+            if project_existence.get(project_id, True):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/0a6b82b55983_.py` around lines 80 - 96, Fix the
classification logic in the message update block so messages with a non-null
message_type and no project_id are still updated. Preserve project_id as None
for the existence-check decision, skip the projects lookup and project_existence
gating when project_id is absent, and only perform that check for messages with
a real project_id; retain task_id/message_type fallback handling as needed.
alembic_task/versions/073a96d114ab_.py-21-36 (1)

21-36: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Backfill workspaces.imageryList before dropping it alembic_task/versions/073a96d114ab_.py:21-36

This migration creates workspaces_imagery and drops workspaces.imageryList, but it never copies existing values over. Any saved imagery settings in the old column will be lost, and the downgrade won’t restore them either. Add a data-migration step first if existing rows need to be preserved.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/073a96d114ab_.py` around lines 21 - 36, Before dropping
imageryList in the migration, backfill workspaces_imagery with one row per
existing workspace containing the parsed imageryList data and appropriate
modified metadata, handling null or empty values safely. Update the downgrade to
recreate imageryList and restore each workspace’s serialized imagery settings
from workspaces_imagery before removing that table.
alembic_task/versions/0aaac86a48dc_.py-21-27 (1)

21-27: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Missing primary key on join table allows duplicate entries.

The project_allowed_users table has no primary key, and both project_id and user_id are nullable. Without a primary key or unique constraint on (project_id, user_id), duplicate associations can be inserted, leading to data integrity issues. Additionally, nullable foreign key columns in a join table are unusual — a row with a null project_id or user_id serves no purpose.

🛡️ Proposed fix
     op.create_table(
         "project_allowed_users",
-        sa.Column("project_id", sa.Integer(), nullable=True),
-        sa.Column("user_id", sa.BigInteger(), nullable=True),
+        sa.Column("project_id", sa.Integer(), nullable=False),
+        sa.Column("user_id", sa.BigInteger(), nullable=False),
         sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
         sa.ForeignKeyConstraint(["user_id"], ["users.id"]),
+        sa.PrimaryKeyConstraint("project_id", "user_id"),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/0aaac86a48dc_.py` around lines 21 - 27, Update the
project_allowed_users table definition to make project_id and user_id
non-nullable and enforce uniqueness for the association, preferably by adding a
composite primary key on both columns (or an equivalent unique constraint).
Preserve the existing foreign key relationships.
alembic_task/versions/924a63857df4_.py-28-32 (1)

28-32: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

User notification preferences are lost during migration.

The upgrade drops comments_notifications (line 28) before migrating its values. Both new columns are unconditionally set to false (lines 29, 31), so any users who had comments_notifications = true silently lose their preference. Consider migrating existing values before dropping the column.

🛡️ Proposed fix to preserve existing preferences
     op.add_column(
         "users",
         sa.Column("projects_comments_notifications", sa.Boolean(), nullable=True),
     )
     op.add_column(
         "users", sa.Column("tasks_comments_notifications", sa.Boolean(), nullable=True)
     )
-    op.drop_column("users", "comments_notifications")
-    op.execute("UPDATE users SET projects_comments_notifications = false")
+    op.execute("UPDATE users SET projects_comments_notifications = comments_notifications")
-    op.alter_column("users", "projects_comments_notifications", nullable=False)
-    op.execute("UPDATE users SET tasks_comments_notifications = false")
+    op.execute("UPDATE users SET tasks_comments_notifications = comments_notifications")
+    op.alter_column("users", "projects_comments_notifications", nullable=False)
+    op.drop_column("users", "comments_notifications")
     op.alter_column("users", "tasks_comments_notifications", nullable=False)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/924a63857df4_.py` around lines 28 - 32, Preserve
existing notification preferences in the migration upgrade: before dropping
comments_notifications, copy its values into both
projects_comments_notifications and tasks_comments_notifications, then enforce
non-null constraints and drop the legacy column only after the data migration
completes. Update the relevant alembic upgrade function and avoid unconditional
false assignments.
alembic_task/versions/48c3aed123cf_.py-28-34 (1)

28-34: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Downgrade may fail if NULL values exist in x, y, or zoom.

The downgrade sets nullable=False on x, y, and zoom. If any rows have NULL values in these columns (which is now possible after the upgrade), the downgrade will fail with a NOT NULL constraint violation. Consider adding a data backfill in the downgrade (e.g., setting NULL values to 0 or another default) before applying the non-nullable constraint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/48c3aed123cf_.py` around lines 28 - 34, Update
downgrade() to backfill NULL values in tasks.x, tasks.y, and tasks.zoom with an
appropriate default before altering those columns to nullable=False; execute the
updates via Alembic’s op.execute, then retain the existing alter_column calls.
alembic_task/versions/451f6bd05a19_.py-26-27 (1)

26-27: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Implement a proper downgrade for the trigger migration.

The downgrade is pass, meaning rolling back this migration leaves the new trigger in place. If a previous version of the trigger existed, the downgrade should restore it. If this is the first time the trigger is created, document why pass is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/451f6bd05a19_.py` around lines 26 - 27, The downgrade()
function is empty, so rolling back does not remove or restore the trigger
introduced by this migration. Implement downgrade() to drop the new trigger and
recreate the prior trigger definition if one existed; otherwise, add a clear
comment documenting why pass is intentionally acceptable.
alembic_task/versions/55790eeec3bf_.py-63-71 (1)

63-71: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Downgrade may fail if NULL values exist in notifications columns.

Same pattern as other migrations in this chain: the downgrade restores nullable=False on date, unread_count, and user_id. If any rows have NULL values after the upgrade, the downgrade will fail with a NOT NULL constraint violation. Consider backfilling NULLs to defaults before re-applying the non-nullable constraints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/55790eeec3bf_.py` around lines 63 - 71, Update the
downgrade function containing these notifications.alter_column calls to backfill
NULL values in user_id, unread_count, and date with appropriate defaults before
restoring nullable=False, using executable SQL or equivalent migration
operations; then apply the non-nullable constraints.
alembic_task/versions/f86698c827cc_.py-70-102 (1)

70-102: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Prevent silent data loss during downgrade.

The downgrade preserves only campaign names and project links, then deletes all campaign-organization relationships and campaign rows. Any logo, url, description, or post-upgrade organization association is irreversibly lost. Add a preflight guard that rejects downgrade when unrepresentable data exists, or archive it explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/f86698c827cc_.py` around lines 70 - 102, The downgrade
migration silently discards campaign metadata and organization associations. In
the downgrade function, add a preflight validation before mutating data that
detects non-null or non-empty campaign logo, URL, description, and organization
relationships that cannot be represented, then raise an explicit migration
error; alternatively, archive those values before deletion and verify the
archive completes.
alembic_task/versions/badf8bb7d56b_.py-52-57 (1)

52-57: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Backfill nullable timestamps before restoring NOT NULL.

Any application_keys row written with created = NULL after the upgrade makes this downgrade fail. Apply an explicit rollback policy before tightening the constraint.

Proposed fix
+    op.execute(
+        "UPDATE application_keys "
+        "SET created = CURRENT_TIMESTAMP "
+        "WHERE created IS NULL"
+    )
     op.alter_column(
         "application_keys",
         "created",
         existing_type=postgresql.TIMESTAMP(),
         nullable=False,
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/badf8bb7d56b_.py` around lines 52 - 57, Before the
nullable=False alteration in the downgrade function, explicitly backfill all
NULL values in application_keys.created using the migration’s rollback policy,
then apply the NOT NULL constraint. Locate the downgrade implementation and its
op.alter_column call to ensure the backfill runs first and covers every affected
row.
alembic_task/versions/0eee8c1abd3a_.py-80-88 (1)

80-88: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Bind the country name in this update. country["properties"]["NAME"] is interpolated directly into SQL, so a name with an apostrophe will break the migration. Use conn.execute(sa.text(...), {"country": ..., "project_id": ...}); conn = op.get_bind() is already available in upgrade().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/0eee8c1abd3a_.py` around lines 80 - 88, In upgrade(),
replace the string-built update_country_info SQL and op.execute call with a
parameterized sa.text statement executed through the existing conn binding,
passing country["properties"]["NAME"] as the country parameter and project_id as
the project_id parameter.
alembic_task/versions/6276c258149c_.py-74-124 (1)

74-124: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Guard the downgrade from truncating live data.
downgrade() narrows several partners columns. If any existing rows now exceed the old limits, the rollback can fail mid-migration. Add a preflight check or an explicit truncation policy before reducing these lengths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/6276c258149c_.py` around lines 74 - 124, Update
downgrade() to protect existing partners data before narrowing columns: add a
preflight query that detects values exceeding each target length and aborts with
a clear error, or explicitly implement and document truncation if that is the
intended policy. Apply the guard before the batch_alter_table("partners") block
and cover permalink, social links, logo_url, hashtags, and name.
alembic_task/versions/14340f1e0d6b_.py-107-177 (1)

107-177: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Scope the downgrade to rows created by this migration
At alembic_task/versions/14340f1e0d6b_.py:117-177, the downgrade still looks up teams and the organisation by name and restores roles from current membership. alembic_task/versions/e3282e2db2d7_.py does not enforce unique names on teams or organisations, so this can delete the wrong rows or multiple rows, and it can overwrite later role changes for users added/removed after the migration. Persist the affected IDs/original roles in upgrade() and use those exact rows in downgrade().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/14340f1e0d6b_.py` around lines 107 - 177, The downgrade
currently identifies teams and organisations by non-unique names and
reconstructs roles from mutable membership data. Update the migration’s
upgrade() to persist the exact created organisation/team IDs, affected user IDs,
and original roles, then revise downgrade() to use those persisted identifiers
and restore only those rows and roles; delete only the teams, memberships,
projects, and organisation created or modified by this migration, including safe
handling when persisted records are absent.
alembic_task/versions/14340f1e0d6b_.py-25-55 (1)

25-55: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use RETURNING id for the team inserts. The organisations lookup is fine here, but select id from teams order by id desc limit 1 is a global latest-row query and can bind memberships/projects to the wrong team under concurrent inserts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/14340f1e0d6b_.py` around lines 25 - 55, Replace the
global latest-team queries after the team inserts in the migration with INSERT
statements using RETURNING id, and assign the returned scalar values directly to
validator_team_id and project_manager_team_id. Update both create_validator_team
and create_pm_team execution paths while leaving the organisations lookup
unchanged.
alembic_task/versions/b5b8370f9262_.py-32-38 (1)

32-38: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Join table project_priority_areas lacks a primary key and has nullable FK columns.

Without a composite primary key on (project_id, priority_area_id), duplicate associations can be inserted. Both FK columns being nullable=True also allows meaningless rows with NULL references. If this migration has already been applied, a follow-up migration is needed to add the PK and set the columns non-nullable.

🛡️ Proposed fix (if migration not yet applied)
     op.create_table(
         "project_priority_areas",
-        sa.Column("project_id", sa.Integer(), nullable=True),
-        sa.Column("priority_area_id", sa.Integer(), nullable=True),
+        sa.Column("project_id", sa.Integer(), nullable=False),
+        sa.Column("priority_area_id", sa.Integer(), nullable=False),
         sa.ForeignKeyConstraint(["priority_area_id"], ["priority_areas.id"]),
         sa.ForeignKeyConstraint(["project_id"], ["projects.id"]),
+        sa.PrimaryKeyConstraint("project_id", "priority_area_id"),
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/b5b8370f9262_.py` around lines 32 - 38, Update the
project_priority_areas table definition to make project_id and priority_area_id
non-nullable and add a composite primary key covering both columns, preventing
duplicate or incomplete associations. If this migration may already have been
applied, create a follow-up migration to alter the existing columns and add the
composite primary key.
alembic_task/versions/c5121cbba124_.py-35-47 (1)

35-47: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Downgrade sets nullable=False without handling existing NULL values.

After the upgrade makes definition nullable, any rows with NULL definition will cause the downgrade's ALTER COLUMN ... SET NOT NULL to fail. The downgrade should backfill or delete NULL rows before enforcing the NOT NULL constraint.

🛡️ Proposed fix for downgrade
 def downgrade():
     # ### commands auto generated by Alembic - please adjust! ###
+    op.execute("DELETE FROM workspaces_long_quests WHERE definition IS NULL")
     with op.batch_alter_table("workspaces_long_quests", schema=None) as batch_op:
         batch_op.alter_column("definition", existing_type=sa.VARCHAR(), nullable=False)
 
+    op.execute("DELETE FROM workspaces_imagery WHERE definition IS NULL")
     with op.batch_alter_table("workspaces_imagery", schema=None) as batch_op:
         batch_op.alter_column(
             "definition",
             existing_type=postgresql.JSON(astext_type=sa.Text()),
             nullable=False,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/c5121cbba124_.py` around lines 35 - 47, Update the
downgrade function to handle existing NULL values in the definition columns
before altering either column to nullable=False: backfill them with appropriate
defaults or remove affected rows using migration-safe SQL, then retain the
existing alter_column calls in the workspaces_long_quests and workspaces_imagery
batch operations.
alembic_task/versions/251a7638da78_.py-30-37 (1)

30-37: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define a safe rollback policy for messages exceeding 250 characters.

After this upgrade permits longer messages, narrowing the column can fail during rollback. Add a preflight check and an explicit archive/truncation policy rather than discovering this mid-deployment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/251a7638da78_.py` around lines 30 - 37, Update
downgrade() to preflight-check project_chat.message for values exceeding 250
characters before narrowing the column; apply an explicit, documented archive or
truncation policy for those messages, then perform op.alter_column only after
ensuring all values satisfy the limit.
alembic_task/versions/bfcf4182dcb5_.py-26-34 (1)

26-34: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Resolve slug collisions before creating the unique constraint.

Distinct names can produce identical or empty slugs—for example, punctuation and capitalization variants. In that case the unique constraint fails and blocks deployment. Generate deterministic fallbacks such as an organization-ID suffix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/bfcf4182dcb5_.py` around lines 26 - 34, The migration’s
slug backfill can create duplicate or empty values before organisations_slug_key
is added. Update the organisations iteration and slug generation to resolve
collisions deterministically, using an organization-ID-based suffix (and
ensuring the result is non-empty), while preserving already-unique slugs before
creating the constraint.
alembic_task/versions/7937dae319b5_.py-22-35 (1)

22-35: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Remove the live third-party dependency from the migration.

Fresh deployments now depend on Nominatim availability and mutable responses. The request also has no timeout or status/schema handling, so it can hang or abort the entire migration. Commit a deterministic country mapping or perform this backfill separately before deployment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7937dae319b5_.py` around lines 22 - 35, Remove the
Nominatim request from upgrade() and replace the live country lookup with a
committed, deterministic country-name mapping used to backfill existing project
records; alternatively, move the backfill into a separately runnable
pre-deployment task. Ensure the migration completes without network access or
mutable third-party responses.

Source: Linters/SAST tools

alembic_task/versions/bfcf4182dcb5_.py-26-32 (1)

26-32: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Update organizations by ID with bound parameters.

The raw query embeds organization names, while the helper escapes only the first apostrophe. Select id, name and bind the generated slug and ID; then remove handle_special_chars().

Proposed query structure
-    orgs = conn.execute(sa.text("select name from organisations;"))
-    for org in orgs:
-        name = handle_special_chars(org[0])
-        query = (
-            f"UPDATE organisations SET slug = '{slugify(name)}' WHERE name = '{name}';"
-        )
-        op.execute(query)
+    orgs = conn.execute(sa.text("SELECT id, name FROM organisations"))
+    for organisation_id, name in orgs:
+        conn.execute(
+            sa.text(
+                "UPDATE organisations SET slug = :slug WHERE id = :organisation_id"
+            ),
+            {
+                "slug": slugify(name),
+                "organisation_id": organisation_id,
+            },
+        )

Also applies to: 45-49

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/bfcf4182dcb5_.py` around lines 26 - 32, Update the
migration logic that iterates over organisations to select both id and name,
remove handle_special_chars(), and update each row by its ID using bound
parameters for the generated slug and organization ID instead of interpolating
names into raw SQL. Apply the same change to the additional affected block.

Source: Linters/SAST tools

alembic_task/versions/7937dae319b5_.py-36-44 (1)

36-44: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bind country values instead of constructing privileged SQL.

Both values can contain quotes or SQL syntax, while handle_special_chars() escapes only the first apostrophe. Parameter binding avoids malformed queries and prevents database-controlled or external values from changing query semantics.

Also applies to: 51-55

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7937dae319b5_.py` around lines 36 - 44, Replace the
dynamically concatenated SQL in the migration’s `update_project` logic with a
parameterized `sa.text` statement, binding `updated_country_name` and `country`
as query parameters; remove reliance on `handle_special_chars()` for SQL safety
and apply the same change to the other occurrence noted in lines 51–55.

Source: Linters/SAST tools

alembic_task/versions/c40e1fdf6b70_.py-46-47 (1)

46-47: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate and constrain permission values before dropping the legacy fields.

The mapping can return None, which is written as 'None' to an integer column. The new nullable, unconstrained columns also allow later values that make downgrade fail at .split(). Reject invalid legacy rows, then add NOT NULL and CHECK constraints for the supported ranges before dropping the source columns.

Proposed safeguards
         validation_permission = d.determine_validation_permission(
             validation_restriction
         )
+        if mapping_permission is None or validation_permission is None:
+            raise RuntimeError(
+                f"Project {project_id} contains unsupported permission values"
+            )
 
         update_query = (

After the backfill:

op.alter_column("projects", "mapping_permission", nullable=False)
op.alter_column("projects", "validation_permission", nullable=False)
op.create_check_constraint(
    "ck_projects_mapping_permission",
    "projects",
    "mapping_permission IN (0, 1)",
)
op.create_check_constraint(
    "ck_projects_validation_permission",
    "projects",
    "validation_permission IN (0, 1, 2, 3)",
)

Also applies to: 59-80, 109-118

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/c40e1fdf6b70_.py` around lines 46 - 47, Validate the
backfilled values before dropping legacy permission columns: reject rows where
the mapping returns None or produces unsupported values, and ensure invalid data
cannot be written. In the migration’s permission backfill logic, then make
projects.mapping_permission and projects.validation_permission non-nullable and
add CHECK constraints allowing only 0–1 and 0–3 respectively, before removing
the source columns; apply the same safeguards to the downgrade/relevant mirrored
sections.
alembic_task/versions/7bbc01082457_.py-199-260 (1)

199-260: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Parameterize all organisation queries.

Names and tags are interpolated into privileged SQL, and the manual escaping handles only one apostrophe. A crafted or simply unusual organisation name can break the migration or alter its query semantics.

Also applies to: 283-293

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7bbc01082457_.py` around lines 199 - 260, Parameterize
every SQL statement in the organisation migration logic, including the queries
in the shown block and the additional statements around the referenced lines.
Replace string concatenation in the select, insert, update, manager lookup, and
manager insertion statements with named bind parameters passed through
sa.text().bindparams or execute keyword arguments; remove the manual apostrophe
escaping. Preserve existing value and ID handling while ensuring organisation
names, tags, and IDs are never interpolated into SQL.

Source: Linters/SAST tools

alembic_task/versions/7bbc01082457_.py-181-186 (1)

181-186: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't gate the migration on SELECT rowcount. org_tags.rowcount can be -1 here, so count == total_orgs never passes and organisation_tag is left behind. Materialize the distinct rows and use len(...), or switch to COUNT(*).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7bbc01082457_.py` around lines 181 - 186, The migration
incorrectly relies on org_tags.rowcount, which may be -1 for SELECT queries. In
the migration logic around org_tags, materialize the distinct organisation_tag
results and use len(...) for total_orgs, or replace the query with a COUNT(*)
query so the subsequent cleanup condition works reliably.
🟡 Minor comments (1)
alembic_task/versions/fcd9cebaa79c_.py-4-14 (1)

4-14: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Docstring Revises does not match actual down_revision.

The docstring says Revises: d77ee40254f1 but the code sets down_revision = "a8a7537985c0". This mismatch will confuse anyone tracing the migration chain. Update the docstring to reflect the actual revision.

📝 Proposed fix
-Revises: d77ee40254f1
+Revises: a8a7537985c0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/fcd9cebaa79c_.py` around lines 4 - 14, Update the
migration module’s header docstring so its “Revises” value matches the actual
down_revision identifier "a8a7537985c0", keeping it consistent with revision
"fcd9cebaa79c".
🧹 Nitpick comments (7)
README.md (2)

17-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for the fenced code block.

The ASCII topology diagram uses a bare ``` fence. Add a language hint (e.g., text) to satisfy markdownlint MD040 and improve rendering in markdown viewers.

🔧 Proposed fix
-```
+```text
                     client  (TDEI/Keycloak JWT)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 17, Update the fenced ASCII topology code block in the
README to use a language hint, such as ```text, on its opening fence while
leaving the diagram content unchanged.

Source: Linters/SAST tools


71-71: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: "outside of pytest" → "outside pytest".

LanguageTool flags "outside of" as redundant. Trivial wording improvement.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 71, Replace the phrase “outside of pytest” with “outside
pytest” in the startup description.

Source: Linters/SAST tools

alembic_task/versions/22e7d7e0fa02_.py (1)

56-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused id keys from initial_categories.

The id field in each category dict is never used — only name is passed to the insert. This is dead data that could confuse future readers into thinking the ids are explicitly set.

♻️ Proposed refactor
     initial_categories = [
-        {"id": 1, "name": "Missed Feature(s)"},
-        {"id": 2, "name": "Feature Geometry"},
+        "Missed Feature(s)",
+        "Feature Geometry",
     ]
 
-    for category in initial_categories:
-        op.execute(categories_table.insert().values(name=category["name"]))
+    for name in initial_categories:
+        op.execute(categories_table.insert().values(name=name))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/22e7d7e0fa02_.py` around lines 56 - 62, Remove the
unused "id" entries from each dictionary in the initial_categories collection,
retaining only the "name" values consumed by the insert loop.
alembic_task/versions/fcd9cebaa79c_.py (1)

22-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use sa.inspect() instead of deprecated Inspector.from_engine.

sa.engine.reflection.Inspector.from_engine is deprecated in SQLAlchemy 2.0. The project targets SQLAlchemy 2.0.36, so prefer sa.inspect(conn) which is the supported replacement.

♻️ Proposed refactor
-    inspector = sa.engine.reflection.Inspector.from_engine(conn)
+    inspector = sa.inspect(conn)
#!/bin/bash
# Verify SQLAlchemy 2.0 deprecation of Inspector.from_engine
rg -n "from_engine" --type=python alembic_task/
rg -n "sa.inspect" --type=python alembic_task/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/fcd9cebaa79c_.py` at line 22, Replace the deprecated
sa.engine.reflection.Inspector.from_engine(conn) call with sa.inspect(conn),
preserving the existing inspector usage in the migration.
alembic_task/versions/7d55a089b5bc_.py (1)

37-44: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Use parameterized queries instead of string concatenation.

Lines 37 and 41–43 build SQL via string concatenation with str(project.id). While the value originates from the database, this pattern is flagged by static analysis (S608) and is fragile. Use sa.text() with bind parameters, and standardize on conn.execute (line 44 currently uses op.execute).

♻️ Proposed refactor
     for project in projects:
-        query = "select distinct zoom from tasks where project_id = " + str(project.id)
-        zooms = conn.execute(sa.text(query)).fetchall()
+        zooms = conn.execute(
+            sa.text("select distinct zoom from tasks where project_id = :pid"),
+            {"pid": project.id},
+        ).fetchall()
 
         if len(zooms) == 1 and zooms[0] == (None,):
-            query = "update projects set task_creation_mode = 1 where id = " + str(
-                project.id
-            )
-            op.execute(sa.text(query))
+            conn.execute(
+                sa.text("update projects set task_creation_mode = 1 where id = :pid"),
+                {"pid": project.id},
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7d55a089b5bc_.py` around lines 37 - 44, Replace the
concatenated SQL in the migration’s zoom query and project update with sa.text()
statements using a named bind parameter for project.id; execute both through
conn.execute, including the update currently using op.execute, while preserving
the existing condition and behavior.

Source: Linters/SAST tools

alembic_task/versions/51ccc46f1b8a_.py (1)

21-30: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Consider adding ondelete cascade to the FK constraint.

The FK from project_custom_editors.project_id to projects.id lacks an ondelete clause. Since project_id is also the primary key (one-to-one), deleting a project will fail if a matching custom editor row exists. Consider adding ondelete="CASCADE" if the intent is that deleting a project should also remove its custom editor configuration.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/51ccc46f1b8a_.py` around lines 21 - 30, Add
ondelete="CASCADE" to the ForeignKeyConstraint in the project_custom_editors
table definition so deleting a projects row automatically removes its associated
custom editor configuration.
alembic_task/versions/5eba4824505e_.py (1)

21-30: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the redundant index on the primary key.

users_with_email.id already has a primary-key index, so ix_users_with_email_id duplicates the same access path while adding storage and write-maintenance cost. Keep the primary key and drop the explicit create_index/drop_index calls unless a database-specific requirement justifies both.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/5eba4824505e_.py` around lines 21 - 30, Remove the
redundant op.create_index call for users_with_email.id in the migration, and
remove the corresponding op.drop_index call from its downgrade logic while
retaining the primary key constraint.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6264ded-1994-4497-8da2-9b4018de2c95

📥 Commits

Reviewing files that changed from the base of the PR and between 6d98fcb and c84583b.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (93)
  • .gitignore
  • README.md
  • alembic_task/env.py
  • alembic_task/versions/05e1ecf9953a_.py
  • alembic_task/versions/05f1b650ddbc_.py
  • alembic_task/versions/068674f06b0f_.py
  • alembic_task/versions/073a96d114ab_.py
  • alembic_task/versions/0a6b82b55983_.py
  • alembic_task/versions/0aaac86a48dc_.py
  • alembic_task/versions/0d47c10c8a66_.py
  • alembic_task/versions/0eeaa5aed53b_.py
  • alembic_task/versions/0eee8c1abd3a_.py
  • alembic_task/versions/14340f1e0d6b_.py
  • alembic_task/versions/14842761654b_.py
  • alembic_task/versions/1579d24928e7_.py
  • alembic_task/versions/22e7d7e0fa02_.py
  • alembic_task/versions/22e9a0e9b254_.py
  • alembic_task/versions/251a7638da78_.py
  • alembic_task/versions/279f8d753529_.py
  • alembic_task/versions/29097876c7e6_.py
  • alembic_task/versions/2cfa981c70e7_.py
  • alembic_task/versions/2d6a7ad4e1eb_.py
  • alembic_task/versions/2ee4bca188a9_.py
  • alembic_task/versions/30b091260689_.py
  • alembic_task/versions/3b8b0956b217_.py
  • alembic_task/versions/3ee58dee57c9_.py
  • alembic_task/versions/42c45e74752b_.py
  • alembic_task/versions/451f6bd05a19_.py
  • alembic_task/versions/48c3aed123cf_.py
  • alembic_task/versions/4b9fb4d3f349_.py
  • alembic_task/versions/4ed992117718_.py
  • alembic_task/versions/5053c01cb170_.py
  • alembic_task/versions/51ccc46f1b8a_.py
  • alembic_task/versions/55790eeec3bf_.py
  • alembic_task/versions/567b75e74955_.py
  • alembic_task/versions/5952780a577e_.py
  • alembic_task/versions/5eba4824505e_.py
  • alembic_task/versions/6276c258149c_.py
  • alembic_task/versions/64b682d53e23_.py
  • alembic_task/versions/6612e4d6524c_.py
  • alembic_task/versions/7435b0a865e6_.py
  • alembic_task/versions/772aff899389_.py
  • alembic_task/versions/7937dae319b5_.py
  • alembic_task/versions/7991a7e1b88d_.py
  • alembic_task/versions/7bbc01082457_.py
  • alembic_task/versions/7d55a089b5bc_.py
  • alembic_task/versions/824268a7a675_.py
  • alembic_task/versions/83eb4cbb0abd_.py
  • alembic_task/versions/84c793a951b2_.py
  • alembic_task/versions/86c0f6b6a176_.py
  • alembic_task/versions/8a6419f289aa_.py
  • alembic_task/versions/8aa8f8d6a0c3_.py
  • alembic_task/versions/8b61ac59bc57_.py
  • alembic_task/versions/8f51fb7f4e40_.py
  • alembic_task/versions/924a63857df4_.py
  • alembic_task/versions/9712d29e24c8_.py
  • alembic_task/versions/9f5b73af01db_.py
  • alembic_task/versions/a43b9748ceee_.py
  • alembic_task/versions/a60315eb2654_.py
  • alembic_task/versions/a7c617755721_.py
  • alembic_task/versions/a8a7537985c0_.py
  • alembic_task/versions/a9cbd2c6c213_.py
  • alembic_task/versions/ac55902fcc3d_.py
  • alembic_task/versions/ae084e0b68b2_changed_imagery_definition_type_to_json.py
  • alembic_task/versions/b25f088d40c2_.py
  • alembic_task/versions/b5b8370f9262_.py
  • alembic_task/versions/ba778ef9c615_.py
  • alembic_task/versions/badf8bb7d56b_.py
  • alembic_task/versions/bcb474128817_.py
  • alembic_task/versions/bcdb7875bd1c_.py
  • alembic_task/versions/bfcf4182dcb5_.py
  • alembic_task/versions/c40e1fdf6b70_.py
  • alembic_task/versions/c4670f3bb828_.py
  • alembic_task/versions/c5121cbba124_.py
  • alembic_task/versions/c5121cbba124_initial_task_schema.py
  • alembic_task/versions/cbc419d1740c_.py
  • alembic_task/versions/d2e18f0f34a9_.py
  • alembic_task/versions/d77ee40254f1_.py
  • alembic_task/versions/dc250d726600_.py
  • alembic_task/versions/dd99815a393c_.py
  • alembic_task/versions/deec8123583d_.py
  • alembic_task/versions/e0741bdfde1d_.py
  • alembic_task/versions/e3282e2db2d7_.py
  • alembic_task/versions/e36f1d0c4947_.py
  • alembic_task/versions/e8ffa33a9c18_.py
  • alembic_task/versions/ee46f5e8723b_.py
  • alembic_task/versions/ee5315dcf3e1_.py
  • alembic_task/versions/f26a7c36eb65_.py
  • alembic_task/versions/f547382df31b_.py
  • alembic_task/versions/f86698c827cc_.py
  • alembic_task/versions/fb96ef86e2f6_.py
  • alembic_task/versions/fcd9cebaa79c_.py
  • pyproject.toml
💤 Files with no reviewable changes (1)
  • alembic_task/versions/c5121cbba124_initial_task_schema.py

op.create_table(
"workspaces_imagery",
sa.Column("workspace_id", sa.Integer(), nullable=False),
sa.Column("definition", sa.Unicode(), nullable=False),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Column type mismatch: definition should be sa.JSON(), not sa.Unicode().

The upstream model WorkspaceImagery in api/src/workspaces/schemas.py defines definition as Column(SAJson, nullable=False), but this migration creates it as sa.Unicode() (VARCHAR/TEXT). The ORM will attempt JSON serialization/deserialization on a text column, causing runtime errors and losing PostgreSQL JSON validation and query capabilities.

🔧 Proposed fix
-        sa.Column("definition", sa.Unicode(), nullable=False),
+        sa.Column("definition", sa.JSON(), nullable=False),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sa.Column("definition", sa.Unicode(), nullable=False),
sa.Column("definition", sa.JSON(), nullable=False),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/073a96d114ab_.py` at line 24, Update the `definition`
column declaration in the migration to use `sa.JSON()` instead of
`sa.Unicode()`, matching the `WorkspaceImagery` model’s JSON type and preserving
database-level JSON behavior.

Comment on lines +90 to +109
project = conn.execute(
sa.text("select * from projects where id = " + str(project_id))
).first()
project_existence[project_id] = project is not None

# Only process messages from projects that still exist
if project_existence[project_id]:
query = (
"update messages "
+ "set message_type="
+ str(message_type)
+ ", project_id="
+ str(project_id)
+ ", task_id="
+ str(task_id)
+ " where id = "
+ str(message.id)
)

op.execute(query)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

SQL injection via string concatenation in query construction.

Both the project existence check (lines 90-91) and the message update (lines 97-109) build SQL by concatenating values derived from message content. While the regex patterns constrain project_id and task_id to digits, this pattern is unsafe and becomes exploitable if the regex is ever loosened. Use parameterized queries with sa.text().

🔒 Proposed fix using parameterized queries
-            if project_id not in project_existence:
-                project = conn.execute(
-                    sa.text("select * from projects where id = " + str(project_id))
-                ).first()
-                project_existence[project_id] = project is not None
+            if project_id not in project_existence:
+                project = conn.execute(
+                    sa.text("select * from projects where id = :pid"),
+                    {"pid": int(project_id) if project_id != "null" else None},
+                ).first()
+                project_existence[project_id] = project is not None

-            query = (
-                "update messages "
-                + "set message_type="
-                + str(message_type)
-                + ", project_id="
-                + str(project_id)
-                + ", task_id="
-                + str(task_id)
-                + " where id = "
-                + str(message.id)
-            )
-
-                op.execute(query)
+            query = sa.text(
+                "update messages set message_type = :mt, project_id = :pid, "
+                "task_id = :tid where id = :id"
+            )
+            params = {
+                "mt": int(message_type) if message_type != "null" else None,
+                "pid": int(project_id) if project_id != "null" else None,
+                "tid": int(task_id) if task_id != "null" else None,
+                "id": message.id,
+            }
+            op.execute(query, params)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
project = conn.execute(
sa.text("select * from projects where id = " + str(project_id))
).first()
project_existence[project_id] = project is not None
# Only process messages from projects that still exist
if project_existence[project_id]:
query = (
"update messages "
+ "set message_type="
+ str(message_type)
+ ", project_id="
+ str(project_id)
+ ", task_id="
+ str(task_id)
+ " where id = "
+ str(message.id)
)
op.execute(query)
project = conn.execute(
sa.text("select * from projects where id = :pid"),
{"pid": int(project_id) if project_id != "null" else None},
).first()
project_existence[project_id] = project is not None
# Only process messages from projects that still exist
if project_existence[project_id]:
query = sa.text(
"update messages set message_type = :mt, project_id = :pid, "
"task_id = :tid where id = :id"
)
params = {
"mt": int(message_type) if message_type != "null" else None,
"pid": int(project_id) if project_id != "null" else None,
"tid": int(task_id) if task_id != "null" else None,
"id": message.id,
}
op.execute(query, params)
🧰 Tools
🪛 Ruff (0.15.20)

[error] 91-91: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/0a6b82b55983_.py` around lines 90 - 109, Replace the
concatenated SQL in the project existence check and message update with
parameterized sa.text() statements. Bind project_id in the projects lookup, and
bind message_type, project_id, task_id, and message.id in the update executed
through op.execute, preserving the existing logic and project_existence
behavior.

Source: Linters/SAST tools

Comment on lines +9 to +16
import json
import sys

import shapely.wkt
import sqlalchemy as sa
from alembic import op
from shapely.geometry import shape
from sqlalchemy.dialects.postgresql import ARRAY

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Include the world data files required by this migration.

Line 42 immediately opens scripts/world/continents.json, but the PR branch's scripts directory contains only ci.sh and fix-lint.sh. This upgrade therefore fails with FileNotFoundError. Also resolve resources relative to the migration rather than the caller's working directory. (github.com)

Proposed path handling
 import json
 import sys
+from pathlib import Path

+WORLD_DATA_DIR = Path(__file__).resolve().parents[2] / "scripts" / "world"
+
-    with open("scripts/world/continents.json") as continents_data:
+    with (WORLD_DATA_DIR / "continents.json").open(encoding="utf-8") as continents_data:
         continents = json.load(continents_data)

-                with open(
-                    "scripts/world/" + project_continent + ".json",
-                    "r",
-                    encoding="utf-8",
-                ) as countries_data:
+                with (WORLD_DATA_DIR / f"{project_continent}.json").open(
+                    encoding="utf-8"
+                ) as countries_data:

Also applies to: 42-70

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/0eee8c1abd3a_.py` around lines 9 - 16, Include the
continents and other world data files required by the migration in the
repository, and update the migration’s resource loading logic to resolve paths
relative to its own file location rather than the process working directory. In
the migration upgrade logic and any helper reading world data, use the migration
module’s directory as the base path before opening the JSON files.

Comment on lines +19 to +23
op.execute("""
DROP TRIGGER IF EXISTS tsvectorupdate ON project_info;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON project_info FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(text_searchable, "pg_catalog.english", project_id_str, short_description, description)
""")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Split multiple SQL statements into separate op.execute() calls.

The CI pipeline confirms this migration fails with asyncpg: cannot insert multiple commands into a prepared statement. The DROP TRIGGER and CREATE TRIGGER must be executed as separate op.execute() calls.

🔧 Proposed fix
 def upgrade():
-    op.execute("""
-        DROP TRIGGER IF EXISTS tsvectorupdate ON project_info;
-        CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON project_info FOR EACH ROW EXECUTE PROCEDURE
-        tsvector_update_trigger(text_searchable, "pg_catalog.english", project_id_str, short_description, description)
-        """)
+    op.execute("DROP TRIGGER IF EXISTS tsvectorupdate ON project_info")
+    op.execute(
+        """
+        CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON project_info
+        FOR EACH ROW EXECUTE PROCEDURE
+        tsvector_update_trigger(text_searchable, "pg_catalog.english", project_id_str, short_description, description)
+        """
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
op.execute("""
DROP TRIGGER IF EXISTS tsvectorupdate ON project_info;
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON project_info FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(text_searchable, "pg_catalog.english", project_id_str, short_description, description)
""")
op.execute("DROP TRIGGER IF EXISTS tsvectorupdate ON project_info")
op.execute(
"""
CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON project_info
FOR EACH ROW EXECUTE PROCEDURE
tsvector_update_trigger(text_searchable, "pg_catalog.english", project_id_str, short_description, description)
"""
)
🧰 Tools
🪛 GitHub Actions: CI / 0_ci (3.12).txt

[error] 19-19: Alembic migration failed during upgrade: cannot insert multiple commands into a prepared statement (asyncpg PostgresSyntaxError). op.execute() contains multiple SQL commands at once: "DROP TRIGGER IF EXISTS tsvectorupdate ON project_info; CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON project_info FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger(...)".

🪛 GitHub Actions: CI / ci (3.12)

[error] 19-19: Alembic upgrade failed during op.execute() with PostgreSQL/asyncpg error: cannot insert multiple commands into a prepared statement. The executed SQL contains multiple statements: "DROP TRIGGER IF EXISTS tsvectorupdate ON project_info; CREATE TRIGGER tsvectorupdate ...".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/451f6bd05a19_.py` around lines 19 - 23, Split the SQL
in the migration’s trigger setup into two separate op.execute() calls: one for
DROP TRIGGER IF EXISTS and one for CREATE TRIGGER, preserving the existing
statements and trigger configuration.

Source: Pipeline failures

Comment on lines +36 to +44
update_project = (
"update projects set country = '{\""
+ handle_special_chars(updated_country_name)
+ "\"}' where country @> ARRAY['"
+ handle_special_chars(country)
+ "']::varchar[]"
+ ";"
)
conn.execute(sa.text(update_project))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Preserve every other value in the country array.

SET country = '{"…"}' replaces the entire array for projects containing the matched country. A project with multiple countries silently loses all other entries. Replace only the matching element, for example with PostgreSQL array_replace.

Proposed update
-            update_project = (
-                "update projects set country = '{\""
-                + handle_special_chars(updated_country_name)
-                + "\"}' where country @> ARRAY['"
-                + handle_special_chars(country)
-                + "']::varchar[]"
-                + ";"
-            )
-            conn.execute(sa.text(update_project))
+            conn.execute(
+                sa.text(
+                    """
+                    UPDATE projects
+                    SET country = array_replace(
+                        country,
+                        CAST(:old_country AS varchar),
+                        CAST(:new_country AS varchar)
+                    )
+                    WHERE CAST(:old_country AS varchar) = ANY(country)
+                    """
+                ),
+                {
+                    "old_country": country,
+                    "new_country": updated_country_name,
+                },
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
update_project = (
"update projects set country = '{\""
+ handle_special_chars(updated_country_name)
+ "\"}' where country @> ARRAY['"
+ handle_special_chars(country)
+ "']::varchar[]"
+ ";"
)
conn.execute(sa.text(update_project))
conn.execute(
sa.text(
"""
UPDATE projects
SET country = array_replace(
country,
CAST(:old_country AS varchar),
CAST(:new_country AS varchar)
)
WHERE CAST(:old_country AS varchar) = ANY(country)
"""
),
{
"old_country": country,
"new_country": updated_country_name,
},
)
🧰 Tools
🪛 Ruff (0.15.20)

[error] 37-42: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7937dae319b5_.py` around lines 36 - 44, Update the SQL
built in the migration’s update_project statement to replace only the matching
country element using PostgreSQL array_replace(country, matched_country,
updated_country), preserving all other array values. Continue escaping both
country names with handle_special_chars and retain the existing filtering
condition and execution through conn.execute(sa.text(...)).

Comment on lines +277 to +305
def downgrade():
conn = op.get_bind()
op.add_column("projects", sa.Column("organisation_tag", sa.String(), nullable=True))
# Remove all mappings made
org_ids = conn.execute(sa.text("select id, name from organisations"))
for org_id, org_name in org_ids:
quote_index = org_name.find("'")
if quote_index > -1:
org_name = org_name[:quote_index] + "'" + org_name[quote_index:]
conn.execute(
sa.text(
"update projects set organisation_tag='"
+ str(org_name)
+ "' where organisation_id="
+ str(org_id)
)
)
conn.execute(sa.text("delete from project_teams where team_id is not null"))
conn.execute(sa.text("delete from team_members where team_id is not null"))
conn.execute(sa.text("delete from teams where organisation_id is not null"))
conn.execute(
sa.text("delete from organisation_managers where organisation_id is not null")
)
conn.execute(
sa.text(
"update projects set organisation_id = null where organisation_id is not null"
)
)
conn.execute(sa.text("delete from organisations where name is not null"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Restrict downgrade deletion to records introduced by this migration.

The downgrade deletes all organization managers and every non-null-named organization, plus broad team data—not merely rows created by upgrade(). Rolling back would destroy pre-existing and subsequently created records and cannot restore original organization tags.

🧰 Tools
🪛 Ruff (0.15.20)

[error] 288-291: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/7bbc01082457_.py` around lines 277 - 305, Restrict
downgrade cleanup to rows created by this migration rather than deleting all
matching records. Update downgrade() to track or identify the organizations,
teams, memberships, mappings, and managers introduced by upgrade(), restore
affected project organization_id and organisation_tag values from pre-migration
state, and delete only those tracked rows; avoid broad conditions such as
non-null organisation_id or name.

Comment on lines +61 to +70
with op.batch_alter_table("workspace_imagery", schema=None) as batch_op:
batch_op.drop_constraint(
"workspace_imagery_workspace_id_fkey", type_="foreignkey"
)
batch_op.create_foreign_key(
"workspace_imagery_workspace_id_fkey",
"workspaces",
["workspace_id"],
["id"],
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Table name mismatch in downgrade: workspace_imagery should be workspaces_imagery.

The upgrade modifies the workspaces_imagery table (line 33), but the downgrade references workspace_imagery (singular, line 61) and its constraint workspace_imagery_workspace_id_fkey (line 63). Since the table is named workspaces_imagery (plural), the downgrade will fail at runtime, making this migration non-reversible.

🐛 Proposed fix: correct table and constraint names in downgrade
-    with op.batch_alter_table("workspace_imagery", schema=None) as batch_op:
+    with op.batch_alter_table("workspaces_imagery", schema=None) as batch_op:
         batch_op.drop_constraint(
-            "workspace_imagery_workspace_id_fkey", type_="foreignkey"
+            "workspaces_imagery_workspace_id_fkey", type_="foreignkey"
         )
         batch_op.create_foreign_key(
-            "workspace_imagery_workspace_id_fkey",
+            "workspaces_imagery_workspace_id_fkey",
             "workspaces",
             ["workspace_id"],
             ["id"],
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
with op.batch_alter_table("workspace_imagery", schema=None) as batch_op:
batch_op.drop_constraint(
"workspace_imagery_workspace_id_fkey", type_="foreignkey"
)
batch_op.create_foreign_key(
"workspace_imagery_workspace_id_fkey",
"workspaces",
["workspace_id"],
["id"],
)
with op.batch_alter_table("workspaces_imagery", schema=None) as batch_op:
batch_op.drop_constraint(
"workspaces_imagery_workspace_id_fkey", type_="foreignkey"
)
batch_op.create_foreign_key(
"workspaces_imagery_workspace_id_fkey",
"workspaces",
["workspace_id"],
["id"],
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/dd99815a393c_.py` around lines 61 - 70, Correct the
downgrade block to consistently use the plural table name and foreign-key
constraint: update the batch_alter_table target and both constraint references
in the downgrade function to workspaces_imagery and
workspaces_imagery_workspace_id_fkey, matching the upgrade migration.

Comment on lines +21 to +49
campaign_tags = conn.execute(
sa.text("select distinct(campaigns) from tags where campaigns is not null")
)
total_campaigns = campaign_tags.rowcount

print("Total distinct campaigns in the DB: " + str(total_campaigns))
for campaign_tag in campaign_tags:
campaign_tag = campaign_tag[0]
conn.execute("insert into campaigns (name) values ('" + campaign_tag + "')")
new_campaign_id = conn.execute(
"select id from campaigns where name ='" + campaign_tag + "'"
).scalar()
projects = conn.execute(
"select id from projects " + " where campaign_tag='" + campaign_tag + "'"
)
for project_id in projects:
project_id = project_id[0]
conn.execute(
sa.text(
"insert into campaign_projects (campaign_id, project_id) values ("
+ str(new_campaign_id)
+ ","
+ str(project_id)
+ ")"
)
)
op.drop_table("tags")
op.drop_index("ix_projects_campaign_tag", table_name="projects")
op.drop_column("projects", "campaign_tag")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Preserve organization tags before dropping tags.

The old table contains both campaigns and organisations, but this upgrade copies only campaign values before Line 47 drops the entire table. The immediately preceding revision creates organization tables without backfilling this data. Migrate organization values and associations first, or abort when non-null organization tags exist.

🧰 Tools
🪛 Ruff (0.15.20)

[error] 29-29: Possible SQL injection vector through string-based query construction

(S608)


[error] 31-31: Possible SQL injection vector through string-based query construction

(S608)


[error] 34-34: Possible SQL injection vector through string-based query construction

(S608)


[error] 40-44: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/f86698c827cc_.py` around lines 21 - 49, The migration
drops the legacy tags table without preserving its organisation data. Update the
upgrade logic alongside the campaign migration to read non-null organisation
tags, create corresponding organisation records, and populate their project
associations using the new organisation tables before op.drop_table("tags");
alternatively, explicitly abort if any organisation tags exist. Reuse the
migration’s existing connection and SQLAlchemy execution patterns, ensuring
organisation values are migrated before the destructive drop.

Comment on lines +29 to +35
conn.execute("insert into campaigns (name) values ('" + campaign_tag + "')")
new_campaign_id = conn.execute(
"select id from campaigns where name ='" + campaign_tag + "'"
).scalar()
projects = conn.execute(
"select id from projects " + " where campaign_tag='" + campaign_tag + "'"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 'sqlalchemy' pyproject.toml
rg -n -C3 'conn\.execute\(' alembic_task/versions/f86698c827cc_.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 2745


🏁 Script executed:

#!/bin/bash
sed -n '20,40p' alembic_task/versions/f86698c827cc_.py
printf '\n---\n'
sed -n '68,92p' alembic_task/versions/f86698c827cc_.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 1955


🏁 Script executed:

#!/bin/bash
python3 - <<'PY'
from pathlib import Path
import re

path = Path("alembic_task/versions/f86698c827cc_.py")
text = path.read_text()

# Show all execute calls and whether they use raw strings or sa.text.
for i, line in enumerate(text.splitlines(), 1):
    if "conn.execute(" in line or "sa.text(" in line:
        print(f"{i}: {line}")
PY

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 1030


🏁 Script executed:

#!/bin/bash
rg -n --hidden -S "RETURNING|psycopg|postgres|postgresql|sqlalchemy>=2|mysql|sqlite" pyproject.toml alembic.ini alembic_task -g '!**/__pycache__/**'

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 3212


🏁 Script executed:

#!/bin/bash
sed -n '1,130p' alembic_task/versions/f86698c827cc_.py

Repository: TaskarCenterAtUW/workspaces-backend

Length of output: 3605


Parameterize all migration SQL

  • conn.execute("...") appears in both upgrade and downgrade, and several statements interpolate campaign_tag/IDs directly. These raw strings won’t execute cleanly under SQLAlchemy 2, and the interpolation will break on quotes. Switch every statement in this revision to sa.text(...), {...}.
🧰 Tools
🪛 Ruff (0.15.20)

[error] 29-29: Possible SQL injection vector through string-based query construction

(S608)


[error] 31-31: Possible SQL injection vector through string-based query construction

(S608)


[error] 34-34: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@alembic_task/versions/f86698c827cc_.py` around lines 29 - 35, Parameterize
every SQL statement in both upgrade and downgrade of the migration, replacing
raw string conn.execute calls with sa.text(...) and bound parameter
dictionaries. Update statements involving campaign_tag and IDs, including the
INSERT, campaign lookup, project query, and any remaining migration queries,
while preserving existing behavior and avoiding direct string interpolation.

Source: Linters/SAST tools

Comment thread alembic_task/env.py
# Add the project root directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

import geoalchemy2.alembic_helpers # noqa: F401
@jeffmaki jeffmaki requested a review from MashB July 10, 2026 17:39

@cyrossignol cyrossignol left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to bring all of the HOT tasking manager tables. Those aren't used anywhere at this point.

Ideally, we just move the workspaces tables and install those in the OSM DB so we can migrate and drop the task DB completely. I think @susrisha was already working on that.

@jeffmaki

jeffmaki commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

We don't want to bring all of the HOT tasking manager tables. Those aren't used anywhere at this point.

Ideally, we just move the workspaces tables and install those in the OSM DB so we can migrate and drop the task DB completely. I think @susrisha was already working on that.

Got it, I thought this is what you suggested at the dev call. That's fine, you can discard this then, maybe keep the docs. Or remove the tables we don't need as a final migration? I'll let y'all take over this then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants