mirror of
https://github.com/MariaDB/server.git
synced 2025-01-27 01:04:19 +01:00
MDEV-35854: Simplify dict_get_referenced_table()
innodb_convert_name(): Convert a schema or table name to my_charset_filename compatible format. dict_table_lookup(): Replaces dict_get_referenced_table(). Make the callers responsible for invoking innodb_convert_name(). innobase_casedn_str(): Remove. Let us invoke my_casedn_str() directly. dict_table_rename_in_cache(): Do not duplicate a call to dict_mem_foreign_table_name_lookup_set(). innobase_convert_to_filename_charset(): Defined static in the only compilation unit that needs it. dict_scan_id(): Remove the constant parameters table_id=FALSE, accept_also_dot=TRUE. Invoke strconvert() directly. innobase_convert_from_id(): Remove; only called from dict_scan_id(). innobase_convert_from_table_id(): Remove (dead code). table_name_t::dblen(), table_name_t::basename(): In non-debug builds, tolerate names that may miss a '/' separator. Reviewed by: Debarun Banerjee
This commit is contained in:
parent
fa74c1a40f
commit
d4da659b43
11 changed files with 245 additions and 324 deletions
|
@ -1118,5 +1118,29 @@ test.binaries check status OK
|
|||
test.collections check status OK
|
||||
disconnect con1;
|
||||
DROP TABLE binaries, collections;
|
||||
CREATE SCHEMA `#mysql50##mysql50#d-b`;
|
||||
CREATE TABLE `#mysql50##mysql50#d-b`.t1 (a INT PRIMARY KEY, b INT UNIQUE) engine=InnoDB;
|
||||
USE `#mysql50##mysql50#d-b`;
|
||||
CREATE TABLE t2 (a INT PRIMARY KEY, b INT UNIQUE REFERENCES t1(b)) ENGINE=InnoDB;
|
||||
SET STATEMENT foreign_key_checks=0 FOR
|
||||
ALTER TABLE t2 ADD FOREIGN KEY (a) REFERENCES t1(a);
|
||||
SHOW CREATE TABLE t2;
|
||||
Table Create Table
|
||||
t2 CREATE TABLE `t2` (
|
||||
`a` int(11) NOT NULL,
|
||||
`b` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`a`),
|
||||
UNIQUE KEY `b` (`b`),
|
||||
CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`b`) REFERENCES `t1` (`b`),
|
||||
CONSTRAINT `t2_ibfk_2` FOREIGN KEY (`a`) REFERENCES `t1` (`a`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci
|
||||
INSERT INTO t1 SET a=1;
|
||||
INSERT INTO t2 SET a=1;
|
||||
DELETE FROM t1;
|
||||
ERROR 23000: Cannot delete or update a parent row: a foreign key constraint fails (`#mysql50#d-b`.`t2`, CONSTRAINT `t2_ibfk_2` FOREIGN KEY (`a`) REFERENCES `t1` (`a`))
|
||||
DELETE FROM t2;
|
||||
DELETE FROM t1;
|
||||
DROP DATABASE `#mysql50##mysql50#d-b`;
|
||||
USE test;
|
||||
# End of 10.6 tests
|
||||
SET GLOBAL innodb_stats_persistent = @save_stats_persistent;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
--disable_query_log
|
||||
call mtr.add_suppression("InnoDB: Transaction was aborted due to ");
|
||||
call mtr.add_suppression("Invalid \\(old\\?\\) table or database name '#mysql50#d-b'");
|
||||
--enable_query_log
|
||||
|
||||
SET GLOBAL innodb_stats_persistent = 0;
|
||||
|
@ -1188,6 +1189,22 @@ CHECK TABLE binaries, collections EXTENDED;
|
|||
# Cleanup
|
||||
DROP TABLE binaries, collections;
|
||||
|
||||
CREATE SCHEMA `#mysql50##mysql50#d-b`;
|
||||
CREATE TABLE `#mysql50##mysql50#d-b`.t1 (a INT PRIMARY KEY, b INT UNIQUE) engine=InnoDB;
|
||||
USE `#mysql50##mysql50#d-b`;
|
||||
CREATE TABLE t2 (a INT PRIMARY KEY, b INT UNIQUE REFERENCES t1(b)) ENGINE=InnoDB;
|
||||
SET STATEMENT foreign_key_checks=0 FOR
|
||||
ALTER TABLE t2 ADD FOREIGN KEY (a) REFERENCES t1(a);
|
||||
SHOW CREATE TABLE t2;
|
||||
INSERT INTO t1 SET a=1;
|
||||
INSERT INTO t2 SET a=1;
|
||||
--error ER_ROW_IS_REFERENCED_2
|
||||
DELETE FROM t1;
|
||||
DELETE FROM t2;
|
||||
DELETE FROM t1;
|
||||
DROP DATABASE `#mysql50##mysql50#d-b`;
|
||||
USE test;
|
||||
|
||||
--echo # End of 10.6 tests
|
||||
|
||||
SET GLOBAL innodb_stats_persistent = @save_stats_persistent;
|
||||
|
|
|
@ -1467,6 +1467,26 @@ dict_table_t::rename_tablespace(span<const char> new_name, bool replace) const
|
|||
return err;
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
Converts an identifier from my_charset_filename to UTF-8 charset.
|
||||
@return result string length, as returned by strconvert() */
|
||||
static
|
||||
uint
|
||||
innobase_convert_to_filename_charset(
|
||||
/*=================================*/
|
||||
char* to, /* out: converted identifier */
|
||||
const char* from, /* in: identifier to convert */
|
||||
ulint len) /* in: length of 'to', in bytes */
|
||||
{
|
||||
uint errors;
|
||||
CHARSET_INFO* cs_to = &my_charset_filename;
|
||||
CHARSET_INFO* cs_from = system_charset_info;
|
||||
|
||||
return(static_cast<uint>(strconvert(
|
||||
cs_from, from, uint(strlen(from)),
|
||||
cs_to, to, static_cast<uint>(len), &errors)));
|
||||
}
|
||||
|
||||
/**********************************************************************//**
|
||||
Renames a table object.
|
||||
@return TRUE if success */
|
||||
|
@ -1599,19 +1619,20 @@ dict_table_rename_in_cache(
|
|||
foreign->referenced_table->referenced_set.erase(foreign);
|
||||
}
|
||||
|
||||
if (strlen(foreign->foreign_table_name)
|
||||
< strlen(table->name.m_name)) {
|
||||
const bool do_alloc = strlen(foreign->foreign_table_name)
|
||||
< strlen(table->name.m_name);
|
||||
|
||||
if (do_alloc) {
|
||||
/* Allocate a longer name buffer;
|
||||
TODO: store buf len to save memory */
|
||||
|
||||
foreign->foreign_table_name = mem_heap_strdup(
|
||||
foreign->heap, table->name.m_name);
|
||||
dict_mem_foreign_table_name_lookup_set(foreign, TRUE);
|
||||
} else {
|
||||
strcpy(foreign->foreign_table_name,
|
||||
table->name.m_name);
|
||||
dict_mem_foreign_table_name_lookup_set(foreign, FALSE);
|
||||
}
|
||||
dict_mem_foreign_table_name_lookup_set(foreign, do_alloc);
|
||||
if (strchr(foreign->id, '/')) {
|
||||
/* This is a >= 4.0.18 format id */
|
||||
|
||||
|
@ -3105,20 +3126,13 @@ dict_scan_id(
|
|||
mem_heap_t* heap, /*!< in: heap where to allocate the id
|
||||
(NULL=id will not be allocated, but it
|
||||
will point to string near ptr) */
|
||||
const char** id, /*!< out,own: the id; NULL if no id was
|
||||
const char** id) /*!< out,own: the id; NULL if no id was
|
||||
scannable */
|
||||
ibool table_id,/*!< in: TRUE=convert the allocated id
|
||||
as a table name; FALSE=convert to UTF-8 */
|
||||
ibool accept_also_dot)
|
||||
/*!< in: TRUE if also a dot can appear in a
|
||||
non-quoted id; in a quoted id it can appear
|
||||
always */
|
||||
{
|
||||
char quote = '\0';
|
||||
ulint len = 0;
|
||||
const char* s;
|
||||
char* str;
|
||||
char* dst;
|
||||
|
||||
*id = NULL;
|
||||
|
||||
|
@ -3154,7 +3168,6 @@ dict_scan_id(
|
|||
}
|
||||
} else {
|
||||
while (!my_isspace(cs, *ptr) && *ptr != '(' && *ptr != ')'
|
||||
&& (accept_also_dot || *ptr != '.')
|
||||
&& *ptr != ',' && *ptr != '\0') {
|
||||
|
||||
ptr++;
|
||||
|
@ -3188,125 +3201,15 @@ dict_scan_id(
|
|||
str = mem_heap_strdupl(heap, s, len);
|
||||
}
|
||||
|
||||
if (!table_id) {
|
||||
convert_id:
|
||||
/* Convert the identifier from connection character set
|
||||
to UTF-8. */
|
||||
len = 3 * len + 1;
|
||||
*id = dst = static_cast<char*>(mem_heap_alloc(heap, len));
|
||||
|
||||
innobase_convert_from_id(cs, dst, str, len);
|
||||
} else if (!strncmp(str, srv_mysql50_table_name_prefix,
|
||||
sizeof(srv_mysql50_table_name_prefix) - 1)) {
|
||||
/* This is a pre-5.1 table name
|
||||
containing chars other than [A-Za-z0-9].
|
||||
Discard the prefix and use raw UTF-8 encoding. */
|
||||
str += sizeof(srv_mysql50_table_name_prefix) - 1;
|
||||
len -= sizeof(srv_mysql50_table_name_prefix) - 1;
|
||||
goto convert_id;
|
||||
} else {
|
||||
/* Encode using filename-safe characters. */
|
||||
len = 5 * len + 1;
|
||||
*id = dst = static_cast<char*>(mem_heap_alloc(heap, len));
|
||||
|
||||
innobase_convert_from_table_id(cs, dst, str, len);
|
||||
}
|
||||
|
||||
ulint dstlen = 3 * len + 1;
|
||||
char *dst = static_cast<char*>(mem_heap_alloc(heap, dstlen));
|
||||
*id = dst;
|
||||
uint errors;
|
||||
strconvert(cs, str, uint(len), system_charset_info, dst,
|
||||
uint(dstlen), &errors);
|
||||
return(ptr);
|
||||
}
|
||||
|
||||
/*********************************************************************//**
|
||||
Open a table from its database and table name, this is currently used by
|
||||
foreign constraint parser to get the referenced table.
|
||||
@return complete table name with database and table name, allocated from
|
||||
heap memory passed in */
|
||||
char*
|
||||
dict_get_referenced_table(
|
||||
const char* name, /*!< in: foreign key table name */
|
||||
const char* database_name, /*!< in: table db name */
|
||||
ulint database_name_len, /*!< in: db name length */
|
||||
const char* table_name, /*!< in: table name */
|
||||
ulint table_name_len, /*!< in: table name length */
|
||||
dict_table_t** table, /*!< out: table object or NULL */
|
||||
mem_heap_t* heap, /*!< in/out: heap memory */
|
||||
CHARSET_INFO* from_cs) /*!< in: table name charset */
|
||||
{
|
||||
char* ref;
|
||||
char db_name[MAX_DATABASE_NAME_LEN];
|
||||
char tbl_name[MAX_TABLE_NAME_LEN];
|
||||
CHARSET_INFO* to_cs = &my_charset_filename;
|
||||
uint errors;
|
||||
ut_ad(database_name || name);
|
||||
ut_ad(table_name);
|
||||
|
||||
if (!strncmp(table_name, srv_mysql50_table_name_prefix,
|
||||
sizeof(srv_mysql50_table_name_prefix) - 1)) {
|
||||
/* This is a pre-5.1 table name
|
||||
containing chars other than [A-Za-z0-9].
|
||||
Discard the prefix and use raw UTF-8 encoding. */
|
||||
table_name += sizeof(srv_mysql50_table_name_prefix) - 1;
|
||||
table_name_len -= sizeof(srv_mysql50_table_name_prefix) - 1;
|
||||
|
||||
to_cs = system_charset_info;
|
||||
}
|
||||
|
||||
table_name_len = strconvert(from_cs, table_name, table_name_len, to_cs,
|
||||
tbl_name, MAX_TABLE_NAME_LEN, &errors);
|
||||
table_name = tbl_name;
|
||||
|
||||
if (database_name) {
|
||||
to_cs = &my_charset_filename;
|
||||
if (!strncmp(database_name, srv_mysql50_table_name_prefix,
|
||||
sizeof(srv_mysql50_table_name_prefix) - 1)) {
|
||||
database_name
|
||||
+= sizeof(srv_mysql50_table_name_prefix) - 1;
|
||||
database_name_len
|
||||
-= sizeof(srv_mysql50_table_name_prefix) - 1;
|
||||
to_cs = system_charset_info;
|
||||
}
|
||||
|
||||
database_name_len = strconvert(
|
||||
from_cs, database_name, database_name_len, to_cs,
|
||||
db_name, MAX_DATABASE_NAME_LEN, &errors);
|
||||
database_name = db_name;
|
||||
} else {
|
||||
/* Use the database name of the foreign key table */
|
||||
|
||||
database_name = name;
|
||||
database_name_len = dict_get_db_name_len(name);
|
||||
}
|
||||
|
||||
/* Copy database_name, '/', table_name, '\0' */
|
||||
const size_t len = database_name_len + table_name_len + 1;
|
||||
ref = static_cast<char*>(mem_heap_alloc(heap, len + 1));
|
||||
memcpy(ref, database_name, database_name_len);
|
||||
ref[database_name_len] = '/';
|
||||
memcpy(ref + database_name_len + 1, table_name, table_name_len + 1);
|
||||
|
||||
/* Values; 0 = Store and compare as given; case sensitive
|
||||
1 = Store and compare in lower; case insensitive
|
||||
2 = Store as given, compare in lower; case semi-sensitive */
|
||||
if (lower_case_table_names == 2) {
|
||||
innobase_casedn_str(ref);
|
||||
*table = dict_sys.load_table({ref, len});
|
||||
memcpy(ref, database_name, database_name_len);
|
||||
ref[database_name_len] = '/';
|
||||
memcpy(ref + database_name_len + 1, table_name, table_name_len + 1);
|
||||
|
||||
} else {
|
||||
#ifndef _WIN32
|
||||
if (lower_case_table_names == 1) {
|
||||
innobase_casedn_str(ref);
|
||||
}
|
||||
#else
|
||||
innobase_casedn_str(ref);
|
||||
#endif /* !_WIN32 */
|
||||
*table = dict_sys.load_table({ref, len});
|
||||
}
|
||||
|
||||
return(ref);
|
||||
}
|
||||
|
||||
/*********************************************************************//**
|
||||
Removes MySQL comments from an SQL string. A comment is either
|
||||
(a) '#' to the end of the line,
|
||||
|
@ -3563,7 +3466,7 @@ loop:
|
|||
}
|
||||
}
|
||||
|
||||
ptr = dict_scan_id(cs, ptr, heap, &id, FALSE, TRUE);
|
||||
ptr = dict_scan_id(cs, ptr, heap, &id);
|
||||
|
||||
if (id == NULL) {
|
||||
|
||||
|
|
|
@ -816,7 +816,7 @@ void
|
|||
dict_mem_foreign_table_name_lookup_set(
|
||||
/*===================================*/
|
||||
dict_foreign_t* foreign, /*!< in/out: foreign struct */
|
||||
ibool do_alloc) /*!< in: is an alloc needed */
|
||||
bool do_alloc) /*!< in: is an alloc needed */
|
||||
{
|
||||
if (lower_case_table_names == 2) {
|
||||
if (do_alloc) {
|
||||
|
@ -830,7 +830,8 @@ dict_mem_foreign_table_name_lookup_set(
|
|||
}
|
||||
strcpy(foreign->foreign_table_name_lookup,
|
||||
foreign->foreign_table_name);
|
||||
innobase_casedn_str(foreign->foreign_table_name_lookup);
|
||||
my_casedn_str(system_charset_info,
|
||||
foreign->foreign_table_name_lookup);
|
||||
} else {
|
||||
foreign->foreign_table_name_lookup
|
||||
= foreign->foreign_table_name;
|
||||
|
@ -860,7 +861,8 @@ dict_mem_referenced_table_name_lookup_set(
|
|||
}
|
||||
strcpy(foreign->referenced_table_name_lookup,
|
||||
foreign->referenced_table_name);
|
||||
innobase_casedn_str(foreign->referenced_table_name_lookup);
|
||||
my_casedn_str(system_charset_info,
|
||||
foreign->referenced_table_name_lookup);
|
||||
} else {
|
||||
foreign->referenced_table_name_lookup
|
||||
= foreign->referenced_table_name;
|
||||
|
|
|
@ -1320,9 +1320,7 @@ static void innodb_drop_database(handlerton*, char *path)
|
|||
namebuf[len] = '/';
|
||||
namebuf[len + 1] = '\0';
|
||||
|
||||
#ifdef _WIN32
|
||||
innobase_casedn_str(namebuf);
|
||||
#endif /* _WIN32 */
|
||||
IF_WIN(my_casedn_str(system_charset_info, namebuf),);
|
||||
|
||||
THD * const thd= current_thd;
|
||||
trx_t *trx= innobase_trx_allocate(thd);
|
||||
|
@ -2435,21 +2433,6 @@ dtype_get_mblen(
|
|||
}
|
||||
}
|
||||
|
||||
/******************************************************************//**
|
||||
Converts an identifier to a table name. */
|
||||
void
|
||||
innobase_convert_from_table_id(
|
||||
/*===========================*/
|
||||
CHARSET_INFO* cs, /*!< in: the 'from' character set */
|
||||
char* to, /*!< out: converted identifier */
|
||||
const char* from, /*!< in: identifier to convert */
|
||||
ulint len) /*!< in: length of 'to', in bytes */
|
||||
{
|
||||
uint errors;
|
||||
|
||||
strconvert(cs, from, FN_REFLEN, &my_charset_filename, to, (uint) len, &errors);
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
Check if the length of the identifier exceeds the maximum allowed.
|
||||
return true when length of identifier is too long. */
|
||||
|
@ -2474,21 +2457,6 @@ innobase_check_identifier_length(
|
|||
DBUG_RETURN(false);
|
||||
}
|
||||
|
||||
/******************************************************************//**
|
||||
Converts an identifier to UTF-8. */
|
||||
void
|
||||
innobase_convert_from_id(
|
||||
/*=====================*/
|
||||
CHARSET_INFO* cs, /*!< in: the 'from' character set */
|
||||
char* to, /*!< out: converted identifier */
|
||||
const char* from, /*!< in: identifier to convert */
|
||||
ulint len) /*!< in: length of 'to', in bytes */
|
||||
{
|
||||
uint errors;
|
||||
|
||||
strconvert(cs, from, FN_REFLEN, system_charset_info, to, (uint) len, &errors);
|
||||
}
|
||||
|
||||
/******************************************************************//**
|
||||
Compares NUL-terminated UTF-8 strings case insensitively.
|
||||
@return 0 if a=b, <0 if a<b, >1 if a>b */
|
||||
|
@ -2537,16 +2505,6 @@ innobase_basename(
|
|||
return((name) ? name : "null");
|
||||
}
|
||||
|
||||
/******************************************************************//**
|
||||
Makes all characters in a NUL-terminated UTF-8 string lower case. */
|
||||
void
|
||||
innobase_casedn_str(
|
||||
/*================*/
|
||||
char* a) /*!< in/out: string to put in lower case */
|
||||
{
|
||||
my_casedn_str(system_charset_info, a);
|
||||
}
|
||||
|
||||
/** Determines the current SQL statement.
|
||||
Thread unsafe, can only be called from the thread owning the THD.
|
||||
@param[in] thd MySQL thread handle
|
||||
|
@ -3683,13 +3641,13 @@ innobase_format_name(
|
|||
ulint buflen, /*!< in: length of buf, in bytes */
|
||||
const char* name) /*!< in: table name to format */
|
||||
{
|
||||
const char* bufend;
|
||||
char* bufend;
|
||||
|
||||
bufend = innobase_convert_name(buf, buflen, name, strlen(name), NULL);
|
||||
|
||||
ut_ad((ulint) (bufend - buf) < buflen);
|
||||
|
||||
buf[bufend - buf] = '\0';
|
||||
*bufend = '\0';
|
||||
}
|
||||
|
||||
/**********************************************************************//**
|
||||
|
@ -5386,7 +5344,7 @@ normalize_table_name_c_low(
|
|||
memcpy(norm_name + db_len + 1, name_ptr, name_len + 1);
|
||||
|
||||
if (set_lower_case) {
|
||||
innobase_casedn_str(norm_name);
|
||||
my_casedn_str(system_charset_info, norm_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6261,7 +6219,7 @@ ha_innobase::open_dict_table(
|
|||
case name, including the partition
|
||||
separator "P" */
|
||||
strcpy(par_case_name, norm_name);
|
||||
innobase_casedn_str(par_case_name);
|
||||
my_casedn_str(system_charset_info, par_case_name);
|
||||
#else
|
||||
/* On Windows platfrom, check
|
||||
whether there exists table name in
|
||||
|
@ -12389,6 +12347,73 @@ public:
|
|||
const char* str() { return buf; }
|
||||
};
|
||||
|
||||
/** Construct an InnoDB table name from a schema and table name.
|
||||
@param table_name buffer InnoDB table name being constructed
|
||||
@param db schema name
|
||||
@param name table name
|
||||
@return table_name filled in */
|
||||
static char *copy_name(char *table_name, LEX_CSTRING db, LEX_CSTRING name)
|
||||
noexcept
|
||||
{
|
||||
memcpy(table_name, db.str, db.length);
|
||||
table_name[db.length] = '/';
|
||||
memcpy(table_name + db.length + 1, name.str, name.length + 1);
|
||||
return table_name;
|
||||
}
|
||||
|
||||
char *dict_table_lookup(LEX_CSTRING db, LEX_CSTRING name,
|
||||
dict_table_t **table, mem_heap_t *heap) noexcept
|
||||
{
|
||||
const size_t len= db.length + name.length + 1;
|
||||
char *ref= static_cast<char*>(mem_heap_alloc(heap, len + 1));
|
||||
copy_name(ref, db, name);
|
||||
|
||||
switch (lower_case_table_names) {
|
||||
case 2: /* store as given, compare in lower case */
|
||||
my_casedn_str(system_charset_info, ref);
|
||||
*table= dict_sys.load_table({ref, len});
|
||||
return copy_name(ref, db, name);
|
||||
case 0: /* store and compare as given; case sensitive */
|
||||
#ifndef _WIN32 /* On Windows, InnoDB treats 0 as lower_case_table_names=1 */
|
||||
break;
|
||||
#endif
|
||||
case 1: /* store and compare in lower case */
|
||||
my_casedn_str(system_charset_info, ref);
|
||||
}
|
||||
|
||||
*table = dict_sys.load_table({ref, len});
|
||||
return ref;
|
||||
}
|
||||
|
||||
/** Convert a schema or table name to InnoDB (and file system) format.
|
||||
@param cs source character set
|
||||
@param name name encoded in cs
|
||||
@param buf output buffer (MAX_TABLE_NAME_LEN + 1 bytes)
|
||||
@return the converted string (within buf) */
|
||||
LEX_CSTRING innodb_convert_name(CHARSET_INFO *cs, LEX_CSTRING name, char *buf)
|
||||
noexcept
|
||||
{
|
||||
CHARSET_INFO *to_cs= &my_charset_filename;
|
||||
if (!strncmp(name.str, srv_mysql50_table_name_prefix,
|
||||
sizeof srv_mysql50_table_name_prefix - 1))
|
||||
{
|
||||
/* Before MySQL 5.1 introduced my_charset_filename, schema and
|
||||
table names were stored in the file system as specified by the
|
||||
user, hopefully in ASCII encoding, but it could also be in ISO
|
||||
8859-1 or UTF-8. Such schema or table names are distinguished by
|
||||
the #mysql50# prefix.
|
||||
|
||||
Let us discard that prefix and convert the name to UTF-8
|
||||
(system_charset_info). */
|
||||
name.str+= sizeof srv_mysql50_table_name_prefix - 1;
|
||||
name.length-= sizeof srv_mysql50_table_name_prefix - 1;
|
||||
to_cs= system_charset_info;
|
||||
}
|
||||
uint errors;
|
||||
return LEX_CSTRING{buf, strconvert(cs, name.str, name.length, to_cs,
|
||||
buf, MAX_TABLE_NAME_LEN, &errors)};
|
||||
}
|
||||
|
||||
/** Create InnoDB foreign keys from MySQL alter_info. Collect all
|
||||
dict_foreign_t items into local_fk_set and then add into system table.
|
||||
@return DB_SUCCESS or specific error code */
|
||||
|
@ -12404,6 +12429,9 @@ create_table_info_t::create_foreign_keys()
|
|||
const char* ref_column_names[MAX_COLS_PER_FK];
|
||||
char create_name[MAX_DATABASE_NAME_LEN + 1 +
|
||||
MAX_TABLE_NAME_LEN + 1];
|
||||
char db_name[MAX_DATABASE_NAME_LEN + 1];
|
||||
char t_name[MAX_TABLE_NAME_LEN + 1];
|
||||
static_assert(MAX_TABLE_NAME_LEN == MAX_DATABASE_NAME_LEN, "");
|
||||
dict_index_t* index = NULL;
|
||||
fkerr_t index_error = FK_SUCCESS;
|
||||
dict_index_t* err_index = NULL;
|
||||
|
@ -12411,59 +12439,57 @@ create_table_info_t::create_foreign_keys()
|
|||
const bool tmp_table = m_flags2 & DICT_TF2_TEMPORARY;
|
||||
const CHARSET_INFO* cs = thd_charset(m_thd);
|
||||
const char* operation = "Create ";
|
||||
const char* name = m_table_name;
|
||||
|
||||
enum_sql_command sqlcom = enum_sql_command(thd_sql_command(m_thd));
|
||||
LEX_CSTRING name= {m_table_name, strlen(m_table_name)};
|
||||
|
||||
if (sqlcom == SQLCOM_ALTER_TABLE) {
|
||||
dict_table_t* table_to_alter;
|
||||
mem_heap_t* heap = mem_heap_create(10000);
|
||||
ulint highest_id_so_far;
|
||||
char* n = dict_get_referenced_table(
|
||||
name, LEX_STRING_WITH_LEN(m_form->s->db),
|
||||
LEX_STRING_WITH_LEN(m_form->s->table_name),
|
||||
&table_to_alter, heap, cs);
|
||||
LEX_CSTRING t{innodb_convert_name(cs, m_form->s->table_name,
|
||||
t_name)};
|
||||
LEX_CSTRING d{innodb_convert_name(cs, m_form->s->db, db_name)};
|
||||
dict_table_t* alter_table;
|
||||
char* n = dict_table_lookup(d, t, &alter_table, heap);
|
||||
|
||||
/* Starting from 4.0.18 and 4.1.2, we generate foreign key id's
|
||||
in the format databasename/tablename_ibfk_[number], where
|
||||
[number] is local to the table; look for the highest [number]
|
||||
for table_to_alter, so that we can assign to new constraints
|
||||
for alter_table, so that we can assign to new constraints
|
||||
higher numbers. */
|
||||
|
||||
/* If we are altering a temporary table, the table name after
|
||||
ALTER TABLE does not correspond to the internal table name, and
|
||||
table_to_alter is NULL. TODO: should we fix this somehow? */
|
||||
alter_table=nullptr. But, we do not support FOREIGN KEY
|
||||
constraints for temporary tables. */
|
||||
|
||||
if (table_to_alter) {
|
||||
n = table_to_alter->name.m_name;
|
||||
highest_id_so_far = dict_table_get_highest_foreign_id(
|
||||
table_to_alter);
|
||||
} else {
|
||||
highest_id_so_far = 0;
|
||||
if (alter_table) {
|
||||
n = alter_table->name.m_name;
|
||||
number = 1 + dict_table_get_highest_foreign_id(
|
||||
alter_table);
|
||||
}
|
||||
|
||||
char* bufend = innobase_convert_name(
|
||||
create_name, sizeof create_name, n, strlen(n), m_thd);
|
||||
create_name[bufend - create_name] = '\0';
|
||||
number = highest_id_so_far + 1;
|
||||
*bufend = '\0';
|
||||
mem_heap_free(heap);
|
||||
operation = "Alter ";
|
||||
} else if (strstr(name, "#P#") || strstr(name, "#p#")) {
|
||||
} else if (strstr(m_table_name, "#P#")
|
||||
|| strstr(m_table_name, "#p#")) {
|
||||
/* Partitioned table */
|
||||
create_name[0] = '\0';
|
||||
} else {
|
||||
char* bufend = innobase_convert_name(create_name,
|
||||
sizeof create_name,
|
||||
name,
|
||||
strlen(name), m_thd);
|
||||
create_name[bufend - create_name] = '\0';
|
||||
LEX_STRING_WITH_LEN(name),
|
||||
m_thd);
|
||||
*bufend = '\0';
|
||||
}
|
||||
|
||||
Alter_info* alter_info = m_create_info->alter_info;
|
||||
ut_ad(alter_info);
|
||||
List_iterator_fast<Key> key_it(alter_info->key_list);
|
||||
|
||||
dict_table_t* table = dict_sys.find_table({name,strlen(name)});
|
||||
dict_table_t* table = dict_sys.find_table({name.str, name.length});
|
||||
if (!table) {
|
||||
ib_foreign_warn(m_trx, DB_CANNOT_ADD_CONSTRAINT, create_name,
|
||||
"%s table %s foreign key constraint"
|
||||
|
@ -12510,27 +12536,27 @@ create_table_info_t::create_foreign_keys()
|
|||
col->field_name.length);
|
||||
success = find_col(table, column_names + i);
|
||||
if (!success) {
|
||||
key_text k(fk);
|
||||
ib_foreign_warn(
|
||||
m_trx, DB_CANNOT_ADD_CONSTRAINT,
|
||||
create_name,
|
||||
"%s table %s foreign key %s constraint"
|
||||
" failed. Column %s was not found.",
|
||||
operation, create_name, k.str(),
|
||||
operation, create_name,
|
||||
key_text(fk).str(),
|
||||
column_names[i]);
|
||||
dict_foreign_free(foreign);
|
||||
return (DB_CANNOT_ADD_CONSTRAINT);
|
||||
}
|
||||
++i;
|
||||
if (i >= MAX_COLS_PER_FK) {
|
||||
key_text k(fk);
|
||||
ib_foreign_warn(
|
||||
m_trx, DB_CANNOT_ADD_CONSTRAINT,
|
||||
create_name,
|
||||
"%s table %s foreign key %s constraint"
|
||||
" failed. Too many columns: %u (%u "
|
||||
"allowed).",
|
||||
operation, create_name, k.str(), i,
|
||||
operation, create_name,
|
||||
key_text(fk).str(), i,
|
||||
MAX_COLS_PER_FK);
|
||||
dict_foreign_free(foreign);
|
||||
return (DB_CANNOT_ADD_CONSTRAINT);
|
||||
|
@ -12542,9 +12568,9 @@ create_table_info_t::create_foreign_keys()
|
|||
&index_error, &err_col, &err_index);
|
||||
|
||||
if (!index) {
|
||||
key_text k(fk);
|
||||
foreign_push_index_error(m_trx, operation, create_name,
|
||||
k.str(), column_names,
|
||||
key_text(fk).str(),
|
||||
column_names,
|
||||
index_error, err_col,
|
||||
err_index, table);
|
||||
dict_foreign_free(foreign);
|
||||
|
@ -12610,14 +12636,12 @@ create_table_info_t::create_foreign_keys()
|
|||
memcpy(foreign->foreign_col_names, column_names,
|
||||
i * sizeof(void*));
|
||||
|
||||
foreign->referenced_table_name = dict_get_referenced_table(
|
||||
name, LEX_STRING_WITH_LEN(fk->ref_db),
|
||||
LEX_STRING_WITH_LEN(fk->ref_table),
|
||||
&foreign->referenced_table, foreign->heap, cs);
|
||||
|
||||
if (!foreign->referenced_table_name) {
|
||||
return (DB_OUT_OF_MEMORY);
|
||||
}
|
||||
LEX_CSTRING t{innodb_convert_name(cs, fk->ref_table, t_name)};
|
||||
LEX_CSTRING d = fk->ref_db.str
|
||||
? innodb_convert_name(cs, fk->ref_db, db_name)
|
||||
: LEX_CSTRING{table->name.m_name, table->name.dblen()};
|
||||
foreign->referenced_table_name = dict_table_lookup(
|
||||
d, t, &foreign->referenced_table, foreign->heap);
|
||||
|
||||
if (!foreign->referenced_table && m_trx->check_foreigns) {
|
||||
char buf[MAX_TABLE_NAME_LEN + 1] = "";
|
||||
|
@ -12627,15 +12651,15 @@ create_table_info_t::create_foreign_keys()
|
|||
buf, MAX_TABLE_NAME_LEN,
|
||||
foreign->referenced_table_name,
|
||||
strlen(foreign->referenced_table_name), m_thd);
|
||||
buf[bufend - buf] = '\0';
|
||||
key_text k(fk);
|
||||
*bufend = '\0';
|
||||
ib_foreign_warn(m_trx, DB_CANNOT_ADD_CONSTRAINT,
|
||||
create_name,
|
||||
"%s table %s with foreign key %s "
|
||||
"constraint failed. Referenced table "
|
||||
"%s not found in the data dictionary.",
|
||||
operation, create_name, k.str(), buf);
|
||||
return (DB_CANNOT_ADD_CONSTRAINT);
|
||||
operation, create_name,
|
||||
key_text(fk).str(), buf);
|
||||
return DB_CANNOT_ADD_CONSTRAINT;
|
||||
}
|
||||
|
||||
/* Don't allow foreign keys on partitioned tables yet. */
|
||||
|
@ -12658,7 +12682,6 @@ create_table_info_t::create_foreign_keys()
|
|||
success = find_col(foreign->referenced_table,
|
||||
ref_column_names + j);
|
||||
if (!success) {
|
||||
key_text k(fk);
|
||||
ib_foreign_warn(
|
||||
m_trx,
|
||||
DB_CANNOT_ADD_CONSTRAINT,
|
||||
|
@ -12667,9 +12690,9 @@ create_table_info_t::create_foreign_keys()
|
|||
"constraint failed. "
|
||||
"Column %s was not found.",
|
||||
operation, create_name,
|
||||
k.str(), ref_column_names[j]);
|
||||
|
||||
return (DB_CANNOT_ADD_CONSTRAINT);
|
||||
key_text(fk).str(),
|
||||
ref_column_names[j]);
|
||||
return DB_CANNOT_ADD_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
++j;
|
||||
|
@ -12689,16 +12712,15 @@ create_table_info_t::create_foreign_keys()
|
|||
&err_index);
|
||||
|
||||
if (!index) {
|
||||
key_text k(fk);
|
||||
foreign_push_index_error(
|
||||
m_trx, operation, create_name, k.str(),
|
||||
m_trx, operation, create_name,
|
||||
key_text(fk).str(),
|
||||
column_names, index_error, err_col,
|
||||
err_index, foreign->referenced_table);
|
||||
|
||||
return (DB_CANNOT_ADD_CONSTRAINT);
|
||||
return DB_CANNOT_ADD_CONSTRAINT;
|
||||
}
|
||||
} else {
|
||||
ut_a(m_trx->check_foreigns == FALSE);
|
||||
ut_a(!m_trx->check_foreigns);
|
||||
index = NULL;
|
||||
}
|
||||
|
||||
|
@ -12735,7 +12757,6 @@ create_table_info_t::create_foreign_keys()
|
|||
NULL
|
||||
if the column is not allowed to be
|
||||
NULL! */
|
||||
key_text k(fk);
|
||||
ib_foreign_warn(
|
||||
m_trx,
|
||||
DB_CANNOT_ADD_CONSTRAINT,
|
||||
|
@ -12746,9 +12767,9 @@ create_table_info_t::create_foreign_keys()
|
|||
"but column '%s' is defined as "
|
||||
"NOT NULL.",
|
||||
operation, create_name,
|
||||
k.str(), col_name);
|
||||
key_text(fk).str(), col_name);
|
||||
|
||||
return (DB_CANNOT_ADD_CONSTRAINT);
|
||||
return DB_CANNOT_ADD_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13596,7 +13617,7 @@ int ha_innobase::delete_table(const char *name)
|
|||
if (!table && lower_case_table_names == 1 && is_partition(norm_name))
|
||||
{
|
||||
IF_WIN(normalize_table_name_c_low(norm_name, name, false),
|
||||
innobase_casedn_str(norm_name));
|
||||
my_casedn_str(system_charset_info, norm_name));
|
||||
table= dict_sys.load_table(n, DICT_ERR_IGNORE_DROP);
|
||||
}
|
||||
#endif
|
||||
|
@ -13903,7 +13924,8 @@ static dberr_t innobase_rename_table(trx_t *trx, const char *from,
|
|||
case name, including the partition
|
||||
separator "P" */
|
||||
strcpy(par_case_name, norm_from);
|
||||
innobase_casedn_str(par_case_name);
|
||||
my_casedn_str(system_charset_info,
|
||||
par_case_name);
|
||||
#else
|
||||
/* On Windows platfrom, check
|
||||
whether there exists table name in
|
||||
|
@ -20924,25 +20946,6 @@ const char* SET_TRANSACTION_MSG =
|
|||
const char* INNODB_PARAMETERS_MSG =
|
||||
"Please refer to https://mariadb.com/kb/en/library/innodb-system-variables/";
|
||||
|
||||
/**********************************************************************
|
||||
Converts an identifier from my_charset_filename to UTF-8 charset.
|
||||
@return result string length, as returned by strconvert() */
|
||||
uint
|
||||
innobase_convert_to_filename_charset(
|
||||
/*=================================*/
|
||||
char* to, /* out: converted identifier */
|
||||
const char* from, /* in: identifier to convert */
|
||||
ulint len) /* in: length of 'to', in bytes */
|
||||
{
|
||||
uint errors;
|
||||
CHARSET_INFO* cs_to = &my_charset_filename;
|
||||
CHARSET_INFO* cs_from = system_charset_info;
|
||||
|
||||
return(static_cast<uint>(strconvert(
|
||||
cs_from, from, uint(strlen(from)),
|
||||
cs_to, to, static_cast<uint>(len), &errors)));
|
||||
}
|
||||
|
||||
/**********************************************************************
|
||||
Converts an identifier from my_charset_filename to UTF-8 charset.
|
||||
@return result string length, as returned by strconvert() */
|
||||
|
|
|
@ -30,6 +30,7 @@ Smart ALTER TABLE
|
|||
#include <sql_class.h>
|
||||
#include <sql_table.h>
|
||||
#include <mysql/plugin.h>
|
||||
#include <strfunc.h>
|
||||
|
||||
/* Include necessary InnoDB headers */
|
||||
#include "btr0sea.h"
|
||||
|
@ -3231,6 +3232,9 @@ innobase_get_foreign_key_info(
|
|||
ulint num_fk = 0;
|
||||
Alter_info* alter_info = ha_alter_info->alter_info;
|
||||
const CHARSET_INFO* cs = thd_charset(trx->mysql_thd);
|
||||
char db_name[MAX_DATABASE_NAME_LEN + 1];
|
||||
char t_name[MAX_TABLE_NAME_LEN + 1];
|
||||
static_assert(MAX_TABLE_NAME_LEN == MAX_DATABASE_NAME_LEN, "");
|
||||
|
||||
DBUG_ENTER("innobase_get_foreign_key_info");
|
||||
|
||||
|
@ -3295,14 +3299,15 @@ innobase_get_foreign_key_info(
|
|||
|
||||
add_fk[num_fk] = dict_mem_foreign_create();
|
||||
|
||||
LEX_CSTRING t = innodb_convert_name(cs, fk_key->ref_table,
|
||||
t_name);
|
||||
LEX_CSTRING d = fk_key->ref_db.str
|
||||
? innodb_convert_name(cs, fk_key->ref_db, db_name)
|
||||
: LEX_CSTRING{table->name.m_name, table->name.dblen()};
|
||||
dict_sys.lock(SRW_LOCK_CALL);
|
||||
|
||||
referenced_table_name = dict_get_referenced_table(
|
||||
table->name.m_name,
|
||||
LEX_STRING_WITH_LEN(fk_key->ref_db),
|
||||
LEX_STRING_WITH_LEN(fk_key->ref_table),
|
||||
&referenced_table,
|
||||
add_fk[num_fk]->heap, cs);
|
||||
referenced_table_name = dict_table_lookup(
|
||||
d, t, &referenced_table, add_fk[num_fk]->heap);
|
||||
|
||||
/* Test the case when referenced_table failed to
|
||||
open, if trx->check_foreigns is not set, we should
|
||||
|
|
|
@ -55,22 +55,6 @@ inline size_t dict_get_db_name_len(const char *name)
|
|||
}
|
||||
|
||||
|
||||
/*********************************************************************//**
|
||||
Open a table from its database and table name, this is currently used by
|
||||
foreign constraint parser to get the referenced table.
|
||||
@return complete table name with database and table name, allocated from
|
||||
heap memory passed in */
|
||||
char*
|
||||
dict_get_referenced_table(
|
||||
/*======================*/
|
||||
const char* name, /*!< in: foreign key table name */
|
||||
const char* database_name, /*!< in: table db name */
|
||||
ulint database_name_len,/*!< in: db name length */
|
||||
const char* table_name, /*!< in: table name */
|
||||
ulint table_name_len, /*!< in: table name length */
|
||||
dict_table_t** table, /*!< out: table object or NULL */
|
||||
mem_heap_t* heap, /*!< in: heap memory */
|
||||
CHARSET_INFO* from_cs); /*!< in: table name charset */
|
||||
/*********************************************************************//**
|
||||
Frees a foreign key struct. */
|
||||
void
|
||||
|
|
|
@ -431,7 +431,7 @@ void
|
|||
dict_mem_foreign_table_name_lookup_set(
|
||||
/*===================================*/
|
||||
dict_foreign_t* foreign, /*!< in/out: foreign struct */
|
||||
ibool do_alloc); /*!< in: is an alloc needed */
|
||||
bool do_alloc); /*!< in: is an alloc needed */
|
||||
|
||||
/**********************************************************************//**
|
||||
Sets the referenced_table_name_lookup pointer based on the value of
|
||||
|
|
|
@ -110,7 +110,7 @@ struct table_name_t
|
|||
table_name_t(char* name) : m_name(name) {}
|
||||
|
||||
/** @return the end of the schema name */
|
||||
const char* dbend() const
|
||||
const char* dbend() const noexcept
|
||||
{
|
||||
const char* sep = strchr(m_name, '/');
|
||||
ut_ad(sep);
|
||||
|
@ -118,11 +118,19 @@ struct table_name_t
|
|||
}
|
||||
|
||||
/** @return the length of the schema name, in bytes */
|
||||
size_t dblen() const { return size_t(dbend() - m_name); }
|
||||
size_t dblen() const noexcept
|
||||
{
|
||||
const char *end= dbend();
|
||||
return UNIV_LIKELY(end != nullptr) ? size_t(end - m_name) : 0;
|
||||
}
|
||||
|
||||
/** Determine the filename-safe encoded table name.
|
||||
@return the filename-safe encoded table name */
|
||||
const char* basename() const { return dbend() + 1; }
|
||||
const char* basename() const noexcept
|
||||
{
|
||||
const char *end= dbend();
|
||||
return UNIV_LIKELY(end != nullptr) ? end + 1 : nullptr;
|
||||
}
|
||||
|
||||
/** The start of the table basename suffix for partitioned tables */
|
||||
static const char part_suffix[4];
|
||||
|
|
|
@ -40,6 +40,8 @@ class Field;
|
|||
struct dict_table_t;
|
||||
struct dict_foreign_t;
|
||||
struct table_name_t;
|
||||
struct mem_block_info_t;
|
||||
typedef struct mem_block_info_t mem_heap_t;
|
||||
|
||||
// JAN: TODO missing features:
|
||||
#undef MYSQL_FT_INIT_EXT
|
||||
|
@ -156,33 +158,6 @@ const char*
|
|||
innobase_basename(
|
||||
const char* path_name);
|
||||
|
||||
/******************************************************************//**
|
||||
Converts an identifier to a table name. */
|
||||
void
|
||||
innobase_convert_from_table_id(
|
||||
/*===========================*/
|
||||
CHARSET_INFO* cs, /*!< in: the 'from' character set */
|
||||
char* to, /*!< out: converted identifier */
|
||||
const char* from, /*!< in: identifier to convert */
|
||||
ulint len); /*!< in: length of 'to', in bytes; should
|
||||
be at least 5 * strlen(to) + 1 */
|
||||
/******************************************************************//**
|
||||
Converts an identifier to UTF-8. */
|
||||
void
|
||||
innobase_convert_from_id(
|
||||
/*=====================*/
|
||||
CHARSET_INFO* cs, /*!< in: the 'from' character set */
|
||||
char* to, /*!< out: converted identifier */
|
||||
const char* from, /*!< in: identifier to convert */
|
||||
ulint len); /*!< in: length of 'to', in bytes;
|
||||
should be at least 3 * strlen(to) + 1 */
|
||||
/******************************************************************//**
|
||||
Makes all characters in a NUL-terminated UTF-8 string lower case. */
|
||||
void
|
||||
innobase_casedn_str(
|
||||
/*================*/
|
||||
char* a); /*!< in/out: string to put in lower case */
|
||||
|
||||
#ifdef WITH_WSREP
|
||||
ulint wsrep_innobase_mysql_sort(int mysql_type, uint charset_number,
|
||||
unsigned char* str, ulint str_length,
|
||||
|
@ -370,15 +345,6 @@ innobase_next_autoinc(
|
|||
MY_ATTRIBUTE((pure, warn_unused_result));
|
||||
|
||||
/**********************************************************************
|
||||
Converts an identifier from my_charset_filename to UTF-8 charset. */
|
||||
uint
|
||||
innobase_convert_to_system_charset(
|
||||
/*===============================*/
|
||||
char* to, /* out: converted identifier */
|
||||
const char* from, /* in: identifier to convert */
|
||||
ulint len, /* in: length of 'to', in bytes */
|
||||
uint* errors); /* out: error return */
|
||||
/**********************************************************************
|
||||
Check if the length of the identifier exceeds the maximum allowed.
|
||||
The input to this function is an identifier in charset my_charset_filename.
|
||||
return true when length of identifier is too long. */
|
||||
|
@ -398,14 +364,13 @@ innobase_convert_to_system_charset(
|
|||
ulint len, /* in: length of 'to', in bytes */
|
||||
uint* errors); /* out: error return */
|
||||
|
||||
/**********************************************************************
|
||||
Converts an identifier from my_charset_filename to UTF-8 charset. */
|
||||
uint
|
||||
innobase_convert_to_filename_charset(
|
||||
/*=================================*/
|
||||
char* to, /* out: converted identifier */
|
||||
const char* from, /* in: identifier to convert */
|
||||
ulint len); /* in: length of 'to', in bytes */
|
||||
/** Convert a schema or table name to InnoDB (and file system) format.
|
||||
@param cs source character set
|
||||
@param name name encoded in cs
|
||||
@param buf output buffer (MAX_TABLE_NAME_LEN + 1 bytes)
|
||||
@return the converted string (within buf) */
|
||||
LEX_CSTRING innodb_convert_name(CHARSET_INFO *cs, LEX_CSTRING name, char *buf)
|
||||
noexcept;
|
||||
|
||||
/** Report that a table cannot be decrypted.
|
||||
@param thd connection context
|
||||
|
@ -460,6 +425,16 @@ void destroy_background_thd(MYSQL_THD thd);
|
|||
void
|
||||
innobase_reset_background_thd(MYSQL_THD);
|
||||
|
||||
/** Open a table based on a database and table name.
|
||||
@param db schema name
|
||||
@param name table name within the schema
|
||||
@param table table
|
||||
@param heap memory heap for allocating a converted name
|
||||
@return InnoDB format table name with database and table name,
|
||||
allocated from heap */
|
||||
char *dict_table_lookup(LEX_CSTRING db, LEX_CSTRING name,
|
||||
dict_table_t **table, mem_heap_t *heap) noexcept;
|
||||
|
||||
#ifdef WITH_WSREP
|
||||
/** Append table-level exclusive key.
|
||||
@param thd MySQL thread handle
|
||||
|
|
|
@ -2608,7 +2608,7 @@ row_rename_table_for_mysql(
|
|||
memcpy(par_case_name, old_name,
|
||||
strlen(old_name));
|
||||
par_case_name[strlen(old_name)] = 0;
|
||||
innobase_casedn_str(par_case_name);
|
||||
my_casedn_str(system_charset_info, par_case_name);
|
||||
#else
|
||||
/* On Windows platfrom, check
|
||||
whether there exists table name in
|
||||
|
|
Loading…
Add table
Reference in a new issue