Skip to content

Fix key migrations creating duplicate splocalecontainer records#8039

Merged
melton-jason merged 18 commits intov7_12_0_5from
issue-7988-2
Apr 29, 2026
Merged

Fix key migrations creating duplicate splocalecontainer records#8039
melton-jason merged 18 commits intov7_12_0_5from
issue-7988-2

Conversation

@grantfitzsimmons
Copy link
Copy Markdown
Member

@grantfitzsimmons grantfitzsimmons commented Apr 28, 2026

Fixes #7988

Including @melton-jason's changes, and co-written with @melton-jason

This PR seeks to eliminate bugs in the fix_schema_config step of the run_key_migration_functions. Particularly, this PR aims to eliminate duplicate SpLocaleContainer, SpLocaleContainerItem, and SpLocaleItemStr creation, as well as prevent in-place modification of existing SpLocaleContainer, SpLocaleContainerItem, and SpLocaleItemStr records.

There were a lot of migration functions which directly updated one or more SpLocaleContainer, SpLocaleContainerItem, or SpLocaleItemStr records. This could cause data destruction in the case where the discipline had already run the migration associated with the function, yet the user(s) had since made changes to the Schema Config.

The following SQL statement can be used to identify duplicate Field Labels:
(In this case, a duplicate field label is one where there's more than one label assigned to a field that has the same language and text. Feel free to modify the query to adjust what is considered a dupllicate)

SELECT container.DisciplineID,
    container.SpLocaleContainerID,
    container.Name,
    item.SpLocaleContainerItemID,
    item.Name,
    item.SpLocaleContainerID,
    str1.Language,
    str1.Text,
    str1.TimestampCreated,
    str2.TimestampCreated
FROM splocaleitemstr str1
    JOIN splocaleitemstr str2 ON str1.SpLocaleContainerItemNameID = str2.SpLocaleContainerItemNameID
    AND str1.Text = str2.Text
    AND str1.Language = str2.Language
    AND str1.Text = str2.Text
    AND str1.SpLocaleItemStrID < str2.SpLocaleItemStrID
    JOIN splocalecontaineritem item ON item.SpLocaleContainerItemID = str1.SpLocaleContainerItemNameID
    JOIN splocalecontainer container ON container.SpLocaleContainerID = item.SpLocaleContainerID;

The following SQL statement can be used to export the overall Schema Configuration structure to a CSV file.
You only need to replace 'output.csv' (within the INTO OUTFILE statement) with the desired path of the CSV:

SELECT string.TimestampCreated AS "String Created",
    string.Country AS "Country",
    string.Language AS "Language",
    string.Text AS "String",
    cname.DisciplineID AS "Label Discipline ID",
    cname.SpLocaleContainerID AS "Label Table ID",
    cname.Name AS "Label Table Name",
    cname.Format AS "Label Table Formatter",
    cname.Aggregator AS "Label Table Aggregator",
    cname.PickListName AS "Label Table PickList",
    CASE
        cname.IsHidden
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Label Table Is Hidden",
    CASE
        cname.IsSystem
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Label Table Is System",
    cdesc.DisciplineID AS "Desc Discipline ID",
    cdesc.SpLocaleContainerID AS "Desc Table ID",
    cdesc.SpLocaleContainerID AS "Desc Table Name",
    cdesc.Format AS "Desc Table Formatter",
    cdesc.Aggregator AS "Desc Table Aggregator",
    cdesc.PickListName AS "Desc Table PickList",
    CASE
        cdesc.IsHidden
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Desc Table Is Hidden",
    CASE
        cdesc.IsSystem
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Desc Table Is System",
    inamecontainer.DisciplineID AS "Label Field Table Discipline ID",
    inamecontainer.SpLocaleContainerID AS "Label Field Table ID",
    inamecontainer.Name AS "Label Field Table Name",
    inamecontainer.Format AS "Label Field Table Formatter",
    inamecontainer.Aggregator AS "Label Field Table Aggregator",
    inamecontainer.PickListName AS "Label Field Table PickList",
    CASE
        inamecontainer.IsHidden
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Label Field Table Is Hidden",
    CASE
        inamecontainer.IsSystem
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Label Field Table Is System",
    iname.SpLocaleContainerItemID AS "Label Field ID",
    iname.name AS "Label Field Name",
    iname.Format AS "Label Field Formatter",
    iname.PickListName AS "Label Field PickListName",
    iname.WebLinkName AS "Label Field WebLink Name",
    CASE
        iname.IsHidden
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Label Field Table Is Hidden",
    CASE
        iname.IsSystem
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Label Field Table Is System",
    idesccontainer.DisciplineID AS "Desc Field Table Discipline ID",
    idesccontainer.SpLocaleContainerID AS "Desc Field Table ID",
    idesccontainer.Name AS "Desc Field Table Name",
    idesccontainer.Format AS "Desc Field Table Formatter",
    idesccontainer.Aggregator AS "Desc Field Table Aggregator",
    idesccontainer.PickListName AS "Desc Field Table PickList",
    CASE
        idesccontainer.IsHidden
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Desc Field Table Is Hidden",
    CASE
        idesccontainer.IsSystem
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Desc Field Table Is System",
    idesc.SpLocaleContainerItemID AS "Desc Field ID",
    idesc.Name AS "Desc Field Name",
    idesc.Format AS "Desc Field Formatter",
    idesc.PickListName AS "Desc Field PickListName",
    idesc.WebLinkName AS "Desc Field WebLink Name",
    CASE
        idesc.IsHidden
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Desc Field Table Is Hidden",
    CASE
        idesc.IsSystem
        WHEN 1 THEN 'True'
        ELSE 'False'
    END AS "Desc Field Table Is System" INTO OUTFILE 'output.csv' FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n'
FROM SpLocaleItemStr string
    LEFT OUTER JOIN SpLocaleContainer cname ON cname.SpLocaleContainerID = string.SpLocaleContainerNameID
    AND string.SpLocaleContainerDescID IS NULL
    AND string.SpLocaleContainerItemNameID IS NULL
    AND string.SpLocaleContainerItemDescID IS NULL
    LEFT OUTER JOIN splocalecontainer cdesc ON cdesc.SpLocaleContainerID = string.SpLocaleContainerDescID
    AND string.SpLocaleContainerNameID IS NULL
    AND string.SpLocaleContainerItemNameID IS NULL
    AND string.SpLocaleContainerItemDescID IS NULL
    LEFT OUTER JOIN SpLocaleContainerItem iname ON iname.SpLocaleContainerItemID = string.SpLocaleContainerItemNameID
    AND string.SpLocaleContainerNameID IS NULL
    AND string.SpLocaleContainerDescID IS NULL
    AND string.SpLocaleContainerItemDescID IS NULL
    LEFT OUTER JOIN SpLocaleContainer inamecontainer ON iname.SpLocaleContainerID = inamecontainer.SpLocaleContainerID
    LEFT OUTER JOIN SpLocaleContainerItem idesc ON idesc.SpLocaleContainerItemID = string.SpLocaleContainerItemDescID
    AND string.SpLocaleContainerNameID IS NULL
    AND string.SpLocaleContainerDescID IS NULL
    AND string.SpLocaleContainerItemNameID IS NULL
    LEFT OUTER JOIN SpLocaleContainer idesccontainer ON idesc.SpLocaleContainerID = idesccontainer.SpLocaleContainerID
ORDER BY COALESCE(cname.DisciplineID, cdesc.DisciplineID),
    COALESCE(cname.Name, cdesc.Name),
    COALESCE(
        inamecontainer.DisciplineID,
        idesccontainer.DisciplineID
    ),
    COALESCE(inamecontainer.Name, idesccontainer.Name),
    COALESCE(iname.Name, idesc.Name),
    string.Text;

Columns 1-4, the SpLocaleItemStr information. This is information for the table/field labels or descriptions

  1. Timestamp Created
  2. Country
  3. Language
  4. Text (the actual string for the label/description)

Columns 5-12, SpLocaleContainer information for Table Labels.
5. DisciplineID
6. SpLocaleContainerID
7. Name (the name of the table)
8. Formatter
9. Aggregator
10. PickList Name
11. Is Hidden
12. Is System

Columns 13-20, SpLocaleContainer information for Table Descriptions.
13. DisciplineID
14. SpLocaleContainerID
15. Name (the name of the table)
16. Formatter
17. Aggregator
18. PickList Name
19. Is Hidden
20. Is System

Columns 21-28, the associated SpLocaleContainer information for Field Labels
21. DisciplineID
22. SpLocaleContainerID
23. Name (the name of the table)
24. Formatter
25. Aggregator
26. PickList Name
27. Is Hidden
28. Is System

Columns 29-35, SpLocaleContainerItem information for Field Labels
29. SpLocaleContainerItemID
30. Name (name of the field)
31. (Field) Formatter
32. PickList Name
33. WebLink Name
34. Is Hidden
35. Is System

Columns 36-43, the associated SpLocaleContainer information for Field Descriptions
36. DisciplineID
37. SpLocaleContainerID
38. Name (the name of the table)
39. Formatter
40. Aggregator
41. PickList Name
42. Is Hidden
43. Is System

Columns 44-50, SpLocaleContainerItem information for Field Descriptions
44. SpLocaleContainerItemID
45. Name (name of the field)
46. (Field) Formatter
47. PickList Name
48. WebLink Name
49. Is Hidden
50. Is System

Checklist

  • Self-review the PR after opening it to make sure the changes look good and
    self-explanatory (or properly documented)
  • Add relevant issue to release milestone
  • Add pr to documentation list
  • Add automated tests
  • Add a reverse migration if a migration is present in the PR
  • Add migration function to
    def fix_schema_config(stdout: WriteToStdOut | None = None):

Testing instructions

In your testing, preferably use a database that has undergone extensive SchemaConfig changes. These database are the most at risk with the previous bugs.

  • Use a database backup for an instance that has not had the v7.12 update applied (Do not update the database yet!)
  • Export or inspect the Schema Config information of the database before the update, paying close attention to the table/field labels/descriptions (i.e., the SpLocaleItemStr records), as well as the Is Hidden value on the SpLocaleContainer and SpLocaleContainerItem records
    • You can use the query provided earlier in the PR description
  • Update the database using this branch
  • Wait for the instance to be ready, after having applied all migrations and run_key_migration_functions.
  • Export or inspect the Schema Config information of the database before the update, paying close attention to the table/field labels/descriptions (i.e., the SpLocaleItemStr records), as well as the Is Hidden value on the SpLocaleContainer and SpLocaleContainerItem records
    • You can use the query provided earlier in the PR description
  • Compare the Schema Config of the two versions, ensuring that no existing SpLocaleContainer, SpLocaleContainerItem, or SpLocaleItemStr records were modified. Make sure there are no duplicate SpLocaleContainer, SpLocaleContainerItem, or SpLocaleItemStr records

Summary by CodeRabbit

  • Refactor

    • Consolidated schema configuration migration pipeline for improved maintainability.
    • Simplified loan and gift agent field migration by removing redundant update/revert functions.
    • Enhanced deterministic database record selection through explicit ordering.
    • Improved locale string persistence with idempotent duplicate handling.
  • Bug Fixes

    • Corrected field enumeration to exclude ID field appropriately during schema generation.

Somewhat related to #7988, in the sense that both had the
same "underlying casue" of using very specific filters in a
QuerySet get_or_create
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c2ba11e-79c7-413d-ad65-69607dbcdc35

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Schema-config migration code was refactored: typed defaults added, deterministic locale-container/item selection enforced, field enumeration centralized via Table._all_fields, locale-string insertion made idempotent (no text overwrites), legacy loan/gift agent migration calls removed, and the startup migration pipeline consolidated.

Changes

Cohort / File(s) Summary
Migration utilities & model field enumeration
specifyweb/specify/migration_utils/update_schema_config.py, specifyweb/specify/models_utils/load_datamodel.py
Adds Table._all_fields(...) and switches callers to ._all_fields(exclude_id_field=True); typed TableDefaults/FieldDefaults added; updates use deterministic .filter(...).order_by("id").first() for Splocalecontainer/Splocalecontaineritem.
Locale string dedupe/idempotency
specifyweb/specify/migration_utils/update_schema_config.py
bulk_create_splocaleitemstr_idempotent now deletes duplicate rows and inserts missing Splocaleitemstr entries without updating existing text values.
Loan/Gift agent migration simplification
specifyweb/specify/migration_utils/update_schema_config.py, specifyweb/specify/migrations/0039_agent_fields_for_loan_and_gift.py, specifyweb/specify/migration_utils/sp7_schemaconfig.py
Removes MIGRATION_0038_UPDATE_FIELDS and the update_loan_and_gift_agents/revert_loan_and_gift_agents calls; hides targeted agent fields by passing defaults={"ishidden": True} to existing defaults-based updater.
Startup migration pipeline / command changes
specifyweb/specify/management/commands/run_key_migration_functions.py
Prunes many startup schema/data update calls from fix_schema_config, adds remove_componentparent_item and create_table_schema_config_with_defaults to the pipeline, and documents deferred/disabled operations.
Helper changes
specifyweb/specify/migration_utils/update_schema_config.py
Introduces remove_componentparent_item helper and removes the older componets_schema_config_migrations helper.

Sequence Diagram(s)

sequenceDiagram
    participant CMD as RunKeyMigrationCommand
    participant USC as update_schema_config
    participant DB as Database
    CMD->>USC: create_table_schema_config_with_defaults(table)
    USC->>DB: filter Splocalecontainer by (name, discipline, schemaType).order_by(id).first()
    alt container missing
        USC->>DB: insert Splocalecontainer
    end
    USC->>DB: filter Splocalecontaineritem.order_by(id).first() / insert missing items
    USC->>DB: bulk_create_splocaleitemstr_idempotent -> delete duplicates, insert missing strings
    CMD->>USC: remove_componentparent_item()
    USC->>DB: delete specific ComponentParent item rows
Loading

Possibly related PRs

  • Merge v7.12.0.4 into main #8009 — Modifies the same schema-config deduplication logic for Splocalecontainer/Splocalecontaineritem/string handling; likely overlaps with these changes.

Suggested reviewers

  • acwhite211
🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Automatic Tests ⚠️ Warning The PR lacks automated tests for critical migration and schema configuration changes despite existing test infrastructure. Add comprehensive automated tests for the new _all_fields() generator, refactored update_schema_config.py functions, migration operations, and run_key_migration_functions.py to prevent regression.
Testing Instructions ⚠️ Warning PR lacks automated pytest test coverage for critical migration functions and provides only manual database-level testing instructions without specific commands or reproducible test cases. Add automated pytest tests verifying idempotency, correctness, and edge cases; document exact manual testing steps with command examples; fix type annotations and unsafe patterns before merging.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly identifies the main fix: preventing duplicate splocalecontainer records in key migrations, matching the primary objective.
Linked Issues check ✅ Passed The PR addresses all coding requirements from issue #7988: eliminating duplicate SpLocaleContainer creation by removing in-place updates, enforcing deterministic record selection with ordered queries, and refactoring the migration pipeline.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fix the duplicate SpLocaleContainer issue: schema config defaults typing, deterministic ordering, locale string persistence logic, loan/gift agent field hiding, and helper function updates.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7988-2

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

@grantfitzsimmons grantfitzsimmons changed the title Fix Fix key migrations creating duplicate splocalecontainer records Apr 28, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@specifyweb/specify/migration_utils/update_schema_config.py`:
- Around line 465-479: The fetched existing Splocalecontaineritem
(fetched_sp_locale_container_item) may have stale fields and is returned as-is;
instead, normalize it to the expected values before returning: set its type to
field_config.java_type, ishidden to field_hidden, isrequired to field_required,
issystem to table.system, picklistname to picklist_name (and version if
desired), detect changes and save the model (sp_locale_container_item.save()) so
reruns heal metadata; perform this update in the else branch where
fetched_sp_locale_container_item is assigned to sp_locale_container_item.
- Around line 314-315: The code filters Splocalecontainer using container_attrs
and Splocalecontaineritem by (container, name) without DB-level uniqueness,
creating a race where concurrent runners can both insert duplicates; fix by
adding DB-level UniqueConstraint/unique_together to the Splocalecontainer Meta
for (discipline, schematype, name) and to Splocalecontaineritem Meta for
(container, name) so the DB prevents duplicates, and/or replace the two-step
filter-then-create logic around Splocalecontainer (where
fetched_sp_locale_container is obtained) and the block creating
Splocalecontaineritem with atomic get_or_create() (or wrap in a transaction with
select_for_update) to eliminate the race window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 91c06950-21b5-4792-8813-0b378ace521a

📥 Commits

Reviewing files that changed from the base of the PR and between 8af9adc and a198a4c.

📒 Files selected for processing (1)
  • specifyweb/specify/migration_utils/update_schema_config.py

Comment thread specifyweb/specify/migration_utils/update_schema_config.py
Comment thread specifyweb/specify/migration_utils/update_schema_config.py
@github-project-automation github-project-automation Bot moved this from 📋Back Log to Dev Attention Needed in General Tester Board Apr 28, 2026
@grantfitzsimmons grantfitzsimmons added this to the 7.12.1 milestone Apr 28, 2026
@grantfitzsimmons grantfitzsimmons changed the base branch from v7.12.0.4-paris to v7_12_0_5 April 28, 2026 02:30
@acwhite211 acwhite211 mentioned this pull request Apr 28, 2026
9 tasks
Also, define a helper on Table to fetch/filter fields
…alecontaineritem

Duplicate items and strings would be created everytime the function is run.
The cause of this is the same as #7988.
There's no easy way to differentiate schema config items for
which these should be applied.
I suppose the BEST method might be to be exact: only updating
Containers/Items/Strings that exactly match the previous values
expected with the migrations they aim to fix/resolve
@melton-jason
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
specifyweb/specify/migration_utils/update_schema_config.py (1)

266-268: Use explicit T | None annotation for description parameter.

The static analysis tool flagged an implicit Optional. PEP 484 recommends explicit type annotations.

🛠️ Suggested fix
 def update_table_schema_config_with_defaults(
     table_name,
     discipline_id: int,
-    description: str = None,
+    description: str | None = None,
     apps = global_apps,
     defaults: TableDefaults | None = None,
     pending_itemstr_rows: list[dict] | None = None,
 ):
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@specifyweb/specify/migration_utils/update_schema_config.py` around lines 266
- 268, Change the implicit Optional for the parameter named "description" to an
explicit union type by updating its annotation from "description: str = None" to
"description: str | None = None" (no runtime change); locate the parameter in
the function signature in update_schema_config.py and update the annotation
accordingly so static checkers accept the explicit T | None form.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@specifyweb/specify/migration_utils/update_schema_config.py`:
- Around line 377-382: FieldDefaults currently types ishidden and isrequired as
str but they are used as booleans; update the TypedDict so ishidden and
isrequired are NotRequired[bool] instead of NotRequired[str], then run
type-checks and adjust any callers if they rely on string values; locate the
FieldDefaults definition and change the two type annotations (ishidden,
isrequired) to bool to match usage in functions that assign them to boolean
variables.

---

Nitpick comments:
In `@specifyweb/specify/migration_utils/update_schema_config.py`:
- Around line 266-268: Change the implicit Optional for the parameter named
"description" to an explicit union type by updating its annotation from
"description: str = None" to "description: str | None = None" (no runtime
change); locate the parameter in the function signature in
update_schema_config.py and update the annotation accordingly so static checkers
accept the explicit T | None form.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 74a3eac1-44ca-47c2-afb6-e9f3f54321cd

📥 Commits

Reviewing files that changed from the base of the PR and between a198a4c and 9864d17.

📒 Files selected for processing (5)
  • specifyweb/specify/management/commands/run_key_migration_functions.py
  • specifyweb/specify/migration_utils/sp7_schemaconfig.py
  • specifyweb/specify/migration_utils/update_schema_config.py
  • specifyweb/specify/migrations/0039_agent_fields_for_loan_and_gift.py
  • specifyweb/specify/models_utils/load_datamodel.py
💤 Files with no reviewable changes (2)
  • specifyweb/specify/migrations/0039_agent_fields_for_loan_and_gift.py
  • specifyweb/specify/migration_utils/sp7_schemaconfig.py

Comment thread specifyweb/specify/migration_utils/update_schema_config.py
@melton-jason melton-jason marked this pull request as ready for review April 29, 2026 16:01
@melton-jason melton-jason requested a review from acwhite211 April 29, 2026 16:19
@melton-jason
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@specifyweb/specify/migration_utils/update_schema_config.py`:
- Around line 264-269: Change the implicit optional annotation for the parameter
named description in the function signature (the one that currently reads
"description: str = None" in update_schema_config.py) to an explicit Optional
type (e.g., "description: Optional[str] = None"); add or update the typing
import (from typing import Optional) if not already present so the code
type-checks and RUF013 is satisfied.
- Around line 854-877: The Splocalecontaineritem.objects.get_or_create call can
raise MultipleObjectsReturned for legacy duplicates; replace it with a safe
lookup using Splocalecontaineritem.objects.filter(name=COT_FIELD_NAME,
container=container).order_by("id").first() and if that returns None, create a
new Splocalecontaineritem with the same defaults
(picklistname=COT_PICKLIST_NAME, type='ManyToOne', isrequired=True) so
container_item holds either the first existing record or the newly created
instance; leave the subsequent Splocaleitemstr.get_or_create calls as-is but
ensure they use the resolved container_item instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b650042-fe04-4561-98a5-e0eda27e9917

📥 Commits

Reviewing files that changed from the base of the PR and between 9864d17 and 822d07e.

📒 Files selected for processing (1)
  • specifyweb/specify/migration_utils/update_schema_config.py

Comment thread specifyweb/specify/migration_utils/update_schema_config.py
Comment thread specifyweb/specify/migration_utils/update_schema_config.py Outdated
@acwhite211
Copy link
Copy Markdown
Member

Nice fix for that duplicate CO->COT issue, that's the only issue I found so far. Will finish testing soon 👍

@melton-jason
Copy link
Copy Markdown
Contributor

melton-jason commented Apr 29, 2026

I've been going through non-schema related run_key_migration_functions, and have found and fixed another potential bug.
I've included the fix on this branch, but can split it to another branch and PR if desired.

The bug was related to detecting and applying the default TectonicUnit Tree structure.

Previously, if the user had made any changes to the tree structure, Specify would create duplicate ranks as the "expected" ranks would not be returned by the get_or_create (essentially the same underlying cause as #7988).

root, _ = TectonicUnitTreeDefItem.objects.get_or_create(
name="Root",
title="Root",
rankid=0,
parent=None,
treedef=tectonic_tree_def,
isenforced=True
)
superstructure, _ = TectonicUnitTreeDefItem.objects.get_or_create(
name="Superstructure",
title="Superstructure",
rankid=10,
parent=root,
treedef=tectonic_tree_def,
)
tectonic_domain, _ = TectonicUnitTreeDefItem.objects.get_or_create(
name="Tectonic Domain",
title="Tectonic Domain",
rankid=20,
parent=superstructure,
treedef=tectonic_tree_def,
)
tectonic_subdomain, _ = TectonicUnitTreeDefItem.objects.get_or_create(
name="Tectonic Subdomain",
title="Tectonic Subdomain",
rankid=30,
parent=tectonic_domain,
treedef=tectonic_tree_def,
)
tectonic_unit, _ = TectonicUnitTreeDefItem.objects.get_or_create(
name="Tectonic Unit",
title="Tectonic Unit",
rankid=40,
parent=tectonic_subdomain,
treedef=tectonic_tree_def,
)
tectonic_subunit, _ = TectonicUnitTreeDefItem.objects.get_or_create(
name="Tectonic Subunit",
title="Tectonic Subunit",
rankid=50,
parent=tectonic_unit,
treedef=tectonic_tree_def,
)

My fix here (4a1246f) was to broaden the matching behavior on the root rank (the user could have modified the name, title, and/or whether it was enforced), and skip checking or creating further TectonicUnit ranks if a root rank for TectonicUnit already exists (at that point we can assume these defaults had already been applied in the past):

root, root_created = TectonicUnitTreeDefItem.objects.get_or_create(
rankid=0,
parent=None,
treedef=tectonic_tree_def,
defaults={
"name": "Root",
"title": "Root",
"isenforced": True
}
)
# The root rank already exists in some capacity in the Discipline
# We can assume the user has made modifications to the tree at this
# point, so shouldn't go further with checking/creating lower ranks
if not root_created:
# BUG?: handle setting the tectonicunittreedef on the Discipline
# here? We can probably practically assume it's already set if the
# root node exists.
continue

@acwhite211
Copy link
Copy Markdown
Member

Tested on multiple databases. No modifications or new duplicates were created to the schema config table records.
Looking good 👍

Besides the TectonicUnit issue, the only other thing I can think of adding is a fix similar to this commit 8f15b93 for better deduplication, but I think we should probably just have a separate deduplication issue for 7.12.1

@melton-jason melton-jason merged commit 888aca8 into v7_12_0_5 Apr 29, 2026
15 checks passed
@melton-jason melton-jason deleted the issue-7988-2 branch April 29, 2026 18:21
@github-project-automation github-project-automation Bot moved this from Dev Attention Needed to ✅Done in General Tester Board Apr 29, 2026
@CarolineDenis CarolineDenis modified the milestones: 7.12.1, 7.12.0.5 May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅Done

Development

Successfully merging this pull request may close these issues.

4 participants