brick icon indicating copy to clipboard operation
brick copied to clipboard

Supabase adapter tries to serialize nested objects even when null, causing errors with foreign keys

Open KiddoV opened this issue 3 months ago • 1 comments

Describe the bug

When using OfflineFirstWithSupabaseModel with nested relationships, Brick generates a Supabase insert/update that includes nested objects even when they are null. This causes errors like:

Could not find the 'assoc' column of 'tables' in the schema cache
insert or update on table violates foreign key constraint

The intended behavior is to serialize only the foreign key column when the nested object is null.

Minimal reproducible example

@ConnectOfflineFirstWithSupabase(
  supabaseConfig: SupabaseSerializable(tableName: "model_a")
)
@MappableClass()
class ModelA extends OfflineFirstWithSupabaseModel with ModelAMappable {
  final String id;

  @Supabase(foreignKey: "model_a_child1_id_fkey")
  final Child1? child1;

  @Supabase(foreignKey: "model_a_child2_id_fkey")
  final Child2? child2;

  @Sqlite(ignore: true)
  String? get child1Id => child1?.id;

  @Sqlite(ignore: true)
  String? get child2Id => child2?.id;

  ModelA({
    required this.id,
    this.child1,
    this.child2,
  });
}

@MappableClass()
class Child1 extends OfflineFirstWithSupabaseModel with Child1Mappable {
  final String id;
  Child1({required this.id});
}

@MappableClass()
class Child2 extends OfflineFirstWithSupabaseModel with Child2Mappable {
  final String id;
  Child2({required this.id});
}

Current behavior

If child1 is not null and child2 is null, Brick still construct upsert query with

child2:model_a_child2_id_fkey(*)....

Expect behavior

Brick should ignore this query in the overall constructed Supabase query string.

KiddoV avatar Oct 03 '25 14:10 KiddoV

With my example model given above, the Supabase adapter currently serializes associations like this:

Future<Map<String, dynamic>> _$ModelASupabase  [...]

  'child1': instance.child1 != null
      ? await Child1Adapter().toSupabase(instance.child1!)
      : null,
  'child2': instance.child2 != null
      ? await Child2Adapter().toSupabase(instance.child2!)
      : null,

That means when child1 or child2 is null, the generated payload still contains:

{
  "child1": null, // Or
  "child2": null // Or
}

Supabase/PostgREST interprets "child1": null as an attempt to resolve a relationship with "null", which causes errors like:

Could not find a relationship between 'model_a' and 'null' in the schema cache

Suggested Fix

Only include the key if the association is non-null.

// Example
Future<Map<String, dynamic>> _$ModelASupabase  [...]

  final map = <String, dynamic>{
    'id': instance.id,
    'child1_id': instance.child1Id,
    'child2_id': instance.child2Id,
  };
  
  if (instance.child1 != null) {
    map['child1'] = await Child1Adapter().toSupabase(instance.child1!);
  }
  if (instance.child2 != null) {
    map['child2'] = await Child2Adapter().toSupabase(instance.child2!);
  }

This way:

  • Scalar foreign key fields (child1_id, child2_id) remain set correctly.
  • Associations are only serialized when present.
  • No "null" relationship payloads are sent to Supabase.

Benefits

  • Prevents PostgREST errors from "null" joins.
  • Matches Supabase’s expected contract: omit absent relations.
  • Backwards compatible (existing models don’t need to change).

If you agree, @tshedor , just tell me where to start, I can work on the PR.

Thanks,

KiddoV avatar Oct 03 '25 17:10 KiddoV