detach method

Future<OrmMigrationRecord> detach(
  1. String relationName, [
  2. List? ids
])
inherited

Detaches related models in a manyToMany relationship.

Deletes pivot table records. If no IDs are provided, detaches all.

After detaching, the relation is reloaded to sync the cache.

Example:

final post = await Post.query().find(1);
await post.detach('tags', [1, 2]); // Detach specific tags
await post.detach('tags'); // Detach all tags

Implementation

Future<TModel> detach(String relationName, [List<dynamic>? ids]) async {
  final def = expectDefinition();
  final resolver = _resolveResolverFor(def);

  final relationDef = def.relations.cast<RelationDefinition?>().firstWhere(
    (r) => r?.name == relationName,
    orElse: () => null,
  );

  if (relationDef == null) {
    throw ArgumentError(
      'Relation "$relationName" not found on ${def.modelName}',
    );
  }

  if (relationDef.kind != RelationKind.manyToMany) {
    throw ArgumentError(
      'detach() can only be used with manyToMany relations. '
      'Relation "$relationName" is ${relationDef.kind}',
    );
  }

  // Get this model's primary key value
  final pk = def.primaryKeyField;
  if (pk == null) {
    throw StateError('Model ${def.modelName} must have a primary key');
  }

  final pkValue = _primaryKeyValue(def);
  if (pkValue == null) {
    throw StateError('Model ${def.modelName} primary key value is null');
  }

  final pivotTable = relationDef.through;
  if (pivotTable == null) {
    throw StateError(
      'Relation "$relationName" is missing pivot table name (through)',
    );
  }

  final pivotForeignKey = relationDef.pivotForeignKey!;
  final pivotRelatedKey = relationDef.pivotRelatedKey!;

  // Get the related model definition to determine column types
  final relatedModelName = relationDef.targetModel;
  final relatedDef = resolver.registry.expectByName(relatedModelName);

  final relatedPk = relatedDef.primaryKeyField;
  if (relatedPk == null) {
    throw StateError(
      'Related model $relatedModelName must have a primary key',
    );
  }

  // Build delete keys
  final List<Map<String, Object?>> deleteKeys;

  if (ids != null && ids.isNotEmpty) {
    // Detach specific IDs
    deleteKeys = ids
        .map((id) => {pivotForeignKey: pkValue, pivotRelatedKey: id})
        .toList();
  } else {
    // Detach all - query for existing pivot records first
    final pivotDef = _createPivotDefinition(pivotTable, def.schema, {
      pivotForeignKey: pk,
      pivotRelatedKey: relatedPk,
    });

    final selectPlan = QueryPlan(
      definition: pivotDef,
      filters: [
        FilterClause(
          field: pivotForeignKey,
          operator: FilterOperator.equals,
          value: pkValue,
        ),
      ],
    );

    final results = await resolver.runSelect(selectPlan);
    deleteKeys = results
        .map(
          (row) => {
            pivotForeignKey: row[pivotForeignKey],
            pivotRelatedKey: row[pivotRelatedKey],
          },
        )
        .toList();
  }

  if (deleteKeys.isNotEmpty) {
    final pivotDef = _createPivotDefinition(pivotTable, def.schema, {
      pivotForeignKey: pk,
      pivotRelatedKey: relatedPk,
    });

    final deleteRows = deleteKeys
        .map((keys) => MutationRow(values: const {}, keys: keys))
        .toList();

    final plan = MutationPlan.delete(definition: pivotDef, rows: deleteRows);

    await resolver.runMutation(plan);
  }

  // Reload the relation to sync cache
  await load(relationName);

  return _self();
}