Skip to content

[change] Replace third-party JSONField with native models.JSONField a…#743

Open
HMTAhir327 wants to merge 4 commits intoopenwisp:masterfrom
HMTAhir327:change/replace-jsonfield-with-builtin
Open

[change] Replace third-party JSONField with native models.JSONField a…#743
HMTAhir327 wants to merge 4 commits intoopenwisp:masterfrom
HMTAhir327:change/replace-jsonfield-with-builtin

Conversation

@HMTAhir327
Copy link
Copy Markdown

…nd optimize indexing

Checklist

  • I have read the OpenWISP Contributing Guidelines.
  • I have manually tested the changes proposed in this pull request.
  • I have confirmed the database schema transformation (jsonb + GIN) via psql.
  • I have updated existing migrations to remove legacy dependencies.

Reference to Existing Issue

Closes ##673

Please open a new issue if there isn't an existing issue yet.

Description of Changes

This PR modernizes how we store and query telemetry data by moving from a deprecated third-party library to Django's native JSONField. This version ensures the database is actually optimized for production performance.

Key Improvements:
Faster Queries with GIN Indexes: Switched from standard B-tree indexes to GIN indexes for main_tags and extra_tags. This allows PostgreSQL to search inside JSON data significantly faster, which is essential for scaling telemetry.

Reliable Database Migration: Added a specific migration (0013) that uses custom SQL to safely convert old text columns into jsonb. This prevents the deployment crashes that happen when trying to change data types while old indexes are still attached.

Cleaner Codebase: Removed all remaining imports of the old jsonfield package from historical migrations to ensure the project doesn't break once the dependency is fully deleted.

Screenshot

Please include any relevant screenshots.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Replaces external jsonfield JSONField usages with Django's built-in models.JSONField across models and migrations (fields: params, main_tags, extra_tags), removes custom dump_kwargs/load_kwargs, adds GinIndex declarations for main_tags and extra_tags, adds migration 0013_migrate_to_jsonfield which on PostgreSQL casts text columns to jsonb, updates/drops/creates indexes and uses SeparateDatabaseAndState, adds a CI step that waits for InfluxDB readiness, and adds test/dev requirements (commitizen, prompt-toolkit exclusion, django-filter constraint).

Sequence Diagram(s)

sequenceDiagram
  participant Runner as Migration Runner
  participant Migration as 0013_migrate_to_jsonfield
  participant DB as PostgreSQL
  participant State as Django Model State

  Runner->>Migration: start migration
  Migration->>DB: DROP existing text indexes (if present)
  Migration->>DB: ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb
  Migration->>DB: ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb
  Migration->>DB: CREATE GIN INDEX on main_tags
  Migration->>DB: CREATE GIN INDEX on extra_tags
  Migration->>State: apply state_operations (fields -> models.JSONField)
  Runner->>Migration: finish
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective of the PR: replacing third-party JSONField with Django's native models.JSONField and optimizing indexing.
Description check ✅ Passed The description covers all required template sections with detailed explanations of changes, objectives, and testing confirmations, though screenshot section is unfilled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

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: 7

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

Inline comments:
In `@openwisp_monitoring/monitoring/base/models.py`:
- Around line 94-99: Remove the redundant B-tree index by deleting db_index=True
from the main_tags JSONField declaration (symbol: main_tags) so only the
GinIndex declared in Meta.indexes is used; then update the corresponding
migration 0004_metric_main_and_extra_tags.py to drop/remove the db_index on
main_tags (or regenerate the migration) and also update
tests/openwisp2/sample_monitoring/migrations/0001_initial.py to reflect the same
field definition (no db_index) so test DB schema and migrations remain
consistent. Ensure the GinIndex referenced in Meta.indexes remains unchanged.
- Around line 119-122: The model's Meta.indexes lists two GinIndex entries
(main_tags_gin_idx and extra_tags_gin_idx) but migration 0013 created those GIN
indexes via RunSQL without updating Django's migration state, so the
autodetector keeps emitting AddIndex migrations; update the third RunSQL in
migration 0013 to use SeparateDatabaseAndState (or add state_operations) that
include AddIndex operations for the same GinIndex instances (reflecting
Meta.indexes) so the migration state records the indexes and makemigrations
stops regenerating them.

In
`@openwisp_monitoring/monitoring/migrations/0004_metric_main_and_extra_tags.py`:
- Around line 13-31: Remove the unnecessary B-tree index creation by deleting
the db_index=True argument from the migrations.AddField that adds the
"main_tags" JSONField in migration 0004_metric_main_and_extra_tags.py (ensure
the AddField for model_name="metric", name="main_tags" keeps blank=True,
default=dict, verbose_name, etc. but no db_index). This prevents creating the
B-tree on fresh installs; migration 0013 already drops any existing B-tree and
creates the GIN index for existing databases.

In `@openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py`:
- Around line 14-19: The reverse_sql for the RunSQL that converts
monitoring_metric.main_tags and extra_tags back to text currently does not
recreate the original B-tree indexes (e.g.,
monitoring_metric_main_tags_2d8550ae) and the earlier DROP INDEX (RunSQL.noop)
prevents automatic restoration; update the migration by either adding reverse
SQL that recreates the dropped B-tree indexes after converting columns back to
text (recreate monitoring_metric_main_tags_* and monitoring_metric_extra_tags_*
with the original index definitions) or, if recreating is not possible, add a
clear inline comment in the migration near migrations.RunSQL explaining that
rollback will not restore those indexes and document the operational impact and
acceptance (including whether a table-scan/check on main_tags after rollback is
acceptable for SLA).
- Around line 20-24: The RunSQL call creates GIN indexes at DB level but doesn't
update Django's migration state, causing makemigrations to later emit AddIndex
for the same indexes; fix by importing GinIndex from
django.contrib.postgres.indexes and adding a state_operations parameter to that
migrations.RunSQL containing migrations.AddIndex entries that mirror the DB
indexes (e.g. migrations.AddIndex(model_name='metric',
index=GinIndex(fields=['main_tags'], name='main_tags_gin_idx')) and
migrations.AddIndex(model_name='metric', index=GinIndex(fields=['extra_tags'],
name='extra_tags_gin_idx'))), leaving the sql and reverse_sql as-is so DB and
Django state stay synchronized.
- Around line 9-24: The migration uses PostgreSQL-only SQL in migrations.RunSQL
blocks (ALTER ... TYPE jsonb, :: cast, GIN indexes) which will fail on SQLite;
modify the migration so the PostgreSQL-specific SQL is only executed when
schema_editor.connection.vendor == 'postgresql' — for example replace each
RunSQL with a RunPython that checks schema_editor.connection.vendor and executes
the SQL only for 'postgresql', or use SeparateDatabaseAndState to include the
Postgres RunSQL operations in the database-only part while providing a noop
state change for other backends; update the reverse_sql similarly or provide
noop reversals for non-Postgres.

In `@tests/openwisp2/sample_monitoring/migrations/0001_initial.py`:
- Around line 104-119: The migration sets db_index=True on the JSONField
"main_tags", which creates a B-tree index in tests and diverges from the
intended production GIN-only schema; remove db_index=True from the "main_tags"
field definition and, if you need GIN behavior in tests, add a GinIndex for the
JSON key on the migration's options["indexes"] (use
django.contrib.postgres.indexes.GinIndex targeting "main_tags" and/or
"extra_tags"); update the migration 0001_initial definition accordingly and
ensure the GinIndex import is added to the migration file.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 331fca2 and 399f06c.

📒 Files selected for processing (7)
  • openwisp_monitoring/check/base/models.py
  • openwisp_monitoring/check/migrations/0001_initial_squashed_0002_check_unique_together.py
  • openwisp_monitoring/monitoring/base/models.py
  • openwisp_monitoring/monitoring/migrations/0004_metric_main_and_extra_tags.py
  • openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py
  • tests/openwisp2/sample_check/migrations/0001_initial.py
  • tests/openwisp2/sample_monitoring/migrations/0001_initial.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-21T18:44:28.852Z
Learnt from: dee077
Repo: openwisp/openwisp-monitoring PR: 738
File: openwisp_monitoring/device/api/views.py:263-281
Timestamp: 2026-02-21T18:44:28.852Z
Learning: In openwisp-monitoring, MonitoringIndoorCoordinatesList inherits organization scoping from the parent IndoorCoordinatesList (from openwisp-controller), which uses FilterByParentManaged mixin and filters by location_id in get_queryset(). The child class only overrides the queryset attribute to add monitoring-specific select_related fields; this pattern is safe as long as get_queryset() from the parent is not bypassed. During reviews, verify that MonitoringIndoorCoordinatesList continues to rely on the parent's get_queryset() and that any added select_related fields in the child do not alter the parent's filtering logic.

Applied to files:

  • openwisp_monitoring/check/migrations/0001_initial_squashed_0002_check_unique_together.py
  • openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py
  • openwisp_monitoring/monitoring/base/models.py
  • openwisp_monitoring/check/base/models.py
  • openwisp_monitoring/monitoring/migrations/0004_metric_main_and_extra_tags.py
🧬 Code graph analysis (1)
openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py (2)
openwisp_monitoring/monitoring/migrations/0004_metric_main_and_extra_tags.py (1)
  • Migration (6-38)
tests/openwisp2/sample_monitoring/migrations/0001_initial.py (1)
  • Migration (21-269)
