Bug#3788

  Crashes with stored procedure return non-string values
  Also fixes Bug#2773
parent 05eb8af4
......@@ -202,7 +202,7 @@ select parameter_style, sql_data_access, dtd_identifier
from information_schema.routines;
parameter_style sql_data_access dtd_identifier
SQL CONTAINS SQL NULL
SQL CONTAINS SQL int
SQL CONTAINS SQL int(11)
show procedure status;
Db Name Type Definer Modified Created Security_type Comment
test sel2 PROCEDURE root@localhost # # DEFINER
......
......@@ -951,7 +951,7 @@ comment 'Characteristics procedure test'
return 42|
show create function chistics|
Function sql_mode Create Function
chistics CREATE FUNCTION `test`.`chistics`() RETURNS int
chistics CREATE FUNCTION `test`.`chistics`() RETURNS int(11)
DETERMINISTIC
SQL SECURITY INVOKER
COMMENT 'Characteristics procedure test'
......@@ -964,7 +964,7 @@ no sql
comment 'Characteristics function test'|
show create function chistics|
Function sql_mode Create Function
chistics CREATE FUNCTION `test`.`chistics`() RETURNS int
chistics CREATE FUNCTION `test`.`chistics`() RETURNS int(11)
NO SQL
DETERMINISTIC
SQL SECURITY INVOKER
......@@ -1214,7 +1214,7 @@ show procedure status;
end
call bug2267_4()|
Function sql_mode Create Function
fac CREATE FUNCTION `test`.`fac`(n int unsigned) RETURNS bigint unsigned
fac CREATE FUNCTION `test`.`fac`(n int unsigned) RETURNS bigint(20) unsigned
begin
declare f bigint unsigned default 1;
while n > 1 do
......@@ -1576,11 +1576,11 @@ bug2564_2 ANSI_QUOTES CREATE PROCEDURE "test"."bug2564_2"()
insert into "t1" values ('foo', 1)
show create function bug2564_3|
Function sql_mode Create Function
bug2564_3 CREATE FUNCTION `test`.`bug2564_3`(x int, y int) RETURNS int
bug2564_3 CREATE FUNCTION `test`.`bug2564_3`(x int, y int) RETURNS int(11)
return x || y
show create function bug2564_4|
Function sql_mode Create Function
bug2564_4 REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,ONLY_FULL_GROUP_BY,ANSI CREATE FUNCTION "test"."bug2564_4"(x int, y int) RETURNS int
bug2564_4 REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,ONLY_FULL_GROUP_BY,ANSI CREATE FUNCTION "test"."bug2564_4"(x int, y int) RETURNS int(11)
return x || y
drop procedure bug2564_1|
drop procedure bug2564_2|
......@@ -1645,6 +1645,28 @@ drop procedure bug4579_1|
drop procedure bug4579_2|
drop table t3|
drop table if exists t3|
drop procedure if exists bug2773|
create function bug2773() returns int return null|
create table t3 as select bug2773()|
show create table t3|
Table Create Table
t3 CREATE TABLE `t3` (
`bug2773()` int(11) default NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1
drop table t3|
drop function bug2773|
drop procedure if exists bug3788|
create function bug3788() returns date return cast("2005-03-04" as date)|
select bug3788()|
bug3788()
2005-03-04
drop function bug3788|
create function bug3788() returns binary(5) return 5|
select bug3788()|
bug3788()
5
drop function bug3788|
drop table if exists t3|
create table t3 (f1 int, f2 int, f3 int)|
insert into t3 values (1,1,1)|
drop procedure if exists bug4726|
......
......@@ -2063,6 +2063,35 @@ drop procedure bug4579_1|
drop procedure bug4579_2|
drop table t3|
#
# BUG#2773: Function's data type ignored in stored procedures
#
--disable_warnings
drop table if exists t3|
drop procedure if exists bug2773|
--enable_warnings
create function bug2773() returns int return null|
create table t3 as select bug2773()|
show create table t3|
drop table t3|
drop function bug2773|
#
# BUG#3788: Stored procedure packet error
#
--disable_warnings
drop procedure if exists bug3788|
--enable_warnings
create function bug3788() returns date return cast("2005-03-04" as date)|
select bug3788()|
drop function bug3788|
create function bug3788() returns binary(5) return 5|
select bug3788()|
drop function bug3788|
#
# BUG#4726
......
......@@ -3524,7 +3524,7 @@ bool Item_ref::fix_fields(THD *thd, TABLE_LIST *tables, Item **reference)
enum_parsing_place place= NO_MATTER;
SELECT_LEX *current_sel= thd->lex->current_select;
if (!ref)
if (!ref || ref == not_found_item)
{
SELECT_LEX_UNIT *prev_unit= current_sel->master_unit();
SELECT_LEX *outer_sel= prev_unit->outer_select();
......
......@@ -4318,13 +4318,33 @@ longlong Item_func_row_count::val_int()
Item_func_sp::Item_func_sp(sp_name *name)
:Item_func(), m_name(name), m_sp(NULL)
{
char *empty_name= (char *) "";
maybe_null= 1;
m_name->init_qname(current_thd);
bzero(&dummy_table, sizeof(dummy_table));
dummy_table.share.table_cache_key = empty_name;
dummy_table.share.table_name = empty_name;
dummy_table.table.alias = empty_name;
dummy_table.share.table_name = empty_name;
dummy_table.table.maybe_null = maybe_null;
dummy_table.table.in_use= current_thd;
dummy_table.table.s = &dummy_table.share;
}
Item_func_sp::Item_func_sp(sp_name *name, List<Item> &list)
:Item_func(list), m_name(name), m_sp(NULL)
{
char *empty_name= (char *) "";
maybe_null= 1;
m_name->init_qname(current_thd);
bzero(&dummy_table, sizeof(dummy_table));
dummy_table.share.table_cache_key = empty_name;
dummy_table.share.table_name = empty_name;
dummy_table.table.alias = empty_name;
dummy_table.share.table_name = empty_name;
dummy_table.table.maybe_null = maybe_null;
dummy_table.table.in_use= current_thd;
dummy_table.table.s = &dummy_table.share;
}
const char *
......@@ -4349,6 +4369,18 @@ Item_func_sp::func_name() const
}
Field *
Item_func_sp::sp_result_field(void) const
{
Field *field= 0;
THD *thd= current_thd;
DBUG_ENTER("Item_func_sp::sp_result_field");
if (m_sp)
field= m_sp->make_field(max_length, name, &dummy_table.table);
DBUG_RETURN(field);
}
int
Item_func_sp::execute(Item **itp)
{
......@@ -4404,17 +4436,38 @@ Item_func_sp::execute(Item **itp)
}
void
Item_func_sp::make_field(Send_field *tmp_field)
{
Field *field;
DBUG_ENTER("Item_func_sp::make_field");
if (! m_sp)
m_sp= sp_find_function(current_thd, m_name, TRUE); // cache only
if ((field= sp_result_field()))
{
field->make_field(tmp_field);
delete field;
DBUG_VOID_RETURN;
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "FUNCTION", m_name->m_qname.str);
init_make_field(tmp_field, MYSQL_TYPE_VARCHAR);
DBUG_VOID_RETURN;
}
enum enum_field_types
Item_func_sp::field_type() const
{
Field *field= 0;
DBUG_ENTER("Item_func_sp::field_type");
if (! m_sp)
m_sp= sp_find_function(current_thd, m_name, TRUE); // cache only
if (m_sp)
if ((field= sp_result_field()))
{
DBUG_PRINT("info", ("m_returns = %d", m_sp->m_returns));
DBUG_RETURN(m_sp->m_returns);
enum_field_types result= field->type();
delete field;
DBUG_RETURN(result);
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "FUNCTION", m_name->m_qname.str);
DBUG_RETURN(MYSQL_TYPE_VARCHAR);
......@@ -4424,14 +4477,17 @@ Item_func_sp::field_type() const
Item_result
Item_func_sp::result_type() const
{
Field *field= 0;
DBUG_ENTER("Item_func_sp::result_type");
DBUG_PRINT("info", ("m_sp = %p", m_sp));
if (! m_sp)
m_sp= sp_find_function(current_thd, m_name, TRUE); // cache only
if (m_sp)
if ((field= sp_result_field()))
{
DBUG_RETURN(m_sp->result());
Item_result result= field->result_type();
delete field;
DBUG_RETURN(result);
}
my_error(ER_SP_DOES_NOT_EXIST, MYF(0), "FUNCTION", m_name->m_qname.str);
DBUG_RETURN(STRING_RESULT);
......@@ -4450,7 +4506,7 @@ Item_func_sp::fix_length_and_dec()
}
else
{
switch (m_sp->result()) {
switch (result_type()) {
case STRING_RESULT:
maybe_null= 1;
max_length= MAX_BLOB_WIDTH;
......@@ -4485,3 +4541,19 @@ longlong Item_func_found_rows::val_int()
return thd->found_rows();
}
Field *
Item_func_sp::tmp_table_field(TABLE *t_arg)
{
Field *res= 0;
enum_field_types ftype;
DBUG_ENTER("Item_func_sp::tmp_table_field");
if (m_sp)
res= m_sp->make_field(max_length, (const char *)name, t_arg);
if (!res)
res= Item_func::tmp_table_field(t_arg);
DBUG_RETURN(res);
}
......@@ -1250,8 +1250,13 @@ class Item_func_sp :public Item_func
private:
sp_name *m_name;
mutable sp_head *m_sp;
mutable struct {
TABLE table;
TABLE_SHARE share;
} dummy_table;
int execute(Item **itp);
Field *sp_result_field(void) const;
public:
......@@ -1266,6 +1271,10 @@ public:
enum enum_field_types field_type() const;
Field *tmp_table_field(TABLE *t_arg);
void make_field(Send_field *tmp_field);
Item_result result_type() const;
longlong val_int()
......
......@@ -629,6 +629,10 @@ int mysql_derived_filling(THD *thd, LEX *lex, TABLE_LIST *t);
Field *create_tmp_field(THD *thd, TABLE *table,Item *item, Item::Type type,
Item ***copy_func, Field **from_field,
bool group, bool modify_item, uint convert_blob_length);
int prepare_create_field(create_field *sql_field,
uint &blob_columns,
int &timestamps, int &timestamps_with_niladic,
uint table_flags);
int mysql_prepare_table(THD *thd, HA_CREATE_INFO *create_info,
List<create_field> &fields,
List<Key> &keys, uint &db_options,
......@@ -837,6 +841,13 @@ bool add_field_to_list(THD *thd, char *field_name, enum enum_field_types type,
char *change, List<String> *interval_list,
CHARSET_INFO *cs,
uint uint_geom_type);
create_field * new_create_field(THD *thd, char *field_name, enum_field_types type,
char *length, char *decimals,
uint type_modifier,
Item *default_value, Item *on_update_value,
LEX_STRING *comment, char *change,
List<String> *interval_list, CHARSET_INFO *cs,
uint uint_geom_type);
void store_position_for_column(const char *name);
bool add_to_list(THD *thd, SQL_LIST &list,Item *group,bool asc=0);
void add_join_on(TABLE_LIST *b,Item *expr);
......
......@@ -334,6 +334,22 @@ db_find_routine(THD *thd, int type, sp_name *name, sp_head **sphp)
}
static void
sp_returns_type(THD *thd, String &result, sp_head *sp)
{
struct {
TABLE table;
TABLE_SHARE share;
} dummy;
Field *field;
bzero(&dummy, sizeof(dummy));
dummy.table.in_use= thd;
dummy.table.s = &dummy.share;
field= sp->make_field(0, 0, &dummy.table);
field->sql_type(result);
delete field;
}
static int
db_create_routine(THD *thd, int type, sp_head *sp)
{
......@@ -388,9 +404,13 @@ db_create_routine(THD *thd, int type, sp_head *sp)
store((longlong)sp->m_chistics->suid);
table->field[MYSQL_PROC_FIELD_PARAM_LIST]->
store(sp->m_params.str, sp->m_params.length, system_charset_info);
if (sp->m_retstr.str)
if (sp->m_type == TYPE_ENUM_FUNCTION)
{
String retstr(64);
sp_returns_type(thd, retstr, sp);
table->field[MYSQL_PROC_FIELD_RETURNS]->
store(sp->m_retstr.str, sp->m_retstr.length, system_charset_info);
store(retstr.ptr(), retstr.length(), system_charset_info);
}
table->field[MYSQL_PROC_FIELD_BODY]->
store(sp->m_body.str, sp->m_body.length, system_charset_info);
table->field[MYSQL_PROC_FIELD_DEFINER]->
......
......@@ -300,11 +300,11 @@ sp_head::init(LEX *lex)
*/
lex->trg_table_fields.empty();
my_init_dynamic_array(&m_instr, sizeof(sp_instr *), 16, 8);
m_param_begin= m_param_end= m_returns_begin= m_returns_end= m_body_begin= 0;
m_qname.str= m_db.str= m_name.str= m_params.str= m_retstr.str=
m_param_begin= m_param_end= m_body_begin= 0;
m_qname.str= m_db.str= m_name.str= m_params.str=
m_body.str= m_defstr.str= 0;
m_qname.length= m_db.length= m_name.length= m_params.length=
m_retstr.length= m_body.length= m_defstr.length= 0;
m_body.length= m_defstr.length= 0;
m_returns_cs= NULL;
DBUG_VOID_RETURN;
}
......@@ -346,41 +346,6 @@ sp_head::init_strings(THD *thd, LEX *lex, sp_name *name)
(char *)m_param_begin, m_params.length);
}
if (m_returns_begin && m_returns_end)
{
/* QQ KLUDGE: We can't seem to cut out just the type in the parser
(without the RETURNS), so we'll have to do it here. :-(
Furthermore, if there's a character type as well, it's not include
(beyond the m_returns_end pointer), in which case we need
m_returns_cs. */
char *p= (char *)m_returns_begin+strspn((char *)m_returns_begin,"\t\n\r ");
p+= strcspn(p, "\t\n\r ");
p+= strspn(p, "\t\n\r ");
if (p < (char *)m_returns_end)
m_returns_begin= (uchar *)p;
/* While we're at it, trim the end too. */
p= (char *)m_returns_end-1;
while (p > (char *)m_returns_begin &&
(*p == '\t' || *p == '\n' || *p == '\r' || *p == ' '))
p-= 1;
m_returns_end= (uchar *)p+1;
if (m_returns_cs)
{
String s((char *)m_returns_begin, m_returns_end - m_returns_begin,
system_charset_info);
s.append(' ');
s.append(m_returns_cs->csname);
m_retstr.length= s.length();
m_retstr.str= strmake_root(root, s.ptr(), m_retstr.length);
}
else
{
m_retstr.length= m_returns_end - m_returns_begin;
m_retstr.str= strmake_root(root,
(char *)m_returns_begin, m_retstr.length);
}
}
m_body.length= lex->ptr - m_body_begin;
/* Trim nuls at the end */
n= 0;
......@@ -396,6 +361,27 @@ sp_head::init_strings(THD *thd, LEX *lex, sp_name *name)
DBUG_VOID_RETURN;
}
TYPELIB *
sp_head::create_typelib(List<String> *src)
{
TYPELIB *result= NULL;
DBUG_ENTER("sp_head::clone_typelib");
if (src->elements)
{
result= (TYPELIB*) alloc_root(mem_root, sizeof(TYPELIB));
result->count= src->elements;
result->name= "";
if (!(result->type_names=(const char **)
alloc_root(mem_root,sizeof(char *)*(result->count+1))))
return 0;
List_iterator<String> it(*src);
for (uint i=0; i<result->count; i++)
result->type_names[i]= strdup_root(mem_root, (it++)->c_ptr());
result->type_names[result->count]= 0;
}
return result;
}
int
sp_head::create(THD *thd)
{
......@@ -464,6 +450,21 @@ sp_head::destroy()
DBUG_VOID_RETURN;
}
Field *
sp_head::make_field(uint max_length, const char *name, TABLE *dummy)
{
Field *field;
DBUG_ENTER("sp_head::make_field");
field= ::make_field((char *)0,
!m_returns_len ? max_length : m_returns_len,
(uchar *)"", 0, m_returns_pack, m_returns, m_returns_cs,
(enum Field::geometry_type)0, Field::NONE,
m_returns_typelib,
name ? name : (const char *)m_name.str, dummy);
DBUG_RETURN(field);
}
int
sp_head::execute(THD *thd)
{
......
......@@ -84,6 +84,9 @@ public:
int m_type; // TYPE_ENUM_FUNCTION or TYPE_ENUM_PROCEDURE
enum enum_field_types m_returns; // For FUNCTIONs only
CHARSET_INFO *m_returns_cs; // For FUNCTIONs only
TYPELIB *m_returns_typelib; // For FUNCTIONs only
uint m_returns_len; // For FUNCTIONs only
uint m_returns_pack; // For FUNCTIONs only
my_bool m_has_return; // For FUNCTIONs only
my_bool m_simple_case; // TRUE if parsing simple case, FALSE otherwise
my_bool m_multi_results; // TRUE if a procedure with SELECT(s)
......@@ -96,7 +99,6 @@ public:
LEX_STRING m_db;
LEX_STRING m_name;
LEX_STRING m_params;
LEX_STRING m_retstr; // For FUNCTIONs only
LEX_STRING m_body;
LEX_STRING m_defstr;
LEX_STRING m_definer_user;
......@@ -105,8 +107,7 @@ public:
longlong m_modified;
HASH m_sptabs; /* Merged table lists */
// Pointers set during parsing
uchar *m_param_begin, *m_param_end, *m_returns_begin, *m_returns_end,
*m_body_begin;
uchar *m_param_begin, *m_param_end, *m_body_begin;
static void *
operator new(size_t size);
......@@ -124,6 +125,9 @@ public:
void
init_strings(THD *thd, LEX *lex, sp_name *name);
TYPELIB *
create_typelib(List<String> *src);
int
create(THD *thd);
......@@ -197,10 +201,7 @@ public:
char *create_string(THD *thd, ulong *lenp);
inline Item_result result()
{
return sp_map_result_type(m_returns);
}
Field *make_field(uint max_length, const char *name, TABLE *dummy);
void set_info(char *definer, uint definerlen,
longlong created, longlong modified,
......
......@@ -5291,9 +5291,6 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
{
register create_field *new_field;
LEX *lex= thd->lex;
uint allowed_type_modifier=0;
uint sign_len;
ulong max_field_charlength= MAX_FIELD_CHARLENGTH;
DBUG_ENTER("add_field_to_list");
if (strlen(field_name) > NAME_LEN)
......@@ -5355,8 +5352,37 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
DBUG_RETURN(1);
}
if (!(new_field=new create_field()))
if (!(new_field= new_create_field(thd, field_name, type, length, decimals,
type_modifier, default_value, on_update_value,
comment, change, interval_list, cs, uint_geom_type)))
DBUG_RETURN(1);
lex->create_list.push_back(new_field);
lex->last_field=new_field;
DBUG_RETURN(0);
}
/*****************************************************************************
** Create field definition for create
** Return 0 on failure, otherwise return create_field instance
******************************************************************************/
create_field *
new_create_field(THD *thd, char *field_name, enum_field_types type,
char *length, char *decimals,
uint type_modifier,
Item *default_value, Item *on_update_value,
LEX_STRING *comment,
char *change, List<String> *interval_list, CHARSET_INFO *cs,
uint uint_geom_type)
{
register create_field *new_field;
uint sign_len, allowed_type_modifier=0;
ulong max_field_charlength= MAX_FIELD_CHARLENGTH;
DBUG_ENTER("new_create_field");
if (!(new_field=new create_field()))
DBUG_RETURN(NULL);
new_field->field=0;
new_field->field_name=field_name;
new_field->def= default_value;
......@@ -5428,7 +5454,7 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
new_field->length >= new_field->decimals)
break;
my_error(ER_WRONG_FIELD_SPEC, MYF(0), field_name);
DBUG_RETURN(1);
DBUG_RETURN(NULL);
case MYSQL_TYPE_VARCHAR:
/*
Long VARCHAR's are automaticly converted to blobs in mysql_prepare_table
......@@ -5451,7 +5477,7 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
{
my_error(ER_BLOB_CANT_HAVE_DEFAULT, MYF(0),
field_name); /* purecov: inspected */
DBUG_RETURN(1); /* purecov: inspected */
DBUG_RETURN(NULL);
}
new_field->def=0;
}
......@@ -5471,7 +5497,7 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
if (tmp_length > PRECISION_FOR_DOUBLE)
{
my_error(ER_WRONG_FIELD_SPEC, MYF(0), field_name);
DBUG_RETURN(1);
DBUG_RETURN(NULL);
}
else if (tmp_length > PRECISION_FOR_FLOAT)
{
......@@ -5568,7 +5594,7 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
if (interval_list->elements > sizeof(longlong)*8)
{
my_error(ER_TOO_BIG_SET, MYF(0), field_name); /* purecov: inspected */
DBUG_RETURN(1); /* purecov: inspected */
DBUG_RETURN(NULL);
}
new_field->pack_length= (interval_list->elements + 7) / 8;
if (new_field->pack_length > 4)
......@@ -5609,7 +5635,7 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
{
my_error(ER_TOO_BIG_FIELDLENGTH, MYF(0), field_name,
MAX_BIT_FIELD_LENGTH);
DBUG_RETURN(1);
DBUG_RETURN(NULL);
}
new_field->pack_length= (new_field->length + 7) / 8;
break;
......@@ -5628,17 +5654,15 @@ bool add_field_to_list(THD *thd, char *field_name, enum_field_types type,
{
my_error(ER_TOO_BIG_FIELDLENGTH, MYF(0),
field_name, max_field_charlength); /* purecov: inspected */
DBUG_RETURN(1); /* purecov: inspected */
DBUG_RETURN(NULL);
}
type_modifier&= AUTO_INCREMENT_FLAG;
if ((~allowed_type_modifier) & type_modifier)
{
my_error(ER_WRONG_FIELD_SPEC, MYF(0), field_name);
DBUG_RETURN(1);
DBUG_RETURN(NULL);
}
lex->create_list.push_back(new_field);
lex->last_field=new_field;
DBUG_RETURN(0);
DBUG_RETURN(new_field);
}
......
This diff is collapsed.
......@@ -1370,19 +1370,37 @@ create_function_tail:
RETURNS_SYM
{
LEX *lex= Lex;
sp_head *sp= lex->sphead;
sp->m_returns_begin= lex->tok_start;
sp->m_returns_cs= lex->charset= NULL;
lex->charset= NULL;
lex->length= lex->dec= NULL;
lex->interval_list.empty();
lex->type= 0;
}
type
{
LEX *lex= Lex;
sp_head *sp= lex->sphead;
LEX_STRING cmt = { 0, 0 };
create_field *new_field;
uint unused1= 0;
int unused2= 0;
if (!(new_field= new_create_field(YYTHD, "", (enum enum_field_types)$8,
lex->length, lex->dec, lex->type,
(Item *)0, (Item *) 0, &cmt, 0, &lex->interval_list,
(lex->charset ? lex->charset : default_charset_info),
lex->uint_geom_type)))
YYABORT;
if (prepare_create_field(new_field, unused1, unused2, unused2, 0))
YYABORT;
sp->m_returns= new_field->sql_type;
sp->m_returns_cs= new_field->charset;
sp->m_returns_len= new_field->length;
sp->m_returns_pack= new_field->pack_flag;
sp->m_returns_typelib=
sp->create_typelib(&new_field->interval_list);
sp->m_returns_end= lex->tok_start;
sp->m_returns= (enum enum_field_types)$8;
sp->m_returns_cs= lex->charset;
bzero((char *)&lex->sp_chistics, sizeof(st_sp_chistics));
}
sp_c_chistics
......
......@@ -695,6 +695,7 @@ static bool make_empty_rec(File file,enum db_type table_type,
field->interval,
field->field_name,
&table);
DBUG_ASSERT(regfield);
if (!(field->flags & NOT_NULL_FLAG))
null_count++;
......
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