diff --git a/lib/dialects/mysql/query-generator.js b/lib/dialects/mysql/query-generator.js index 556e857bae7e..90a2c0104b3c 100644 --- a/lib/dialects/mysql/query-generator.js +++ b/lib/dialects/mysql/query-generator.js @@ -164,7 +164,7 @@ const QueryGenerator = { // Parse nested object if (smth.conditions) { const conditions = _.map(this.parseConditionObject(smth.conditions), condition => - `${this.quoteIdentifier(_.first(condition.path))}->>'\$.${_.tail(condition.path).join('.')}' = '${condition.value}'` + `${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'` ); return conditions.join(' and '); @@ -176,26 +176,9 @@ const QueryGenerator = { str = smth.path; } else { // Also support json dot notation - let path = smth.path; - let startWithDot = true; - - // Convert .number. to [number]. - path = path.replace(/\.(\d+)\./g, '[$1].'); - // Convert .number$ to [number] - path = path.replace(/\.(\d+)$/, '[$1]'); - - path = path.split('.'); - - let columnName = path.shift(); - const match = columnName.match(/\[\d+\]$/); - // If columnName ends with [\d+] - if (match !== null) { - path.unshift(columnName.substr(match.index)); - columnName = columnName.substr(0, match.index); - startWithDot = false; - } - - str = `${this.quoteIdentifier(columnName)}->>'\$${startWithDot ? '.' : ''}${path.join('.')}'`; + const paths = _.toPath(smth.path); + const column = paths.shift(); + str = this.jsonPathExtractionQuery(column, paths); } if (smth.value) { @@ -474,10 +457,14 @@ const QueryGenerator = { * * https://bugs.mysql.com/bug.php?id=81896 */ - const paths = _.toPath(path).map(subPath => Utils.addTicks(subPath, '"')); - const pathStr = `${['$'].concat(paths).join('.')}`; + const paths = _.toPath(path).map(subPath => { + return /\D/.test(subPath) + ? Utils.addTicks(subPath, '"') + : subPath; + }); + const pathStr = this.escape(`${['$'].concat(paths).join('.').replace(/\.(\d+)(?:(?=\.)|$)/g, (__, digit) => `[${digit}]`)}`); const quotedColumn = this.isIdentifierQuoted(column) ? column : this.quoteIdentifier(column); - return `(${quotedColumn}->>'${pathStr}')`; + return `(${quotedColumn}->>${pathStr})`; }, /** diff --git a/test.sqlite b/test.sqlite new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/integration/model/json.test.js b/test/integration/model/json.test.js index 15ad9ee06e65..e090a87412d7 100644 --- a/test/integration/model/json.test.js +++ b/test/integration/model/json.test.js @@ -684,6 +684,14 @@ describe(Support.getTestDialectTeaser('Model'), () => { }); }); + it('should properly escape path keys with sequelize.json', function() { + return this.Model.findAll({ + raw: true, + attributes: ['id'], + where: this.sequelize.json("data.id')) AS DECIMAL) = 1 DELETE YOLO INJECTIONS; -- ", '1') + }); + }); + it('should properly escape the single quotes in array', function() { return this.Model.create({ data: { diff --git a/test/unit/sql/json.test.js b/test/unit/sql/json.test.js index 8a28e754fd62..f8fbface3b1f 100644 --- a/test/unit/sql/json.test.js +++ b/test/unit/sql/json.test.js @@ -81,7 +81,7 @@ if (current.dialect.supports.JSON) { expectsql(sql.whereItemQuery(undefined, Sequelize.json({ id: 1 })), { postgres: '("id"#>>\'{}\') = \'1\'', sqlite: "json_extract(`id`, '$') = '1'", - mysql: "`id`->>'$.' = '1'" + mysql: "(`id`->>'$') = '1'" }); }); @@ -89,7 +89,7 @@ if (current.dialect.supports.JSON) { expectsql(sql.whereItemQuery(undefined, Sequelize.json({ profile: { id: 1 } })), { postgres: '("profile"#>>\'{id}\') = \'1\'', sqlite: "json_extract(`profile`, '$.id') = '1'", - mysql: "`profile`->>'$.id' = '1'" + mysql: "(`profile`->>'$.\\\"id\\\"') = '1'" }); }); @@ -97,7 +97,7 @@ if (current.dialect.supports.JSON) { expectsql(sql.whereItemQuery(undefined, Sequelize.json({ property: { value: 1 }, another: { value: 'string' } })), { postgres: '("property"#>>\'{value}\') = \'1\' AND ("another"#>>\'{value}\') = \'string\'', sqlite: "json_extract(`property`, '$.value') = '1' AND json_extract(`another`, '$.value') = 'string'", - mysql: "`property`->>'$.value' = '1' and `another`->>'$.value' = 'string'" + mysql: "(`property`->>'$.\\\"value\\\"') = '1' and (`another`->>'$.\\\"value\\\"') = 'string'" }); }); @@ -105,7 +105,7 @@ if (current.dialect.supports.JSON) { expectsql(sql.whereItemQuery(Sequelize.json('profile.id'), '1'), { postgres: '("profile"#>>\'{id}\') = \'1\'', sqlite: "json_extract(`profile`, '$.id') = '1'", - mysql: "`profile`->>'$.id' = '1'" + mysql: "(`profile`->>'$.\\\"id\\\"') = '1'" }); }); @@ -113,7 +113,7 @@ if (current.dialect.supports.JSON) { expectsql(sql.whereItemQuery(Sequelize.json('json'), '{}'), { postgres: '("json"#>>\'{}\') = \'{}\'', sqlite: "json_extract(`json`, '$') = '{}'", - mysql: "`json`->>'$.' = '{}'" + mysql: "(`json`->>'$') = '{}'" }); }); }); diff --git a/test/unit/sql/where.test.js b/test/unit/sql/where.test.js index 39b9dd170718..3a16ef19c88e 100755 --- a/test/unit/sql/where.test.js +++ b/test/unit/sql/where.test.js @@ -778,7 +778,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { expectsql(sql.whereItemQuery(undefined, this.sequelize.json('profile.id', this.sequelize.cast('12346-78912', 'text'))), { postgres: "(\"profile\"#>>'{id}') = CAST('12346-78912' AS TEXT)", sqlite: "json_extract(`profile`, '$.id') = CAST('12346-78912' AS TEXT)", - mysql: "`profile`->>'$.id' = CAST('12346-78912' AS CHAR)" + mysql: "(`profile`->>'$.\\\"id\\\"') = CAST('12346-78912' AS CHAR)" }); }); @@ -786,7 +786,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { expectsql(sql.whereItemQuery(undefined, this.sequelize.json({profile: {id: '12346-78912', name: 'test'}})), { postgres: "(\"profile\"#>>'{id}') = '12346-78912' AND (\"profile\"#>>'{name}') = 'test'", sqlite: "json_extract(`profile`, '$.id') = '12346-78912' AND json_extract(`profile`, '$.name') = 'test'", - mysql: "`profile`->>'$.id' = '12346-78912' and `profile`->>'$.name' = 'test'" + mysql: "(`profile`->>'$.\\\"id\\\"') = '12346-78912' and (`profile`->>'$.\\\"name\\\"') = 'test'" }); }); @@ -800,7 +800,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { }, prefix: 'User' }, { - mysql: "(`User`.`data`->>'$.\"nested\".\"attribute\"') = 'value'", + mysql: "(`User`.`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') = 'value'", postgres: "(\"User\".\"data\"#>>'{nested,attribute}') = 'value'", sqlite: "json_extract(`User`.`data`, '$.nested.attribute') = 'value'" }); @@ -814,7 +814,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "CAST((`data`->>'$.\"nested\"') AS DECIMAL) IN (1, 2)", + mysql: "CAST((`data`->>'$.\\\"nested\\\"') AS DECIMAL) IN (1, 2)", postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) IN (1, 2)", sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) IN (1, 2)" }); @@ -828,7 +828,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "CAST((`data`->>'$.\"nested\"') AS DECIMAL) BETWEEN 1 AND 2", + mysql: "CAST((`data`->>'$.\\\"nested\\\"') AS DECIMAL) BETWEEN 1 AND 2", postgres: "CAST((\"data\"#>>'{nested}') AS DOUBLE PRECISION) BETWEEN 1 AND 2", sqlite: "CAST(json_extract(`data`, '$.nested') AS DOUBLE PRECISION) BETWEEN 1 AND 2" }); @@ -846,7 +846,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { }, prefix: current.literal(sql.quoteTable.call(current.dialect.QueryGenerator, {tableName: 'User'})) }, { - mysql: "((`User`.`data`->>'$.\"nested\".\"attribute\"') = 'value' AND (`User`.`data`->>'$.\"nested\".\"prop\"') != 'None')", + mysql: "((`User`.`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') = 'value' AND (`User`.`data`->>'$.\\\"nested\\\".\\\"prop\\\"') != 'None')", postgres: "((\"User\".\"data\"#>>'{nested,attribute}') = 'value' AND (\"User\".\"data\"#>>'{nested,prop}') != 'None')", sqlite: "(json_extract(`User`.`data`, '$.nested.attribute') = 'value' AND json_extract(`User`.`data`, '$.nested.prop') != 'None')" }); @@ -864,7 +864,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { }, prefix: 'User' }, { - mysql: "((`User`.`data`->>'$.\"name\".\"last\"') = 'Simpson' AND (`User`.`data`->>'$.\"employment\"') != 'None')", + mysql: "((`User`.`data`->>'$.\\\"name\\\".\\\"last\\\"') = 'Simpson' AND (`User`.`data`->>'$.\\\"employment\\\"') != 'None')", postgres: "((\"User\".\"data\"#>>'{name,last}') = 'Simpson' AND (\"User\".\"data\"#>>'{employment}') != 'None')", sqlite: "(json_extract(`User`.`data`, '$.name.last') = 'Simpson' AND json_extract(`User`.`data`, '$.employment') != 'None')" }); @@ -877,7 +877,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "(CAST((`data`->>'$.\"price\"') AS DECIMAL) = 5 AND (`data`->>'$.\"name\"') = 'Product')", + mysql: "(CAST((`data`->>'$.\\\"price\\\"') AS DECIMAL) = 5 AND (`data`->>'$.\\\"name\\\"') = 'Product')", postgres: "(CAST((\"data\"#>>'{price}') AS DOUBLE PRECISION) = 5 AND (\"data\"#>>'{name}') = 'Product')", sqlite: "(CAST(json_extract(`data`, '$.price') AS DOUBLE PRECISION) = 5 AND json_extract(`data`, '$.name') = 'Product')" }); @@ -891,7 +891,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { } } }, { - mysql: "(`data`->>'$.\"nested\".\"attribute\"') = 'value'", + mysql: "(`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') = 'value'", postgres: "(\"data\"#>>'{nested,attribute}') = 'value'", sqlite: "json_extract(`data`, '$.nested.attribute') = 'value'" }); @@ -905,7 +905,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { } } }, { - mysql: "CAST((`data`->>'$.\"nested\".\"attribute\"') AS DECIMAL) = 4", + mysql: "CAST((`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') AS DECIMAL) = 4", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) = 4", sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) = 4" }); @@ -921,7 +921,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { } } }, { - mysql: "CAST((`data`->>'$.\"nested\".\"attribute\"') AS DECIMAL) IN (3, 7)", + mysql: "CAST((`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') AS DECIMAL) IN (3, 7)", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) IN (3, 7)", sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) IN (3, 7)" }); @@ -937,7 +937,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "CAST((`data`->>'$.\"nested\".\"attribute\"') AS DECIMAL) > 2", + mysql: "CAST((`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') AS DECIMAL) > 2", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS DOUBLE PRECISION) > 2", sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS DOUBLE PRECISION) > 2" }); @@ -953,7 +953,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "CAST((`data`->>'$.\"nested\".\"attribute\"') AS DECIMAL) > 2", + mysql: "CAST((`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') AS DECIMAL) > 2", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS INTEGER) > 2", sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS INTEGER) > 2" }); @@ -970,7 +970,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "CAST((`data`->>'$.\"nested\".\"attribute\"') AS DATETIME) > "+sql.escape(dt), + mysql: "CAST((`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') AS DATETIME) > "+sql.escape(dt), postgres: "CAST((\"data\"#>>'{nested,attribute}') AS TIMESTAMPTZ) > "+sql.escape(dt), sqlite: "json_extract(`data`, '$.nested.attribute') > " + sql.escape(dt.toISOString()) }); @@ -984,7 +984,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { type: new DataTypes.JSONB() } }, { - mysql: "(`data`->>'$.\"nested\".\"attribute\"') = 'true'", + mysql: "(`data`->>'$.\\\"nested\\\".\\\"attribute\\\"') = 'true'", postgres: "CAST((\"data\"#>>'{nested,attribute}') AS BOOLEAN) = true", sqlite: "CAST(json_extract(`data`, '$.nested.attribute') AS BOOLEAN) = 1" }); @@ -1000,7 +1000,7 @@ suite(Support.getTestDialectTeaser('SQL'), () => { } } }, { - mysql: "(`meta_data`->>'$.\"nested\".\"attribute\"') = 'value'", + mysql: "(`meta_data`->>'$.\\\"nested\\\".\\\"attribute\\\"') = 'value'", postgres: "(\"meta_data\"#>>'{nested,attribute}') = 'value'", sqlite: "json_extract(`meta_data`, '$.nested.attribute') = 'value'" });