Commit fc52f5e8 authored by Yuchen Pei's avatar Yuchen Pei

MDEV-25008 Estimate outer_lookup_keys later in mysql_delete and mysql_update

This helps make better choice of materialization vs in-to-exists
parent 561e22ed
......@@ -636,3 +636,36 @@ c2
1
DROP TABLE t1, t2;
End of 11.6 tests
#
# MDEV-25008: Delete query gets stuck on mariadb, same query works
# on MySQL 8.0.21
#
CREATE TABLE t1 (
id int NOT NULL PRIMARY KEY,
item_id varchar(100),
seller_name varchar(400),
variant varchar(400),
FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant)
)engine=innodb;
insert into t1 select seq,seq,seq,seq from seq_1_to_10000;
explain
DELETE FROM t1 WHERE id NOT IN
(SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 ALL NULL NULL NULL NULL # Using where
2 MATERIALIZED <derived3> ALL NULL NULL NULL NULL #
3 DERIVED t1 ALL NULL NULL NULL NULL # Using temporary; Using filesort
drop table t1;
create table t1 (a int primary key, b int, c int, key(b));
insert into t1 select seq, seq, seq from seq_1_to_20000;
create table t2 as select * from t1;
explain delete from t1 where b <= 2 and a not in (select b from t2);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 range b b 5 NULL 2 Using where
2 DEPENDENT SUBQUERY t2 ALL NULL NULL NULL NULL 20000 Using where
explain delete from t1 where b <= 3 and a not in (select b from t2);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 range b b 5 NULL 3 Using where
2 MATERIALIZED t2 ALL NULL NULL NULL NULL 20000
drop table t1, t2;
# End of 11.7 tests
......@@ -691,3 +691,40 @@ SELECT * FROM t2;
DROP TABLE t1, t2;
--echo End of 11.6 tests
--echo #
--echo # MDEV-25008: Delete query gets stuck on mariadb, same query works
--echo # on MySQL 8.0.21
--echo #
--source include/have_sequence.inc
--source include/have_innodb.inc
# original case
CREATE TABLE t1 (
id int NOT NULL PRIMARY KEY,
item_id varchar(100),
seller_name varchar(400),
variant varchar(400),
FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant)
)engine=innodb;
insert into t1 select seq,seq,seq,seq from seq_1_to_10000;
# Masking the `rows` column as the value might vary a bit
--replace_column 9 #
explain
DELETE FROM t1 WHERE id NOT IN
(SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable);
drop table t1;
# example of not full table
create table t1 (a int primary key, b int, c int, key(b));
insert into t1 select seq, seq, seq from seq_1_to_20000;
create table t2 as select * from t1;
explain delete from t1 where b <= 2 and a not in (select b from t2);
explain delete from t1 where b <= 3 and a not in (select b from t2);
drop table t1, t2;
--echo # End of 11.7 tests
......@@ -5968,4 +5968,25 @@ DROP VIEW t1;
#
# End of 10.4 tests
#
#
# MDEV-25008 Delete query gets stuck on mariadb , same query works on MySQL 8.0.21
#
create table t1 (a int, b int, primary key (a), key (b));
insert into t1 values (1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);
CREATE TABLE t2 ( id INT(10) );
insert into t2 values (1),(2);
set @@optimizer_switch="index_merge=off";
set sql_safe_updates=1;
set @var1=1;
set @var2=2;
prepare stmt from 'update t1 set b=(SELECT 1 FROM t2 WHERE id = t1.a) where 1=? or b=2';
execute stmt using @var1;
ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column
execute stmt using @var2;
delete from t1 where a=1 or b=2;
ERROR HY000: You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column
drop table t1, t2;
#
# End of 11.7 tests
#
ALTER DATABASE test CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci;
......@@ -5440,4 +5440,30 @@ DROP VIEW t1;
--echo # End of 10.4 tests
--echo #
--echo #
--echo # MDEV-25008 Delete query gets stuck on mariadb , same query works on MySQL 8.0.21
--echo #
create table t1 (a int, b int, primary key (a), key (b));
insert into t1 values (1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);
CREATE TABLE t2 ( id INT(10) );
insert into t2 values (1),(2);
# calls select_lex->optimize_unflattened_subqueries(false) in the err label
set @@optimizer_switch="index_merge=off";
set sql_safe_updates=1;
set @var1=1;
set @var2=2;
prepare stmt from 'update t1 set b=(SELECT 1 FROM t2 WHERE id = t1.a) where 1=? or b=2';
--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE
execute stmt using @var1;
execute stmt using @var2;
--error ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE
delete from t1 where a=1 or b=2;
drop table t1, t2;
--echo #
--echo # End of 11.7 tests
--echo #
--source include/test_db_charset_restore.inc
......@@ -170,7 +170,9 @@ DROP PROCEDURE p1;
--echo #
--echo # MDEV-34757: assertion of (mem_root->flags & 4) == 0 fails in 2nd ps execution with partition pruning
--echo #
# same as the first MDEV-34444 testcase but with explain
# same as the first MDEV-34447 testcase but with explain; calling
# select_lex->optimize_unflattened_subqueries(false) under the
# emit_explain_and_leave label
CREATE TABLE t1 (id INT, value INT);
CREATE TABLE t2 (id INT);
......@@ -183,7 +185,9 @@ SELECT * FROM t1;
DEALLOCATE PREPARE stmt;
DROP TABLE t1, t2;
# 2nd ps mem leak; partition pruning
# 2nd ps mem leak; partition pruning; calls
# select_lex->optimize_unflattened_subqueries(false) under if
# (prune_partitions())...
set @var1=5;
set @var2=4;
create table t1 (a int) partition by list(a) (
......@@ -219,7 +223,9 @@ select * from t1;
deallocate prepare stmt;
drop table t1, t2;
# top level impossible where
# top level impossible where; calling
# select_lex->optimize_unflattened_subqueries(false) in if (... ||
# (select && select->check_quick(...)))
set @var1=1;
set @var2=2;
CREATE TABLE t1 ( id INT(10), value INT(10) );
......@@ -233,7 +239,9 @@ execute stmt using @var1, @var1;
deallocate prepare stmt;
DROP TABLE t1,t2;
# top level impossible where, with explain
# top level impossible where, with explain; also calling
# select_lex->optimize_unflattened_subqueries(false) under the
# emit_explain_and_leave label
set @var1=1;
set @var2=2;
CREATE TABLE t1 ( id INT(10), value INT(10) );
......@@ -264,7 +272,9 @@ CALL p1(2);
DROP TABLE t1;
DROP PROCEDURE p1;
# partition pruning
# partition pruning; calling
# select_lex->optimize_unflattened_subqueries(false) under if
# (prune_partitions())...
set @var1=5;
set @var2=4;
create table t1 (a int) partition by list(a) (
......@@ -300,7 +310,15 @@ select * from t1;
deallocate prepare stmt;
drop table t1, t2;
# top level impossible where
# top level impossible where;
# execute stmt using @var1, @var2; calling
# select_lex->optimize_unflattened_subqueries(false) under if (...
# (select && select->check_quick(...)) ...)
#
# execute stmt using @var1, @var1; calling
# select_lex->optimize_unflattened_subqueries(false)) after jumping to
# the cleanup label
set @var1=1;
set @var2=2;
CREATE TABLE t1 ( id INT(10), value INT(10) );
......@@ -314,7 +332,9 @@ execute stmt using @var1, @var1;
deallocate prepare stmt;
DROP TABLE t1,t2;
# top level impossible where, with explain
# top level impossible where, with explain; calling
# select_lex->optimize_unflattened_subqueries(false) under the
# send_nothing_and_leave label
set @var1=1;
set @var2=2;
CREATE TABLE t1 ( id INT(10), value INT(10) );
......
......@@ -802,4 +802,37 @@ DROP FUNCTION f1;
DROP FUNCTION f2;
DROP TABLE t1;
# End of MariaDB 10.10 tests
#
# MDEV-25008: Delete query gets stuck on mariadb, same query works
# on MySQL 8.0.21
#
CREATE TABLE t1 (
id int NOT NULL PRIMARY KEY,
item_id varchar(100),
seller_name varchar(400),
variant varchar(400),
FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant)
)engine=innodb;
insert into t1 select seq,seq,seq,seq from seq_1_to_10000;
explain
UPDATE t1 SET item_id="foo" WHERE id NOT IN
(SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 index NULL PRIMARY 4 NULL # Using where
2 MATERIALIZED <derived3> ALL NULL NULL NULL NULL #
3 DERIVED t1 ALL NULL NULL NULL NULL # Using temporary; Using filesort
drop table t1;
create table t1 (a int primary key, b int, c int, key(b));
insert into t1 select seq, seq, seq from seq_1_to_20000;
create table t2 as select * from t1;
explain update t1 set c = 5 where b <= 2 and a not in (select b from t2);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 range b b 5 NULL 2 Using where
2 DEPENDENT SUBQUERY t2 ALL NULL NULL NULL NULL 20000 Using where
explain update t1 set c = 5 where b <= 3 and a not in (select b from t2);
id select_type table type possible_keys key key_len ref rows Extra
1 PRIMARY t1 range b b 5 NULL 3 Using where
2 MATERIALIZED t2 ALL NULL NULL NULL NULL 20000
drop table t1, t2;
# End of 11.7 tests
ALTER DATABASE test CHARACTER SET utf8mb4 COLLATE utf8mb4_uca1400_ai_ci;
......@@ -746,4 +746,40 @@ DROP TABLE t1;
--echo # End of MariaDB 10.10 tests
--echo #
--echo # MDEV-25008: Delete query gets stuck on mariadb, same query works
--echo # on MySQL 8.0.21
--echo #
--source include/have_sequence.inc
# original case
CREATE TABLE t1 (
id int NOT NULL PRIMARY KEY,
item_id varchar(100),
seller_name varchar(400),
variant varchar(400),
FULLTEXT KEY t1_serial_IDX (item_id,seller_name,variant)
)engine=innodb;
insert into t1 select seq,seq,seq,seq from seq_1_to_10000;
# Masking the `rows` column as the value might vary a bit
--replace_column 9 #
explain
UPDATE t1 SET item_id="foo" WHERE id NOT IN
(SELECT m FROM (SELECT max(id) m FROM t1 GROUP BY item_id, seller_name, variant) AS innertable);
drop table t1;
# example of not full table
create table t1 (a int primary key, b int, c int, key(b));
insert into t1 select seq, seq, seq from seq_1_to_20000;
create table t2 as select * from t1;
explain update t1 set c = 5 where b <= 2 and a not in (select b from t2);
explain update t1 set c = 5 where b <= 3 and a not in (select b from t2);
drop table t1, t2;
--echo # End of 11.7 tests
--source include/test_db_charset_restore.inc
......@@ -2663,9 +2663,7 @@ static int fill_used_fields_bitmap(PARAM *param)
In the table struct the following information is updated:
quick_keys - Which keys can be used
quick_rows - How many rows the key matches
opt_range_condition_rows - E(# rows that will satisfy the table
condition)
opt_range_condition_rows - E(# rows that will satisfy the table condition)
IMPLEMENTATION
opt_range_condition_rows value is obtained as follows:
......
......@@ -6803,15 +6803,19 @@ bool JOIN::choose_subquery_plan(table_map join_tables)
&dummy,
&outer_lookup_keys);
}
/*
In case of a DELETE or UPDATE, get number of scanned rows as an
(upper bound) estimate of how many times the subquery will be
executed.
*/
else if (outer_join && outer_join->sql_cmd_delete)
outer_lookup_keys=
rows2double(outer_join->sql_cmd_delete->get_scanned_rows());
else if (outer_join && outer_join->sql_cmd_update)
outer_lookup_keys=
rows2double(outer_join->sql_cmd_update->get_scanned_rows());
else
{
/*
TODO: outer_join can be NULL for DELETE statements.
How to compute its cost?
*/
outer_lookup_keys= 1;
}
/*
B. Estimate the cost and number of records of the subquery both
unmodified, and with injected IN->EXISTS predicates.
......
......@@ -8314,7 +8314,7 @@ bool setup_tables(THD *thd, Name_resolution_context *context,
0);
SELECT_LEX *select_lex= select_insert ? thd->lex->first_select_lex() :
thd->lex->current_select;
if (select_lex->first_cond_optimization)
if (select_lex->first_cond_optimization || !select_lex->leaf_tables_saved)
{
leaves.empty();
if (select_lex->prep_leaf_list_state != SELECT_LEX::SAVED)
......
......@@ -20,6 +20,8 @@
#ifndef SQL_CMD_INCLUDED
#define SQL_CMD_INCLUDED
#include "my_base.h"
/*
When a command is added here, be sure it's also added in mysqld.cc
in "struct show_var_st status_vars[]= {" ...
......@@ -324,10 +326,12 @@ class Sql_cmd_dml : public Sql_cmd
select_result *get_result() { return result; }
ha_rows get_scanned_rows() { return scanned_rows; }
protected:
Sql_cmd_dml()
: Sql_cmd(), lex(nullptr), result(nullptr),
m_empty_query(false)
m_empty_query(false), scanned_rows(0)
{}
/**
......@@ -394,6 +398,7 @@ class Sql_cmd_dml : public Sql_cmd
LEX *lex; /**< Pointer to LEX for this statement */
select_result *result; /**< Pointer to object for handling of the result */
bool m_empty_query; /**< True if query will produce no rows */
ha_rows scanned_rows; /**< Number of scanned rows */
};
......
......@@ -352,6 +352,16 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
bool delete_record= false;
bool delete_while_scanning= table_list->delete_while_scanning;
bool portion_of_time_through_update;
/*
TRUE if we are after the call to
select_lex->optimize_unflattened_subqueries(true) and before the
call to select_lex->optimize_unflattened_subqueries(false), to
ensure a call to
select_lex->optimize_unflattened_subqueries(false) happens which
avoid 2nd ps mem leaks when e.g. the first execution produces
empty result and the second execution produces a non-empty set
*/
bool need_to_optimize= FALSE;
DBUG_ENTER("Sql_cmd_delete::delete_single_table");
......@@ -388,9 +398,18 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
thd->lex->promote_select_describe_flag_if_needed();
/* Apply the IN=>EXISTS transformation to all subqueries and optimize them. */
if (select_lex->optimize_unflattened_subqueries(false))
/*
Apply the IN=>EXISTS transformation to all constant subqueries
and optimize them.
It is too early to choose subquery optimization strategies without
an estimate of how many times the subquery will be executed so we
call optimize_unflattened_subqueries() with const_only= true, and
choose between materialization and in-to-exists later.
*/
if (select_lex->optimize_unflattened_subqueries(true))
DBUG_RETURN(TRUE);
need_to_optimize= TRUE;
const_cond= (!conds || conds->const_item());
safe_update= (thd->variables.option_bits & OPTION_SAFE_UPDATES) &&
......@@ -491,6 +510,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
#ifdef WITH_PARTITION_STORAGE_ENGINE
if (prune_partitions(thd, table, conds))
{
if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
free_underlaid_joins(thd, select_lex);
query_plan.set_no_partitions();
......@@ -529,6 +551,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
goto produce_explain_and_leave;
delete select;
if (select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
free_underlaid_joins(thd, select_lex);
/*
Error was already created by quick select evaluation (check_quick()).
......@@ -559,6 +584,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
if (safe_update && !using_limit)
{
delete select;
if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
free_underlaid_joins(thd, select_lex);
my_message(ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE,
ER_THD(thd, ER_UPDATE_WITHOUT_KEY_IN_SAFE_MODE), MYF(0));
......@@ -568,7 +596,18 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
if (options & OPTION_QUICK)
(void) table->file->extra(HA_EXTRA_QUICK);
query_plan.scanned_rows= select? select->records: table->file->stats.records;
/*
Estimate the number of scanned rows and have it accessible in
JOIN::choose_subquery_plan() from the outer join through
JOIN::sql_cmd_delete
*/
scanned_rows= query_plan.scanned_rows= select ?
select->records : table->file->stats.records;
select_lex->join->sql_cmd_delete= this;
DBUG_ASSERT(need_to_optimize);
if (select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
if (order)
{
table->update_const_key_parts(conds);
......@@ -983,6 +1022,8 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
DBUG_PRINT("info",("%ld records deleted",(long) deleted));
}
delete file_sort;
if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
free_underlaid_joins(thd, select_lex);
if (table->file->pushed_cond)
table->file->cond_pop();
......@@ -1006,6 +1047,9 @@ bool Sql_cmd_delete::delete_from_single_table(THD *thd)
delete select;
delete file_sort;
if (!thd->is_error() && need_to_optimize &&
select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
free_underlaid_joins(thd, select_lex);
if (table->file->pushed_cond)
table->file->cond_pop();
......
......@@ -1052,6 +1052,7 @@ class st_select_lex: public st_select_lex_node
*/
List<Item_func_in> in_funcs;
List<TABLE_LIST> leaf_tables;
/* Saved leaf tables for subsequent executions */
List<TABLE_LIST> leaf_tables_exec;
List<TABLE_LIST> leaf_tables_prep;
......
......@@ -564,6 +564,8 @@ void JOIN::init(THD *thd_arg, List<Item> &fields_arg,
is_orig_degenerated= false;
with_ties_order_count= 0;
prepared= false;
sql_cmd_delete= NULL;
sql_cmd_update= NULL;
};
......@@ -33,6 +33,8 @@
#include "records.h" /* READ_RECORD */
#include "opt_range.h" /* SQL_SELECT, QUICK_SELECT_I */
#include "filesort.h"
#include "sql_delete.h"
#include "sql_update.h"
#include "cset_narrowing.h"
......@@ -1722,6 +1724,14 @@ class JOIN :public Sql_alloc
*/
bool is_orig_degenerated;
/*
DELETE and UPDATE may have an imitation JOIN, which is not NULL,
but has NULL join_tab. In such cases we may want to access
sql_cmd_dml::scanned_rows to choose optimization strategies.
*/
Sql_cmd_delete *sql_cmd_delete;
Sql_cmd_update *sql_cmd_update;
JOIN(THD *thd_arg, List<Item> &fields_arg, ulonglong select_options_arg,
select_result *result_arg)
:fields_list(fields_arg)
......
......@@ -376,6 +376,16 @@ bool Sql_cmd_update::update_single_table(THD *thd)
List<Item> all_fields;
killed_state killed_status= NOT_KILLED;
bool has_triggers, binlog_is_row, do_direct_update= FALSE;
/*
TRUE if we are after the call to
select_lex->optimize_unflattened_subqueries(true) and before the
call to select_lex->optimize_unflattened_subqueries(false), to
ensure a call to
select_lex->optimize_unflattened_subqueries(false) happens which
avoid 2nd ps mem leaks when e.g. the first execution produces
empty result and the second execution produces a non-empty set
*/
bool need_to_optimize= FALSE;
Update_plan query_plan(thd->mem_root);
Explain_update *explain;
query_plan.index= MAX_KEY;
......@@ -422,9 +432,18 @@ bool Sql_cmd_update::update_single_table(THD *thd)
switch_to_nullable_trigger_fields(*fields, table);
switch_to_nullable_trigger_fields(*values, table);
/* Apply the IN=>EXISTS transformation to all subqueries and optimize them */
if (select_lex->optimize_unflattened_subqueries(false))
/*
Apply the IN=>EXISTS transformation to all constant subqueries
and optimize them.
It is too early to choose subquery optimization strategies without
an estimate of how many times the subquery will be executed so we
call optimize_unflattened_subqueries() with const_only= true, and
choose between materialization and in-to-exists later.
*/
if (select_lex->optimize_unflattened_subqueries(true))
DBUG_RETURN(TRUE);
need_to_optimize= TRUE;
if (conds)
{
......@@ -458,6 +477,9 @@ bool Sql_cmd_update::update_single_table(THD *thd)
#ifdef WITH_PARTITION_STORAGE_ENGINE
if (prune_partitions(thd, table, conds))
{
if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
free_underlaid_joins(thd, select_lex);
query_plan.set_no_partitions();
......@@ -493,6 +515,9 @@ bool Sql_cmd_update::update_single_table(THD *thd)
goto produce_explain_and_leave;
delete select;
if (need_to_optimize && select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
free_underlaid_joins(thd, select_lex);
/*
There was an error or the error was already sent by
......@@ -545,8 +570,20 @@ bool Sql_cmd_update::update_single_table(THD *thd)
table->update_const_key_parts(conds);
order= simple_remove_const(order, conds);
query_plan.scanned_rows= select? select->records: table->file->stats.records;
/*
Estimate the number of scanned rows and have it accessible in
JOIN::choose_subquery_plan() from the outer join through
JOIN::sql_cmd_update
*/
scanned_rows= query_plan.scanned_rows= select ?
select->records : table->file->stats.records;
select_lex->join->sql_cmd_update= this;
DBUG_ASSERT(need_to_optimize);
if (select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
need_to_optimize= FALSE;
if (select && select->quick && select->quick->unique_key_range())
{
/* Single row select (always "ordered"): Ok to use with key field UPDATE */
......@@ -1288,6 +1325,9 @@ bool Sql_cmd_update::update_single_table(THD *thd)
err:
delete select;
delete file_sort;
if (!thd->is_error() && need_to_optimize &&
select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
free_underlaid_joins(thd, select_lex);
table->file->ha_end_keyread();
if (table->file->pushed_cond)
......@@ -1308,6 +1348,9 @@ bool Sql_cmd_update::update_single_table(THD *thd)
int err2= thd->lex->explain->send_explain(thd, extended);
delete select;
if (!thd->is_error() && need_to_optimize &&
select_lex->optimize_unflattened_subqueries(false))
DBUG_RETURN(TRUE);
free_underlaid_joins(thd, select_lex);
DBUG_RETURN((err2 || thd->is_error()) ? 1 : 0);
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment