Strip undeclared keys from application_settings.zoekt_settings
What does this MR do and why?
An undeclared key in the zoekt_settings jsonb column on application_settings makes every PUT /api/v4/application/settings call fail — including payloads with nothing to do with search, such as {"gravatar_enabled": true} — because the column's JSON schema declares additionalProperties: false. The only remedy available today is a manual database edit.
This adds a post-deployment migration that removes keys the schema does not declare, and keeps additionalProperties: false so an undeclared key is still rejected on write.
References
- Closes #621732, split from #607625 (Problem 3)
- Schema
ee/app/validators/json_schemas/application_setting_zoekt_settings.json; the validator and thejsonb_accessor :zoekt_settingsblock inee/app/models/ee/application_setting.rb - Precedent:
db/migrate/20251211100451_remove_zoekt_minimum_projects_for_traversal_id_search_from_application_settings.rb
Query plans — Database Lab gitlab-production-main; the link carries the plan and the query as shipped, including the jsonb_typeof guard:
- Cleanup
UPDATE, whole table — 37 buffers, 1.16 ms,rows=0updated (postgres.ai cmd 499784)
application_settings is a single-row table, so the statement is one sequential scan of one page plus a jsonb_each per row. It updates 0 rows because GitLab.com's own row is healthy — 13 keys present, all declared — which is the expected no-op.
How to set up and validate locally
- Put an undeclared key into the column:
ApplicationSetting.current.update_column(:zoekt_settings, ApplicationSetting.current.zoekt_settings.merge('zoekt_orphaned_key' => 2)) PUT /api/v4/application/settingswith{"gravatar_enabled": true}→400,{"message":{"zoekt_settings":["must be a valid json schema"]}}bin/rails db:migrate- The same
PUT→200
Design choice and key provenance
Why a broken row is skipped. jsonb_each raises on a jsonb that is not an object, and the column carries no jsonb_typeof check constraint, so an array or scalar value would abort the whole migration and the upgrade with it. The WHERE therefore skips such a row: it is broken beyond what a key strip can repair, and rewriting it to {} would destroy data.
Why the schema stays strict. Loosening additionalProperties to true would make the same settings save succeed, but it would also start silently accepting misspelled and stale keys into a column nothing reads — trading a loud failure for a quiet one. Cleaning up the data preserves the write-time rejection that makes a typo visible.
Where the orphaned key came from. zoekt_max_restarts_15m is a real setting: added in 35641669 (2026-06-10, %19.1), while the reporter's instance ran 9208f123 (2026-06-02) — which is why their git log -S search found nothing. That instance is a downgrade artifact. The migration does not depend on it: it removes whatever the schema does not declare, whatever put it there.
Test evidence. Two mutations of the shipped code, each measured rather than inferred. Reverting #up to a no-op turns three of the five examples red (5 examples, 3 failures); removing the jsonb_typeof guard turns one red (5, 1), and it is the non-object example the guard exists for. Restored, all five pass. The fourth example is the regression test for this bug — it asserts that saving gravatar_enabled is refused while the undeclared key is present and accepted once the migration has run. Pipeline 2865212892 ran all five examples green in three job classes at the current head. Full output: snippet 6058298.
What I did not verify
- No
#down. The removed keys are undeclared by definition, so nothing reads them and their values cannot be reconstructed; rollback is a documented no-op rather than an omission. - The query plan is a no-op on GitLab.com's row, because that row is healthy. The non-empty case was measured only locally — no production row carrying an undeclared key exists to measure.
- Not exercised against a live self-managed instance; the reproduction steps above are the path a reviewer can run.