1
0
mirror of https://github.com/MariaDB/server.git synced 2025-07-30 16:24:05 +03:00

MDEV-27744 LPAD in vcol created in ORACLE mode makes table corrupted in non-ORACLE

The crash happened with an indexed virtual column whose
value is evaluated using a function that has a different meaning
in sql_mode='' vs sql_mode=ORACLE:

- DECODE()
- LTRIM()
- RTRIM()
- LPAD()
- RPAD()
- REPLACE()
- SUBSTR()

For example:

CREATE TABLE t1 (
  b VARCHAR(1),
  g CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL,
  KEY g(g)
);

So far we had replacement XXX_ORACLE() functions for all mentioned function,
e.g. SUBSTR_ORACLE() for SUBSTR(). So it was possible to correctly re-parse
SUBSTR_ORACLE() even in sql_mode=''.

But it was not possible to re-parse the MariaDB version of SUBSTR()
after switching to sql_mode=ORACLE. It was erroneously mis-interpreted
as SUBSTR_ORACLE().

As a result, this combination worked fine:

SET sql_mode=ORACLE;
CREATE TABLE t1 ... g CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL, ...;
INSERT ...
FLUSH TABLES;
SET sql_mode='';
INSERT ...

But the other way around it crashed:

SET sql_mode='';
CREATE TABLE t1 ... g CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL, ...;
INSERT ...
FLUSH TABLES;
SET sql_mode=ORACLE;
INSERT ...

At CREATE time, SUBSTR was instantiated as Item_func_substr and printed
in the FRM file as substr(). At re-open time with sql_mode=ORACLE, "substr()"
was erroneously instantiated as Item_func_substr_oracle.

Fix:

The fix proposes a symmetric solution. It provides a way to re-parse reliably
all sql_mode dependent functions to their original CREATE TABLE time meaning,
no matter what the open-time sql_mode is.

We take advantage of the same idea we previously used to resolve sql_mode
dependent data types.

Now all sql_mode dependent functions are printed by SHOW using a schema
qualifier when the current sql_mode differs from the function sql_mode:

SET sql_mode='';
CREATE TABLE t1 ... SUBSTR(a,b,c) ..;
SET sql_mode=ORACLE;
SHOW CREATE TABLE t1;   ->   mariadb_schema.substr(a,b,c)

SET sql_mode=ORACLE;
CREATE TABLE t2 ... SUBSTR(a,b,c) ..;
SET sql_mode='';
SHOW CREATE TABLE t1;   ->   oracle_schema.substr(a,b,c)

Old replacement names like substr_oracle() are still understood for
backward compatibility and used in FRM files (for downgrade compatibility),
but they are not printed by SHOW any more.
This commit is contained in:
Alexander Barkov
2022-04-04 14:50:21 +04:00
parent 228b7e4db5
commit 2b6d241ee4
34 changed files with 3754 additions and 126 deletions

View File

