Skip to content

Eagerly loaded empty relations seem to short-circuit conversion to internal structure #292

@davidgwking

Description

@davidgwking

Description

When used in conjunction with the JoinEagerAlgorithm setting, it appears that tree-like eager relations seem to be short-circuiting if they encounter an empty relation during conversion to internal structure (from database tuple).

For example, consider the following relation expression: Bs.[Cs, Ds]. I expect to receive the model instances for Ds, even if Cs is effectively empty. In the reproduction case that follows, you can see that Ds are never mapped to a Model instance, if relation Cs is empty.

The ouput of the following bug reproduction is as follows:

[
  {
    "id": 1,
    "Bs": [
      {
        "id": 1,
        "aId": 1,
        "Cs": [],
        "Ds": []
      }
    ]
  }
]

Sadly, I do not receive my instances of D under Ds. Using the default eager algorithm (rather than JoinEagerAlgorithm), I receive the expected results:

[
  {
    "id": 1,
    "Bs": [
      {
        "id": 1,
        "aId": 1,
        "Cs": [],
        "Ds": [
          { "id": 1 },
          { "id": 2 }
        ]
      }
    ]
  }
]

Finally, modifying the order of B's relations (whilst using JoinEagerAlgorithm) also impacts results. That is, swapping the order of the definitions of relations Cs and Ds, provides correct results. I expect that this observation may be helpful for identifying a root cause.

Reproduction Code

const Knex = require('knex');
const Objection = require('objection');

const { Model } = Objection;
const knexConfig = require('./knexfile');

const knex = Knex(knexConfig);
Model.knex(knex);

class A extends Model {
  static get tableName() {
    return 'a';
  }

  static get idColumn() {
    return 'id';
  }

  static get relationMappings() {
    return {
      Bs: {
        relation: Model.HasManyRelation,
        modelClass: B,
        join: {
          from: 'a.id',
          to: 'b.aId',
        },
      },
    };
  }
}

class B extends Model  {
  static get tableName() {
    return 'b';
  }

  static get idColumn() {
    return 'id';
  }

  static get relationMappings() {
    return {
      Cs: {
        relation: Model.ManyToManyRelation,
        modelClass: C,
        join: {
          from: 'b.id',
          through: {
            from: 'b_c.bId',
            to: 'b_c.cId',
          },
          to: 'c.id',
        },
      },
      Ds: {
        relation: Model.ManyToManyRelation,
        modelClass: D,
        join: {
          from: 'b.id',
          through: {
            from: 'b_d.bId',
            to: 'b_d.dId',
          },
          to: 'd.id',
        },
      }
    };
  }
}

class C extends Model  {
  static get tableName() {
    return 'c';
  }

  static get idColumn() {
    return 'id';
  }
}

class D extends Model  {
  static get tableName() {
    return 'd';
  }

  static get idColumn() {
    return 'id';
  }
}

A.query()
  .eagerAlgorithm(Model.JoinEagerAlgorithm)
  .eager('Bs.[Cs, Ds]')
  .then((results) => {
    console.log(JSON.stringify(results, null, 2));
    process.exit(0);
  })
  .catch((err) => {
    console.error(err.message);
    process.exit(1);
  })
begin;

drop table if exists b_d;
drop table if exists b_c;
drop table if exists d;
drop table if exists c;
drop table if exists b;
drop table if exists a;

create table a (
  id int primary key
);

create table b (
  id int primary key,
  "aId" int references a (id)
);

create table c (
  id int primary key
);

create table d (
  id int primary key
);

create table b_c (
  "bId" int references b (id),
  "cId" int references c (id)
);

create table b_d (
  "bId" int references b (id),
  "dId" int references d (id)
);

insert into a values (1);
insert into b values (1, 1);
insert into d values (1);
insert into d values (2);
insert into b_d values (1, 1);
insert into b_d values (1, 2);

commit;

Dependencies

"dependencies": {
  "knex": "^0.12.6",
  "objection": "^0.6.2",
  "pg": "^6.1.2"
}

Knex Debug Output

knex:client acquired connection from pool: __knexUid1 +0ms
  knex:query select "a"."id" as "id", "Bs"."id" as "Bs:id", "Bs"."aId" as "Bs:aId", "Bs:Cs"."id" as "Bs:Cs:id", "Bs:Ds"."id" as "Bs:Ds:id" from "a" as "a" left join (select * from "b") as "Bs" on "Bs"."aId" = "a"."id" left join "b_c" as "Bs:Cs_join" on "Bs:Cs_join"."bId" = "Bs"."id" left join (select * from "c") as "Bs:Cs" on "Bs:Cs_join"."cId" = "Bs:Cs"."id" left join "b_d" as "Bs:Ds_join" on "Bs:Ds_join"."bId" = "Bs"."id" left join (select * from "d") as "Bs:Ds" on "Bs:Ds_join"."dId" = "Bs:Ds"."id" +3ms
  knex:bindings [] +1ms
  knex:client releasing connection to pool: __knexUid1 +2ms
  knex:pool INFO pool postgresql:pg:client0 - dispense() clients=0 available=1 +1ms
davidgwking=# select "a"."id" as "id", "Bs"."id" as "Bs:id", "Bs"."aId" as "Bs:aId", "Bs:Cs"."id" as "Bs:Cs:id", "Bs:Ds"."id" as "Bs:Ds:id" from "a" as "a" left join (select * from "b") as "Bs" on "Bs"."aId" = "a"."id" left join "b_c" as "Bs:Cs_join" on "Bs:Cs_join"."bId" = "Bs"."id" left join (select * from "c") as "Bs:Cs" on "Bs:Cs_join"."cId" = "Bs:Cs"."id" left join "b_d" as "Bs:Ds_join" on "Bs:Ds_join"."bId" = "Bs"."id" left join (select * from "d") as "Bs:Ds" on "Bs:Ds_join"."dId" = "Bs:Ds"."id";
┌────┬───────┬────────┬──────────┬──────────┐
│ id │ Bs:id │ Bs:aId │ Bs:Cs:id │ Bs:Ds:id │
├────┼───────┼────────┼──────────┼──────────┤
│  1 │     1 │      1 │   (NULL) │        1 │
│  1 │     1 │      1 │   (NULL) │        2 │
└────┴───────┴────────┴──────────┴──────────┘
(2 rows)

Time: 1.489 ms

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions