Skip to content

Commit

Permalink
feat: allow null field values in loadManyByFieldEqualityConjunctionAs…
Browse files Browse the repository at this point in the history
…ync (#130)
  • Loading branch information
wschurman authored Jul 1, 2021
1 parent 5401be0 commit 2f37dc6
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,36 @@ export default class PostgresEntityDatabaseAdapter<TFields> extends EntityDataba

if (tableFieldSingleValueEqualityOperands.length > 0) {
const whereObject: { [key: string]: any } = {};
for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) {
whereObject[tableField] = tableValue;
const nonNullTableFieldSingleValueEqualityOperands = tableFieldSingleValueEqualityOperands.filter(
({ tableValue }) => tableValue !== null
);
const nullTableFieldSingleValueEqualityOperands = tableFieldSingleValueEqualityOperands.filter(
({ tableValue }) => tableValue === null
);

if (nonNullTableFieldSingleValueEqualityOperands.length > 0) {
for (const { tableField, tableValue } of nonNullTableFieldSingleValueEqualityOperands) {
whereObject[tableField] = tableValue;
}
query = query.where(whereObject);
}
if (nullTableFieldSingleValueEqualityOperands.length > 0) {
for (const { tableField } of nullTableFieldSingleValueEqualityOperands) {
query = query.whereNull(tableField);
}
}
query = query.where(whereObject);
}

if (tableFieldMultiValueEqualityOperands.length > 0) {
for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
query = query.whereRaw('?? = ANY(?)', [tableField, [...tableValues]]);
const nonNullTableValues = tableValues.filter((tableValue) => tableValue !== null);
query = query.where((builder) => {
builder.whereRaw('?? = ANY(?)', [tableField, [...nonNullTableValues]]);
// there was at least one null, allow null in this equality clause
if (nonNullTableValues.length !== tableValues.length) {
builder.orWhereNull(tableField);
}
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,59 @@ describe('postgres entity integration', () => {
expect(results).toHaveLength(2);
expect(results.map((e) => e.getField('name'))).toEqual(['b', 'a']);
});

it('supports null field values', async () => {
const vc1 = new ViewerContext(createKnexIntegrationTestEntityCompanionProvider(knexInstance));
await enforceAsyncResult(
PostgresTestEntity.creator(vc1)
.setField('name', 'a')
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(vc1)
.setField('name', 'b')
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(vc1)
.setField('name', null)
.setField('hasADog', true)
.createAsync()
);
await enforceAsyncResult(
PostgresTestEntity.creator(vc1)
.setField('name', null)
.setField('hasADog', false)
.createAsync()
);

const results = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync([{ fieldName: 'name', fieldValue: null }]);
expect(results).toHaveLength(2);
expect(results[0].getField('name')).toBeNull();

const results2 = await PostgresTestEntity.loader(vc1)
.enforcing()
.loadManyByFieldEqualityConjunctionAsync(
[
{ fieldName: 'name', fieldValues: ['a', null] },
{ fieldName: 'hasADog', fieldValue: true },
],
{
orderBy: [
{
fieldName: 'name',
order: OrderByOrdering.DESCENDING,
},
],
}
);
expect(results2).toHaveLength(2);
expect(results2.map((e) => e.getField('name'))).toEqual([null, 'a']);
});
});

describe('raw where clause loading', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/entity/src/EntityDatabaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {

interface SingleValueFieldEqualityCondition<TFields, N extends keyof TFields = keyof TFields> {
fieldName: N;
fieldValue: NonNullable<TFields[N]>;
fieldValue: TFields[N];
}

interface MultiValueFieldEqualityCondition<TFields, N extends keyof TFields = keyof TFields> {
fieldName: N;
fieldValues: readonly NonNullable<TFields[N]>[];
fieldValues: readonly TFields[N][];
}

export type FieldEqualityCondition<TFields, N extends keyof TFields = keyof TFields> =
Expand Down
2 changes: 1 addition & 1 deletion packages/entity/src/EntityLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ export default class EntityLoader<

private validateFieldValues<N extends keyof Pick<TFields, TSelectedFields>>(
fieldName: N,
fieldValues: readonly NonNullable<TFields[N]>[]
fieldValues: readonly TFields[N][]
): void {
const fieldDefinition = this.entityConfiguration.schema.get(fieldName);
invariant(fieldDefinition, `must have field definition for field = ${fieldName}`);
Expand Down
24 changes: 22 additions & 2 deletions packages/entity/src/utils/testing/StubDatabaseAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,38 @@ export default class StubDatabaseAdapter<T> extends EntityDatabaseAdapter<T> {
const aField = objectA[currentOrderBy.columnName];
const bField = objectB[currentOrderBy.columnName];
switch (currentOrderBy.order) {
case OrderByOrdering.DESCENDING:
case OrderByOrdering.DESCENDING: {
// simulate NULLS FIRST for DESC
if (aField === null && bField === null) {
return 0;
} else if (aField === null) {
return -1;
} else if (bField === null) {
return 1;
}

return aField > bField
? -1
: aField < bField
? 1
: this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
case OrderByOrdering.ASCENDING:
}
case OrderByOrdering.ASCENDING: {
// simulate NULLS LAST for ASC
if (aField === null && bField === null) {
return 0;
} else if (bField === null) {
return -1;
} else if (aField === null) {
return 1;
}

return bField > aField
? -1
: bField < aField
? 1
: this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,81 @@ describe(StubDatabaseAdapter, () => {
expect(results).toHaveLength(3);
expect(results.map((e) => e.stringField)).toEqual(['c', 'b', 'a']);
});

it('supports null field values', async () => {
const queryContext = instance(mock(EntityQueryContext));
const databaseAdapter = new StubDatabaseAdapter<TestFields>(
testEntityConfiguration,
StubDatabaseAdapter.convertFieldObjectsToDataStore(
testEntityConfiguration,
new Map([
[
testEntityConfiguration.tableName,
[
{
customIdField: '1',
testIndexedField: 'h1',
numberField: 1,
stringField: 'a',
dateField: new Date(),
nullableField: 'a',
},
{
customIdField: '2',
testIndexedField: 'h2',
numberField: 2,
stringField: 'a',
dateField: new Date(),
nullableField: 'b',
},
{
customIdField: '3',
testIndexedField: 'h3',
numberField: 3,
stringField: 'a',
dateField: new Date(),
nullableField: null,
},
{
customIdField: '4',
testIndexedField: 'h4',
numberField: 4,
stringField: 'b',
dateField: new Date(),
nullableField: null,
},
],
],
])
)
);

const results = await databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
queryContext,
[{ fieldName: 'nullableField', fieldValue: null }],
{}
);
expect(results).toHaveLength(2);
expect(results[0].nullableField).toBeNull();

const results2 = await databaseAdapter.fetchManyByFieldEqualityConjunctionAsync(
queryContext,
[
{ fieldName: 'nullableField', fieldValues: ['a', null] },
{ fieldName: 'stringField', fieldValue: 'a' },
],
{
orderBy: [
{
fieldName: 'nullableField',
order: OrderByOrdering.DESCENDING,
},
],
}
);
expect(results2).toHaveLength(2);
expect(results2.map((e) => e.nullableField)).toEqual([null, 'a']);
});
});

describe('fetchManyByRawWhereClauseAsync', () => {
Expand Down Expand Up @@ -318,4 +393,74 @@ describe(StubDatabaseAdapter, () => {
'Unsupported ID type for StubDatabaseAdapter: DateField'
);
});

describe('compareByOrderBys', () => {
describe('comparison', () => {
it.each([
// nulls compare with 0
[OrderByOrdering.DESCENDING, null, 0, -1],
[OrderByOrdering.ASCENDING, null, 0, 1],
[OrderByOrdering.DESCENDING, 0, null, 1],
[OrderByOrdering.ASCENDING, 0, null, -1],

// nulls compare with nulls
[OrderByOrdering.DESCENDING, null, null, 0],
[OrderByOrdering.ASCENDING, null, null, 0],

// nulls compare with -1
[OrderByOrdering.DESCENDING, null, -1, -1],
[OrderByOrdering.ASCENDING, null, -1, 1],
[OrderByOrdering.DESCENDING, -1, null, 1],
[OrderByOrdering.ASCENDING, -1, null, -1],

// basic compares
[OrderByOrdering.ASCENDING, 'a', 'b', -1],
[OrderByOrdering.ASCENDING, 'b', 'a', 1],
[OrderByOrdering.DESCENDING, 'a', 'b', 1],
[OrderByOrdering.DESCENDING, 'b', 'a', -1],
])('case (%p; %p; %p)', (order, v1, v2, expectedResult) => {
expect(
StubDatabaseAdapter['compareByOrderBys'](
[
{
columnName: 'hello',
order,
},
],
{
hello: v1,
},
{
hello: v2,
}
)
).toEqual(expectedResult);
});
});

describe('recursing', () => {
expect(
StubDatabaseAdapter['compareByOrderBys'](
[
{
columnName: 'hello',
order: OrderByOrdering.ASCENDING,
},
{
columnName: 'world',
order: OrderByOrdering.ASCENDING,
},
],
{
hello: 'a',
world: 1,
},
{
hello: 'a',
world: 2,
}
)
).toEqual(-1);
});
});
});

0 comments on commit 2f37dc6

Please sign in to comment.