From 5a90aca40f39d19308c43b972493c8919adfbd1a Mon Sep 17 00:00:00 2001 From: Yarchik Date: Tue, 4 Aug 2026 02:01:41 +0100 Subject: [PATCH] Fix SUBSTRING with a non-positive start position SUBSTRING/SUBSTR/MID compiled 'SUBSTRING(str, pos, len)' to str.substr(pos - 1, len). When pos was 0 or negative, pos - 1 became a negative index and JavaScript's substr() counts that from the end of the string, so 'SUBSTRING("abcdef", 0, 3)' returned 'f' and 'SUBSTRING("abcdef", -1, 3)' returned 'ef'. Follow MySQL semantics instead: a start of 0 yields an empty string, and a negative start counts from the end (clamped to empty when it reaches past the beginning). Positive start positions are unchanged. --- src/55functions.js | 20 +++++++++++++-- test/test-substring-negative-start.js | 35 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/test-substring-negative-start.js diff --git a/src/55functions.js b/src/55functions.js index 9bdc42ff24..c94e00b0c7 100644 --- a/src/55functions.js +++ b/src/55functions.js @@ -186,8 +186,24 @@ stdlib.SUBSTRING = stdlib.SUBSTR = stdlib.MID = function (a, b, c) { - if (arguments.length == 2) return und(a, 'y.substr(' + b + '-1)'); - else if (arguments.length == 3) return und(a, 'y.substr(' + b + '-1,' + c + ')'); + if (arguments.length == 2) + return und( + a, + '(__alasql_tmp=(' + + b + + '),__alasql_tmp==0?"":(__alasql_tmp<0?(y.length+__alasql_tmp<0?"":y.substr(y.length+__alasql_tmp)):y.substr(__alasql_tmp-1)))' + ); + else if (arguments.length == 3) + return und( + a, + '(__alasql_tmp=(' + + b + + '),__alasql_tmp==0?"":(__alasql_tmp<0?(y.length+__alasql_tmp<0?"":y.substr(y.length+__alasql_tmp,' + + c + + ')):y.substr(__alasql_tmp-1,' + + c + + ')))' + ); }; stdfn.REGEXP_LIKE = function (a, b, c) { diff --git a/test/test-substring-negative-start.js b/test/test-substring-negative-start.js new file mode 100644 index 0000000000..a07e86b525 --- /dev/null +++ b/test/test-substring-negative-start.js @@ -0,0 +1,35 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('SUBSTRING with non-positive start position', function () { + it('follows MySQL semantics for start = 0', function () { + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', 0, 3)"), ''); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', 0)"), ''); + }); + + it('follows MySQL semantics for a negative start (counts from the end)', function () { + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -1, 3)"), 'f'); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -2, 3)"), 'ef'); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -2)"), 'ef'); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -6)"), 'abcdef'); + }); + + it('returns empty when a negative start reaches past the string start', function () { + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -7, 3)"), ''); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -10, 3)"), ''); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', -7)"), ''); + }); + + it('leaves positive start positions unchanged', function () { + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', 1, 3)"), 'abc'); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', 2, 3)"), 'bcd'); + assert.strictEqual(alasql("SELECT VALUE SUBSTRING('abcdef', 4)"), 'def'); + }); + + it('applies the same rules to the SUBSTR and MID aliases', function () { + assert.strictEqual(alasql("SELECT VALUE SUBSTR('abcdef', 0, 3)"), ''); + assert.strictEqual(alasql("SELECT VALUE MID('abcdef', -2, 3)"), 'ef'); + }); +});