Skip to content

Commit a20ec1c

Browse files
authored
Handle invalid characters in STRING to MONEY type conversion (#4253)
In Babelfish, the scanfixeddecimal() function currently accepts any non-digit character (except +, -, and .) as a valid currency symbol, leading to incorrect MONEY type conversions. For example: --BBF select cast('!' as money); money -------- 0.0000 Solution: Added predefined list of currency symbols supported by Babelfish with a validation function Enhanced scanfixeddecimal() to accept only valid currency symbols and numeric inputs Issues Resolved BABEL-704 and BABEL-2187 Signed-off-by: Vineetha125 <bvineets@amazon.com>
1 parent 89deaf2 commit a20ec1c

28 files changed

+1417
-74
lines changed

contrib/babelfishpg_money/fixeddecimal.c

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,69 @@ typedef struct FixedDecimalAggState
197197
int64 sumX; /* sum of processed numbers */
198198
} FixedDecimalAggState;
199199

200+
/*
201+
* NOTE: Keep this currency symbol list synchronized with the scan-tsql-decl.l file.
202+
* Currency symbols supported by TSQL
203+
*/
204+
static const char *valid_currency_symbols[] = {
205+
"$", /* dollar */
206+
"\xC2\xA2", /* cent */
207+
"\xC2\xA4", /* currency */
208+
"\xC2\xA3", /* pound */
209+
"\xC2\xA5", /* yen */
210+
"\xE0\xA7\xB2", /* bengali_rupee_mark */
211+
"\xE0\xA7\xB3", /* bengali_rupee_sign */
212+
"\xE0\xB8\xBF", /* thai_baht */
213+
"\xE1\x9F\x9B", /* khmer_riel */
214+
"\xE2\x82\xA0", /* euro_currency */
215+
"\xE2\x82\xA1", /* colon_sign */
216+
"\xE2\x82\xA2", /* cruzeiro */
217+
"\xE2\x82\xA3", /* franc */
218+
"\xE2\x82\xA4", /* lira */
219+
"\xE2\x82\xA5", /* mill */
220+
"\xE2\x82\xA6", /* naira */
221+
"\xE2\x82\xA7", /* peseta */
222+
"\xE2\x82\xA8", /* rupee */
223+
"\xE2\x82\xA9", /* won */
224+
"\xE2\x82\xAA", /* new_sheqel */
225+
"\xE2\x82\xAB", /* dong */
226+
"\xE2\x82\xAC", /* euro */
227+
"\xE2\x82\xAD", /* kip */
228+
"\xE2\x82\xAE", /* tugrik */
229+
"\xE2\x82\xAF", /* drachma */
230+
"\xE2\x82\xB0", /* german_penny */
231+
"\xE2\x82\xB1", /* peso */
232+
"\xEF\xB7\xBC", /* rial */
233+
"\xEF\xB9\xA9", /* small_dollar */
234+
"\xEF\xBC\x84", /* fullwidth_dollar */
235+
"\xEF\xBF\xA0", /* fullwidth_cent */
236+
"\xEF\xBF\xA1", /* fullwidth_pound */
237+
"\xEF\xBF\xA5", /* fullwidth_yen */
238+
"\xEF\xBF\xA6", /* fullwidth_won */
239+
};
240+
241+
#define NUM_CURRENCY_SYMBOLS (sizeof(valid_currency_symbols) / sizeof(valid_currency_symbols[0]))
242+
243+
/*
244+
* Checks if the string starts with a valid currency symbol
245+
*/
246+
static size_t
247+
is_valid_currency_symbol(const char *ptr)
248+
{
249+
int i;
250+
if (ptr == NULL)
251+
return 0;
252+
253+
for (i = 0; i < NUM_CURRENCY_SYMBOLS; i++)
254+
{
255+
const char *symbol = valid_currency_symbols[i];
256+
size_t symbol_len = strlen(symbol);
257+
if (strncmp(ptr, symbol, symbol_len) == 0)
258+
return symbol_len;
259+
}
260+
return 0;
261+
}
262+
200263
static char *pg_int64tostr(char *str, int64 value);
201264
static char *pg_int64tostr_zeropad(char *str, int64 value, int64 padding);
202265
static bool apply_typmod(int64 value, int32 typmod, int precision, int scale, FunctionCallInfo *fcinfo);
@@ -425,6 +488,7 @@ scanfixeddecimal(const char *str, int *precision, int *scale, FunctionCallInfo *
425488
int vscale = 0;
426489
bool has_seen_sign = false;
427490
Node *escontext = (*fcinfo)->context;
491+
size_t currency_symbol_len;
428492

429493
/*
430494
* Do our own scan, rather than relying on sscanf which might be broken
@@ -456,32 +520,31 @@ scanfixeddecimal(const char *str, int *precision, int *scale, FunctionCallInfo *
456520
/* skip leading spaces */
457521
while (isspace((unsigned char) *ptr))
458522
ptr++;
459-
460-
/* skip currency symbol bytes */
461-
while (!isdigit((unsigned char) *ptr) &&
462-
(unsigned int) *ptr != '.' &&
463-
(unsigned int) *ptr != '-' &&
464-
(unsigned int) *ptr != '+' &&
465-
(unsigned int) *ptr != ' ' &&
466-
(unsigned int) *ptr != '\0')
523+
524+
currency_symbol_len = is_valid_currency_symbol(ptr);
525+
if (currency_symbol_len > 0)
467526
{
468-
/*
469-
* Current workaround for BABEL-704 - this will accept multiple
470-
* currency symbols until BABEL-704 is fixed
471-
*/
472-
if ((*ptr >= 'a' && *ptr <= 'z') || (*ptr >= 'A' && *ptr <= 'Z'))
473-
{
474-
ereturn(escontext, (Datum) 0,
475-
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
476-
errmsg("invalid characters found: cannot cast value \"%s\" to money",
477-
str)));
478-
}
479-
ptr++;
527+
ptr += currency_symbol_len;
480528
}
481-
482529
/* skip leading spaces */
483530
while (isspace((unsigned char) *ptr))
484531
ptr++;
532+
533+
/*
534+
* Rejects invalid characters when no currency symbol is present.
535+
* Only digits, signs, decimal points, or spaces are allowed.
536+
*/
537+
if (*ptr != '\0' &&
538+
!isdigit((unsigned char) *ptr) &&
539+
*ptr != '.' &&
540+
*ptr != '-' &&
541+
*ptr != '+')
542+
{
543+
ereturn(escontext, (Datum) 0,
544+
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
545+
errmsg("invalid characters found: cannot cast value \"%s\" to money",
546+
str)));
547+
}
485548