@ -0,0 +1,247 @@
--let $MYSQLD_DATADIR= `select @@datadir`
--echo #
--echo # MDEV-27744 LPAD in vcol created in ORACLE mode makes table corrupted in non-ORACLE
--echo #
#
# Testing that the error message for DECODE preserves
# the exact letter case as typed by the user
#
SET sql_mode=DEFAULT;
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT decode_oracle(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT DECODE_ORACLE(1);
SET sql_mode=ORACLE;
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT decode_oracle(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT DECODE_ORACLE(1);
SET sql_mode=DEFAULT;
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT decode(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT DECODE(1);
SET sql_mode=ORACLE;
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT decode(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT DECODE(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT mariadb_schema.decode(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT mariadb_schema.DECODE(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT mariadb_schema.decode_oracle(1);
--error ER_WRONG_PARAMCOUNT_TO_NATIVE_FCT
SELECT mariadb_schema.DECODE_ORACLE(1);
#
# Testing that REPLACE, SUBSTR, TRIM print the exact name
# as typed by the user in "Function .. is not defined"
#
SET sql_mode=DEFAULT;
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.TRIM(1);
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.trim(1);
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.TRIM();
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.TRIM('a','b');
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.TRIM('a','b','c','d');
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.SUBSTR('a',1,2);
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.substr('a',1,2);
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.SUBSTRING('a',1,2);
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.substring('a',1,2);
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.REPLACE('a','b','c');
--error ER_FUNC_INEXISTENT_NAME_COLLISION
SELECT unknown.replace('a','b','c');
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.REPLACE();
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.REPLACE('a');
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.REPLACE('a','b');
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.REPLACE('a','b','c','d');
#
# Testing EXPLAIN EXTENDED SELECT
#
SET sql_mode=DEFAULT;
DELIMITER $$;
CREATE PROCEDURE p1(sqlmode TEXT, qualifier TEXT, expr TEXT)
BEGIN
DECLARE query TEXT DEFAULT 'SELECT $(QUALIFIER)$(EXPR)';
DECLARE errmsg TEXT DEFAULT NULL;
DECLARE CONTINUE HANDLER FOR 1064, 1128, 1305, 1582, 1630
BEGIN
GET DIAGNOSTICS CONDITION 1 errmsg = MESSAGE_TEXT;
END;
SET sql_mode=sqlmode;
SET query=REPLACE(query, '$(QUALIFIER)', qualifier);
SET query=REPLACE(query, '$(EXPR)', expr);
SET query= CONCAT('EXPLAIN EXTENDED ', query);
SELECT CONCAT('sql_mode=''',sqlmode,'''', ' ',
'qualifier=''',qualifier,'''') AS `----------`;
SELECT query;
EXECUTE IMMEDIATE query;
IF errmsg IS NOT NULL THEN
SELECT CONCAT('ERROR: ', errmsg) AS errmsg;
ELSE
SHOW WARNINGS;
END IF;
END;
$$
CREATE PROCEDURE p2(sqlmode TEXT, expr TEXT)
BEGIN
CALL p1(sqlmode, '', expr);
CALL p1(sqlmode, 'unknown_schema.', expr);
CALL p1(sqlmode, 'mariadb_schema.', expr);
CALL p1(sqlmode, 'maxdb_schema.', expr);
CALL p1(sqlmode, 'oracle_schema.', expr);
END;
$$
CREATE PROCEDURE p3(expr TEXT)
BEGIN
CALL p2('', expr);
CALL p2('ORACLE', expr);
END;
$$
DELIMITER ;$$
CALL p3('CONCAT(''a'')');
# MariaDB style
CALL p3('DECODE(''1'',''2'')');
# Oracle style
CALL p3('DECODE(1,1,10)');
CALL p3('LTRIM(''a'')');
CALL p3('RTRIM(''a'')');
CALL p3('LPAD(''a'',3)');
CALL p3('LPAD(''a'',3, '' '')');
CALL p3('RPAD(''a'',3)');
CALL p3('RPAD(''a'',3, '' '')');
CALL p3('REPLACE()');
CALL p3('REPLACE(''a'',''b'')');
CALL p3('REPLACE(''a'',''b'',''c'',''d'')');
CALL p3('REPLACE(''a'',''b'',''c'')');
CALL p3('SUBSTR()');
CALL p3('SUBSTR(''a'',1,2,3)');
CALL p3('SUBSTR(''a'',1,2)');
CALL p3('SUBSTR(''a'' FROM 1)');
CALL p3('SUBSTRING(''a'',1,2)');
CALL p3('SUBSTRING(''a'' FROM 1)');
CALL p3('TRIM()');
CALL p3('TRIM(1,2)');
CALL p3('TRIM(''a'')');
CALL p3('TRIM(BOTH '' '' FROM ''a'')');
# Deprecated compatibility XXX_ORACLE functions.
# These functions are implemented as simple native functions
# and have no special grammar rules in sql_yacc.yy.
# So they support the qualified syntax automatically,
# which is not absolutely required, but is not harmful.
CALL p3('CONCAT_OPERATOR_ORACLE(''a'')');
CALL p3('DECODE_ORACLE(1,1,10)');
CALL p3('LTRIM_ORACLE(''a'')');
CALL p3('RTRIM_ORACLE(''a'')');
CALL p3('LPAD_ORACLE(''a'',3)');
CALL p3('RPAD_ORACLE(''a'',3)');
CALL p3('REPLACE_ORACLE(''a'',''b'',''c'')');
CALL p3('SUBSTR_ORACLE(''a'',1,2)');
# Deprecated compatibility XXX_ORACLE variants for functions
# with a special syntax in sql_yacc.yy.
# These compatibility functions do not support qualified syntax.
# One should use a qualified variant without the _ORACLE suffix instead.
--error ER_PARSE_ERROR
SELECT oracle_schema.SUBSTR_ORACLE('a' FROM 1 FOR 2);
# Use this instead:
SELECT oracle_schema.SUBSTR('a' FROM 1 FOR 2);
--error ER_PARSE_ERROR
SELECT oracle_schema.TRIM_ORACLE(LEADING ' ' FROM 'a');
# Use this instead:
SELECT oracle_schema.TRIM(LEADING ' ' FROM 'a');
--error ER_FUNCTION_NOT_DEFINED
SELECT oracle_schema.TRIM_ORACLE('a');
# Use this instead:
SELECT oracle_schema.TRIM('a');
DROP PROCEDURE p1;
DROP PROCEDURE p2;
DROP PROCEDURE p3;
SET sql_mode='';
CREATE VIEW v1 AS SELECT
concat('a','b'),
decode('1','2'),
ltrim('1'),
rtrim('1'),
lpad('1','2', 3),
rpad('1','2', 3),
replace('1','2','3'),
substr('a',1,2),
trim(both 'a' FROM 'b');
CREATE TABLE kv (v BLOB);
--replace_result $MYSQLD_DATADIR MYSQLD_DATADIR
eval LOAD DATA INFILE '$MYSQLD_DATADIR/test/v1.frm' REPLACE INTO TABLE kv;
SELECT v FROM kv WHERE v RLIKE '^(query|view_body_utf8)=' ORDER BY v;
SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME='v1' AND TABLE_SCHEMA='test';
DROP TABLE kv;
DROP VIEW v1;
SET sql_mode='ORACLE';
CREATE VIEW v1 AS SELECT
concat('a','b'),
decode('1',2,3),
ltrim('1'),
rtrim('1'),
lpad('1','2', 3),
rpad('1','2', 3),
replace('1','2','3'),
substr('a',1,2),
trim(both 'a' FROM 'b');
CREATE TABLE kv (v BLOB);
--replace_result $MYSQLD_DATADIR MYSQLD_DATADIR
eval LOAD DATA INFILE '$MYSQLD_DATADIR/test/v1.frm' REPLACE INTO TABLE kv;
SELECT v FROM kv WHERE v RLIKE '^(query|view_body_utf8)=' ORDER BY v;
SELECT VIEW_DEFINITION FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME='v1' AND TABLE_SCHEMA='test';
DROP TABLE kv;
DROP VIEW v1;

View File

@ -0,0 +1,50 @@
# See comments in mysql-test/main/mysqldump_restore.test
--source include/not_embedded.inc
let $mysqldumpfile = $MYSQLTEST_VARDIR/tmp/mysqldump_func_qualified.sql;
--echo #
--echo # Start of 10.4 tests
--echo #
--echo #
--echo # MDEV-27744 LPAD in vcol created in ORACLE mode makes table corrupted in non-ORACLE
--echo #
SET sql_mode=DEFAULT;
CREATE TABLE t1 (
a0 VARCHAR(64) NOT NULL DEFAULT LTRIM(now()),
a1 VARCHAR(64) AS (LTRIM(a0)) PERSISTENT,
b0 VARCHAR(64) NOT NULL DEFAULT LPAD(now(),10),
b1 VARCHAR(64) AS (LPAD(b0,10)) PERSISTENT
);
CREATE VIEW v1 AS SELECT
LTRIM(now()) AS a0,
LPAD(now(),10) AS b0;
SET sql_mode=ORACLE;
CREATE TABLE t2 (
a0 VARCHAR(64) NOT NULL DEFAULT LTRIM(now()),
a1 VARCHAR(64) AS (LTRIM(a0)) PERSISTENT,
b0 VARCHAR(64) NOT NULL DEFAULT LPAD(now(),10),
b1 VARCHAR(64) AS (LPAD(b0,10)) PERSISTENT
);
CREATE VIEW v2 AS SELECT
LTRIM(now()) AS a0,
LPAD(now(),10) AS b0;
--exec $MYSQL_DUMP --skip-extended-insert test --skip-comments --compact t1 t2 v1 v2
--exec $MYSQL_DUMP --skip-extended-insert test --skip-comments t1 t2 v1 v2 > $mysqldumpfile
DROP TABLE t1,t2;
DROP VIEW v1,v2;
--exec $MYSQL test < $mysqldumpfile
SET sql_mode=DEFAULT;
SHOW CREATE TABLE t1;
SHOW CREATE TABLE t2;
SHOW CREATE VIEW v1;
SHOW CREATE VIEW v2;
--remove_file $mysqldumpfile
DROP TABLE t1,t2;
DROP VIEW v1, v2;
--echo #
--echo # End of 10.4 tests
--echo #

View File

@ -0,0 +1,47 @@
--source include/have_innodb.inc
--echo #
--echo # MDEV-27744 LPAD in vcol created in ORACLE mode makes table corrupted in non-ORACLE
--echo #
FLUSH TABLES;
SET sql_mode='';
CREATE TABLE t (d INT,b VARCHAR(1),c CHAR(1),g CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL,PRIMARY KEY(b),KEY g(g)) ENGINE=InnoDB;
--error ER_WRONG_VALUE_COUNT_ON_ROW
INSERT INTO t VALUES (0);
SET sql_mode='ORACLE';
INSERT INTO t SET c=REPEAT (1,0);
--error ER_BAD_FIELD_ERROR
ALTER TABLE t CHANGE COLUMN a b INT;
DELETE FROM t;
SET sql_mode='';
FLUSH TABLES;
INSERT INTO t SET c='0';
DROP TABLE t;
FLUSH TABLES;
SET sql_mode='';
CREATE TABLE t (a INT(1),d INT(1),b VARCHAR(1),c CHAR(1),vadc INT(1) GENERATED ALWAYS AS ( (a + length (d))) STORED,vbc CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL,vbidxc CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL,PRIMARY KEY(b (1),a,d),KEY d (d),KEY a (a),KEY c_renamed (c (1),b (1)),KEY b (b (1),c (1),a),KEY vbidxc (vbidxc),KEY a_2 (a,vbidxc),KEY vbidxc_2 (vbidxc,d)) DEFAULT CHARSET=latin1 ENGINE=InnoDB;
--error ER_WRONG_VALUE_COUNT_ON_ROW
INSERT INTO t VALUES (0,0,1,0,1,0,1,0,0);
SET SESSION sql_mode='ORACLE';
INSERT INTO t SET c=REPEAT (1,0);
--error ER_DUP_FIELDNAME
ALTER TABLE t CHANGE COLUMN a b CHAR(1);
DELETE FROM t;
SET SESSION sql_mode=DEFAULT;
DROP TABLE t;
SET sql_mode='';
CREATE TABLE t1 (d INT,b VARCHAR(1),c CHAR(1),g CHAR(1) GENERATED ALWAYS AS (SUBSTR(b,0,0)) VIRTUAL,PRIMARY KEY(b),KEY g(g)) ENGINE=InnoDB;
--error ER_WRONG_VALUE_COUNT_ON_ROW
INSERT INTO t1 VALUES (0);
SET sql_mode='ORACLE';
INSERT INTO t1 SET c=REPEAT (1,0);
--error ER_BAD_FIELD_ERROR
ALTER TABLE t1 CHANGE COLUMN a b INT;
DELETE FROM t1;
SET sql_mode='';
FLUSH TABLES;
INSERT INTO t1 SET c='0';
DROP TABLE t1;