diff --git a/packages/graphile-build-pg/src/QueryBuilder.d.ts b/packages/graphile-build-pg/src/QueryBuilder.d.ts index 2baa3ecbc..7f53f9fa7 100644 --- a/packages/graphile-build-pg/src/QueryBuilder.d.ts +++ b/packages/graphile-build-pg/src/QueryBuilder.d.ts @@ -51,6 +51,7 @@ export default class QueryBuilder { public offset(offsetGen: NumberGen): void; public first(first: number): void; public last(last: number): void; + public distinctOn(exprGen: SQLGen): void; // ---------------------------------------- diff --git a/packages/graphile-build-pg/src/QueryBuilder.js b/packages/graphile-build-pg/src/QueryBuilder.js index 9ba9e587b..7296ff51a 100644 --- a/packages/graphile-build-pg/src/QueryBuilder.js +++ b/packages/graphile-build-pg/src/QueryBuilder.js @@ -106,6 +106,7 @@ class QueryBuilder { offset: ?NumberGen, first: ?number, last: ?number, + distinctOn: Array, beforeLock: { [string]: Array<() => void> | null, }, @@ -133,6 +134,7 @@ class QueryBuilder { offset: ?number, first: ?number, last: ?number, + distinctOn: Array, cursorComparator: ?CursorComparator, }; lockContext: { @@ -170,6 +172,7 @@ class QueryBuilder { last: false, limit: false, offset: false, + distinctOn: false, }; this.finalized = false; this.selectedIdentifiers = false; @@ -192,6 +195,7 @@ class QueryBuilder { offset: null, first: null, last: null, + distinctOn: [], beforeLock: { // As a performance optimisation, we're going to list a number of lock // types so that V8 doesn't need to mutate the object too much @@ -209,6 +213,7 @@ class QueryBuilder { last: [], limit: [], offset: [], + distinctOn: [], }, cursorComparator: null, liveConditions: [], @@ -231,6 +236,7 @@ class QueryBuilder { offset: null, first: null, last: null, + distinctOn: [], cursorComparator: null, }; this._children = new Map(); @@ -258,6 +264,19 @@ class QueryBuilder { this.lock("limit"); this.lock("offset"); }); + this.beforeLock("distinctOn", () => { + this.lock("selectCursor"); + this.lock("cursorComparator"); + }); + this.beforeLock("orderBy", () => { + this.lock("distinctOn"); + }); + this.beforeLock("selectCursor", () => { + this.lock("distinctOn"); + }); + this.beforeLock("cursorComparator", () => { + this.lock("distinctOn"); + }); this.lockContext = Object.freeze({ queryBuilder: this, }); @@ -511,6 +530,10 @@ ${sql.join( } this.data.last = last; } + distinctOn(exprGen: SQLGen) { + this.checkLock("distinctOn"); + this.data.distinctOn.push(exprGen); + } // ---------------------------------------- @@ -578,6 +601,10 @@ ${sql.join( throw new Error("Cannot combine 'last' and 'offset'"); } else { if (this.compiledData.orderBy.length > 0) { + if (this.compiledData.distinctOn.length > 0) { + // TODO: improve this + throw new Error(`'distinctOn' cannot be combined with 'last'`); + } flip = true; limit = this.compiledData.last; } else { @@ -597,9 +624,31 @@ ${sql.join( getFinalLimit() { return this.getFinalLimitAndOffset().limit; } + _orderByExpressionsAndDirections: Array< + [sql.SQL, boolean, boolean | null] + > | null = null; getOrderByExpressionsAndDirections() { this.lock("orderBy"); - return this.compiledData.orderBy; + if (!this._orderByExpressionsAndDirections) { + const orderBy: Array<[sql.SQL, boolean, boolean | null]> = []; + const remainingOrderBy = [...this.compiledData.orderBy]; + for (const distinctExpression of this.compiledData.distinctOn) { + const relevantOrderByIndex = remainingOrderBy.findIndex(o => + sql.isEquivalent(o[0], distinctExpression) + ); + if (relevantOrderByIndex >= 0) { + orderBy.push(remainingOrderBy[relevantOrderByIndex]); + remainingOrderBy.splice(relevantOrderByIndex, 1); + } else { + orderBy.push([distinctExpression, true, null]); + } + } + for (const orderSpec of remainingOrderBy) { + orderBy.push(orderSpec); + } + this._orderByExpressionsAndDirections = orderBy; + } + return this._orderByExpressionsAndDirections; } getSelectFieldsCount() { this.lockEverything(); @@ -743,8 +792,18 @@ ${sql.join( })} as object` : this.buildSelectFields(); + const orderBy = this.getOrderByExpressionsAndDirections(); + let fragment = sql.fragment`\ -select ${useAsterisk ? sql.fragment`${this.getTableAlias()}.*` : fields} +select ${ + this.compiledData.distinctOn.length + ? sql.fragment`distinct on (${sql.join( + this.compiledData.distinctOn, + ", " + )})` + : sql.blank + } +${useAsterisk ? sql.fragment`${this.getTableAlias()}.*` : fields} ${ this.compiledData.from && sql.fragment`from ${this.compiledData.from[0]} as ${this.getTableAlias()}` @@ -752,9 +811,9 @@ ${ ${this.compiledData.join.length && sql.join(this.compiledData.join, " ")} where ${this.buildWhereClause(true, true, options)} ${ - this.compiledData.orderBy.length + orderBy.length ? sql.fragment`order by ${sql.join( - this.compiledData.orderBy.map( + orderBy.map( ([expr, ascending, nullsFirst]) => sql.fragment`${expr} ${ Number(ascending) ^ Number(flip) @@ -894,6 +953,8 @@ order by (row_number() over (partition by 1)) desc`; /* We don't need to factor this.compiledData[type] = this.data[type]; } else if (type === "last") { this.compiledData[type] = this.data[type]; + } else if (type === "distinctOn") { + this.compiledData[type] = callIfNecessaryArray(this.data[type], context); } else { throw new Error(`Wasn't expecting to lock '${type}'`); } @@ -925,6 +986,7 @@ order by (row_number() over (partition by 1)) desc`; /* We don't need to factor this.lock("limit"); this.lock("first"); this.lock("last"); + this.lock("distinctOn"); // We must execute select after orderBy otherwise we cannot generate a cursor this.lock("fixedSelectExpression"); this.lock("selectCursor"); diff --git a/packages/graphile-build-pg/src/utils.js b/packages/graphile-build-pg/src/utils.js index fec346b24..ef17ba982 100644 --- a/packages/graphile-build-pg/src/utils.js +++ b/packages/graphile-build-pg/src/utils.js @@ -29,3 +29,5 @@ export const parseTags = (str: string) => { } ); }; + +export { arraysMatch } from "pg-sql2"; diff --git a/packages/pg-sql2/src/index.ts b/packages/pg-sql2/src/index.ts index 9add48c74..f2529bf2a 100644 --- a/packages/pg-sql2/src/index.ts +++ b/packages/pg-sql2/src/index.ts @@ -1,6 +1,7 @@ import * as debugFactory from "debug"; import { QueryConfig } from "pg"; import LRU from "@graphile/lru"; +import { inspect } from "util"; const debug = debugFactory("pg-sql2"); @@ -326,6 +327,71 @@ export function join(items: Array, rawSeparator = ""): SQLQuery { return currentItems; } +export function arraysMatch( + array1: ReadonlyArray, + array2: ReadonlyArray, + comparator?: (val1: T, val2: T) => boolean +): boolean { + if (array1 === array2) return true; + const l = array1.length; + if (l !== array2.length) { + return false; + } + for (let i = 0; i < l; i++) { + if ( + comparator ? !comparator(array1[i]!, array2[i]!) : array1[i] !== array2[i] + ) { + return false; + } + } + return true; +} + +export function isEquivalent( + sql1: SQL, + sql2: SQL, + options?: { + symbolSubstitutes?: Map; + } +): boolean { + if (sql1 === sql2) { + return true; + } else if (Array.isArray(sql1)) { + if (!Array.isArray(sql2)) { + return false; + } + return arraysMatch(sql1, sql2, (a, b) => isEquivalent(a, b, options)); + } else if (Array.isArray(sql2)) { + return false; + } else { + switch (sql1.type) { + case "RAW": { + if (sql2.type !== sql1.type) { + return false; + } + return sql1.text === sql2.text; + } + case "VALUE": { + if (sql2.type !== sql1.type) { + return false; + } + return sql1.value === sql2.value; + } + case "IDENTIFIER": { + if (sql2.type !== sql1.type) { + return false; + } + return arraysMatch(sql1.names, sql2.names); + } + default: { + const never: never = sql1; + console.error(`Unhandled node type: ${inspect(never)}`); + return false; + } + } + } +} + // Copied from https://github.com/brianc/node-postgres/blob/860cccd53105f7bc32fed8b1de69805f0ecd12eb/lib/client.js#L285-L302 // Ported from PostgreSQL 9.2.4 source code in src/interfaces/libpq/fe-exec.c // Trivial performance optimisations by Benjie. diff --git a/packages/postgraphile-core/__tests__/helpers-v5.js b/packages/postgraphile-core/__tests__/helpers-v5.js index 40f7fd493..ddd5e0c46 100644 --- a/packages/postgraphile-core/__tests__/helpers-v5.js +++ b/packages/postgraphile-core/__tests__/helpers-v5.js @@ -24,6 +24,7 @@ import { withPgClient, getServerVersionNum } from "./helpers"; import jsonwebtoken from "jsonwebtoken"; import { createPostGraphileSchema } from ".."; import { makeExtendSchemaPlugin, gql } from "graphile-utils"; +import DistinctOnPlugin from "./integration/DistinctOnPlugin"; import ToyCategoriesPlugin from "./integration/ToyCategoriesPlugin"; /** @@ -468,6 +469,7 @@ const makeSchema = config => { appendPlugins: [ ExtendedPlugin, config.ToyCategoriesPlugin ? ToyCategoriesPlugin : null, + config.DistinctOnPlugin ? DistinctOnPlugin : null, ].filter(isNotNullish), } ); diff --git a/packages/postgraphile-core/__tests__/integration/DistinctOnPlugin.js b/packages/postgraphile-core/__tests__/integration/DistinctOnPlugin.js new file mode 100644 index 000000000..35574cf44 --- /dev/null +++ b/packages/postgraphile-core/__tests__/integration/DistinctOnPlugin.js @@ -0,0 +1,48 @@ +module.exports = builder => { + builder.hook( + "GraphQLObjectType:fields:field:args", + (args, build, context) => { + const { + graphql: { GraphQLString, GraphQLList }, + pgSql: sql, + } = build; + + const { + Self, + scope: { fieldName }, + addArgDataGenerator, + } = context; + + if (!(Self.name === "Query" && fieldName === "allToys")) { + return args; + } + + addArgDataGenerator(({ distinct }) => { + return { + pgQuery: queryBuilder => { + distinct?.map(field => { + // THIS IS AN EXAMPLE FOR THE TESTS. DO NOT USE THIS IN REAL PRODUCTION CODE! + // Instead you should use an enum to indicate the allowed identifiers; otherwise + // you risk interacting with the system columns and maybe worse. + const id = sql.fragment`${queryBuilder.getTableAlias()}.${sql.identifier( + field + )}`; + + queryBuilder.distinctOn(id); + }); + }, + }; + }); + + return build.extend( + args, + { + distinct: { + type: new GraphQLList(GraphQLString), + }, + }, + "test" + ); + } + ); +}; diff --git a/packages/postgraphile-core/__tests__/kitchen-sink-data.sql b/packages/postgraphile-core/__tests__/kitchen-sink-data.sql index 2070c9671..672fb1785 100644 --- a/packages/postgraphile-core/__tests__/kitchen-sink-data.sql +++ b/packages/postgraphile-core/__tests__/kitchen-sink-data.sql @@ -276,6 +276,12 @@ insert into named_query_builder.toy_categories(toy_id, category_id, approved) va (4, 2, true), (1, 3, false); +insert into distinct_query_builder.toys (id, name, color) values + (1, 'Rex', 'green'), + (2, 'Toy Soldiers', 'green'), + (3, 'Dino-Rocket Launcher', 'red'), + (4, 'History of Dinosaurs book', 'red'); + -------------------------------------------------------------------------------- alter sequence enum_tables.letter_descriptions_id_seq restart with 101; insert into enum_tables.letter_descriptions(letter, letter_via_view, description) values diff --git a/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql b/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql index f26de5d5f..b0d551653 100644 --- a/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql +++ b/packages/postgraphile-core/__tests__/kitchen-sink-schema.sql @@ -13,6 +13,7 @@ drop schema if exists large_bigint, network_types, named_query_builder, + distinct_query_builder, enum_tables, geometry cascade; @@ -1140,6 +1141,16 @@ create table named_query_builder.toy_categories ( -------------------------------------------------------------------------------- +create schema distinct_query_builder; + +create table distinct_query_builder.toys ( + id serial primary key, + name text not null, + color text not null +); + +-------------------------------------------------------------------------------- + create schema enum_tables; create table enum_tables.abcd (letter text primary key, description text); comment on column enum_tables.abcd.description is E'@enumDescription'; diff --git a/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.json5 b/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.json5 new file mode 100644 index 000000000..403985e11 --- /dev/null +++ b/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.json5 @@ -0,0 +1,132 @@ +{ + t1: { + nodes: [ + { + id: 1, + name: "Rex", + color: "green", + }, + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + ], + }, + t2: { + nodes: [ + { + id: 1, + name: "Rex", + color: "green", + }, + { + id: 2, + name: "Toy Soldiers", + color: "green", + }, + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + { + id: 4, + name: "History of Dinosaurs book", + color: "red", + }, + ], + }, + t3: { + nodes: [ + { + id: 1, + name: "Rex", + color: "green", + }, + { + id: 2, + name: "Toy Soldiers", + color: "green", + }, + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + { + id: 4, + name: "History of Dinosaurs book", + color: "red", + }, + ], + }, + t4: { + totalCount: 4, + nodes: [ + { + id: 1, + name: "Rex", + color: "green", + }, + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + ], + }, + t5: { + nodes: [ + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + { + id: 1, + name: "Rex", + color: "green", + }, + ], + }, + t6: { + totalCount: 4, + nodes: [ + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + ], + pageInfo: { + endCursor: "WyJuYW1lX2FzYyIsImNvbG9yX2Rlc2MiLFsicmVkIiwiRGluby1Sb2NrZXQgTGF1bmNoZXIiLDNdXQ==", + }, + }, + t7: { + totalCount: 4, + nodes: [ + { + id: 4, + name: "History of Dinosaurs book", + color: "red", + }, + ], + pageInfo: { + endCursor: "WyJuYW1lX2FzYyIsImNvbG9yX2Rlc2MiLFsicmVkIiwiSGlzdG9yeSBvZiBEaW5vc2F1cnMgYm9vayIsNF1d", + }, + }, + t8: { + totalCount: 4, + nodes: [ + { + id: 3, + name: "Dino-Rocket Launcher", + color: "red", + }, + ], + pageInfo: { + endCursor: "WyJuYW1lX2FzYyIsImNvbG9yX2Rlc2MiLFsicmVkIiwiRGluby1Sb2NrZXQgTGF1bmNoZXIiLDNdXQ==", + }, + }, +} diff --git a/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.sql b/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.sql new file mode 100644 index 000000000..7debc8d6d --- /dev/null +++ b/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.sql @@ -0,0 +1,444 @@ +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes" + from ( + select distinct on (__local_1__."color") __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) and (TRUE) + order by __local_1__."color" ASC, + __local_1__."id" ASC + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes" + from ( + select distinct on ( + __local_1__."id", + __local_1__."name" + ) __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) and (TRUE) + order by __local_1__."id" ASC, + __local_1__."name" ASC + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes" + from ( + select __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) and (TRUE) + order by __local_1__."id" ASC + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes" + from ( + select distinct on (__local_1__."color") __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) and (TRUE) + order by __local_1__."color" ASC, + __local_1__."id" ASC + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data", +( + select json_build_object( + 'totalCount'::text, + count(1) + ) + from "distinct_query_builder"."toys" as __local_1__ + where 1 = 1 +) as "aggregates" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes" + from ( + select distinct on (__local_1__."color") __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) and (TRUE) + order by __local_1__."color" DESC, + __local_1__."id" ASC + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes", + to_json( + json_build_array( + 'name_asc', + 'color_desc', + json_build_array( + __local_1__."color", + __local_1__."name", + __local_1__."id" + ) + ) + ) as "__cursor" + from ( + select distinct on (__local_1__."color") __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) and (TRUE) + order by __local_1__."color" DESC, + __local_1__."name" ASC, + __local_1__."id" ASC + limit 1 + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data", +( + select json_build_object( + 'totalCount'::text, + count(1) + ) + from "distinct_query_builder"."toys" as __local_1__ + where 1 = 1 +) as "aggregates" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes", + to_json( + json_build_array( + 'name_asc', + 'color_desc', + json_build_array( + __local_1__."color", + __local_1__."name", + __local_1__."id" + ) + ) + ) as "__cursor" + from ( + select distinct on (__local_1__."color") __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where ( + ( + ( + ( + ( + 'name_asc', + 'color_desc' + ) = ( + $1, + $2 + ) + ) AND ( + ( + ( + __local_1__."color" < $3 + ) OR ( + __local_1__."color" = $3 AND ( + ( + __local_1__."name" > $4 + ) OR ( + __local_1__."name" = $4 AND ( + ( + __local_1__."id" > $5 + ) OR ( + __local_1__."id" = $5 AND false + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) and (TRUE) + order by __local_1__."color" DESC, + __local_1__."name" ASC, + __local_1__."id" ASC + limit 1 + ) __local_1__ +), +__local_2__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_2__.data + from __local_2__ + ), + '[]'::json +) as "data", +( + select json_build_object( + 'totalCount'::text, + count(1) + ) + from "distinct_query_builder"."toys" as __local_1__ + where 1 = 1 +) as "aggregates" + +with __local_0__ as ( + select to_json( + ( + json_build_object( + '__identifiers'::text, + json_build_array(__local_1__."id"), + 'id'::text, + (__local_1__."id"), + 'name'::text, + (__local_1__."name"), + 'color'::text, + (__local_1__."color") + ) + ) + ) as "@nodes", + to_json( + json_build_array( + 'name_asc', + 'color_desc', + json_build_array( + __local_1__."color", + __local_1__."name", + __local_1__."id" + ) + ) + ) as "__cursor" + from ( + with __local_2__ as ( + select distinct on (__local_1__."color") __local_1__.* + from "distinct_query_builder"."toys" as __local_1__ + where (TRUE) + and ( + ( + ( + ( + ( + 'name_asc', + 'color_desc' + ) = ( + $1, + $2 + ) + ) AND ( + ( + ( + __local_1__."color" > $3 + ) OR ( + __local_1__."color" = $3 AND ( + ( + __local_1__."name" < $4 + ) OR ( + __local_1__."name" = $4 AND ( + ( + __local_1__."id" < $5 + ) OR ( + __local_1__."id" = $5 AND false + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + order by __local_1__."color" ASC, + __local_1__."name" DESC, + __local_1__."id" DESC + limit 1 + ) + select * + from __local_2__ + order by ( + row_number( ) over (partition by 1) + ) desc + ) __local_1__ +), +__local_3__ as ( + select json_agg( + to_json(__local_0__) + ) as data + from __local_0__ +) +select coalesce( + ( + select __local_3__.data + from __local_3__ + ), + '[]'::json +) as "data", +( + select json_build_object( + 'totalCount'::text, + count(1) + ) + from "distinct_query_builder"."toys" as __local_1__ + where 1 = 1 +) as "aggregates" \ No newline at end of file diff --git a/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.test.graphql b/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.test.graphql new file mode 100644 index 000000000..383fdf3d2 --- /dev/null +++ b/packages/postgraphile-core/__tests__/queries/base/distinct_query_builder.toys.test.graphql @@ -0,0 +1,85 @@ +## expect(errors).toBeFalsy(); +#> schema: ["distinct_query_builder"] +#> subscriptions: true +#> DistinctOnPlugin: true +{ + t1: allToys(distinct: ["color"]) { + nodes { + id + name + color + } + } + t2: allToys(distinct: ["id", "name"]) { + nodes { + id + name + color + } + } + t3: allToys { + nodes { + id + name + color + } + } + t4: allToys(distinct: ["color"]) { + totalCount + nodes { + id + name + color + } + } + t5: allToys(distinct: ["color"], orderBy: COLOR_DESC) { + nodes { + id + name + color + } + } + t6: allToys(distinct: ["color"], orderBy: [NAME_ASC, COLOR_DESC], first: 1) { + totalCount + nodes { + id + name + color + } + pageInfo { + endCursor + } + } + t7: allToys( + distinct: ["color"] + orderBy: [NAME_ASC, COLOR_DESC] + first: 1 + after: "WyJuYW1lX2FzYyIsImNvbG9yX2Rlc2MiLFsicmVkIiwiRGluby1Sb2NrZXQgTGF1bmNoZXIiLDNdXQ==" # cursor from t6 + ) { + totalCount + nodes { + id + name + color + } + pageInfo { + endCursor + } + } + t8: allToys( + distinct: ["color"] + orderBy: [NAME_ASC, COLOR_DESC] + last: 1 + before: "WyJuYW1lX2FzYyIsImNvbG9yX2Rlc2MiLFsicmVkIiwiSGlzdG9yeSBvZiBEaW5vc2F1cnMgYm9vayIsNF1d" # cursor from t7 + ) { + totalCount + nodes { + id + name + color + } + pageInfo { + endCursor + } + } +}