486549
/*
487550
* Handle sign again. This is needed so that a sign after the currency

test/JDBC/expected/babel_datatype.out

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -388,10 +388,9 @@ money
388388
-- Test unsupoorted currency symbol
389389
select CAST('←100.123' AS money);
390390
GO
391-
~~START~~
392-
money
393-
100.1230
394-
~~END~~
391+
~~ERROR (Code: 293)~~
392+
393+
~~ERROR (Message: invalid characters found: cannot cast value "←100.123" to money)~~
395394

396395

397396
-- Test that space is allowed between currency symbol and number, this is
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
DROP PROCEDURE IF EXISTS TestMoneyCast;
2+
GO
3+
4+
DROP FUNCTION IF EXISTS test_money_func;
5+
GO
6+
7+
DROP VIEW test_validcurrency;
8+
GO
9+
10+
DROP VIEW test1;
11+
GO
12+
13+
DROP FUNCTION IF EXISTS money_func;
14+
GO
15+
16+
DROP TABLE IF EXISTS transactions;
17+
GO
18+
19+
DROP FUNCTION IF EXISTS fn_SmallToMoney;
20+
GO
21+
22+
DROP PROCEDURE IF EXISTS Test_Arithmetic;
23+
GO
24+
25+
DROP PROCEDURE IF EXISTS Test_Boundaries;
26+
GO
27+
28+
DROP FUNCTION IF EXISTS round_money;
29+
GO
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
--Procedure
2+
CREATE PROCEDURE TestMoneyCast(@input VARCHAR(20))
3+
AS
4+
BEGIN
5+
SELECT CAST(@input AS MONEY);
6+
END;
7+
GO
8+
9+
--Function
10+
CREATE FUNCTION test_money_func(@amount MONEY)
11+
RETURNS MONEY
12+
AS
13+
BEGIN
14+
RETURN @amount * 2;
15+
END;
16+
GO
17+
18+
--Views
19+
CREATE VIEW test_validcurrency AS SELECT CAST('$10050.78' AS MONEY) AS Amount;
20+
GO
21+
22+
CREATE VIEW test1 AS SELECT CAST('$ ' AS MONEY) AS Amount;
23+
GO
24+
25+
--Function
26+
CREATE FUNCTION money_func()
27+
RETURNS @test TABLE(
28+
a MONEY, b MONEY, c MONEY, d MONEY
29+
)
30+
AS
31+
BEGIN
32+
INSERT INTO @test
33+
SELECT
34+
CAST('$123.45' AS MONEY),
35+
CAST('$-123.45' AS MONEY),
36+
CAST(NULL AS MONEY),
37+
CAST('$+.657' AS MONEY);
38+
39+
RETURN;
40+
END;
41+
GO
42+
43+
CREATE TABLE transactions (
44+
id INT,
45+
debit MONEY,
46+
credit MONEY,
47+
balance MONEY,
48+
transaction_date DATE,
49+
amount MONEY
50+
);
51+
GO
52+
53+
--insert data with dates
54+
INSERT INTO transactions VALUES
55+
(1, '$100.00', '€95.00', '$5.00', '2024-01-15', '$1500.00'),
56+
(2, '£75.50', '$80.00', '-$4.50', '2024-01-20', '$500.00'),
57+
(3, '$400.00', '$390.00', '$10.00', '2024-03-15', '$900.00');
58+
GO
59+
~~ROW COUNT: 3~~
60+
61+
62+
--Function to convert smallmoney to money
63+
CREATE FUNCTION fn_SmallToMoney(@small SMALLMONEY)
64+
RETURNS MONEY
65+
AS
66+
BEGIN
67+
DECLARE @money MONEY;
68+
SET @money = CAST(@small AS MONEY);
69+
RETURN @money;
70+
END;
71+
GO
72+
73+
--Procedure
74+
CREATE PROCEDURE Test_Arithmetic
75+
AS
76+
BEGIN
77+
DECLARE @a MONEY = '$00000';
78+
DECLARE @b MONEY = '¤10,0,13';
79+
80+
SELECT
81+
@a + @b AS Addition,
82+
@a - @b AS Subtraction,
83+
@a * 2 AS Multiplication,
84+
@a / @b AS Division,
85+
@a % @b AS Modulo;
86+
END;
87+
GO
88+
--Procedure to test boundary values
89+
CREATE PROCEDURE Test_Boundaries
90+
AS
91+
BEGIN
92+
DECLARE @maxMoney MONEY = 922337203685477.5807;
93+
DECLARE @minMoney MONEY = -922337203685477.5808;
94+
95+
SELECT
96+
@maxMoney AS MaxMoney,
97+
@minMoney AS MinMoney
98+
END;
99+
GO
100+
--Function to round off money
101+
CREATE FUNCTION round_money(
102+
@amount MONEY,
103+
@decimals INT = 2
104+
)
105+
RETURNS MONEY
106+
AS
107+
BEGIN
108+
RETURN ROUND(@amount, @decimals);
109+
END;
110+
GO

0 commit comments

Comments
 (0)