I have firmware version strings into my table (like \"4.2.2\" or \"4.2.16\")
How can I compare, select or sort them ?
I cannot use standard strings compariso
I've created a flexible SQL-only solution based on the excellent answer of Salman A above:
In this logic, I compare the first 4 version-segments. When the version string has more segments, the tailing ones are ignored.
The code fetches the id
and ver
columns from a table and then "sanitizes" the ver
value to always contain 3 dots - this sanitized version is returned by the sane_ver
field.
That sanitized version is then split into 4 integer values, each representing one version segment. You can compare or sort the results based on those 4 integers.
SELECT
id,
ver,
SUBSTRING_INDEX(sane_ver, '.', 1) + 0 AS ver1,
SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 2), '.', -1) + 0 AS ver2,
SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 3), '.', -1) + 0 AS ver3,
SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 4), '.', -1) + 0 AS ver4
FROM (
SELECT
id,
ver,
CONCAT(
ver,
REPEAT('.0', 3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', '')))
) AS sane_ver
FROM (
SELECT id, ver FROM some_table
) AS raw_data
) AS sane_data
Here's a full query with some sample data and a filter that returns only versions that are lower than 1.2.3.4
SELECT
id,
ver,
SUBSTRING_INDEX(sane_ver, '.', 1) + 0 AS ver1,
SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 2), '.', -1) + 0 AS ver2,
SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 3), '.', -1) + 0 AS ver3,
SUBSTRING_INDEX(SUBSTRING_INDEX(sane_ver, '.', 4), '.', -1) + 0 AS ver4
FROM (
SELECT
id,
ver,
CONCAT(
ver,
REPEAT('.0', 3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', '')))
) AS sane_ver
FROM (
SELECT 1 AS id, '1' AS ver UNION
SELECT 2, '1.1' UNION
SELECT 3, '1.2.3.4.5' UNION
SELECT 4, '1.01' UNION
SELECT 5, '1.01.03' UNION
SELECT 6, '1.01.04a' UNION
SELECT 7, '1.01.04' UNION
SELECT 8, '1.01.04b' UNION
SELECT 9, '1.01.1.9.2.1.0' UNION
SELECT 10, '1.11' UNION
SELECT 11, '1.2' UNION
SELECT 12, '1.2.0' UNION
SELECT 13, '1.2.1' UNION
SELECT 14, '1.2.11' UNION
SELECT 15, '1.2.2' UNION
SELECT 16, '2.0' UNION
SELECT 17, '2.0.1' UNION
SELECT 18, '11.1.1' UNION
SELECT 19, '2020.11.18.11'
) AS raw_data
) AS sane_data
HAVING
ver1 <= 1
AND (ver2 <= 2 OR ver1 < 1)
AND (ver3 <= 3 OR ver2 < 2 OR ver1 < 1)
AND (ver4 < 4 OR ver3 < 3 OR ver2 < 2 OR ver1 < 1)
Note how this logic is different than the original code by Salman A:
The original answer uses CAST AS DECIMAL()
which converts 1.02
to 1.020
, and 1.1.0
to 1.100
→ That compares 1.02.0 to be lower than 1.1.0 (which is wrong, in my understanding)
The code in this answer converts 1.02
to the integers 1, 2
, and 1.1
to the integers 1, 1
→ That compares 1.1.0 to be lower than 1.02.0
Also, both our solutions completely ignore any non-numeric characters, treating 1.2-alpha
to be equal to 1.2.0
.
If all your version numbers look like any of these:
X
X.X
X.X.X
X.X.X.X
where X is an integer from 0 to 255 (inclusive), then you could use the INET_ATON()
function to transform the strings into integers fit for comparison.
Before you apply the function, though, you'll need to make sure the function's argument is of the X.X.X.X
form by appending the necessary quantity of '.0'
to it. To do that, you will first need to find out how many .
's the string already contains, which can be done like this:
CHAR_LENGTH(ver) - CHAR_LENGTH(REPLACE(ver, '.', '')
That is, the number of periods in the string is the length of the string minus its length after removing the periods.
The obtained result should then be subtracted from 3
and, along with '.0'
, passed to the REPEAT()
function:
REPEAT('.0', 3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', ''))
This will give us the substring that must be appended to the original ver
value, to conform with the X.X.X.X
format. So, it will, in its turn, be passed to the CONCAT()
function along with ver
. And the result of that CONCAT()
can now be directly passed to INET_ATON()
. So here's what we get eventually:
INET_ATON(
CONCAT(
ver,
REPEAT(
'.0',
3 - CHAR_LENGTH(ver) + CHAR_LENGTH(REPLACE(ver, '.', ''))
)
)
)
And this is only for one value! :) A similar expression should be constructed for the other string, afterwards you can compare the results.
References:
INET_ATON()
CHAR_LENGTH()
CONCAT()
REPEAT()
REPLACE()
I was searching for the same thing and instead ended up doing this -- but staying in mysql :
using this statement
case when version is null then null
when '' then 0
else
preg_replace( '/[^.]*([^.]{10})[.]+/', '$1',
preg_replace('/([^".,\\/_ ()-]+)([".,\\/_ ()-]*)/','000000000$1.',
preg_replace('/(?<=[0-9])([^".,\\/_ ()0-9-]+)/','.!$1',version
)))
end
I'll break down what that means:
preg_replace
is a function that the UDF library created. Because it's a UDF you can just call it from any user or dbspace like that^".,\\/_ ()
right now i'm considering all of these characters as separators or traditional "dots" in a versionpreg_replace('/(?<=[0-9])([^".,\\/_ ()0-9-]+)/','.!$1',version)
means to replace all the non-"dots" and non-numbers that are preceded by a number to be preceded by a "dot" and an exclamation point. preg_replace('/([^".,\\/_ ()-]+)([".,\\/_ ()-]*)/','000000000$1.', ...)
means to additionally replace all the "dots" with actual dots and to pad all the numbers with 9 zero's. Also any adjacent dots would be reduced to 1.preg_replace( '/0*([^.]{10})[.]+/', '$1', ... )
means to additionally strip all the number blocks down to only 10 digits long and to preserve as many blocks as needed. I wanted to force 6 blocks to keep it under 64-bytes but needing 7 blocks was surprisingly common and thus necessary for my accuracy. Also needed blocks of 10 so 7 blocks of 9 was not an option. But the variable length is working well for me. -- remember strings are compared left to rightSo now I can handle versions like:
1.2 < 1.10
1.2b < 1.2.0
1.2a < 1.2b
1.2 = 1.2.0
1.020 = 1.20
11.1.1.3.0.100806.0408.000 < 11.1.1.3.0.100806.0408.001
5.03.2600.2180 (xpsp_sp2_rtm.040803-2158)
A.B.C.D = a.B.C.D
A.A < A.B
I chose exclamation point because it sorts in the collations sequences (that I'm using anyway) before 0. It's relative sort to 0 allows letters like b and a when used immediately adjacent to a number above to be treated like a new section and be sort before 0 -- which is the padding I am using.
I am using 0 as padding so that vendor's mistakes like moving from a fixed 3 digit block to a variable one don't bite me.
You can easily choose more padding if you want to handle silly versions like "2.11.0 Under development (unstable) (2010-03-09)" -- the string development
is 11 bytes.
You can easily request more blocks in the final replace.
I could have done more but I was trying to do a as few paces as possible with a high-level of accuracy since I have several millions records to scan regularly. If anyone sees an optimization please repsond.
I chose to keep it as a string and not cast into a number because the cast has a cost and also letters are important as we saw. One thing i was thinking about is doing a test on the string and returning an option that isn't as many passes or less expensive function for tidier cases. like 11.1.1.3
is a very common format
This is my solution. It not depends on number of subversion.
For example:
select SF_OS_VERSION_COMPARE('2016.10.1712.58','2016.9.1712.58');
returns 'HIGH'
select SF_OS_VERSION_COMPARE('2016.10.1712.58','2016.10.1712.58');
returns 'EQUAL'
delimiter //
DROP FUNCTION IF EXISTS SF_OS_VERSION_COMPARE //
CREATE FUNCTION SF_OS_VERSION_COMPARE(ver_1 VARCHAR(50), ver_2 VARCHAR(50)) RETURNS VARCHAR(5)
DETERMINISTIC
COMMENT 'Return "HIGH", "LOW" OR "EQUAL" comparing VER_1 with VER_2'
BEGIN
DECLARE v_ver1 VARCHAR(50);
DECLARE v_ver2 VARCHAR(50);
DECLARE v_ver1_num INT;
DECLARE v_ver2_num INT;
SET v_ver1 = ver_1;
SET v_ver2 = ver_2;
WHILE ( v_ver1 <> v_ver2 AND ( v_ver1 IS NOT NULL OR v_ver2 IS NOT NULL )) DO
SET v_ver1_num = CAST(SUBSTRING_INDEX(v_ver1, '.', 1) AS UNSIGNED INTEGER);
SET v_ver2_num = CAST(SUBSTRING_INDEX(v_ver2, '.', 1) AS UNSIGNED INTEGER);
IF ( v_ver1_num > v_ver2_num )
THEN
return 'HIGH';
ELSEIF ( v_ver1_num < v_ver2_num )
THEN
RETURN 'LOW';
ELSE
SET v_ver1 = SUBSTRING(v_ver1,LOCATE('.', v_ver1)+1);
SET v_ver2 = SUBSTRING(v_ver2,LOCATE('.', v_ver2)+1);
END IF;
END WHILE;
RETURN 'EQUAL';
END //