diff --git a/src/get.js b/src/get.js index c100e50..93538b1 100644 --- a/src/get.js +++ b/src/get.js @@ -141,6 +141,18 @@ export default function buildQuery(opts, dareInstance) { throw new DareError(DareError.INVALID_REQUEST, 'Missing fields'); } + /* + * Workaround for MySQL 8.0 bug https://bugs.mysql.com/bug.php?id=109585 + * -> When, all fields are aggregates + * -> And, this is a subquery + * -> Then, remove the limit + */ + if (dareInstance.engine?.startsWith('mysql:8') && alias) { + if (fields.every(item => item.agg)) { + opts.limit = null; + }; + } + // Put it all together let sql = SQL`SELECT ${join(sql_fields)} FROM ${raw(sql_table)} ${raw(sql_alias)} diff --git a/test/integration/disparities.spec.js b/test/integration/disparities.spec.js new file mode 100644 index 0000000..00956de --- /dev/null +++ b/test/integration/disparities.spec.js @@ -0,0 +1,115 @@ +import Dare from '../../src/index.js'; +import Debug from 'debug'; +import assert from 'node:assert/strict'; +import mysql from 'mysql2/promise'; +import db from './helpers/db.js'; +import {options} from './helpers/api.js'; +import SQL from 'sql-template-tag'; +const debug = Debug('sql'); + +// Connect to db + +describe(`Disparities`, () => { + let dare; + + beforeEach(() => { + // Initiate + dare = new Dare(options); + + // Set a test instance + // eslint-disable-next-line arrow-body-style + dare.execute = query => { + // DEBUG + debug(mysql.format(query.sql, query.values)); + + return db.query(query); + }; + }); + + it('MySQL 8 fails to correctly count the items in this scenario', async () => { + /* + * See Bug report: https://bugs.mysql.com/bug.php?id=109585 + */ + await dare.sql(SQL` + + CREATE TABLE members ( + id int NOT NULL, + name VARCHAR(30) NOT NULL, + PRIMARY KEY (id) + ); + + CREATE TABLE content ( + id int NOT NULL, + name VARCHAR(30) NOT NULL, + PRIMARY KEY (id) + ); + + CREATE TABLE domains ( + id smallint NOT NULL, + name VARCHAR(100) NULL, + PRIMARY KEY (id) + ); + + CREATE TABLE userContent ( + id int NOT NULL, + content_id int NOT NULL, + user_id int NOT NULL, + domain_id smallint NOT NULL, + status VARCHAR(20) NOT NULL DEFAULT 'notstarted', + PRIMARY KEY (id), + CONSTRAINT content_user_domain_id UNIQUE (content_id,user_id,domain_id), + CONSTRAINT fk_userContent_domain_id FOREIGN KEY (domain_id) REFERENCES domains (id), + CONSTRAINT userContent_content FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE, + CONSTRAINT userContent_user FOREIGN KEY (user_id) REFERENCES members (id) ON DELETE CASCADE + ); + + -- --------------------------------- + -- INSERT + -- --------------------------------- + + INSERT INTO domains (id, name) VALUES (1, 'Test'); + + INSERT INTO content (id, name) VALUES (1, 'A'), (2, 'B'), (3, 'C'); + + INSERT INTO members (id,name) + VALUES + (11, 'devuser11@example.com'), + (12, 'devuser12@example.com'); + + INSERT INTO userContent (id, domain_id,content_id,user_id,status) + VALUES + (1, 1, 3, 11, 'completed'), + (2, 1, 3, 12, 'completed'), + (3, 1, 2, 12, 'completed'); + + `); + + const status = 'completed'; + const domain_id = 1; + + dare.options.models.userContent = { + schema: { + content_id: ['content.id'], + }, + }; + + // Construct a query which counts these + const resp = await dare.get({ + table: 'content', + fields: ['id', {count: 'COUNT(DISTINCT userContent.user_id)'}], + join: { + userContent: { + status, + domain_id, + }, + }, + limit: 3, + }); + + assert.deepStrictEqual(resp, [ + {id: 1, count: 0}, + {id: 2, count: 1}, + {id: 3, count: 2}, + ]); + }); +}); diff --git a/test/integration/helpers/Postgres.js b/test/integration/helpers/Postgres.js index bbe147c..c64e5c1 100644 --- a/test/integration/helpers/Postgres.js +++ b/test/integration/helpers/Postgres.js @@ -3,6 +3,13 @@ import pg from 'pg'; import fs from 'node:fs'; import QueryStream from 'pg-query-stream'; +/* + * Aggregate functions are returned as BigInts which by default are converted to strings + * This changes floats and ints to their respective types + */ +pg.types.setTypeParser(1700, parseFloat) +pg.types.setTypeParser(20, parseInt); + const {TEST_DB_DATA_PATH, TEST_DB_SCHEMA_PATH} = process.env; const schemaSql = fs.readFileSync(TEST_DB_SCHEMA_PATH); diff --git a/test/specs/get-subquery.spec.js b/test/specs/get-subquery.spec.js index e8ae266..40398fe 100644 --- a/test/specs/get-subquery.spec.js +++ b/test/specs/get-subquery.spec.js @@ -376,4 +376,35 @@ describe('get - subquery', () => { }); }); }); + + + + describe(`Disparities`, () => { + + it('MySQL 8 fails to correctly count the items in this scenario', async () => { + /* + * See Bug report: https://bugs.mysql.com/bug.php?id=109585 + */ + const dareInst = dare.use({engine: 'mysql:8.0.36'}); + + dareInst.options.models.userContent = { + schema: { + content_id: ['content.id'], + }, + }; + + dareInst.sql = async ({sql}) => { + expect(sql).to.not.contain('LIMIT 1'); + }; + + // Construct a query which counts these + await dareInst.get({ + table: 'content', + fields: ['id', {count: 'COUNT(DISTINCT userContent.user_id)'}], + limit: 3, + }); + + }); + }); + });