Skip to content

Fix block height not being applied to certain historical queries #2398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
packages/**/dist/**
packages/**/lib/**
.eslintrc.js
*.proto
*.proto
*.ts.snap
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
*.cmd
*.sh
generate-project*.yaml
*.proto
*.proto
*.ts.snap
2 changes: 2 additions & 0 deletions packages/query/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Fixed
- Certain historical queries not appliying block height (#2398)

## [2.10.5] - 2024-05-03
### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`GraphqlHistorical to filter historical items when ordering 1`] = `
"with __local_0__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_1__."_id"), '@listings'::text, (with __local_2__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_3__."_id"), 'priceAmount'::text, ((__local_3__."price_amount"))::text))) as "@nodes" from (select
__local_3__.*
from "subquery_2"."listings" as __local_3__

where (__local_3__."item_id" = __local_1__."id") and (__local_3__._block_range @> $1::bigint) and (__local_3__._block_range @> $1::bigint) and (TRUE) and (TRUE)
order by __local_3__."_id" ASC

) __local_3__), __local_4__ as (select json_agg(to_json(__local_2__)) as data from __local_2__) select json_build_object('data'::text, coalesce((select __local_4__.data from __local_4__), '[]'::json)) )))) as "@nodes" from (select
__local_1__.*
from "subquery_2"."items" as __local_1__

where (__local_1__._block_range @> $1::bigint) and (TRUE) and (TRUE)
order by __local_1__."last_traded_price_amount" ASC,__local_1__."_id" ASC

) __local_1__), __local_5__ as (select json_agg(to_json(__local_0__)) as data from __local_0__) select coalesce((select __local_5__.data from __local_5__), '[]'::json) as "data" "
`;

exports[`GraphqlHistorical to filter historical nested (backward) 1`] = `
"with __local_0__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_1__."_id"), 'id'::text, (__local_1__."id")))) as "@nodes" from (select
__local_1__.*
from "subquery_2"."items" as __local_1__

where (__local_1__._block_range @> $1::bigint) and (((exists(select 1 from "subquery_2"."listings" as __local_2__ where (__local_2__."item_id" = __local_1__."id") and (__local_2__._block_range @> $1::bigint) and (((__local_2__."price_token" = $2))) and (__local_2__._block_range @> $1::bigint))))) and (TRUE) and (TRUE)
order by __local_1__."_id" ASC

) __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" "
`;

exports[`GraphqlHistorical to filter historical nested (forward) 1`] = `
"with __local_0__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_1__."_id"), 'id'::text, (__local_1__."id")))) as "@nodes" from (select
__local_1__.*
from "subquery_2"."listings" as __local_1__

where (__local_1__._block_range @> $1::bigint) and (( exists(
select 1 from "subquery_2"."items" as __local_2__
where (__local_1__."item_id" = __local_2__."id") and (__local_1__._block_range @> $1::bigint) and
(((__local_2__."approved" = $2))) and (__local_2__._block_range @> $1::bigint)
))) and (TRUE) and (TRUE)
order by __local_1__."_id" ASC

) __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" "
`;

exports[`GraphqlHistorical to filter historical top level 1`] = `
"with __local_0__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_1__."_id"), 'id'::text, (__local_1__."id"), '@listings'::text, (with __local_2__ as (select to_json((json_build_object('__identifiers'::text, json_build_array(__local_3__."_id"), 'id'::text, (__local_3__."id")))) as "@nodes" from (select
__local_3__.*
from "subquery_2"."listings" as __local_3__

where (__local_3__."item_id" = __local_1__."id") and (__local_3__._block_range @> $1::bigint) and (__local_3__._block_range @> $1::bigint) and (TRUE) and (TRUE)
order by __local_3__."_id" ASC

) __local_3__), __local_4__ as (select json_agg(to_json(__local_2__)) as data from __local_2__) select json_build_object('data'::text, coalesce((select __local_4__.data from __local_4__), '[]'::json)) )))) as "@nodes" from (select
__local_1__.*
from "subquery_2"."items" as __local_1__

where (__local_1__._block_range @> $1::bigint) and ((exists(select 1 from "subquery_2"."listings" as __local_5__ where (__local_5__."item_id" = __local_1__."id") and (__local_5__._block_range @> $1::bigint)))) and (TRUE) and (TRUE)
order by __local_1__."_id" ASC

) __local_1__), __local_6__ as (select json_agg(to_json(__local_0__)) as data from __local_0__) select coalesce((select __local_6__.data from __local_6__), '[]'::json) as "data", (
select json_build_object('totalCount'::text, count(1))
from "subquery_2"."items" as __local_1__
where (__local_1__._block_range @> $1::bigint) and ((exists(select 1 from "subquery_2"."listings" as __local_5__ where (__local_5__."item_id" = __local_1__."id") and (__local_5__._block_range @> $1::bigint))))
) as "aggregates" "
`;
191 changes: 191 additions & 0 deletions packages/query/src/graphql/graphql.historical.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2020-2024 SubQuery Pte Ltd authors & contributors
// SPDX-License-Identifier: GPL-3.0

import {getPostGraphileBuilder} from '@subql/x-postgraphile-core';
import {ApolloServer, ExpressContext, gql} from 'apollo-server-express';
import {Pool} from 'pg';
import {Config} from '../configure';
import {getYargsOption} from '../yargs';
import {plugins} from './plugins';

jest.mock('../yargs', () => jest.createMockFromModule('../yargs'));

(getYargsOption as jest.Mock).mockImplementation(() => {
return {argv: {name: 'test', aggregate: true}};
});

describe('GraphqlHistorical', () => {
const dbSchema = 'subquery_2';

const config = new Config({});

const pool: Pool = new Pool({
user: config.get('DB_USER'),
password: config.get('DB_PASS'),
host: config.get('DB_HOST_READ') ?? config.get('DB_HOST'),
port: config.get('DB_PORT'),
database: config.get('DB_DATABASE'),
});

pool.on('error', (err) => {
console.error('PostgreSQL client generated error: ', err.message);
});

let server: ApolloServer<ExpressContext>;
let sqlSpy: jest.SpyInstance<void, [queryText: string, values: any[], callback?: any]>;

async function createApolloServer() {
const builder = await getPostGraphileBuilder(pool, [dbSchema], {
replaceAllPlugins: plugins,
subscriptions: true,
dynamicJson: true,
});

const schema = builder.buildSchema();

const server = new ApolloServer({
schema,
context: {
pgClient: pool,
},
});

return server;
}

beforeAll(async () => {
await pool.query(`CREATE SCHEMA IF NOT EXISTS ${dbSchema}`);
await pool.query(`CREATE TABLE IF NOT EXISTS "${dbSchema}".listings (
id text NOT NULL,
item_id text NOT NULL,
collection_id text NOT NULL,
price_amount numeric NOT NULL,
price_token text NULL,
expires_at jsonb NULL,
created_at_block_height numeric NOT NULL,
created_at_block_time timestamp NOT NULL,
created_at_tx_hash text NOT NULL,
updated_at_block_height numeric NULL,
updated_at_block_time timestamp NULL,
updated_at_tx_hash text NULL,
"_id" uuid NOT NULL,
"_block_range" int8range NOT NULL,
CONSTRAINT listings_pkey PRIMARY KEY (_id)
);`);
await pool.query(`CREATE TABLE IF NOT EXISTS "${dbSchema}".items (
id text NOT NULL,
collection_id text NOT NULL,
token_id text NOT NULL,
owner_id text NOT NULL,
token_uri text NULL,
"extension" text NULL,
metadata jsonb NULL,
last_traded_price_amount numeric NULL,
approved bool NOT NULL,
created_at_block_height numeric NOT NULL,
created_at_block_time timestamp NOT NULL,
created_at_tx_hash text NOT NULL,
updated_at_block_height numeric NULL,
updated_at_block_time timestamp NULL,
updated_at_tx_hash text NULL,
"_id" uuid NOT NULL,
"_block_range" int8range NOT NULL,
CONSTRAINT items_pkey PRIMARY KEY (_id)
);`);
await pool.query(`COMMENT ON TABLE "${dbSchema}".listings IS '@foreignFieldName listings
@foreignKey (item_id) REFERENCES items (id)|@singleForeignFieldName listing';`);
await pool.query(`COMMENT ON TABLE "${dbSchema}".items IS '@foreignFieldName items';`);

server = await createApolloServer();
sqlSpy = jest.spyOn(pool, 'query');
});

beforeEach(() => {
sqlSpy.mockClear();
});

afterAll(async () => {
await pool.query(`DROP TABLE ${dbSchema}.listings;`);
await pool.query(`DROP TABLE ${dbSchema}.items;`);
await pool.query(`DROP SCHEMA ${dbSchema};`);
await pool.end();
});

it('to filter historical items when ordering', async () => {
const GQL_QUERY = gql`
query nfts {
items(orderBy: LAST_TRADED_PRICE_AMOUNT_ASC) {
nodes {
listings {
nodes {
priceAmount
}
}
}
}
}
`;

const res = await server.executeOperation({query: GQL_QUERY});
expect(res.errors).toBeUndefined();

expect(sqlSpy.mock.calls[0][0]).toMatchSnapshot();
});

it('to filter historical top level', async () => {
const GQL_QUERY = gql`
query NFTsOnSale {
items(filter: {listingsExist: true}) {
nodes {
id
listings {
nodes {
id
}
}
}
totalCount
}
}
`;

const res = await server.executeOperation({query: GQL_QUERY});
expect(res.errors).toBeUndefined();

expect(sqlSpy.mock.calls[0][0]).toMatchSnapshot();
});

it('to filter historical nested (forward)', async () => {
const GQL_QUERY = gql`
query {
listings(filter: {item: {approved: {equalTo: true}}}) {
nodes {
id
}
}
}
`;

const res = await server.executeOperation({query: GQL_QUERY});
expect(res.errors).toBeUndefined();

expect(sqlSpy.mock.calls[0][0]).toMatchSnapshot();
});

it('to filter historical nested (backward)', async () => {
const GQL_QUERY = gql`
query {
items(filter: {listings: {some: {priceToken: {equalTo: "foo"}}}}) {
nodes {
id
}
}
}
`;

const res = await server.executeOperation({query: GQL_QUERY});
expect(res.errors).toBeUndefined();

expect(sqlSpy.mock.calls[0][0]).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,37 @@ export function buildWhereConditionBackward(
return sql.query`(${sql.join(fkMatches, ') and (')})`;
}

export function connectionFilterResolveBlockHeight(
fieldValue: unknown,
foreignTableAlias: SQL,
foreignTableFilterTypeName: string,
queryBuilder: QueryBuilder,
connectionFilterResolve: (
fieldValue: unknown,
foreignTableAlias: SQL,
foreignTableFilterTypeName: string,
queryBuilder: QueryBuilder
// There are more args but they don't seem to be used
) => SQL | null,
foreignTable: PgEntity,
sql: any
): SQL | null {
const sqlFragment = connectionFilterResolve(fieldValue, foreignTableAlias, foreignTableFilterTypeName, queryBuilder);

if (sqlFragment === null) {
return null;
}

if (queryBuilder.context.args?.blockHeight === undefined || !hasBlockRange(foreignTable)) {
return sqlFragment;
}

return sql.join(
[sqlFragment, makeRangeQuery(foreignTableAlias, queryBuilder.context.args.blockHeight, sql)],
') and ('
);
}

const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = (
builder,
{pgSimpleCollections, pgOmitListSuffix, connectionFilterUseListInflectors}
Expand Down Expand Up @@ -205,17 +236,21 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = (
sql
);

/******************************
* HISTORICAL CHANGES END
*******************************/