🔇 Additional comments (3)
openwisp_monitoring/check/base/models.py (1)

37-42: LGTM — clean migration to models.JSONField.

Removing dump_kwargs/load_kwargs is correct; the built-in field handles serialisation internally and the public field contract (blank=True, default=dict) is preserved.

tests/openwisp2/sample_check/migrations/0001_initial.py (1)

73-81: LGTM.

Historical migration correctly updated to models.JSONField, removing the now-unnecessary third-party import.

openwisp_monitoring/check/migrations/0001_initial_squashed_0002_check_unique_together.py (1)

67-74: LGTM.

Squashed migration updated cleanly to models.JSONField, necessary to allow removal of the jsonfield package dependency.

Comment on lines +94 to 99
main_tags = models.JSONField(
_("main tags"),
default=dict,
blank=True,
load_kwargs={"object_pairs_hook": OrderedDict},
dump_kwargs={"indent": 4},
db_index=True,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove db_index=True from main_tags — redundant alongside GinIndex.

Django's own documentation states: "Index and Field.db_index both create a B-tree index, which isn't particularly helpful when querying JSONField. On PostgreSQL only, you can use GinIndex that is better suited." Keeping db_index=True here creates a redundant, less-useful B-tree index on top of the GIN index declared in Meta.indexes, wasting both index storage and write overhead on every row mutation.

Remove db_index=True from main_tags; the GIN index in Meta.indexes (lines 119–122) is sufficient. Also update migration 0004_metric_main_and_extra_tags.py and tests/openwisp2/sample_monitoring/migrations/0001_initial.py consistently.

🔧 Proposed fix
-    main_tags = models.JSONField(
-        _("main tags"),
-        default=dict,
-        blank=True,
-        db_index=True,
-    )
+    main_tags = models.JSONField(
+        _("main tags"),
+        default=dict,
+        blank=True,
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/base/models.py` around lines 94 - 99, Remove
the redundant B-tree index by deleting db_index=True from the main_tags
JSONField declaration (symbol: main_tags) so only the GinIndex declared in
Meta.indexes is used; then update the corresponding migration
0004_metric_main_and_extra_tags.py to drop/remove the db_index on main_tags (or
regenerate the migration) and also update
tests/openwisp2/sample_monitoring/migrations/0001_initial.py to reflect the same
field definition (no db_index) so test DB schema and migrations remain
consistent. Ensure the GinIndex referenced in Meta.indexes remains unchanged.

Comment on lines +119 to +122
indexes = [
GinIndex(fields=["main_tags"], name="main_tags_gin_idx"),
GinIndex(fields=["extra_tags"], name="extra_tags_gin_idx"),
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "GinIndex\|AddIndex" openwisp_monitoring/monitoring/migrations/

Repository: openwisp/openwisp-monitoring

Length of output: 54


🏁 Script executed:

# Find all migrations in the monitoring app
fd -t f "\.py$" openwisp_monitoring/monitoring/migrations/ | head -20

# Also check if the model file exists and examine its Meta.indexes
fd -t f "models\.py" openwisp_monitoring/monitoring/ | head -10

# Check for RunSQL in migration files more broadly
rg -l "RunSQL" openwisp_monitoring/monitoring/migrations/

Repository: openwisp/openwisp-monitoring

Length of output: 1495


🏁 Script executed:

# Examine migration 0013
cat -n openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py

# Examine the model file for Meta.indexes
sed -n '100,150p' openwisp_monitoring/monitoring/base/models.py | cat -n

Repository: openwisp/openwisp-monitoring

Length of output: 3585


Meta.indexes GIN entries are not reflected in migration state → makemigrations will generate spurious AddIndex migrations.

Migration 0013 creates the GIN indexes via raw RunSQL without state_operations, so Django's migration state after applying 0013 has no record of main_tags_gin_idx or extra_tags_gin_idx. The migration autodetector compares the model's Meta.indexes (which lists both GinIndex entries) against this state and will always detect a "pending change", regenerating AddIndex migrations on every makemigrations run. Applying those generated migrations against a database that already has the indexes from migration 0013 will fail with a "relation already exists" error.

Per Django documentation: "Much like RunSQL, ensure that if you change schema inside here you're either doing it outside the scope of the Django model system (e.g. triggers) or that you use SeparateDatabaseAndState to add in operations that will reflect your changes to the model state – otherwise, the versioned ORM and the autodetector will stop working correctly."

Fix: add state_operations to the third RunSQL in migration 0013 to include the corresponding AddIndex operations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/base/models.py` around lines 119 - 122, The
model's Meta.indexes lists two GinIndex entries (main_tags_gin_idx and
extra_tags_gin_idx) but migration 0013 created those GIN indexes via RunSQL
without updating Django's migration state, so the autodetector keeps emitting
AddIndex migrations; update the third RunSQL in migration 0013 to use
SeparateDatabaseAndState (or add state_operations) that include AddIndex
operations for the same GinIndex instances (reflecting Meta.indexes) so the
migration state records the indexes and makemigrations stops regenerating them.

Comment on lines 13 to 31
migrations.AddField(
model_name="metric",
name="main_tags",
field=jsonfield.fields.JSONField(
field=models.JSONField(
blank=True,
db_index=True,
default=dict,
dump_kwargs={"indent": 4},
load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="main tags",
),
),
migrations.AddField(
model_name="metric",
name="extra_tags",
field=jsonfield.fields.JSONField(
field=models.JSONField(
blank=True,
default=dict,
dump_kwargs={"indent": 4},
load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="extra tags",
),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

main_tags's db_index=True creates a wasteful B-tree that migration 0013 immediately drops on fresh installs.

On a fresh PostgreSQL installation, this migration creates a B-tree index on main_tags, then migration 0013 drops it and creates a GIN index instead. Django docs note that db_index creates a B-tree index which "isn't particularly helpful when querying JSONField." The B-tree creation and drop is unnecessary churn for fresh deployments.

Remove db_index=True from both main_tags definitions here (consistent with removing it from the model) so migration 0004 no longer creates the B-tree at all; migration 0013's DROP INDEX IF EXISTS already handles the case where the old B-tree exists on existing installations.

🔧 Proposed fix
         migrations.AddField(
             model_name="metric",
             name="main_tags",
             field=models.JSONField(
                 blank=True,
-                db_index=True,
                 default=dict,
                 verbose_name="main tags",
             ),
         ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0004_metric_main_and_extra_tags.py`
around lines 13 - 31, Remove the unnecessary B-tree index creation by deleting
the db_index=True argument from the migrations.AddField that adds the
"main_tags" JSONField in migration 0004_metric_main_and_extra_tags.py (ensure
the AddField for model_name="metric", name="main_tags" keeps blank=True,
default=dict, verbose_name, etc. but no db_index). This prevents creating the
B-tree on fresh installs; migration 0013 already drops any existing B-tree and
creates the GIN index for existing databases.

Comment thread openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py Outdated
Comment on lines +14 to +19
migrations.RunSQL(
sql='ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb; '
'ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb;',
reverse_sql='ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE text; '
'ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE text;'
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

reverse_sql for column type conversion does not restore the dropped B-tree indexes — incomplete rollback.

When unapplying this migration (rolling back to 0012), the columns revert to text but the previously-existing B-tree indexes (monitoring_metric_main_tags_2d8550ae etc.) are not recreated, leaving the table without any index on main_tags. The reverse_sql=migrations.RunSQL.noop on the first operation (the DROP INDEX) makes this impossible to automatically restore.

While perfect rollback isn't always achievable, at minimum add a comment in the migration acknowledging this, and consider whether a check=False table-scan on main_tags after rollback is acceptable for your SLA.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py` around
lines 14 - 19, The reverse_sql for the RunSQL that converts
monitoring_metric.main_tags and extra_tags back to text currently does not
recreate the original B-tree indexes (e.g.,
monitoring_metric_main_tags_2d8550ae) and the earlier DROP INDEX (RunSQL.noop)
prevents automatic restoration; update the migration by either adding reverse
SQL that recreates the dropped B-tree indexes after converting columns back to
text (recreate monitoring_metric_main_tags_* and monitoring_metric_extra_tags_*
with the original index definitions) or, if recreating is not possible, add a
clear inline comment in the migration near migrations.RunSQL explaining that
rollback will not restore those indexes and document the operational impact and
acceptance (including whether a table-scan/check on main_tags after rollback is
acceptable for SLA).

Comment on lines +20 to +24
migrations.RunSQL(
sql='CREATE INDEX IF NOT EXISTS main_tags_gin_idx ON monitoring_metric USING gin (main_tags); '
'CREATE INDEX IF NOT EXISTS extra_tags_gin_idx ON monitoring_metric USING gin (extra_tags);',
reverse_sql='DROP INDEX IF EXISTS main_tags_gin_idx; DROP INDEX IF EXISTS extra_tags_gin_idx;'
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing state_operations on the GIN index RunSQL — will generate spurious AddIndex migrations every time makemigrations runs.

The raw SQL creates main_tags_gin_idx and extra_tags_gin_idx in the database, but nothing in this RunSQL updates Django's migration state. Because AbstractMetric.Meta.indexes declares both GinIndex entries and the migration state has no record of them, the Django documentation warns: "If the actual state of the database and Django's view of the state get out of sync, this can break the migration framework, even leading to data loss. It's worth exercising caution and checking your database and state operations carefully. You can use sqlmigrate and dbshell to check your database operations. You can use makemigrations, especially with --dry-run, to check your state operations."

Every subsequent makemigrations run will detect the unrecorded GIN indexes and generate a new migration with migrations.AddIndex. Applying that migration against the live database (which already has the indexes from this raw SQL) will fail with "relation already exists".

Fix: Add state_operations to the third RunSQL (note you also need to import GinIndex in this migration file):

🔧 Proposed fix
+from django.contrib.postgres.indexes import GinIndex
 from django.db import migrations
 
 class Migration(migrations.Migration):
     dependencies = [
         ('monitoring', '0012_migrate_signal_metrics'),
     ]
 
     operations = [
         migrations.RunSQL(
             sql='DROP INDEX IF EXISTS monitoring_metric_main_tags_2d8550ae_like; '
                 'DROP INDEX IF EXISTS monitoring_metric_main_tags_2d8550ae;',
             reverse_sql=migrations.RunSQL.noop
         ),
         migrations.RunSQL(
             sql='ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb; '
                 'ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb;',
             reverse_sql='ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE text; '
                         'ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE text;'
         ),
         migrations.RunSQL(
             sql='CREATE INDEX IF NOT EXISTS main_tags_gin_idx ON monitoring_metric USING gin (main_tags); '
                 'CREATE INDEX IF NOT EXISTS extra_tags_gin_idx ON monitoring_metric USING gin (extra_tags);',
             reverse_sql='DROP INDEX IF EXISTS main_tags_gin_idx; DROP INDEX IF EXISTS extra_tags_gin_idx;',
+            state_operations=[
+                migrations.AddIndex(
+                    model_name='metric',
+                    index=GinIndex(fields=['main_tags'], name='main_tags_gin_idx'),
+                ),
+                migrations.AddIndex(
+                    model_name='metric',
+                    index=GinIndex(fields=['extra_tags'], name='extra_tags_gin_idx'),
+                ),
+            ],
         ),
     ]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_auto_20260224_0730.py` around
lines 20 - 24, The RunSQL call creates GIN indexes at DB level but doesn't
update Django's migration state, causing makemigrations to later emit AddIndex
for the same indexes; fix by importing GinIndex from
django.contrib.postgres.indexes and adding a state_operations parameter to that
migrations.RunSQL containing migrations.AddIndex entries that mirror the DB
indexes (e.g. migrations.AddIndex(model_name='metric',
index=GinIndex(fields=['main_tags'], name='main_tags_gin_idx')) and
migrations.AddIndex(model_name='metric', index=GinIndex(fields=['extra_tags'],
name='extra_tags_gin_idx'))), leaving the sql and reverse_sql as-is so DB and
Django state stay synchronized.

Comment on lines 104 to 119
"main_tags",
jsonfield.fields.JSONField(
models.JSONField(
blank=True,
db_index=True,
default=dict,
dump_kwargs={"indent": 4},
load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="main tags",
),
),
(
"extra_tags",
jsonfield.fields.JSONField(
models.JSONField(
blank=True,
default=dict,
dump_kwargs={"indent": 4},
load_kwargs={"object_pairs_hook": collections.OrderedDict},
verbose_name="extra tags",
),
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

db_index=True inconsistency with the GIN-only intent.

main_tags keeps db_index=True here, which creates a B-tree index in the test database but no GIN index (there is no migration 0013 equivalent for the test app). The test schema diverges from the production schema target (GIN, no B-tree). Consider removing db_index=True and, if GIN index behaviour needs to be covered, adding it to the test migration's options["indexes"].

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/openwisp2/sample_monitoring/migrations/0001_initial.py` around lines
104 - 119, The migration sets db_index=True on the JSONField "main_tags", which
creates a B-tree index in tests and diverges from the intended production
GIN-only schema; remove db_index=True from the "main_tags" field definition and,
if you need GIN behavior in tests, add a GinIndex for the JSON key on the
migration's options["indexes"] (use django.contrib.postgres.indexes.GinIndex
targeting "main_tags" and/or "extra_tags"); update the migration 0001_initial
definition accordingly and ensure the GinIndex import is added to the migration
file.

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 `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`:
- Around line 9-26: The migration currently runs PostgreSQL-specific migrations
via migrations.RunSQL (the operations list containing migrations.RunSQL that
ALTER TABLE to jsonb, create GIN indexes, and DROP INDEX) which will fail on
SpatiaLite; update the migration to only execute those RunSQL statements when
connection.vendor == 'postgresql' by wrapping them in a migrations.RunPython
that checks connection.vendor and runs the SQL via schema_editor.execute (or use
migrations.SeparateDatabaseAndState to keep state changes while skipping SQL on
non-postgres). Specifically, locate the migrations.RunSQL entries for ALTER
TABLE ... TYPE jsonb, CREATE INDEX ... USING gin and the DROP INDEX IF EXISTS
statements and replace them with a Postgres-only guard using connection.vendor
(or SeparateDatabaseAndState) so tests using SpatiaLite/SQLite skip these
Postgres-specific statements.
- Around line 21-25: The migration 0013_migrate_to_jsonfield.py currently
creates main_tags_gin_idx and extra_tags_gin_idx via a migrations.RunSQL block
while the Metric model's Meta.indexes also declares GinIndex entries for those
same names (main_tags_gin_idx, extra_tags_gin_idx), causing dual management;
pick one approach and make the code match it: either remove the RunSQL block
from 0013_migrate_to_jsonfield.py if you want
Django/Metric.Meta.indexes/GinIndex to be the single source of truth (then
re-run makemigrations to let Django manage the indexes), or remove the GinIndex
entries from Metric.Meta.indexes (and keep the RunSQL) so the raw SQL migration
is the source of truth — update the code accordingly and ensure the names
(main_tags_gin_idx, extra_tags_gin_idx) no longer appear in both places.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 399f06c and 4617aea.

📒 Files selected for processing (1)
  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.1.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=4.2.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-21T18:44:28.852Z
Learnt from: dee077
Repo: openwisp/openwisp-monitoring PR: 738
File: openwisp_monitoring/device/api/views.py:263-281
Timestamp: 2026-02-21T18:44:28.852Z
Learning: In openwisp-monitoring, MonitoringIndoorCoordinatesList inherits organization scoping from the parent IndoorCoordinatesList (from openwisp-controller), which uses FilterByParentManaged mixin and filters by location_id in get_queryset(). The child class only overrides the queryset attribute to add monitoring-specific select_related fields; this pattern is safe as long as get_queryset() from the parent is not bypassed. During reviews, verify that MonitoringIndoorCoordinatesList continues to rely on the parent's get_queryset() and that any added select_related fields in the child do not alter the parent's filtering logic.

Applied to files:

  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
🔇 Additional comments (2)
openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py (2)

10-14: The original review's primary claim is factually incorrect. Migration 0004 definitively shows that extra_tags was created without db_index=True, so it never had B-tree indexes. Only main_tags had db_index=True (line 18 of migration 0004), making Operation 1's removal of only main_tags indexes correct.

The GIN indexes created in Operation 3 have names (main_tags_gin_idx and extra_tags_gin_idx) that match the model's Meta.indexes declarations, which is the correct approach and prevents duplicate index creation by Django.

The migration is properly structured and requires no changes.

Likely an incorrect or invalid review comment.


15-20: The bare ::jsonb cast is safe—all existing data in these columns contains valid JSON objects (specifically '{}' from the default=dict setting), not empty strings or malformed data. Since migration 0004 defined these fields with default=dict and blank=True, the database stores '{}' (valid JSON), not empty strings. There is no evidence of empty strings in the data.

The reverse SQL could optionally include an explicit USING main_tags::text clause for consistency, but this is minor and not critical—PostgreSQL handles the implicit jsonb-to-text cast reliably.

Likely an incorrect or invalid review comment.

Comment thread openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py Outdated
Comment on lines +21 to +25
migrations.RunSQL(
sql="CREATE INDEX IF NOT EXISTS main_tags_gin_idx ON monitoring_metric USING gin (main_tags); "
"CREATE INDEX IF NOT EXISTS extra_tags_gin_idx ON monitoring_metric USING gin (extra_tags);",
reverse_sql="DROP INDEX IF EXISTS main_tags_gin_idx; DROP INDEX IF EXISTS extra_tags_gin_idx;",
),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n 'GinIndex' --type=py -A5 -B5

Repository: openwisp/openwisp-monitoring

Length of output: 2113


🏁 Script executed:

rg -n 'main_tags|extra_tags' --type=py -g '**/monitoring/migrations/*.py' -A3 -B3

Repository: openwisp/openwisp-monitoring

Length of output: 5475


🏁 Script executed:

fd -e py -p '*/models*' | head -20

Repository: openwisp/openwisp-monitoring

Length of output: 360


🏁 Script executed:

# Check the migration file more thoroughly to understand the complete context
cat -n openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py

Repository: openwisp/openwisp-monitoring

Length of output: 1459


🏁 Script executed:

# Check if there are any subsequent migrations that might interact with these indexes
fd -type f -name '*.py' -path '*/monitoring/migrations/*' | sort | tail -10

Repository: openwisp/openwisp-monitoring

Length of output: 242


🏁 Script executed:

# Check the model definition to confirm Meta.indexes is the source
sed -n '110,130p' openwisp_monitoring/monitoring/base/models.py

Repository: openwisp/openwisp-monitoring

Length of output: 663


Remove the RunSQL index creation or remove the GinIndex declarations from Meta.indexes—both cannot coexist.

The migration creates main_tags_gin_idx and extra_tags_gin_idx via RunSQL, but the Metric model's Meta.indexes also declares identical GinIndex objects with the same names. This creates dual management of the same indexes: the migration manages them explicitly via SQL, while Django's ORM expects to manage indexes declared in Meta.indexes through its own migration system.

Although the IF NOT EXISTS clause prevents immediate errors, this inconsistency will cause problems:

  • When future migrations try to drop or modify these indexes, Django may not recognize them as ORM-managed indexes, leading to unexpected behavior.
  • It's unclear which source (migration or ORM) should be the source of truth.

Choose one approach:

  1. Keep the RunSQL and remove the GinIndex declarations from Meta.indexes, or
  2. Keep the Meta.indexes declarations and remove this RunSQL block, letting Django auto-generate the migration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 21 - 25, The migration 0013_migrate_to_jsonfield.py currently
creates main_tags_gin_idx and extra_tags_gin_idx via a migrations.RunSQL block
while the Metric model's Meta.indexes also declares GinIndex entries for those
same names (main_tags_gin_idx, extra_tags_gin_idx), causing dual management;
pick one approach and make the code match it: either remove the RunSQL block
from 0013_migrate_to_jsonfield.py if you want
Django/Metric.Meta.indexes/GinIndex to be the single source of truth (then
re-run makemigrations to let Django manage the indexes), or remove the GinIndex
entries from Metric.Meta.indexes (and keep the RunSQL) so the raw SQL migration
is the source of truth — update the code accordingly and ensure the names
(main_tags_gin_idx, extra_tags_gin_idx) no longer appear in both places.

@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch from 4617aea to dd5bf29 Compare February 24, 2026 15:52
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

♻️ Duplicate comments (2)
openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py (2)

38-59: Good use of SeparateDatabaseAndState with vendor-guarded RunPython.

The pattern correctly keeps Django's model state in sync via state_operations while performing the actual PostgreSQL-specific DDL through database_operations. The vendor check ensures non-PostgreSQL backends (e.g., SpatiaLite in tests) are not affected.

However, the dual management concern for GIN indexes (created here via raw SQL and declared in Meta.indexes on the model) is still outstanding — see prior review comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 38 - 59, The migration uses SeparateDatabaseAndState with database
operations that create GIN indexes via raw SQL in
migrate_to_jsonb/rollback_jsonb while the Metric model also declares those GIN
indexes in its Meta.indexes, causing duplicate-index creation/conflicts; remove
the raw SQL GIN creation/drop from the database_operations (or conversely remove
the index entries from Metric.Meta.indexes) so only one mechanism manages the
GIN indexes, and update migrate_to_jsonb and rollback_jsonb to no longer
create/drop the same GIN indexes (or to only create/drop them when Meta.indexes
is not declaring them) to avoid double-management.

16-19: Dual GIN index management: raw SQL CREATE INDEX here conflicts with GinIndex in Meta.indexes.

The CREATE INDEX IF NOT EXISTS main_tags_gin_idx / extra_tags_gin_idx here will coexist with the identical GinIndex declarations on the model's Meta.indexes. While IF NOT EXISTS prevents an immediate error, Django's migration framework will consider itself the owner of the Meta.indexes-declared indexes, leading to confusion in future auto-generated migrations (e.g., RemoveIndex or RenameIndex may target an index it didn't create).

Pick one owner: either keep raw SQL here and remove the GinIndex from Meta, or remove this CREATE INDEX block and let makemigrations generate the index migration from Meta.indexes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 16 - 19, Remove the raw SQL index creation: delete the
cursor.execute(...) block that runs "CREATE INDEX IF NOT EXISTS
main_tags_gin_idx ..." in the migration and let the model-level GinIndex
declarations in Meta.indexes own the indexes; this ensures Django's migration
framework (via the GinIndex entries) will create/manage the GIN indexes instead
of having duplicate raw-SQL-created indexes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`:
- Around line 22-30: The rollback_jsonb function currently drops the GIN indexes
and converts columns back to text but does not recreate the original B-tree
indexes removed by migrate_to_jsonb; update rollback_jsonb to, after altering
main_tags and extra_tags back to text, execute CREATE INDEX statements to
re-create the original B-tree indexes (specifically
monitoring_metric_main_tags_2d8550ae_like and
monitoring_metric_main_tags_2d8550ae and any equivalent extra_tags indexes that
migrate_to_jsonb dropped), ensuring index names and types match the originals
and only running on PostgreSQL (same vendor check) so the DB returns to its
pre-migration state.
- Around line 12-15: The migration's SQL in cursor.execute alters
monitoring_metric columns main_tags and extra_tags using a direct text→jsonb
cast which will fail for empty strings; change the USING expressions to guard
empty strings (e.g. use NULLIF(main_tags, '')::jsonb and NULLIF(extra_tags,
'')::jsonb) so empty strings become NULL before casting (update the SQL string
inside the cursor.execute call that performs ALTER TABLE monitoring_metric ALTER
COLUMN main_tags and extra_tags).

---

Duplicate comments:
In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`:
- Around line 38-59: The migration uses SeparateDatabaseAndState with database
operations that create GIN indexes via raw SQL in
migrate_to_jsonb/rollback_jsonb while the Metric model also declares those GIN
indexes in its Meta.indexes, causing duplicate-index creation/conflicts; remove
the raw SQL GIN creation/drop from the database_operations (or conversely remove
the index entries from Metric.Meta.indexes) so only one mechanism manages the
GIN indexes, and update migrate_to_jsonb and rollback_jsonb to no longer
create/drop the same GIN indexes (or to only create/drop them when Meta.indexes
is not declaring them) to avoid double-management.
- Around line 16-19: Remove the raw SQL index creation: delete the
cursor.execute(...) block that runs "CREATE INDEX IF NOT EXISTS
main_tags_gin_idx ..." in the migration and let the model-level GinIndex
declarations in Meta.indexes own the indexes; this ensures Django's migration
framework (via the GinIndex entries) will create/manage the GIN indexes instead
of having duplicate raw-SQL-created indexes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4617aea and dd5bf29.

📒 Files selected for processing (1)
  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.11 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=5.1.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.1.0
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-21T18:44:28.852Z
Learnt from: dee077
Repo: openwisp/openwisp-monitoring PR: 738
File: openwisp_monitoring/device/api/views.py:263-281
Timestamp: 2026-02-21T18:44:28.852Z
Learning: In openwisp-monitoring, MonitoringIndoorCoordinatesList inherits organization scoping from the parent IndoorCoordinatesList (from openwisp-controller), which uses FilterByParentManaged mixin and filters by location_id in get_queryset(). The child class only overrides the queryset attribute to add monitoring-specific select_related fields; this pattern is safe as long as get_queryset() from the parent is not bypassed. During reviews, verify that MonitoringIndoorCoordinatesList continues to rely on the parent's get_queryset() and that any added select_related fields in the child do not alter the parent's filtering logic.

Applied to files:

  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
🔇 Additional comments (1)
openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py (1)

8-14: The migration correctly handles index drops — extra_tags never had B-tree indexes.

Migration 0004 added main_tags with db_index=True but extra_tags without it. Only main_tags has auto-generated B-tree indexes (the ones with hash 2d8550ae_like and 2d8550ae), which the migration correctly drops. Since extra_tags never had db_index=True, no B-tree indexes exist for it, so the ALTER COLUMN to jsonb is safe. The migration logic is sound.

Likely an incorrect or invalid review comment.

Comment on lines +12 to +15
cursor.execute(
"ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb; "
"ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb;"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Unsafe cast: empty-string values will crash the text → jsonb conversion.

If any row has main_tags = '' or extra_tags = '' (possible since blank=True), ''::jsonb raises invalid input syntax for type json. Use a guarded cast:

Proposed fix
         cursor.execute(
-            "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb; "
-            "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb;"
+            "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb "
+            "USING CASE WHEN main_tags IS NULL THEN NULL "
+            "WHEN main_tags = '' THEN '{}'::jsonb "
+            "ELSE main_tags::jsonb END; "
+            "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb "
+            "USING CASE WHEN extra_tags IS NULL THEN NULL "
+            "WHEN extra_tags = '' THEN '{}'::jsonb "
+            "ELSE extra_tags::jsonb END;"
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 12 - 15, The migration's SQL in cursor.execute alters
monitoring_metric columns main_tags and extra_tags using a direct text→jsonb
cast which will fail for empty strings; change the USING expressions to guard
empty strings (e.g. use NULLIF(main_tags, '')::jsonb and NULLIF(extra_tags,
'')::jsonb) so empty strings become NULL before casting (update the SQL string
inside the cursor.execute call that performs ALTER TABLE monitoring_metric ALTER
COLUMN main_tags and extra_tags).

Comment on lines +22 to +30
def rollback_jsonb(apps, schema_editor):
if schema_editor.connection.vendor != "postgresql":
return
with schema_editor.connection.cursor() as cursor:
cursor.execute("DROP INDEX IF EXISTS main_tags_gin_idx; DROP INDEX IF EXISTS extra_tags_gin_idx;")
cursor.execute(
"ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE text; "
"ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE text;"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rollback does not recreate the original B-tree indexes dropped by the forward migration.

migrate_to_jsonb drops monitoring_metric_main_tags_2d8550ae_like and monitoring_metric_main_tags_2d8550ae, but rollback_jsonb only drops the GIN indexes and converts back to text — it never restores the B-tree indexes. After a rollback the main_tags column (and extra_tags, if applicable) would lack the indexes that existed before this migration ran.

Consider adding CREATE INDEX statements in the rollback to restore the original state, or document that a full re-migration of 0004 onward is needed after rollback.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 22 - 30, The rollback_jsonb function currently drops the GIN
indexes and converts columns back to text but does not recreate the original
B-tree indexes removed by migrate_to_jsonb; update rollback_jsonb to, after
altering main_tags and extra_tags back to text, execute CREATE INDEX statements
to re-create the original B-tree indexes (specifically
monitoring_metric_main_tags_2d8550ae_like and
monitoring_metric_main_tags_2d8550ae and any equivalent extra_tags indexes that
migrate_to_jsonb dropped), ensuring index names and types match the originals
and only running on PostgreSQL (same vendor check) so the DB returns to its
pre-migration state.

@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch from dd5bf29 to 2012a75 Compare February 24, 2026 16:15
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: 3

♻️ Duplicate comments (3)
openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py (3)

12-15: ⚠️ Potential issue | 🔴 Critical

Unsafe text → jsonb cast on empty strings will crash the migration.

blank=True on the legacy text field means rows can have main_tags = '' or extra_tags = ''. ''::jsonb raises invalid input syntax for type json on PostgreSQL, aborting the entire migration. Apply a guarded CASE expression as previously proposed:

🐛 Proposed fix
         cursor.execute(
-            "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb; "
-            "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb;"
+            "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb "
+            "USING CASE WHEN main_tags IS NULL THEN NULL "
+            "WHEN main_tags = '' THEN '{}'::jsonb "
+            "ELSE main_tags::jsonb END; "
+            "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb "
+            "USING CASE WHEN extra_tags IS NULL THEN NULL "
+            "WHEN extra_tags = '' THEN '{}'::jsonb "
+            "ELSE extra_tags::jsonb END;"
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 12 - 15, The ALTER TABLE SQL in
migrations/0013_migrate_to_jsonfield.py uses unsafe casts like main_tags::jsonb
and extra_tags::jsonb which will fail on empty strings; update the string in
cursor.execute to use a guarded CASE/trim expression for both columns (e.g., in
the USING clause, replace main_tags::jsonb with a CASE that treats empty/blank
main_tags as an empty JSON array/object and otherwise casts main_tags to jsonb,
and do the same for extra_tags) so the migration skips or converts blank values
instead of crashing.

16-19: ⚠️ Potential issue | 🟠 Major

Verify no dual management of main_tags_gin_idx / extra_tags_gin_idx.

If Metric.Meta.indexes still declares GinIndex objects with the same names (main_tags_gin_idx, extra_tags_gin_idx), two mechanisms manage the same physical indexes: this RunPython cursor block and Django's ORM migration machinery. The IF NOT EXISTS guard prevents an immediate crash, but future ORM-generated migrations may attempt to create, drop, or rename indexes that were already created (or not) by this SQL block, causing silent divergence or errors.

Run the following to confirm whether the duplicate declaration still exists:

#!/bin/bash
# Check Metric model's Meta.indexes for GinIndex declarations
rg -n 'GinIndex|main_tags_gin_idx|extra_tags_gin_idx' --type=py -A3 -B3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 16 - 19, The migration's RunPython SQL creates physical indexes
named main_tags_gin_idx and extra_tags_gin_idx which may be duplicated by
GinIndex entries in Metric.Meta.indexes; search for Metric.Meta.indexes (look
for GinIndex, main_tags_gin_idx, extra_tags_gin_idx) and if those GinIndex
declarations exist either remove or rename them so Django's ORM migration
machinery doesn't try to manage the same physical indexes, or conversely remove
the raw SQL index creation from the RunPython block and let Django create the
indexes via model Meta.indexes; update the migration comment to document which
mechanism (raw SQL vs Django index) is authoritative to avoid future drift.

22-30: ⚠️ Potential issue | 🟡 Minor

rollback_jsonb leaves the schema without the original B-tree indexes.

The forward migration drops monitoring_metric_main_tags_2d8550ae_like and monitoring_metric_main_tags_2d8550ae, but the reverse never recreates them. After a rollback, main_tags is back to text but the previously existing B-tree indexes are permanently gone, leaving queries that relied on them unindexed.

Add CREATE INDEX statements in rollback_jsonb to restore the originals:

🔄 Proposed fix
 def rollback_jsonb(apps, schema_editor):
     if schema_editor.connection.vendor != "postgresql":
         return
     with schema_editor.connection.cursor() as cursor:
         cursor.execute("DROP INDEX IF EXISTS main_tags_gin_idx; DROP INDEX IF EXISTS extra_tags_gin_idx;")
         cursor.execute(
             "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE text; "
             "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE text;"
         )
+        cursor.execute(
+            "CREATE INDEX IF NOT EXISTS monitoring_metric_main_tags_2d8550ae "
+            "ON monitoring_metric (main_tags); "
+            "CREATE INDEX IF NOT EXISTS monitoring_metric_main_tags_2d8550ae_like "
+            "ON monitoring_metric (main_tags varchar_pattern_ops);"
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 22 - 30, rollback_jsonb currently drops the GIN indexes but fails
to recreate the original B-tree indexes; update rollback_jsonb to execute CREATE
INDEX statements to restore the two B-tree indexes that were removed in the
forward migration (monitoring_metric_main_tags_2d8550ae and
monitoring_metric_main_tags_2d8550ae_like) by running SQL like CREATE INDEX
monitoring_metric_main_tags_2d8550ae ON monitoring_metric (main_tags) and CREATE
INDEX monitoring_metric_main_tags_2d8550ae_like ON monitoring_metric (main_tags
text_pattern_ops) (and similarly recreate any equivalent index for extra_tags if
one existed) immediately after altering the column types back to text so that
queries relying on those B-tree indexes are restored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Line 99: The current health-check condition using the line `if curl -sI
http://localhost:8086/ping | grep "204" > /dev/null; then` is fragile because it
can match "204" anywhere in headers; update that check to explicitly assert the
HTTP status line equals 204 by either anchoring the grep/regex to the start of
the status line (e.g., match lines beginning with "HTTP/" followed by the 204
status) or by using curl’s status-code output (write-out / -w) to capture the
numeric HTTP status and compare it to 204; apply this change to the existing
if-condition so only the actual HTTP status determines success.
- Around line 98-105: The loop currently always runs sleep 3 on every iteration
including the final failed attempt; update the for-loop block so that after the
curl check (curl -sI http://localhost:8086/ping | grep "204") you immediately
exit on success, and only call sleep 3 when you will retry (i.e., when the
current iteration is not the last). Concretely, keep the curl check and echo
"InfluxDB is ready!" / exit 0 on success, and move or gate the sleep 3 so it
only runs when i < 20 (or otherwise before the next iteration), avoiding the
trailing 3s stall after the 20th failed check.

In `@requirements-test.txt`:
- Line 8: The requirements line "django-filter<25.0" lacks a minimum version and
an explanation; update that requirement to include a sensible lower bound (for
example >=23.2) to prevent resolving very old releases and add an inline comment
on the same line explaining the <25.0 ceiling (e.g., note that django-filter
25.x removed the in‑built API schema generation methods deprecated since v23.2)
so future maintainers understand the constraint.

---

Duplicate comments:
In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`:
- Around line 12-15: The ALTER TABLE SQL in
migrations/0013_migrate_to_jsonfield.py uses unsafe casts like main_tags::jsonb
and extra_tags::jsonb which will fail on empty strings; update the string in
cursor.execute to use a guarded CASE/trim expression for both columns (e.g., in
the USING clause, replace main_tags::jsonb with a CASE that treats empty/blank
main_tags as an empty JSON array/object and otherwise casts main_tags to jsonb,
and do the same for extra_tags) so the migration skips or converts blank values
instead of crashing.
- Around line 16-19: The migration's RunPython SQL creates physical indexes
named main_tags_gin_idx and extra_tags_gin_idx which may be duplicated by
GinIndex entries in Metric.Meta.indexes; search for Metric.Meta.indexes (look
for GinIndex, main_tags_gin_idx, extra_tags_gin_idx) and if those GinIndex
declarations exist either remove or rename them so Django's ORM migration
machinery doesn't try to manage the same physical indexes, or conversely remove
the raw SQL index creation from the RunPython block and let Django create the
indexes via model Meta.indexes; update the migration comment to document which
mechanism (raw SQL vs Django index) is authoritative to avoid future drift.
- Around line 22-30: rollback_jsonb currently drops the GIN indexes but fails to
recreate the original B-tree indexes; update rollback_jsonb to execute CREATE
INDEX statements to restore the two B-tree indexes that were removed in the
forward migration (monitoring_metric_main_tags_2d8550ae and
monitoring_metric_main_tags_2d8550ae_like) by running SQL like CREATE INDEX
monitoring_metric_main_tags_2d8550ae ON monitoring_metric (main_tags) and CREATE
INDEX monitoring_metric_main_tags_2d8550ae_like ON monitoring_metric (main_tags
text_pattern_ops) (and similarly recreate any equivalent index for extra_tags if
one existed) immediately after altering the column types back to text so that
queries relying on those B-tree indexes are restored.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd5bf29 and 2012a75.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
  • requirements-test.txt
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Python==3.12 | django~=4.2.0
  • GitHub Check: Python==3.12 | django~=5.2.0
  • GitHub Check: Python==3.10 | django~=5.1.0
  • GitHub Check: Python==3.10 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.2.0
  • GitHub Check: Python==3.13 | django~=5.1.0
  • GitHub Check: Python==3.11 | django~=4.2.0
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-21T18:44:28.852Z
Learnt from: dee077
Repo: openwisp/openwisp-monitoring PR: 738
File: openwisp_monitoring/device/api/views.py:263-281
Timestamp: 2026-02-21T18:44:28.852Z
Learning: In openwisp-monitoring, MonitoringIndoorCoordinatesList inherits organization scoping from the parent IndoorCoordinatesList (from openwisp-controller), which uses FilterByParentManaged mixin and filters by location_id in get_queryset(). The child class only overrides the queryset attribute to add monitoring-specific select_related fields; this pattern is safe as long as get_queryset() from the parent is not bypassed. During reviews, verify that MonitoringIndoorCoordinatesList continues to rely on the parent's get_queryset() and that any added select_related fields in the child do not alter the parent's filtering logic.

Applied to files:

  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml Outdated
Comment thread requirements-test.txt Outdated
freezegun~=1.5.5
commitizen~=4.13.0
prompt-toolkit!=3.0.52
django-filter<25.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

django-filter<25.0 is missing a lower bound and the rationale for the upper cap is undocumented.

django-filter 25.1 removed the in-built API schema generation methods that had been deprecated since v23.2, which explains the <25.0 ceiling. However:

  1. No lower bound is specified — pip could technically resolve a very old version of django-filter if nothing else narrows it down.
  2. There is no inline comment explaining why 25.x is excluded, which makes the intent opaque for future maintainers.
🔧 Suggested fix
-django-filter<25.0
+django-filter>=24.0,<25.0  # cap at <25.0 avoids breaking removal of deprecated DRF schema methods in 25.1
📝 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
django-filter<25.0
django-filter>=24.0,<25.0 # cap at <25.0 avoids breaking removal of deprecated DRF schema methods in 25.1
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@requirements-test.txt` at line 8, The requirements line "django-filter<25.0"
lacks a minimum version and an explanation; update that requirement to include a
sensible lower bound (for example >=23.2) to prevent resolving very old releases
and add an inline comment on the same line explaining the <25.0 ceiling (e.g.,
note that django-filter 25.x removed the in‑built API schema generation methods
deprecated since v23.2) so future maintainers understand the constraint.

@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch from 2012a75 to c767b07 Compare February 24, 2026 16:43
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: 3

♻️ Duplicate comments (6)
.github/workflows/ci.yml (2)

98-105: Trailing sleep 3 on the final failed iteration — duplicate of existing review comment.

On the 20th iteration, when the curl check fails, sleep 3 still executes before the loop falls through to the timeout error, adding a needless 3-second stall. Moving sleep 3 to the top of the loop (before the curl check) avoids this.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 98 - 105, The loop that waits for
InfluxDB currently runs curl then sleeps, which causes an unnecessary final
sleep after the 20th failed attempt; modify the for loop in the CI workflow (the
block starting with for i in {1..20}; do and referencing the curl check of
http://localhost:8086/ping and the sleep 3) to move the sleep 3 to the top of
the loop (before the curl check) so the script sleeps between attempts but does
not perform the trailing 3-second pause after the final failed iteration.

99-99: grep "204" is fragile — duplicate of existing review comment.

The bare substring match can spuriously succeed on any header containing "204" (e.g., Content-Length: 2048). The previously suggested fix (grep -q "^HTTP/.* 204") still applies and has not been addressed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 99, The current health-check line using the
pipeline "curl -sI http://localhost:8086/ping | grep \"204\"" is fragile because
it can match any header containing "204"; update the conditional that invokes
curl piped to grep to use an anchored HTTP-status match instead (e.g., change
the grep invocation to use a pattern like ^HTTP/.* 204 or use curl's numeric
status output) so only a true HTTP 204 response succeeds; locate the line
containing the curl -sI ... | grep invocation and replace the grep pattern with
the anchored match (or switch to curl -s -o /dev/null -w '%{http_code}' and
compare to 204) to make the check robust.
openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py (3)

12-15: ⚠️ Potential issue | 🔴 Critical

Guard empty-string values before casting to jsonb.

''::jsonb fails, and blank=True allows empty strings, so this migration can crash on existing data. Use NULLIF or a CASE guard.

🛠️ Suggested fix
-            "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb USING main_tags::jsonb; "
-            "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb USING extra_tags::jsonb;"
+            "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE jsonb "
+            "USING NULLIF(main_tags, '')::jsonb; "
+            "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE jsonb "
+            "USING NULLIF(extra_tags, '')::jsonb;"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 12 - 15, The ALTER TABLE statements in cursor.execute cast
main_tags and extra_tags directly to jsonb which will fail on empty strings;
modify the SQL in the cursor.execute call so each cast guards empty-string
values (e.g., use NULLIF(main_tags, '')::jsonb or a CASE expression to convert
'' to NULL before casting) for both main_tags and extra_tags to avoid migration
crashes on blank values.

26-30: ⚠️ Potential issue | 🟡 Minor

Rollback should recreate the dropped B-tree indexes.

Forward migration drops monitoring_metric_main_tags_2d8550ae_like and monitoring_metric_main_tags_2d8550ae, but rollback never restores them, leaving post-rollback schema under-indexed.

🛠️ Suggested fix (ensure opclass matches original)
         cursor.execute(
             "ALTER TABLE monitoring_metric ALTER COLUMN main_tags TYPE text; "
             "ALTER TABLE monitoring_metric ALTER COLUMN extra_tags TYPE text;"
         )
+        cursor.execute(
+            "CREATE INDEX IF NOT EXISTS monitoring_metric_main_tags_2d8550ae "
+            "ON monitoring_metric (main_tags); "
+            "CREATE INDEX IF NOT EXISTS monitoring_metric_main_tags_2d8550ae_like "
+            "ON monitoring_metric (main_tags varchar_pattern_ops);"
+        )

Verification script (find original index definitions in earlier migrations):

#!/bin/bash
rg -n --type=py 'monitoring_metric_main_tags_2d8550ae' -C3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 26 - 30, The rollback currently doesn't recreate the two B-tree
indexes dropped in the forward migration; update the reverse migration to
recreate monitoring_metric_main_tags_2d8550ae_like and
monitoring_metric_main_tags_2d8550ae using cursor.execute statements so the
schema isn't left under-indexed, ensuring the index definitions
(columns/expressions and opclass) match the original definitions exactly (e.g.,
same operator class for text/array ops) when you add the CREATE INDEX IF NOT
EXISTS statements in the migration's reverse logic.

16-18: ⚠️ Potential issue | 🟠 Major

Avoid dual ownership of GIN indexes.

These indexes are created via raw SQL here. If Metric.Meta.indexes also declares GinIndex with the same names, Django migrations and the manual SQL will conflict. Pick one source of truth.

Verification script (expect only one definition source):

#!/bin/bash
# Find any ORM-managed GinIndex or explicit index names.
rg -n --type=py 'GinIndex|main_tags_gin_idx|extra_tags_gin_idx' -C3
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`
around lines 16 - 18, The migration currently creates GIN indexes
"main_tags_gin_idx" and "extra_tags_gin_idx" via raw SQL which will conflict if
the ORM also defines GinIndex entries for Metric (check Metric.Meta.indexes);
pick a single source of truth: either remove these raw SQL statements from
0013_migrate_to_jsonfield.py and let Django manage the GinIndex declarations on
the Metric model, or remove the GinIndex entries from Metric.Meta.indexes and
keep the raw SQL (and, if keeping raw SQL, ensure the migration
documents/creates them via RunSQL with reversible SQL). Locate the raw SQL in
0013_migrate_to_jsonfield.py (the cursor.execute call) and the GinIndex
definitions in the Metric model's Meta.indexes to make the change.
requirements-test.txt (1)

8-8: django-filter<25.0 still lacks a lower bound and an explanatory comment.

This was raised in a previous review — the constraint still has no minimum version and no inline documentation of why >=25.0 is excluded.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@requirements-test.txt` at line 8, The requirement line "django-filter<25.0"
lacks a minimum version and an explanatory comment; update that token in
requirements-test.txt to include an explicit lower bound (e.g., change to
"django-filter>=<min_version>,<25.0>") and add an inline comment explaining why
versions >=25.0 are excluded (compatibility/breaking change reference or
ticket/PR number) so future reviewers know the rationale.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 95-96: The "Start InfluxDB" step currently has no id so later
steps can't see its outcome; add an id (e.g., id: start_influxdb) to the "Start
InfluxDB" step and then change the "Wait for InfluxDB" step condition to also
require that steps.start_influxdb.conclusion == 'success' in addition to the
existing !cancelled() and steps.deps.conclusion == 'success' checks so the wait
loop is skipped if starting InfluxDB failed.

In `@requirements-test.txt`:
- Line 6: Remove the unused test dependency "commitizen" from
requirements-test.txt: delete the line containing commitizen~=4.13.0 (symbol:
commitizen) to avoid inflating test environment installs; if you believe it is
actually required, replace the line instead with the same entry plus an inline
comment explaining the hidden dependency and where it's used so reviewers/CI can
understand why it must remain.
- Line 7: Add an inline comment next to the prompt-toolkit!=3.0.52 entry
explaining that prompt-toolkit 3.0.52 introduced internal layout changes that
broke questionary (which commitizen depends on), reference questionary issue
`#454`, and note that the break was fixed in questionary 2.1.1 but the project
uses an older questionary via commitizen so the exclusion is required to avoid
the regression; keep the comment succinct and place it on the same line or
immediately above the prompt-toolkit!=3.0.52 requirement in
requirements-test.txt.

---

Duplicate comments:
In @.github/workflows/ci.yml:
- Around line 98-105: The loop that waits for InfluxDB currently runs curl then
sleeps, which causes an unnecessary final sleep after the 20th failed attempt;
modify the for loop in the CI workflow (the block starting with for i in
{1..20}; do and referencing the curl check of http://localhost:8086/ping and the
sleep 3) to move the sleep 3 to the top of the loop (before the curl check) so
the script sleeps between attempts but does not perform the trailing 3-second
pause after the final failed iteration.
- Line 99: The current health-check line using the pipeline "curl -sI
http://localhost:8086/ping | grep \"204\"" is fragile because it can match any
header containing "204"; update the conditional that invokes curl piped to grep
to use an anchored HTTP-status match instead (e.g., change the grep invocation
to use a pattern like ^HTTP/.* 204 or use curl's numeric status output) so only
a true HTTP 204 response succeeds; locate the line containing the curl -sI ... |
grep invocation and replace the grep pattern with the anchored match (or switch
to curl -s -o /dev/null -w '%{http_code}' and compare to 204) to make the check
robust.

In `@openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py`:
- Around line 12-15: The ALTER TABLE statements in cursor.execute cast main_tags
and extra_tags directly to jsonb which will fail on empty strings; modify the
SQL in the cursor.execute call so each cast guards empty-string values (e.g.,
use NULLIF(main_tags, '')::jsonb or a CASE expression to convert '' to NULL
before casting) for both main_tags and extra_tags to avoid migration crashes on
blank values.
- Around line 26-30: The rollback currently doesn't recreate the two B-tree
indexes dropped in the forward migration; update the reverse migration to
recreate monitoring_metric_main_tags_2d8550ae_like and
monitoring_metric_main_tags_2d8550ae using cursor.execute statements so the
schema isn't left under-indexed, ensuring the index definitions
(columns/expressions and opclass) match the original definitions exactly (e.g.,
same operator class for text/array ops) when you add the CREATE INDEX IF NOT
EXISTS statements in the migration's reverse logic.
- Around line 16-18: The migration currently creates GIN indexes
"main_tags_gin_idx" and "extra_tags_gin_idx" via raw SQL which will conflict if
the ORM also defines GinIndex entries for Metric (check Metric.Meta.indexes);
pick a single source of truth: either remove these raw SQL statements from
0013_migrate_to_jsonfield.py and let Django manage the GinIndex declarations on
the Metric model, or remove the GinIndex entries from Metric.Meta.indexes and
keep the raw SQL (and, if keeping raw SQL, ensure the migration
documents/creates them via RunSQL with reversible SQL). Locate the raw SQL in
0013_migrate_to_jsonfield.py (the cursor.execute call) and the GinIndex
definitions in the Metric model's Meta.indexes to make the change.

In `@requirements-test.txt`:
- Line 8: The requirement line "django-filter<25.0" lacks a minimum version and
an explanatory comment; update that token in requirements-test.txt to include an
explicit lower bound (e.g., change to "django-filter>=<min_version>,<25.0>") and
add an inline comment explaining why versions >=25.0 are excluded
(compatibility/breaking change reference or ticket/PR number) so future
reviewers know the rationale.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2012a75 and c767b07.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
  • requirements-test.txt
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-21T18:44:28.852Z
Learnt from: dee077
Repo: openwisp/openwisp-monitoring PR: 738
File: openwisp_monitoring/device/api/views.py:263-281
Timestamp: 2026-02-21T18:44:28.852Z
Learning: In openwisp-monitoring, MonitoringIndoorCoordinatesList inherits organization scoping from the parent IndoorCoordinatesList (from openwisp-controller), which uses FilterByParentManaged mixin and filters by location_id in get_queryset(). The child class only overrides the queryset attribute to add monitoring-specific select_related fields; this pattern is safe as long as get_queryset() from the parent is not bypassed. During reviews, verify that MonitoringIndoorCoordinatesList continues to rely on the parent's get_queryset() and that any added select_related fields in the child do not alter the parent's filtering logic.

Applied to files:

  • openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py
🔇 Additional comments (1)
openwisp_monitoring/monitoring/migrations/0013_migrate_to_jsonfield.py (1)

33-59: LGTM: state vs DB operations are cleanly separated.

SeparateDatabaseAndState with RunPython keeps ORM state aligned while controlling DB-side conversion explicitly.

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +95 to +96
- name: Wait for InfluxDB
if: ${{ !cancelled() && steps.deps.conclusion == 'success' }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The wait step should also guard on "Start InfluxDB" succeeding.

The "Start InfluxDB" step (lines 91–93) has no id, so its outcome is invisible to subsequent steps. If docker compose up -d influxdb fails while deps succeeded, the wait step's condition (!cancelled() && steps.deps.conclusion == 'success') still evaluates to true, causing the loop to spin for the full 60 seconds before timing out — wasting CI time across all matrix combinations.

🔧 Proposed fix

Add an id to the "Start InfluxDB" step (existing context, not shown in diff above):

      - name: Start InfluxDB
+       id: start_influxdb
        if: ${{ !cancelled() && steps.deps.conclusion == 'success' }}
        run: docker compose up -d influxdb

Then tighten the wait step's condition:

-        if: ${{ !cancelled() && steps.deps.conclusion == 'success' }}
+        if: ${{ !cancelled() && steps.deps.conclusion == 'success' && steps.start_influxdb.conclusion == 'success' }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 95 - 96, The "Start InfluxDB" step
currently has no id so later steps can't see its outcome; add an id (e.g., id:
start_influxdb) to the "Start InfluxDB" step and then change the "Wait for
InfluxDB" step condition to also require that steps.start_influxdb.conclusion ==
'success' in addition to the existing !cancelled() and steps.deps.conclusion ==
'success' checks so the wait loop is skipped if starting InfluxDB failed.

Comment thread requirements-test.txt Outdated
mock-ssh-server~=0.9.1
channels_redis~=4.3.0
freezegun~=1.5.5
commitizen~=4.13.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all requirements files to understand the structure
fd -type f -name 'requirements*.txt' -o -name 'requirements*.txt' | head -20
fd -type f -name '*.txt' | grep -E 'req|depend|setup'

Repository: openwisp/openwisp-monitoring

Length of output: 413


🏁 Script executed:

#!/bin/bash
# Check what's in the current requirements-test.txt file
cat -n requirements-test.txt

Repository: openwisp/openwisp-monitoring

Length of output: 373


🏁 Script executed:

#!/bin/bash
# Search for commitizen references across the codebase to understand its usage
rg 'commitizen' -i --type txt --type py --type md -B 2 -A 2

Repository: openwisp/openwisp-monitoring

Length of output: 281


🏁 Script executed:

#!/bin/bash
# Check if there's any CI/CD configuration that uses commitizen
fd -type f \( -name '.github' -o -name '.gitlab-ci.yml' -o -name 'tox.ini' -o -name 'setup.cfg' -o -name 'pyproject.toml' \) 
rg 'commitizen' -i --type yaml --type ini --type toml

Repository: openwisp/openwisp-monitoring

Length of output: 270


🌐 Web query:

commitizen 4.13.8 latest version pypi release date 2026

💡 Result:

Commitizen’s latest version on PyPI is 4.13.8, and its PyPI release date is February 18, 2026. [1]

Sources
[1] https://pypi.org/project/commitizen/


🏁 Script executed:

#!/bin/bash
# Find all requirements files more carefully
find . -type f -name 'requirements*.txt' 2>/dev/null
find . -type f \( -name '*.txt' -o -name '*.cfg' -o -name '*.toml' -o -name '*.yml' -o -name '*.yaml' \) 2>/dev/null | head -30

Repository: openwisp/openwisp-monitoring

Length of output: 349


🏁 Script executed:

#!/bin/bash
# Check for CI/CD configuration files
find . -type f \( -name '.github' -o -path '*/.github/*' -o -name 'tox.ini' -o -name 'setup.cfg' -o -name 'pyproject.toml' -o -name '.gitlab-ci.yml' -o -name '.travis.yml' \) 2>/dev/null | head -20

Repository: openwisp/openwisp-monitoring

Length of output: 397


🏁 Script executed:

#!/bin/bash
# Search for commitizen in CI/CD and config files using rg
rg 'commitizen' --max-count=20 2>/dev/null

Repository: openwisp/openwisp-monitoring

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Check setup.py or pyproject.toml for package definition
find . -maxdepth 2 -type f \( -name 'setup.py' -o -name 'pyproject.toml' \) -exec cat {} \; 2>/dev/null | head -100

Repository: openwisp/openwisp-monitoring

Length of output: 2320


🏁 Script executed:

#!/bin/bash
# Look at git history for this requirements file
git log --oneline requirements-test.txt 2>/dev/null | head -10
git show HEAD:requirements-test.txt 2>/dev/null

Repository: openwisp/openwisp-monitoring

Length of output: 394


🏁 Script executed:

#!/bin/bash
# Check requirements.txt (production)
cat requirements.txt

Repository: openwisp/openwisp-monitoring

Length of output: 244


🏁 Script executed:

#!/bin/bash
# Check the CI workflow to see how tests are run and if commitizen is used
cat .github/workflows/ci.yml

Repository: openwisp/openwisp-monitoring

Length of output: 4891


🏁 Script executed:

#!/bin/bash
# Check setup.cfg for test configuration
cat setup.cfg

Repository: openwisp/openwisp-monitoring

Length of output: 362


🌐 Web query:

commitizen tool purpose what does it do

💡 Result:

Commitizen is a CLI tool that helps teams standardize Git commit messages (by default using the Conventional Commits format) by prompting you interactively when you commit, so messages are consistent and machine-parseable. [1]

Because commits follow a predictable structure, Commitizen can then automate parts of the release workflow—notably bumping semantic versions from commit history and generating/updating changelogs (e.g., via cz bump). [1]


🏁 Script executed:

#!/bin/bash
# Find and examine the run-qa-checks script
find . -name 'run-qa-checks' -o -name 'run-qa-checks.sh' 2>/dev/null
ls -la run-qa-checks 2>/dev/null
cat run-qa-checks

Repository: openwisp/openwisp-monitoring

Length of output: 1020


🏁 Script executed:

#!/bin/bash
# Check if commitizen is referenced in any Python or bash scripts
rg 'commitizen|cz ' --max-count=10 2>/dev/null

Repository: openwisp/openwisp-monitoring

Length of output: 112


Remove commitizen from requirements-test.txt—it is unused in tests, QA checks, and CI workflows.

commitizen is a commit-standardization and release-management tool (for enforcing conventional commits and automating version bumps), not a test dependency. It does not appear in the QA check script, CI workflows, or any test execution path. Keeping it in test requirements unnecessarily inflates every test environment install without providing value. Either remove it or document its necessity with an inline comment if there's a hidden dependency we've missed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@requirements-test.txt` at line 6, Remove the unused test dependency
"commitizen" from requirements-test.txt: delete the line containing
commitizen~=4.13.0 (symbol: commitizen) to avoid inflating test environment
installs; if you believe it is actually required, replace the line instead with
the same entry plus an inline comment explaining the hidden dependency and where
it's used so reviewers/CI can understand why it must remain.

Comment thread requirements-test.txt Outdated
channels_redis~=4.3.0
freezegun~=1.5.5
commitizen~=4.13.0
prompt-toolkit!=3.0.52
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "requirements-test.txt" -type f | head -5

Repository: openwisp/openwisp-monitoring

Length of output: 95


🌐 Web query:

prompt-toolkit 3.0.52 questionary commitizen crash issue

💡 Result:

prompt-toolkit==3.0.52 (released Aug 27, 2025) introduced an internal layout/container structure change that broke questionary (and therefore commitizen, which depends on questionary). The typical crash is:

  • AttributeError: 'VSplit' object has no attribute 'content' coming from questionary/prompts/common.py while traversing the prompt-toolkit layout tree. [1]

Upstream status / fixes

  • The breakage was reported as questionary issue #454 (“prompt_toolkit==3.0.52 break questionary”). [1]
  • It was fixed by questionary PR #456, switching to PromptSession.layout.find_all_windows() instead of relying on a fixed .layout.container path. [2]
  • A questionary release 2.1.1 (Aug 28, 2025) exists (i.e., after the fix). [3]

What to do

  • Prefer: upgrade questionary to >= 2.1.1 (so it’s compatible with prompt-toolkit 3.0.52). [2][3]
  • Workaround: pin prompt_toolkit away from 3.0.52 (e.g., 3.0.51). commitizen itself added prompt_toolkit!=3.0.52 with an explicit note pointing to questionary #454. [4]

Sources

  1. questionary issue #454 (crash + traceback) [1]
  2. questionary PR #456 (fix) [2]
  3. PyPI: questionary release history (shows 2.1.1 on Aug 28, 2025) [3]
  4. commitizen pyproject.toml (excludes prompt_toolkit!=3.0.52 due to questionary #454) [4]

🏁 Script executed:

cat -n requirements-test.txt

Repository: openwisp/openwisp-monitoring

Length of output: 373


🏁 Script executed:

# Check git history to understand the context better
git log --oneline requirements-test.txt | head -10

Repository: openwisp/openwisp-monitoring

Length of output: 148


🏁 Script executed:

rg "questionary" --type txt --type py . | head -20

Repository: openwisp/openwisp-monitoring

Length of output: 54


🏁 Script executed:

# Check if there are other requirements files that might pin questionary
fd "requirements" -type f | xargs grep -l "questionary"

Repository: openwisp/openwisp-monitoring

Length of output: 244


🏁 Script executed:

fd "requirements" -type f | xargs cat

Repository: openwisp/openwisp-monitoring

Length of output: 242


🏁 Script executed:

# Also check pyproject.toml or setup.py for dependencies
git ls-files | grep -E "(setup\.py|pyproject\.toml|setup\.cfg)"

Repository: openwisp/openwisp-monitoring

Length of output: 105


🏁 Script executed:

cat setup.py

Repository: openwisp/openwisp-monitoring

Length of output: 1798


🏁 Script executed:

cat pyproject.toml

Repository: openwisp/openwisp-monitoring

Length of output: 593


🏁 Script executed:

cat requirements.txt

Repository: openwisp/openwisp-monitoring

Length of output: 244


🏁 Script executed:

# Check git blame to see when prompt-toolkit!=3.0.52 was added
git log -p --all -- requirements-test.txt | grep -A 5 -B 5 "prompt-toolkit" | head -30

Repository: openwisp/openwisp-monitoring

Length of output: 54


Add inline comment documenting the questionary compatibility issue.

prompt-toolkit!=3.0.52 excludes a version that broke questionary (and therefore commitizen, which depends on questionary). Add an inline comment explaining the root cause so future maintainers understand why this exclusion exists.

The breakage stems from prompt-toolkit 3.0.52's internal layout changes, which questionary relied on. This was fixed in questionary 2.1.1, but the project uses commitizen with an older questionary version. See questionary issue #454.

🔧 Suggested fix
-prompt-toolkit!=3.0.52
+prompt-toolkit!=3.0.52  # Breaks questionary (see questionary#454); fixed in questionary 2.1.1+
📝 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
prompt-toolkit!=3.0.52
prompt-toolkit!=3.0.52 # Breaks questionary (see questionary#454); fixed in questionary 2.1.1+
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@requirements-test.txt` at line 7, Add an inline comment next to the
prompt-toolkit!=3.0.52 entry explaining that prompt-toolkit 3.0.52 introduced
internal layout changes that broke questionary (which commitizen depends on),
reference questionary issue `#454`, and note that the break was fixed in
questionary 2.1.1 but the project uses an older questionary via commitizen so
the exclusion is required to avoid the regression; keep the comment succinct and
place it on the same line or immediately above the prompt-toolkit!=3.0.52
requirement in requirements-test.txt.

@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch 2 times, most recently from 25e94eb to 1fa05e4 Compare February 24, 2026 17:13
…p#673

Optimized PostgreSQL with jsonb and GIN indexes. Synchronized migration state with models and verified locally.

Closes openwisp#673
@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch from 1fa05e4 to 95e4653 Compare February 24, 2026 17:36
Reverted the previous migration adjustments while investigating CI failures.
@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch from 22a8daf to e6370fa Compare February 24, 2026 19:03
…nd optimize indexing openwisp#673

Modernized JSON storage using native Django fields and PostgreSQL GIN indexes. Includes SeparateDatabaseAndState for safe schema migration, SpatiaLite compatibility guards, and enhanced CI health-checks.

Closes openwisp#673
@HMTAhir327 HMTAhir327 force-pushed the change/replace-jsonfield-with-builtin branch from e6370fa to 912f18c Compare February 24, 2026 20:14
@HMTAhir327
Copy link
Copy Markdown
Author

I have finalized the implementation by:

  • Consolidating all migration state into 0013 (and 0004 for the sample app) using SeparateDatabaseAndState.

  • Adding connection.vendor guards to ensure compatibility with SpatiaLite/SQLite during testing.

  • Implementing PostgreSQL-specific GIN indexes with safe USING casts for jsonb conversion.

  • Addressing reviewer feedback regarding CI robustness and requirement documentation.

@HMTAhir327 HMTAhir327 marked this pull request as ready for review February 24, 2026 20:58
@atif09
Copy link
Copy Markdown
Contributor

atif09 commented Mar 20, 2026

Please resolve the comments posted by code rabbit until it PR is approved. You can do this by:

  • replying to it stating why you dont agree with the changes it suggested if you think it is wrong
  • applying the change(s) in the next commit.

Ive checked the changes and I didn't find anything that hasnt been flagged by code rabbit already

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