From f27518c60fa212d1abe6c5cbc5530ea9406fcec8 Mon Sep 17 00:00:00 2001 From: Muhammad Aaqil Date: Sun, 1 Nov 2026 13:23:54 +0500 Subject: [PATCH] feat: add support for jsonContains operator on array fields Signed-off-by: Muhammad Aaqil --- lib/mysql.js | 19 +++++++++++++++++ test/mysql.test.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/lib/mysql.js b/lib/mysql.js index e8f7165c6..d66d41f1b 100644 --- a/lib/mysql.js +++ b/lib/mysql.js @@ -649,6 +649,25 @@ MySQL.prototype.buildExpression = function(columnName, operator, operatorValue, clause = ` MATCH (${columnName}) AGAINST (?${mode})`; return new ParameterizedSQL(clause, [operatorValue]); + // Map the abstract 'jsonContains' operator to MySQL's native JSON_CONTAINS function, + // supporting array or string-serialized inputs for array-based containment checks. + case 'jsonContains': + if (typeof operatorValue === 'string') { + try { + operatorValue = JSON.parse(operatorValue); + } catch (e) { + operatorValue = [operatorValue]; + } + } + if (!Array.isArray(operatorValue)) { + operatorValue = [operatorValue]; + } + const conditions = []; + operatorValue.forEach(value => { + conditions.push(`JSON_CONTAINS(${columnName}, CAST(? as JSON))`); + }); + clause = '(' + conditions.join(' OR ') + ')'; + return new ParameterizedSQL(clause, operatorValue); } // invoke the base implementation of `buildExpression` diff --git a/test/mysql.test.js b/test/mysql.test.js index 44c02808b..77c2d5dbf 100644 --- a/test/mysql.test.js +++ b/test/mysql.test.js @@ -1076,6 +1076,59 @@ describe('mysql', function() { }); }); + context('jsonContains operator', function() { + let Customer; + + before(function(done) { + // Define a test model with an array property to mimic referencesMany relation mapping. + Customer = db.define('CustomerWithJson', { + name: {type: String}, + ids: {type: [Number]}, + }, { + forceId: false, + }); + db.automigrate(['CustomerWithJson'], done); + }); + + beforeEach(function(done) { + Customer.destroyAll(done); + }); + + after(function(done) { + Customer.destroyAll(done); + }); + + it('should build where with jsonContains and match records', function(done) { + Customer.createAll([ + {name: 'Alice', ids: [1, 2]}, + {name: 'Bob', ids: [3, 4]}, + ], function(err) { + should.not.exist(err); + + // Execute a find query leveraging the mapped jsonContains operator. + Customer.find({where: {ids: {jsonContains: [2]}}}, function(err, customers) { + should.not.exist(err); + customers.should.have.property('length', 1); + customers[0].name.should.equal('Alice'); + done(); + }); + }); + }); + + it('should support unit-level SQL building verification for jsonContains', function() { + // Verify that buildWhere constructs the exact MySQL-native JSON_CONTAINS syntax and parameter bindings. + const where = db.connector.buildWhere( + 'CustomerWithJson', + {ids: {jsonContains: [1, 2]}}, + ); + + where.toJSON().should.eql({ + sql: 'WHERE (JSON_CONTAINS(`ids`, CAST(? as JSON)) OR JSON_CONTAINS(`ids`, CAST(? as JSON)))', + params: [1, 2], + }); + }); + }); + function deleteAllModelInstances() { const models = [ Post, PostWithStringId, PostWithUniqueTitle, PostWithNumId, Student,