Implementing parts of rfc4226 (HOTP) in mysql - mysql

Like the title says, I'm trying to implement the programmatic parts of RFC4226 "HOTP: An HMAC-Based One-Time Password Algorithm" in SQL. I think I've got a version that works (in that for a small test sample, it produces the same result as the Java version in the code), but it contains a nested pair of hex(unhex()) calls, which I feel can be done better. I am constrained by a) needing to do this algorithm, and b) needing to do it in mysql, otherwise I'm happy to look at other ways of doing this.
What I've got so far:
-- From the inside out...
-- Concatinate the users secret, and the number of time its been used
-- find the SHA1 hash of that string
-- Turn a 40 byte hex encoding into a 20 byte binary string
-- keep the first 4 bytes
-- turn those back into a hex represnetation
-- convert that into an integer
-- Throw away the most-significant bit (solves signed/unsigned problems)
-- Truncate to 6 digits
-- store into otp
-- from the otpsecrets table
select (conv(hex(substr(unhex(sha1(concat(secret, uses))), 1, 4)), 16, 10) & 0x7fffffff) % 1000000
into otp
from otpsecrets;
Is there a better (more efficient) way of doing this?

I haven't read the spec, but I think you don't need to convert back and forth between hex and binary, so this might be a little more efficient:
SELECT (conv(substr(sha1(concat(secret, uses)), 1, 8), 16, 10) & 0x7fffffff) % 1000000
INTO otp
FROM otpsecrets;
This seems to give the same result as your query for a few examples I tested.

This is absolutely horrific, but it works with my 6-digit OTP tokens. Call as:
select HOTP( floor( unix_timestamp()/60), secret ) 'OTP' from SecretKeyTable;
drop function HOTP;
delimiter //
CREATE FUNCTION HOTP(C integer, K BINARY(64)) RETURNS char(6)
BEGIN
declare i INTEGER;
declare ipad BINARY(64);
declare opad BINARY(64);
declare hmac BINARY(20);
declare cbin BINARY(8);
set i = 1;
set ipad = repeat( 0x36, 64 );
set opad = repeat( 0x5c, 64 );
repeat
set ipad = insert( ipad, i, 1, char( ascii( substr( K, i, 1 ) ) ^ 0x36 ) );
set opad = insert( opad, i, 1, char( ascii( substr( K, i, 1 ) ) ^ 0x5C ) );
set i = i + 1;
until (i > 64) end repeat;
set cbin = unhex( lpad( hex( C ), 16, '0' ) );
set hmac = unhex( sha1( concat( opad, unhex( sha1( concat( ipad, cbin ) ) ) ) ) );
return lpad( (conv(hex(substr( hmac, (ascii( right( hmac, 1 ) ) & 0x0f) + 1, 4 )),16,10) & 0x7fffffff) % 1000000, 6, '0' );
END
//
delimiter ;

Related

How to decode BASE64 text (from JSON data) in TSQL with accents intact [duplicate]

