apache/superset · error · ValidationError

A rule with this name already exists.

Error message

A rule with this name already exists.

What it means

Raised by UpdateRLSRuleCommand.run when RLSDAO.update + db.session.flush() hits an IntegrityError on the rule-name unique constraint. The preflight validate_uniqueness in validate() is not atomic with the update, so concurrent renames can collide here; the constraint error is translated into a ValidationError on 'name'.

Source

Thrown at superset/commands/security/update.py:59

    def __init__(self, model_id: int, data: dict[str, Any]):
        self._model_id = model_id
        self._properties = data.copy()
        self._tables = self._properties.get("tables", [])
        self._subjects = self._properties.get("subjects", [])
        self._model: Optional[RowLevelSecurityFilter] = None

    @transaction()
    def run(self) -> Any:
        self.validate()
        assert self._model
        try:
            updated_model = RLSDAO.update(self._model, self._properties)
            db.session.flush()
        except IntegrityError as ex:
            # The preflight uniqueness check in ``validate`` isn't atomic with
            # this update, so fall back to the database's unique constraint
            # and translate it into the same descriptive validation error.
            raise ValidationError(
                {"name": [_("A rule with this name already exists.")]}
            ) from ex
        return updated_model

    def validate(self) -> None:
        self._model = RLSDAO.find_by_id(int(self._model_id))
        if not self._model:
            raise RLSRuleNotFoundError()

        # Datasource access is validated before revealing whether the
        # requested name is already in use, so an unauthorized caller can't
        # use the duplicate-name response to enumerate rule names.
        if "tables" in self._properties:
            tables = (
                db.session.query(SqlaTable)
                .filter(SqlaTable.id.in_(self._tables))  # type: ignore[attr-defined]
                .all()
            )

View on GitHub (pinned to f4587218dd)

Solutions

  1. On this 400, GET the rules list and reconcile: either pick a different name or update the other rule that now owns the name
  2. Retry the PUT with a distinct name
  3. Serialize rule renames through a single writer to avoid the race

Example fix

# before
client.put(f'/api/v1/rowlevelsecurity/{rid}', json={'name': 'tenant_filter'})

# after
name = 'tenant_filter'
clash = client.get('/api/v1/rowlevelsecurity/?q=(name:eq:tenant_filter)').json()['count']
if clash:
    name = 'tenant_filter_v2'
client.put(f'/api/v1/rowlevelsecurity/{rid}', json={'name': name})
Defensive patterns

Strategy: try-catch

Validate before calling

taken = any(r['name'] == new_name and r['id'] != rid
            for r in client.get('/api/v1/rowlevelsecurity/').json()['result'])
assert not taken

Try / catch

from superset.commands.exceptions import ValidationError
try:
    UpdateRLSRuleCommand(rid, props).run()
except ValidationError as e:
    if 'name' in e.normalized_messages():
        props['name'] = uniquify(props['name']); UpdateRLSRuleCommand(rid, props).run()
    else:
        raise

Prevention

When it happens

Trigger: PUT /api/v1/rowlevelsecurity/{id} renaming a rule to a name another session created (or renamed to) between the preflight check and the flush; two concurrent PUTs converging on the same name.

Common situations: Parallel automation renaming RLS rules; retry-after-timeout logic that re-sends a rename which another attempt already applied.

Related errors


AI-assisted analysis of apache/superset@f4587218dd (2026-08-14). Data as JSON: /api/errors/ea34a2e93c8a1670. Report an issue: GitHub.