const sqlSelectWhereKeysMatch = sql.query`select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`;
const sqlFragment = connectionFilterResolve(
const sqlFragment = connectionFilterResolveBlockHeight(
fieldValue,
foreignTableAlias,
foreignTableFilterTypeName,
queryBuilder
queryBuilder,
connectionFilterResolve,
foreignTable,
sql
);

/******************************
* HISTORICAL CHANGES END
*******************************/

return sqlFragment == null ? null : sql.query`exists(${sqlSelectWhereKeysMatch} and (${sqlFragment}))`;
};

Expand All @@ -231,7 +266,6 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = (
/******************************
* HISTORICAL CHANGES BEGIN
*******************************/

const sqlKeysMatch = buildWhereConditionBackward(
table,
foreignTableAlias,
Expand Down Expand Up @@ -443,7 +477,6 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = (
/******************************
* HISTORICAL CHANGES BEGIN
*******************************/

const sqlKeysMatch = buildWhereConditionBackward(
foreignTable,
foreignTableAlias,
Expand All @@ -460,12 +493,16 @@ const PgConnectionArgFilterBackwardRelationsPlugin: Plugin = (

const sqlSelectWhereKeysMatch = sql.query`select 1 from ${sqlIdentifier} as ${foreignTableAlias} where ${sqlKeysMatch}`;

const sqlFragment = connectionFilterResolve(
const sqlFragment = connectionFilterResolveBlockHeight(
fieldValue,
foreignTableAlias,
foreignTableFilterTypeName,
queryBuilder
queryBuilder,
connectionFilterResolve,
foreignTable,
sql
);

if (sqlFragment == null) {
return null;
} else if (fieldName === 'every') {
Expand Down
Loading
Loading