I have a column in SQL Server with utf8 SQL_Latin1_General_CP1_CI_AS encoding. How can I convert and save the text in ISO 8859-1 encoding? I would like to do thing in a query on SQL Server. Any tips?
Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito
I have written a function to repair UTF-8 text that is stored in a varchar field.
To check the fixed values you can use it like this:
CREATE TABLE #Table1 (Column1 varchar(max))
INSERT #Table1
VALUES ('Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito')
SELECT *, NewColumn1 = dbo.DecodeUTF8String(Column1)
FROM Table1
WHERE Column1 <> dbo.DecodeUTF8String(Column1)
Output:
Column1
-------------------------------
Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito
NewColumn1
-------------------------------
Olá. Gostei do jogo. Quando "baixei" até achei que não iria curtir muito
The code:
CREATE FUNCTION dbo.DecodeUTF8String (#value varchar(max))
RETURNS nvarchar(max)
AS
BEGIN
-- Transforms a UTF-8 encoded varchar string into Unicode
-- By Anthony Faull 2014-07-31
DECLARE #result nvarchar(max);
-- If ASCII or null there's no work to do
IF (#value IS NULL
OR #value NOT LIKE '%[^ -~]%' COLLATE Latin1_General_BIN
)
RETURN #value;
-- Generate all integers from 1 to the length of string
WITH e0(n) AS (SELECT TOP(POWER(2,POWER(2,0))) NULL FROM (VALUES (NULL),(NULL)) e(n))
, e1(n) AS (SELECT TOP(POWER(2,POWER(2,1))) NULL FROM e0 CROSS JOIN e0 e)
, e2(n) AS (SELECT TOP(POWER(2,POWER(2,2))) NULL FROM e1 CROSS JOIN e1 e)
, e3(n) AS (SELECT TOP(POWER(2,POWER(2,3))) NULL FROM e2 CROSS JOIN e2 e)
, e4(n) AS (SELECT TOP(POWER(2,POWER(2,4))) NULL FROM e3 CROSS JOIN e3 e)
, e5(n) AS (SELECT TOP(POWER(2.,POWER(2,5)-1)-1) NULL FROM e4 CROSS JOIN e4 e)
, numbers(position) AS
(
SELECT TOP(DATALENGTH(#value)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM e5
)
-- UTF-8 Algorithm (http://en.wikipedia.org/wiki/UTF-8)
-- For each octet, count the high-order one bits, and extract the data bits.
, octets AS
(
SELECT position, highorderones, partialcodepoint
FROM numbers a
-- Split UTF8 string into rows of one octet each.
CROSS APPLY (SELECT octet = ASCII(SUBSTRING(#value, position, 1))) b
-- Count the number of leading one bits
CROSS APPLY (SELECT highorderones = 8 - FLOOR(LOG( ~CONVERT(tinyint, octet) * 2 + 1)/LOG(2))) c
CROSS APPLY (SELECT databits = 7 - highorderones) d
CROSS APPLY (SELECT partialcodepoint = octet % POWER(2, databits)) e
)
-- Compute the Unicode codepoint for each sequence of 1 to 4 bytes
, codepoints AS
(
SELECT position, codepoint
FROM
(
-- Get the starting octect for each sequence (i.e. exclude the continuation bytes)
SELECT position, highorderones, partialcodepoint
FROM octets
WHERE highorderones <> 1
) lead
CROSS APPLY (SELECT sequencelength = CASE WHEN highorderones in (1,2,3,4) THEN highorderones ELSE 1 END) b
CROSS APPLY (SELECT endposition = position + sequencelength - 1) c
CROSS APPLY
(
-- Compute the codepoint of a single UTF-8 sequence
SELECT codepoint = SUM(POWER(2, shiftleft) * partialcodepoint)
FROM octets
CROSS APPLY (SELECT shiftleft = 6 * (endposition - position)) b
WHERE position BETWEEN lead.position AND endposition
) d
)
-- Concatenate the codepoints into a Unicode string
SELECT #result = CONVERT(xml,
(
SELECT NCHAR(codepoint)
FROM codepoints
ORDER BY position
FOR XML PATH('')
)).value('.', 'nvarchar(max)');
RETURN #result;
END
GO
Jason Penny has also written an SQL function to convert UTF-8 to Unicode (MIT licence) which worked on a simple example for me:
CREATE FUNCTION dbo.UTF8_TO_NVARCHAR(#in VarChar(MAX))
RETURNS NVarChar(MAX)
AS
BEGIN
DECLARE #out NVarChar(MAX), #i int, #c int, #c2 int, #c3 int, #nc int
SELECT #i = 1, #out = ''
WHILE (#i <= Len(#in))
BEGIN
SET #c = Ascii(SubString(#in, #i, 1))
IF (#c < 128)
BEGIN
SET #nc = #c
SET #i = #i + 1
END
ELSE IF (#c > 191 AND #c < 224)
BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #nc = (((#c & 31) * 64 /* << 6 */) | (#c2 & 63))
SET #i = #i + 2
END
ELSE
BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #c3 = Ascii(SubString(#in, #i + 2, 1))
SET #nc = (((#c & 15) * 4096 /* << 12 */) | ((#c2 & 63) * 64 /* << 6 */) | (#c3 & 63))
SET #i = #i + 3
END
SET #out = #out + NChar(#nc)
END
RETURN #out
END
GO
The ticked answer by Anthony "looks" better to me, but maybe run both if doing conversion and investigate any discrepencies?!
Also we used the very ugly code below to detect BMP page unicode characters that were encoded as UTF-8 and then converted from varchar to nvarchar fields, that can be converted to UCS-16.
LIKE (N'%[' + CONVERT(NVARCHAR,(CHAR(192))) + CONVERT(NVARCHAR,(CHAR(193))) + CONVERT(NVARCHAR,(CHAR(194))) + CONVERT(NVARCHAR,(CHAR(195))) + CONVERT(NVARCHAR,(CHAR(196))) + CONVERT(NVARCHAR,(CHAR(197))) + CONVERT(NVARCHAR,(CHAR(198))) + CONVERT(NVARCHAR,(CHAR(199))) + CONVERT(NVARCHAR,(CHAR(200))) + CONVERT(NVARCHAR,(CHAR(201))) + CONVERT(NVARCHAR,(CHAR(202))) + CONVERT(NVARCHAR,(CHAR(203))) + CONVERT(NVARCHAR,(CHAR(204))) + CONVERT(NVARCHAR,(CHAR(205))) + CONVERT(NVARCHAR,(CHAR(206))) + CONVERT(NVARCHAR,(CHAR(207))) + CONVERT(NVARCHAR,(CHAR(208))) + CONVERT(NVARCHAR,(CHAR(209))) + CONVERT(NVARCHAR,(CHAR(210))) + CONVERT(NVARCHAR,(CHAR(211))) + CONVERT(NVARCHAR,(CHAR(212))) + CONVERT(NVARCHAR,(CHAR(213))) + CONVERT(NVARCHAR,(CHAR(214))) + CONVERT(NVARCHAR,(CHAR(215))) + CONVERT(NVARCHAR,(CHAR(216))) + CONVERT(NVARCHAR,(CHAR(217))) + CONVERT(NVARCHAR,(CHAR(218))) + CONVERT(NVARCHAR,(CHAR(219))) + CONVERT(NVARCHAR,(CHAR(220))) + CONVERT(NVARCHAR,(CHAR(221))) + CONVERT(NVARCHAR,(CHAR(222))) + CONVERT(NVARCHAR,(CHAR(223))) + CONVERT(NVARCHAR,(CHAR(224))) + CONVERT(NVARCHAR,(CHAR(225))) + CONVERT(NVARCHAR,(CHAR(226))) + CONVERT(NVARCHAR,(CHAR(227))) + CONVERT(NVARCHAR,(CHAR(228))) + CONVERT(NVARCHAR,(CHAR(229))) + CONVERT(NVARCHAR,(CHAR(230))) + CONVERT(NVARCHAR,(CHAR(231))) + CONVERT(NVARCHAR,(CHAR(232))) + CONVERT(NVARCHAR,(CHAR(233))) + CONVERT(NVARCHAR,(CHAR(234))) + CONVERT(NVARCHAR,(CHAR(235))) + CONVERT(NVARCHAR,(CHAR(236))) + CONVERT(NVARCHAR,(CHAR(237))) + CONVERT(NVARCHAR,(CHAR(238))) + CONVERT(NVARCHAR,(CHAR(239)))
+ N'][' + CONVERT(NVARCHAR,(CHAR(128))) + CONVERT(NVARCHAR,(CHAR(129))) + CONVERT(NVARCHAR,(CHAR(130))) + CONVERT(NVARCHAR,(CHAR(131))) + CONVERT(NVARCHAR,(CHAR(132))) + CONVERT(NVARCHAR,(CHAR(133))) + CONVERT(NVARCHAR,(CHAR(134))) + CONVERT(NVARCHAR,(CHAR(135))) + CONVERT(NVARCHAR,(CHAR(136))) + CONVERT(NVARCHAR,(CHAR(137))) + CONVERT(NVARCHAR,(CHAR(138))) + CONVERT(NVARCHAR,(CHAR(139))) + CONVERT(NVARCHAR,(CHAR(140))) + CONVERT(NVARCHAR,(CHAR(141))) + CONVERT(NVARCHAR,(CHAR(142))) + CONVERT(NVARCHAR,(CHAR(143))) + CONVERT(NVARCHAR,(CHAR(144))) + CONVERT(NVARCHAR,(CHAR(145))) + CONVERT(NVARCHAR,(CHAR(146))) + CONVERT(NVARCHAR,(CHAR(147))) + CONVERT(NVARCHAR,(CHAR(148))) + CONVERT(NVARCHAR,(CHAR(149))) + CONVERT(NVARCHAR,(CHAR(150))) + CONVERT(NVARCHAR,(CHAR(151))) + CONVERT(NVARCHAR,(CHAR(152))) + CONVERT(NVARCHAR,(CHAR(153))) + CONVERT(NVARCHAR,(CHAR(154))) + CONVERT(NVARCHAR,(CHAR(155))) + CONVERT(NVARCHAR,(CHAR(156))) + CONVERT(NVARCHAR,(CHAR(157))) + CONVERT(NVARCHAR,(CHAR(158))) + CONVERT(NVARCHAR,(CHAR(159))) + CONVERT(NVARCHAR,(CHAR(160))) + CONVERT(NVARCHAR,(CHAR(161))) + CONVERT(NVARCHAR,(CHAR(162))) + CONVERT(NVARCHAR,(CHAR(163))) + CONVERT(NVARCHAR,(CHAR(164))) + CONVERT(NVARCHAR,(CHAR(165))) + CONVERT(NVARCHAR,(CHAR(166))) + CONVERT(NVARCHAR,(CHAR(167))) + CONVERT(NVARCHAR,(CHAR(168))) + CONVERT(NVARCHAR,(CHAR(169))) + CONVERT(NVARCHAR,(CHAR(170))) + CONVERT(NVARCHAR,(CHAR(171))) + CONVERT(NVARCHAR,(CHAR(172))) + CONVERT(NVARCHAR,(CHAR(173))) + CONVERT(NVARCHAR,(CHAR(174))) + CONVERT(NVARCHAR,(CHAR(175))) + CONVERT(NVARCHAR,(CHAR(176))) + CONVERT(NVARCHAR,(CHAR(177))) + CONVERT(NVARCHAR,(CHAR(178))) + CONVERT(NVARCHAR,(CHAR(179))) + CONVERT(NVARCHAR,(CHAR(180))) + CONVERT(NVARCHAR,(CHAR(181))) + CONVERT(NVARCHAR,(CHAR(182))) + CONVERT(NVARCHAR,(CHAR(183))) + CONVERT(NVARCHAR,(CHAR(184))) + CONVERT(NVARCHAR,(CHAR(185))) + CONVERT(NVARCHAR,(CHAR(186))) + CONVERT(NVARCHAR,(CHAR(187))) + CONVERT(NVARCHAR,(CHAR(188))) + CONVERT(NVARCHAR,(CHAR(189))) + CONVERT(NVARCHAR,(CHAR(190))) + CONVERT(NVARCHAR,(CHAR(191)))
+ N']%') COLLATE Latin1_General_BIN
The above:
detects multi-byte sequences encoding U+0080 to U+FFFF (U+0080 to U+07FF is encoded as 110xxxxx 10xxxxxx, U+0800 to U+FFFF is encoded as 1110xxxx 10xxxxxx 10xxxxxx)
i.e. it detects hex byte 0xC0 to 0xEF followed by hex byte 0x80 to 0xBF
ignores ASCII control characters U+0000 to U+001F
ignores characters that are already correctly encoded to unicode >= U+0100 (i.e. not UTF-8)
ignores unicode characters U+0080 to U+00FF if they don't appear to be part of a UTF-8 sequence e.g. "coöperatief".
doesn't use LIKE "%[X-Y]" for X=0x80 to Y=0xBF because of potential collation issues
uses CONVERT(VARCHAR,CHAR(X)) instead of NCHAR because we had problems with NCHAR getting converted to the wrong value (for some values).
ignores UTF characters greater than U+FFFF (4 to 6 byte sequences which have a first byte of hex 0xF0 to 0xFD)
I made a solution that also handles 4 byte sequences (like emojis) by combining the answer from #robocat, some more cases with the logic taken from https://github.com/benkasminbullock/unicode-c/blob/master/unicode.c, and a solution for the problem of encoding extended unicode characters from https://dba.stackexchange.com/questions/139551/how-do-i-set-a-sql-server-unicode-nvarchar-string-to-an-emoji-or-supplementary. It's not fast or pretty, but it's working for me anyway. This particular solution includes Unicode replacement characters wherever it finds unknown bytes. It may be better just to throw an exception in these cases, or leave the bytes as they were, as future encoding could be off, but I preferred this for my use case.
-- Started with https://stackoverflow.com/questions/28168055/convert-text-value-in-sql-server-from-utf8-to-iso-8859-1
-- Modified following source in https://github.com/benkasminbullock/unicode-c/blob/master/unicode.c
-- Made characters > 65535 work using https://dba.stackexchange.com/questions/139551/how-do-i-set-a-sql-server-unicode-nvarchar-string-to-an-emoji-or-supplementary
CREATE FUNCTION dbo.UTF8_TO_NVARCHAR(#in VarChar(MAX)) RETURNS NVarChar(MAX) AS
BEGIN
DECLARE #out NVarChar(MAX), #thisOut NVARCHAR(MAX), #i int, #c int, #c2 int, #c3 int, #c4 int
SELECT #i = 1, #out = ''
WHILE (#i <= Len(#in)) BEGIN
SET #c = Ascii(SubString(#in, #i, 1))
IF #c <= 0x7F BEGIN
SET #thisOut = NCHAR(#c)
SET #i = #i + 1
END
ELSE IF #c BETWEEN 0xC2 AND 0xDF BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
IF #c2 < 0x80 OR #c2 > 0xBF BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
ELSE BEGIN
SET #thisOut = NCHAR(((#c & 31) * 64 /* << 6 */) | (#c2 & 63))
SET #i = #i + 2
END
END
ELSE IF #c BETWEEN 0xE0 AND 0xEF BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #c3 = Ascii(SubString(#in, #i + 2, 1))
IF #c2 < 0x80 OR #c2 > 0xBF OR #c3 < 0x80 OR (#c = 0xE0 AND #c2 < 0xA0) BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
ELSE BEGIN
SET #thisOut = NCHAR(((#c & 15) * 4096 /* << 12 */) | ((#c2 & 63) * 64 /* << 6 */) | (#c3 & 63))
SET #i = #i + 3
END
END
ELSE IF #c BETWEEN 0xF0 AND 0xF4 BEGIN
SET #c2 = Ascii(SubString(#in, #i + 1, 1))
SET #c3 = Ascii(SubString(#in, #i + 2, 1))
SET #c4 = Ascii(SubString(#in, #i + 3, 1))
IF #c2 < 0x80 OR #c2 >= 0xC0 OR #c3 < 0x80 OR #c3 >= 0xC0 OR #c4 < 0x80 OR #c4 >= 0xC0 OR (#c = 0xF0 AND #c2 < 0x90) BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
ELSE BEGIN
DECLARE #nc INT = (((#c & 0x07) * 262144 /* << 18 */) | ((#c2 & 0x3F) * 4096 /* << 12 */) | ((#c3 & 0x3F) * 64) | (#c4 & 0x3F))
DECLARE #HighSurrogateInt INT = 55232 + (#nc / 1024), #LowSurrogateInt INT = 56320 + (#nc % 1024)
SET #thisOut = NCHAR(#HighSurrogateInt) + NCHAR(#LowSurrogateInt)
SET #i = #i + 4
END
END
ELSE BEGIN
SET #thisOut = NCHAR(0xFFFD)
SET #i = #i + 1
END
SET #out = #out + #thisOut
END
RETURN #out
END
GO
i add a little modification to use new string aggregation function string_agg, from sql server 2017 and 2019
SELECT #result=STRING_AGG(NCHAR([codepoint]),'') WITHIN GROUP (ORDER BY position ASC)
FROM codepoints
change de #result parts to this one. The XML still work in old fashion way.
in 2019, string_agg works extreme faster than xml version (obvious... string_agg now is native, and is not fair compare)
Here's my version written as an inline table-valued function (TVF) for SQL Server 2017. It is limited to 4000 byte input strings as that was more than enough for my needs. Limiting the input size and writing as a TVF makes this version significantly faster than the scaler valued functions posted so far. It also handles four-byte UTF-8 sequences (such as those created by emoji), which cannot be represented in UCS-2 strings, by outputting a replacement character in their place.
CREATE OR ALTER FUNCTION [dbo].[fnUTF8Decode](#UTF8 VARCHAR(4001)) RETURNS TABLE AS RETURN
/* Converts a UTF-8 encoded VARCHAR to NVARCHAR (UCS-2). Based on UTF-8 documentation on Wikipedia and the
code/discussion at https://stackoverflow.com/a/31064459/1979220.
One can quickly detect strings that need conversion using the following expression:
<FIELD> LIKE CONCAT('%[', CHAR(192), '-', CHAR(255), ']%') COLLATE Latin1_General_BIN.
Be aware, however, that this may return true for strings that this function has already converted to UCS-2.
See robocat's answer on the above referenced Stack Overflow thread for a slower but more robust expression.
Notes/Limitations
1) Written as a inline table-valued function for optimized performance.
2) Only tested on a database with SQL_Latin1_General_CP1_CI_AS collation. More specifically, this was
not designed to output Supplementary Characters and converts all such UTF-8 sequences to �.
3) Empty input strings, '', and strings with nothing but invalid UTF-8 chars are returned as NULL.
4) Assumes input is UTF-8 compliant. For example, extended ASCII characters such as en dash CHAR(150)
are not allowed unless part of a multi-byte sequence and will be skipped otherwise. In other words:
SELECT * FROM dbo.fnUTF8Decode(CHAR(150)) -> NULL
5) Input should be limited to 4000 characters to ensure that output will fit in NVARCHAR(4000), which is
what STRING_AGG outputs when fed a sequence of NVARCHAR(1) characters generated by NCHAR. However,
T-SQL silently truncates overlong parameters so we've declared our input as VARCHAR(4001) to allow
STRING_AGG to generate an error on overlong input. If we didn't do this, callers would never be
notified about truncation.
6) If we need to process more than 4000 chars in the future, we'll need to change input to VARCHAR(MAX) and
CAST the CASE WHEN expression to NVARCHAR(MAX) to force STRING_AGG to output NVARCHAR(MAX). Note that
this change will significantly degrade performance, which is why we didn't do it in the first place.
7) Due to use of STRING_AGG, this is only compatible with SQL 2017. It will probably work fine on 2019
but that version has native UTF-8 support so you're probably better off using that. For earlier versions,
replace STRING_AGG with a CLR equivalent (ms-sql-server-group-concat-sqlclr) or FOR XML PATH(''), TYPE...
*/
SELECT STRING_AGG (
CASE
WHEN A1 & 0xF0 = 0xF0 THEN --Four byte sequences (like emoji) can't be represented in UCS-2
NCHAR(0xFFFD) --Output U+FFFD (Replacement Character) instead
WHEN A1 & 0xE0 = 0xE0 THEN --Three byte sequence; get/combine relevant bits from A1-A3
NCHAR((A1 & 0x0F) * 4096 | (A2 & 0x3F) * 64 | (A3 & 0x3F))
WHEN A1 & 0xC0 = 0xC0 THEN --Two byte sequence; get/combine relevant bits from A1-A2
NCHAR((A1 & 0x3F) * 64 | (A2 & 0x3F))
ELSE NCHAR(A1) --Regular ASCII character; output as is
END
, '') UCS2
FROM dbo.fnNumbers(ISNULL(DATALENGTH(#UTF8), 0))
CROSS APPLY (SELECT ASCII(SUBSTRING(#UTF8, I, 1)) A1, ASCII(SUBSTRING(#UTF8, I + 1, 1)) A2, ASCII(SUBSTRING(#UTF8, I + 2, 1)) A3) A
WHERE A1 <= 127 OR A1 >= 192 --Output only ASCII chars and one char for each multi-byte sequence
GO
Note that the above requires a "Numbers" table or generator function. Here's the function I use:
CREATE OR ALTER FUNCTION [dbo].[fnNumbers](#MaxNumber BIGINT) RETURNS TABLE AS RETURN
/* Generates a table of numbers up to the specified #MaxNumber, limited to 4,294,967,296. Useful for special case
situations and algorithms. Copied from https://www.itprotoday.com/sql-server/virtual-auxiliary-table-numbers
with minor formatting and name changes.
*/
WITH L0 AS (
SELECT 1 I UNION ALL SELECT 1 --Generates 2 rows
), L1 AS (
SELECT 1 I FROM L0 CROSS JOIN L0 L -- 4 rows
), L2 AS (
SELECT 1 I FROM L1 CROSS JOIN L1 L -- 16 rows
), L3 AS (
SELECT 1 I FROM L2 CROSS JOIN L2 L -- 256 rows
), L4 AS (
SELECT 1 I FROM L3 CROSS JOIN L3 L -- 65,536 rows
), L5 AS (
SELECT 1 I FROM L4 CROSS JOIN L4 L -- 4,294,967,296 rows
), Numbers AS (
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) I FROM L5
)
SELECT TOP (#MaxNumber) I FROM Numbers ORDER BY I
GO
I just succeeded by creating a new field as varchar(255) and setting the new field to the old field which was nvarchar(255). This produced the 'Americanized' version of the international places.
Update WorldCities
Set admin_correct = admin_name
varchar(255) nvarchar(255)
I found the query I need to do, just not the encoding yet.
ALTER TABLE dbo.MyTable ALTER COLUMN CharCol
varchar(10)COLLATE Latin1_General_CI_AS NOT NULL;

What happend if convert a very large hex to dec using Mysql conv function? [duplicate]

I have a very large table in MySQL. I'm using a CHAR(32) field which contains an MD5 as a string of course. I'm running into an issue where I need to convert this to a decimal value using MySQL. A third party tool runs the query so writing code to do this isn't really an option.
MySQL does support storing hex values natively and converting them to integers. But it gets hung up converting it from a string. Here's what I've tried so far (md5_key is the name of my column)
First I just tried the UNHEX function but that returns a string so it gave me gooblygoop. I won't put that here. Next I tried the CAST function
SELECT CAST( CONCAT('0x',md5_key) AS UNSIGNED ) FROM bigtable limit 1
Result = 0
Show warnings gives me: "Truncated incorrect INTEGER value: '0x000002dcc38af6f209e91518db3e79d3'"
BUT if I do:
SELECT CAST( 0x000002dcc38af6f209e91518db3e79d3 AS UNSIGNED );
I get the correct decimal value.
So I guess what I need to know, is there a way to get MySQL to see that string as a hex value? (I also tried converting it to BINARY and then to the UNSIGNED but that didn't work either).
Thanks in advance!
conv() is limited to 64 bit integers. You can convert the high and low part to decimal and then add them together:
> select cast(conv(substr("000002dcc38af6f209e91518db3e79d3", 1, 16), 16, 10) as
decimal(65))*18446744073709551616 +
cast(conv(substr("000002dcc38af6f209e91518db3e79d3", 17, 16), 16, 10) as
decimal(65));
58055532535286745202684464101843
Where 18446744073709551616 = 2^64. So in your case:
> select cast(conv(substr(md5_key, 1, 16), 16, 10) as
decimal(65))*18446744073709551616 +
cast(conv(substr(md5_key, 17, 16), 16, 10) as
decimal(65))
from bigtable limit 1;
Beware MD5 are 16 Byte long, and BIGINT UNSIGNED is 8 Byte long, so even in your second case you don't get the right answer, the number can't fit you are receiving the value of the lowest 8 Byte=> 09e91518db3e79d3.
I wrote a function to convert big hex numbers to decimal(65).
CREATE FUNCTION `hexnum_to_decimal`(hex varchar(66)) RETURNS decimal(65,0)
DETERMINISTIC
BEGIN
declare group1 decimal(65);
declare group2 decimal(65);
declare group3 decimal(65);
declare group4 decimal(65);
declare multiplier decimal(65);
if (substr(hex, 1, 2) = "0x") then
set hex = substr(hex, 3); -- trim 0x if exists
end if;
set hex = trim(LEADING '0' from hex);
if (length(hex) > 54) then
return null; -- too big number
end if;
set hex = lpad(hex, 64, 0);
set group1 = cast(conv(substr(hex, 49, 16), 16, 10) as decimal(65));
set group2 = cast(conv(substr(hex, 33, 16), 16, 10) as decimal(65));
set group3 = cast(conv(substr(hex, 17, 16), 16, 10) as decimal(65));
set group4 = cast(conv(substr(hex, 1, 16), 16, 10) as decimal(65));
set multiplier = 18446744073709551616; -- 2 ^ 16
-- check for overflow
if (
(group4 > 15930919) or
(group4 = 15930919 and group3 > 2053574980671369030) or
(group4 = 15930919 and group3 = 2053574980671369030 and group2 > 5636613303479645705) or
(group4 = 15930919 and group3 = 2053574980671369030 and group2 = 5636613303479645705 and group1 > 18446744073709551615)
) then
return null;
end if;
return cast(
group1 +
group2 * multiplier +
group3 * multiplier * multiplier +
group4 * multiplier * multiplier * multiplier
as decimal(65));
END
In your case for 000002dcc38af6f209e91518db3e79d3
select hexnum_to_decimal("000002dcc38af6f209e91518db3e79d3");
58055532535286745202684464101843
select hexnum_to_decimal('F316271C7FC3908A8BEF464E3945EF7A253609FFFFFFFFFFFFFFFF');
99999999999999999999999999999999999999999999999999999999999999999
if bigger hex number is passed, the function will return null.

ORDER BY Color with Hex Code as a criterio in MySQL

I have a table that contains color options for a product. The color options include a hex color code, which is used to generate the UI (HTML).
I would like to sort the rows so that the colors in the UI look like a rainbow, instead of the current order that sorts based off of the Name of the color (not very useful).
Here is what my query looks like. I get the R G B decimal values from the hex code. I just don't know how to order it.
I've looked into color difference algorithms. They seem more useful to compare 2 colors' similarity, not sort.
I'm using MySQL:
select a.*, (a.c_r + a.c_g + a.c_b) color_sum
from (
select co.customization_option_id,
co.designer_image_url,
concat(co.name, " (",cog.name, ")") name,
co.customization_option_group_id gr,
designer_hex_color,
conv(substr(designer_hex_color, 1, 2), 16, 10) c_r,
conv(substr(designer_hex_color, 3, 2), 16, 10) c_g,
conv(substr(designer_hex_color, 5, 2), 16, 10) c_b
from customization_options co
left join customization_option_groups cog
on cog.id = co.customization_option_group_id
where co.customization_id = 155
and co.customization_option_group_id
in (1,2,3,4)) a
order by ????
You want to sort hex codes by wavelength, this roughly maps onto the hue-value. Given a hexcode as a six character string: RRGGBB.
You just need to make a function that takes in a hexcode string and outputs the hue value, here's the formula from this Math.SO answer:
R' = R/255
G' = G/255
B' = B/255
Cmax = max(R', G', B')
Cmin = min(R', G', B')
Δ = Cmax - Cmin
I wanted to see if this would work, so I whipped up a sample program in Ruby, it samples 200 random colors uniformly from RGB-space, and sorts them, the output looks like a rainbow!
Here's the Ruby source:
require 'paint'
def hex_to_rgb(hex)
/(?<r>..)(?<g>..)(?<b>..)/ =~ hex
[r,g,b].map {|cs| cs.to_i(16) }
end
def rgb_to_hue(r,g,b)
# normalize r, g and b
r_ = r / 255.0
g_ = g / 255.0
b_ = b / 255.0
c_min = [r_,g_,b_].min
c_max = [r_,g_,b_].max
delta = (c_max - c_min).to_f
# compute hue
hue = 60 * ((g_ - b_)/delta % 6) if c_max == r_
hue = 60 * ((b_ - r_)/delta + 2) if c_max == g_
hue = 60 * ((r_ - g_)/delta + 4) if c_max == b_
return hue
end
# sample uniformly at random from RGB space
colors = 200.times.map { (0..255).to_a.sample(3).map { |i| i.to_s(16).rjust(2, '0')}.join }
# sort by hue
colors.sort_by { |color| rgb_to_hue(*hex_to_rgb(color)) }.each do |color|
puts Paint[color, color]
end
Note, make sure to gem install paint to get the colored text output.
Here's the output:
It should be relatively straight-forward to write this as a SQL user-defined function and ORDER BY RGB_to_HUE(hex_color_code), however, my SQL knowledge is pretty basic.
EDIT: I posted this question on dba.SE about converting the Ruby to a SQL user defined function.
This is based on the answer by #dliff. I initially edited it, but it turns out my edit was rejected saying "it should have been written as a comment or an answer". Seeing this would be too large to post as a comment, here goes.
The reason for editing (and now posting) is this: there seems to be a problem with colors like 808080 because their R, G and B channels are equal. If one needs this to sort or group colors and keep the passed grayscale/non-colors separate, that answer won't work, so I edited it.
DELIMITER $$
DROP FUNCTION IF EXISTS `hex_to_hue`$$
CREATE FUNCTION `hex_to_hue`(HEX VARCHAR(6)) RETURNS FLOAT
BEGIN
DECLARE r FLOAT;
DECLARE b FLOAT;
DECLARE g FLOAT;
DECLARE MIN FLOAT;
DECLARE MAX FLOAT;
DECLARE delta FLOAT;
DECLARE hue FLOAT;
IF(HEX = '') THEN
RETURN NULL;
END IF;
SET r = CONV(SUBSTR(HEX, 1, 2), 16, 10)/255.0;
SET g = CONV(SUBSTR(HEX, 3, 2), 16, 10)/255.0;
SET b = CONV(SUBSTR(HEX, 5, 2), 16, 10)/255.0;
SET MAX = GREATEST(r,g,b);
SET MIN = LEAST(r,g,b);
SET delta = MAX - MIN;
SET hue=
(CASE
WHEN MAX=r THEN (60 * ((g - b)/delta % 6))
WHEN MAX=g THEN (60 * ((b - r)/delta + 2))
WHEN MAX=b THEN (60 * ((r - g)/delta + 4))
ELSE NULL
END);
IF(ISNULL(hue)) THEN
SET hue=999;
END IF;
RETURN hue;
END$$
DELIMITER ;
Again, I initially wanted to edit the original answer, not post as a separate one.
If your products can have lots of color probably a good UI will require a color picker, normally those are rectangular, so not really something possible with the order by.
If the products have a manageable number of colors you have different choice, the easiest to implement is an order table, where for every possible color is defined an order position, this table can then be joined to your query, something like
SELECT ...
FROM (SELECT ...
...
...
, ci.color_order
FROM customization_options co
LEFT JOIN customization_option_groups cog
ON cog.id = co.customization_option_group_id
LEFT JOIN color_ ci
ON designer_hex_color = ci.color
WHERE ...) a
ORDER BY color_order
Another way to go is to transform the RGB color to hue and use this as the order.
There are different formula for this conversion, depending on wich order you want the primary color to have, all of them can be found on the wikipedia page for hue, I can update the answer to help you convert one of those to T-SQL, if needed.
MySQL function Hex to Hue. Based on Tobi's answer. :)
CREATE FUNCTION `hex_to_hue`(hex varchar(6)) RETURNS float
BEGIN
declare r float;
declare b float;
declare g float;
declare min float;
declare max float;
declare delta float;
declare hue float;
set r = conv(substr(hex, 1, 2), 16, 10)/255.0;
set g = conv(substr(hex, 3, 2), 16, 10)/255.0;
set b = conv(substr(hex, 5, 2), 16, 10)/255.0;
set max = greatest(r,g,b);
set min = least(r,g,b);
set delta = max - min;
set hue=
(case
when max=r then (60 * ((g - b)/delta % 6))
when max=g then (60 * ((b - r)/delta + 2))
when max=b then (60 * ((r - g)/delta + 4))
else null
end);
RETURN hue;
END

mysql function to pretty print sizes (pg_size_pretty equivialent)?

postgresql have the pg_size_pretty() convenience function:
> select pg_size_pretty(100000);
pg_size_pretty
----------------
98 kB
Does MySQL have something similar ?
If not, before I make my own have anyone already made this, that they can share ?
There is no shipped function like that in MySQL.
So, I just created one : function pretty_size
https://github.com/lqez/pastebin/blob/master/mysql/pretty_size.sql
CREATE FUNCTION pretty_size( size DOUBLE )
RETURNS VARCHAR(255) DETERMINISTIC
BEGIN
DECLARE c INT DEFAULT 0;
DECLARE unit INT DEFAULT 1000;
DECLARE unitChar CHAR(6) DEFAULT 'KMGTPE';
DECLARE binaryPrefix BOOLEAN DEFAULT 1;
/* Set binaryPrefix = 1 to use binary unit & prefix */
/* See IEC 60027-2 A.2 and ISO/IEC 80000 */
IF binaryPrefix = 1 THEN
SET unit = 1024;
END IF;
WHILE size >= unit AND c < 6 DO
SET size = size / unit;
SET c = c + 1;
END WHILE;
/* Under 1K, just add 'Byte(s)' */
IF c = 0 THEN
RETURN CONCAT( size, ' B' );
END IF;
/* Modify as your taste */
RETURN CONCAT(
FORMAT( size, 2 ),
' ',
SUBSTR( unitChar, c, 1 ),
IF( binaryPrefix, 'iB', 'B' )
);
END $$
DELIMITER ;
pg_size_pretty
will give you Kb or MB or GB ,etc according to the internal code ,and you wont operate or sum this result ...
better use :
pg_relation_size :
returns the on-disk size in bytes ,so you can convert it to the format (kb,mb,gb) that you want.

Convert a stored md5 string to a decimal value in MySQL

I have a very large table in MySQL. I'm using a CHAR(32) field which contains an MD5 as a string of course. I'm running into an issue where I need to convert this to a decimal value using MySQL. A third party tool runs the query so writing code to do this isn't really an option.
MySQL does support storing hex values natively and converting them to integers. But it gets hung up converting it from a string. Here's what I've tried so far (md5_key is the name of my column)
First I just tried the UNHEX function but that returns a string so it gave me gooblygoop. I won't put that here. Next I tried the CAST function
SELECT CAST( CONCAT('0x',md5_key) AS UNSIGNED ) FROM bigtable limit 1
Result = 0
Show warnings gives me: "Truncated incorrect INTEGER value: '0x000002dcc38af6f209e91518db3e79d3'"
BUT if I do:
SELECT CAST( 0x000002dcc38af6f209e91518db3e79d3 AS UNSIGNED );
I get the correct decimal value.
So I guess what I need to know, is there a way to get MySQL to see that string as a hex value? (I also tried converting it to BINARY and then to the UNSIGNED but that didn't work either).
Thanks in advance!
conv() is limited to 64 bit integers. You can convert the high and low part to decimal and then add them together:
> select cast(conv(substr("000002dcc38af6f209e91518db3e79d3", 1, 16), 16, 10) as
decimal(65))*18446744073709551616 +
cast(conv(substr("000002dcc38af6f209e91518db3e79d3", 17, 16), 16, 10) as
decimal(65));
58055532535286745202684464101843
Where 18446744073709551616 = 2^64. So in your case:
> select cast(conv(substr(md5_key, 1, 16), 16, 10) as
decimal(65))*18446744073709551616 +
cast(conv(substr(md5_key, 17, 16), 16, 10) as
decimal(65))
from bigtable limit 1;
Beware MD5 are 16 Byte long, and BIGINT UNSIGNED is 8 Byte long, so even in your second case you don't get the right answer, the number can't fit you are receiving the value of the lowest 8 Byte=> 09e91518db3e79d3.
I wrote a function to convert big hex numbers to decimal(65).
CREATE FUNCTION `hexnum_to_decimal`(hex varchar(66)) RETURNS decimal(65,0)
DETERMINISTIC
BEGIN
declare group1 decimal(65);
declare group2 decimal(65);
declare group3 decimal(65);
declare group4 decimal(65);
declare multiplier decimal(65);
if (substr(hex, 1, 2) = "0x") then
set hex = substr(hex, 3); -- trim 0x if exists
end if;
set hex = trim(LEADING '0' from hex);
if (length(hex) > 54) then
return null; -- too big number
end if;
set hex = lpad(hex, 64, 0);
set group1 = cast(conv(substr(hex, 49, 16), 16, 10) as decimal(65));
set group2 = cast(conv(substr(hex, 33, 16), 16, 10) as decimal(65));
set group3 = cast(conv(substr(hex, 17, 16), 16, 10) as decimal(65));
set group4 = cast(conv(substr(hex, 1, 16), 16, 10) as decimal(65));
set multiplier = 18446744073709551616; -- 2 ^ 16
-- check for overflow
if (
(group4 > 15930919) or
(group4 = 15930919 and group3 > 2053574980671369030) or
(group4 = 15930919 and group3 = 2053574980671369030 and group2 > 5636613303479645705) or
(group4 = 15930919 and group3 = 2053574980671369030 and group2 = 5636613303479645705 and group1 > 18446744073709551615)
) then
return null;
end if;
return cast(
group1 +
group2 * multiplier +
group3 * multiplier * multiplier +
group4 * multiplier * multiplier * multiplier
as decimal(65));
END
In your case for 000002dcc38af6f209e91518db3e79d3
select hexnum_to_decimal("000002dcc38af6f209e91518db3e79d3");
58055532535286745202684464101843
select hexnum_to_decimal('F316271C7FC3908A8BEF464E3945EF7A253609FFFFFFFFFFFFFFFF');
99999999999999999999999999999999999999999999999999999999999999999
if bigger hex number is passed, the function will return null.