Commit be5a3426 authored by unknown's avatar unknown

ha_ndbcluster.h, ha_ndbcluster.cc:

  Refactored code for engine_condition_pushdown to ha_ndbcluster_cond
Makefile.am:
  Added compilation of ha_ndbcluster_cond
ha_ndbcluster_cond.h, ha_ndbcluster_cond.cc:
  Merge


libmysqld/Makefile.am:
  Added compilation of ha_ndbcluster_cond
sql/Makefile.am:
  Added compilation of ha_ndbcluster_cond
sql/ha_ndbcluster.cc:
  Refactored code for engine_condition_pushdown to ha_ndbcluster_cond
sql/ha_ndbcluster.h:
  Refactored code for engine_condition_pushdown to ha_ndbcluster_cond
sql/ha_ndbcluster_cond.cc:
  Merge
sql/ha_ndbcluster_cond.h:
  Merge
parent 5506f6ca
...@@ -45,7 +45,7 @@ libmysqlsources = errmsg.c get_password.c libmysql.c client.c pack.c \ ...@@ -45,7 +45,7 @@ libmysqlsources = errmsg.c get_password.c libmysql.c client.c pack.c \
noinst_HEADERS = embedded_priv.h emb_qcache.h noinst_HEADERS = embedded_priv.h emb_qcache.h
sqlsources = derror.cc field.cc field_conv.cc strfunc.cc filesort.cc \ sqlsources = derror.cc field.cc field_conv.cc strfunc.cc filesort.cc \
ha_ndbcluster.cc \ ha_ndbcluster.cc ha_ndbcluster_cond.cc \
ha_ndbcluster_binlog.cc ha_partition.cc \ ha_ndbcluster_binlog.cc ha_partition.cc \
handler.cc sql_handler.cc \ handler.cc sql_handler.cc \
hostname.cc init.cc password.c \ hostname.cc init.cc password.c \
...@@ -105,6 +105,9 @@ endif ...@@ -105,6 +105,9 @@ endif
ha_ndbcluster.o:ha_ndbcluster.cc ha_ndbcluster.o:ha_ndbcluster.cc
$(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $< $(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $<
ha_ndbcluster_cond.o:ha_ndbcluster_cond.cc
$(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $<
ha_ndbcluster_binlog.o: ha_ndbcluster_binlog.cc ha_ndbcluster_binlog.o: ha_ndbcluster_binlog.cc
$(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $< $(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $<
......
...@@ -48,9 +48,9 @@ noinst_HEADERS = item.h item_func.h item_sum.h item_cmpfunc.h \ ...@@ -48,9 +48,9 @@ noinst_HEADERS = item.h item_func.h item_sum.h item_cmpfunc.h \
procedure.h sql_class.h sql_lex.h sql_list.h \ procedure.h sql_class.h sql_lex.h sql_list.h \
sql_map.h sql_string.h unireg.h \ sql_map.h sql_string.h unireg.h \
sql_error.h field.h handler.h mysqld_suffix.h \ sql_error.h field.h handler.h mysqld_suffix.h \
ha_partition.h \ ha_ndbcluster.h ha_ndbcluster_cond.h \
ha_ndbcluster.h ha_ndbcluster_binlog.h \ ha_ndbcluster_binlog.h ha_ndbcluster_tables.h
ha_ndbcluster_tables.h rpl_constants.h \ ha_partition.h rpl_constants.h \
opt_range.h protocol.h rpl_tblmap.h rpl_utility.h \ opt_range.h protocol.h rpl_tblmap.h rpl_utility.h \
log.h sql_show.h rpl_rli.h rpl_mi.h \ log.h sql_show.h rpl_rli.h rpl_mi.h \
sql_select.h structs.h table.h sql_udf.h hash_filo.h \ sql_select.h structs.h table.h sql_udf.h hash_filo.h \
...@@ -89,8 +89,8 @@ mysqld_SOURCES = sql_lex.cc sql_handler.cc sql_partition.cc \ ...@@ -89,8 +89,8 @@ mysqld_SOURCES = sql_lex.cc sql_handler.cc sql_partition.cc \
unireg.cc des_key_file.cc \ unireg.cc des_key_file.cc \
discover.cc time.cc opt_range.cc opt_sum.cc \ discover.cc time.cc opt_range.cc opt_sum.cc \
records.cc filesort.cc handler.cc \ records.cc filesort.cc handler.cc \
ha_partition.cc \ ha_ndbcluster.cc ha_ndbcluster_cond.cc \
ha_ndbcluster.cc ha_ndbcluster_binlog.cc \ ha_ndbcluster_binlog.cc ha_partition.cc \
sql_db.cc sql_table.cc sql_rename.cc sql_crypt.cc \ sql_db.cc sql_table.cc sql_rename.cc sql_crypt.cc \
sql_load.cc mf_iocache.cc field_conv.cc sql_show.cc \ sql_load.cc mf_iocache.cc field_conv.cc sql_show.cc \
sql_udf.cc sql_analyse.cc sql_analyse.h sql_cache.cc \ sql_udf.cc sql_analyse.cc sql_analyse.h sql_cache.cc \
...@@ -153,10 +153,13 @@ lex_hash.h: gen_lex_hash.cc lex.h ...@@ -153,10 +153,13 @@ lex_hash.h: gen_lex_hash.cc lex.h
./gen_lex_hash$(EXEEXT) > $@-t ./gen_lex_hash$(EXEEXT) > $@-t
$(MV) $@-t $@ $(MV) $@-t $@
# the following three should eventually be moved out of this directory # the following four should eventually be moved out of this directory
ha_ndbcluster.o:ha_ndbcluster.cc ha_ndbcluster.h ha_ndbcluster.o:ha_ndbcluster.cc ha_ndbcluster.h
$(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $< $(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $<
ha_ndbcluster_cond.o:ha_ndbcluster_cond.cc ha_ndbcluster_cond.h
$(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $<
ha_ndbcluster_binlog.o:ha_ndbcluster_binlog.cc ha_ndbcluster_binlog.h ha_ndbcluster_binlog.o:ha_ndbcluster_binlog.cc ha_ndbcluster_binlog.h
$(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $< $(CXXCOMPILE) @ndbcluster_includes@ $(LM_CFLAGS) -c $<
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
#ifdef WITH_NDBCLUSTER_STORAGE_ENGINE #ifdef WITH_NDBCLUSTER_STORAGE_ENGINE
#include "ha_ndbcluster.h" #include "ha_ndbcluster.h"
#include <ndbapi/NdbApi.hpp> #include <ndbapi/NdbApi.hpp>
#include <ndbapi/NdbScanFilter.hpp> #include "ha_ndbcluster_cond.h"
#include <../util/Bitmask.hpp> #include <../util/Bitmask.hpp>
#include <ndbapi/NdbIndexStat.hpp> #include <ndbapi/NdbIndexStat.hpp>
...@@ -2402,7 +2402,7 @@ int ha_ndbcluster::ordered_index_scan(const key_range *start_key, ...@@ -2402,7 +2402,7 @@ int ha_ndbcluster::ordered_index_scan(const key_range *start_key,
if (!restart) if (!restart)
{ {
if (generate_scan_filter(m_cond_stack, op)) if (m_cond && m_cond->generate_scan_filter(op))
DBUG_RETURN(ndb_err(trans)); DBUG_RETURN(ndb_err(trans));
if ((res= define_read_attrs(buf, op))) if ((res= define_read_attrs(buf, op)))
...@@ -2508,8 +2508,14 @@ int ha_ndbcluster::unique_index_scan(const KEY* key_info, ...@@ -2508,8 +2508,14 @@ int ha_ndbcluster::unique_index_scan(const KEY* key_info,
(get_ndb_partition_id(op))) (get_ndb_partition_id(op)))
ERR_RETURN(trans->getNdbError()); ERR_RETURN(trans->getNdbError());
} }
if (!m_cond)
if (generate_scan_filter_from_key(op, key_info, key, key_len, buf)) m_cond= new ha_ndbcluster_cond;
if (!m_cond)
{
my_errno= HA_ERR_OUT_OF_MEM;
DBUG_RETURN(my_errno);
}
if (m_cond->generate_scan_filter_from_key(op, key_info, key, key_len, buf))
DBUG_RETURN(ndb_err(trans)); DBUG_RETURN(ndb_err(trans));
if ((res= define_read_attrs(buf, op))) if ((res= define_read_attrs(buf, op)))
DBUG_RETURN(res); DBUG_RETURN(res);
...@@ -2578,7 +2584,7 @@ int ha_ndbcluster::full_table_scan(byte *buf) ...@@ -2578,7 +2584,7 @@ int ha_ndbcluster::full_table_scan(byte *buf)
ERR_RETURN(trans->getNdbError()); ERR_RETURN(trans->getNdbError());
} }
if (generate_scan_filter(m_cond_stack, op)) if (m_cond && m_cond->generate_scan_filter(op))
DBUG_RETURN(ndb_err(trans)); DBUG_RETURN(ndb_err(trans));
if ((res= define_read_attrs(buf, op))) if ((res= define_read_attrs(buf, op)))
DBUG_RETURN(res); DBUG_RETURN(res);
...@@ -3949,7 +3955,11 @@ int ha_ndbcluster::extra(enum ha_extra_function operation) ...@@ -3949,7 +3955,11 @@ int ha_ndbcluster::extra(enum ha_extra_function operation)
int ha_ndbcluster::reset() int ha_ndbcluster::reset()
{ {
DBUG_ENTER("ha_ndbcluster::reset"); DBUG_ENTER("ha_ndbcluster::reset");
cond_clear(); if (m_cond)
{
m_cond->cond_clear();
}
/* /*
Regular partition pruning will set the bitmap appropriately. Regular partition pruning will set the bitmap appropriately.
Some queries like ALTER TABLE doesn't use partition pruning and Some queries like ALTER TABLE doesn't use partition pruning and
...@@ -6001,7 +6011,7 @@ ha_ndbcluster::ha_ndbcluster(handlerton *hton, TABLE_SHARE *table_arg): ...@@ -6001,7 +6011,7 @@ ha_ndbcluster::ha_ndbcluster(handlerton *hton, TABLE_SHARE *table_arg):
m_force_send(TRUE), m_force_send(TRUE),
m_autoincrement_prefetch((ha_rows) 32), m_autoincrement_prefetch((ha_rows) 32),
m_transaction_on(TRUE), m_transaction_on(TRUE),
m_cond_stack(NULL), m_cond(NULL),
m_multi_cursor(NULL) m_multi_cursor(NULL)
{ {
int i; int i;
...@@ -6060,9 +6070,13 @@ ha_ndbcluster::~ha_ndbcluster() ...@@ -6060,9 +6070,13 @@ ha_ndbcluster::~ha_ndbcluster()
} }
DBUG_ASSERT(m_active_trans == NULL); DBUG_ASSERT(m_active_trans == NULL);
// Discard the condition stack // Discard any generated condition
DBUG_PRINT("info", ("Clearing condition stack")); DBUG_PRINT("info", ("Deleting generated condition"));
cond_clear(); if (m_cond)
{
delete m_cond;
m_cond= NULL;
}
DBUG_VOID_RETURN; DBUG_VOID_RETURN;
} }
...@@ -8434,7 +8448,7 @@ ha_ndbcluster::read_multi_range_first(KEY_MULTI_RANGE **found_range_p, ...@@ -8434,7 +8448,7 @@ ha_ndbcluster::read_multi_range_first(KEY_MULTI_RANGE **found_range_p,
else if ((scanOp= m_active_trans->getNdbIndexScanOperation(idx, tab)) else if ((scanOp= m_active_trans->getNdbIndexScanOperation(idx, tab))
&&!scanOp->readTuples(lm, 0, parallelism, sorted, &&!scanOp->readTuples(lm, 0, parallelism, sorted,
FALSE, TRUE, need_pk, TRUE) FALSE, TRUE, need_pk, TRUE)
&&!generate_scan_filter(m_cond_stack, scanOp) &&!(m_cond && m_cond->generate_scan_filter(scanOp))
&&!define_read_attrs(end_of_buffer-reclength, scanOp)) &&!define_read_attrs(end_of_buffer-reclength, scanOp))
{ {
m_multi_cursor= scanOp; m_multi_cursor= scanOp;
...@@ -9020,28 +9034,15 @@ COND* ...@@ -9020,28 +9034,15 @@ COND*
ha_ndbcluster::cond_push(const COND *cond) ha_ndbcluster::cond_push(const COND *cond)
{ {
DBUG_ENTER("cond_push"); DBUG_ENTER("cond_push");
Ndb_cond_stack *ndb_cond = new Ndb_cond_stack(); if (!m_cond)
if (ndb_cond == NULL) m_cond= new ha_ndbcluster_cond;
if (!m_cond)
{ {
my_errno= HA_ERR_OUT_OF_MEM; my_errno= HA_ERR_OUT_OF_MEM;
DBUG_RETURN(NULL); DBUG_RETURN(NULL);
} }
DBUG_EXECUTE("where",print_where((COND *)cond, m_tabname);); DBUG_EXECUTE("where",print_where((COND *)cond, m_tabname););
if (m_cond_stack) DBUG_RETURN(m_cond->cond_push(cond, table, (NDBTAB *)m_table));
ndb_cond->next= m_cond_stack;
else
ndb_cond->next= NULL;
m_cond_stack= ndb_cond;
if (serialize_cond(cond, ndb_cond))
{
DBUG_RETURN(NULL);
}
else
{
cond_pop();
}
DBUG_RETURN(cond);
} }
/* /*
...@@ -9050,1356 +9051,8 @@ ha_ndbcluster::cond_push(const COND *cond) ...@@ -9050,1356 +9051,8 @@ ha_ndbcluster::cond_push(const COND *cond)
void void
ha_ndbcluster::cond_pop() ha_ndbcluster::cond_pop()
{ {
Ndb_cond_stack *ndb_cond_stack= m_cond_stack; if (m_cond)
if (ndb_cond_stack) m_cond->cond_pop();
{
m_cond_stack= ndb_cond_stack->next;
delete ndb_cond_stack;
}
}
/*
Clear the condition stack
*/
void
ha_ndbcluster::cond_clear()
{
DBUG_ENTER("cond_clear");
while (m_cond_stack)
cond_pop();
DBUG_VOID_RETURN;
}
/*
Serialize the item tree into a linked list represented by Ndb_cond
for fast generation of NbdScanFilter. Adds information such as
position of fields that is not directly available in the Item tree.
Also checks if condition is supported.
*/
void ndb_serialize_cond(const Item *item, void *arg)
{
Ndb_cond_traverse_context *context= (Ndb_cond_traverse_context *) arg;
DBUG_ENTER("ndb_serialize_cond");
// Check if we are skipping arguments to a function to be evaluated
if (context->skip)
{
DBUG_PRINT("info", ("Skiping argument %d", context->skip));
context->skip--;
switch (item->type()) {
case Item::FUNC_ITEM:
{
Item_func *func_item= (Item_func *) item;
context->skip+= func_item->argument_count();
break;
}
case Item::INT_ITEM:
case Item::REAL_ITEM:
case Item::STRING_ITEM:
case Item::VARBIN_ITEM:
case Item::DECIMAL_ITEM:
break;
default:
context->supported= FALSE;
break;
}
DBUG_VOID_RETURN;
}
if (context->supported)
{
Ndb_rewrite_context *rewrite_context2= context->rewrite_stack;
const Item_func *rewrite_func_item;
// Check if we are rewriting some unsupported function call
if (rewrite_context2 &&
(rewrite_func_item= rewrite_context2->func_item) &&
rewrite_context2->count++ == 0)
{
switch (rewrite_func_item->functype()) {
case Item_func::BETWEEN:
/*
Rewrite
<field>|<const> BETWEEN <const1>|<field1> AND <const2>|<field2>
to <field>|<const> > <const1>|<field1> AND
<field>|<const> < <const2>|<field2>
or actually in prefix format
BEGIN(AND) GT(<field>|<const>, <const1>|<field1>),
LT(<field>|<const>, <const2>|<field2>), END()
*/
case Item_func::IN_FUNC:
{
/*
Rewrite <field>|<const> IN(<const1>|<field1>, <const2>|<field2>,..)
to <field>|<const> = <const1>|<field1> OR
<field> = <const2>|<field2> ...
or actually in prefix format
BEGIN(OR) EQ(<field>|<const>, <const1><field1>),
EQ(<field>|<const>, <const2>|<field2>), ... END()
Each part of the disjunction is added for each call
to ndb_serialize_cond and end of rewrite statement
is wrapped in end of ndb_serialize_cond
*/
if (context->expecting(item->type()))
{
// This is the <field>|<const> item, save it in the rewrite context
rewrite_context2->left_hand_item= item;
if (item->type() == Item::FUNC_ITEM)
{
Item_func *func_item= (Item_func *) item;
if (func_item->functype() == Item_func::UNKNOWN_FUNC &&
func_item->const_item())
{
// Skip any arguments since we will evaluate function instead
DBUG_PRINT("info", ("Skip until end of arguments marker"));
context->skip= func_item->argument_count();
}
else
{
DBUG_PRINT("info", ("Found unsupported functional expression in BETWEEN|IN"));
context->supported= FALSE;
DBUG_VOID_RETURN;
}
}
}
else
{
// Non-supported BETWEEN|IN expression
DBUG_PRINT("info", ("Found unexpected item of type %u in BETWEEN|IN",
item->type()));
context->supported= FALSE;
DBUG_VOID_RETURN;
}
break;
}
default:
context->supported= FALSE;
break;
}
DBUG_VOID_RETURN;
}
else
{
Ndb_cond_stack *ndb_stack= context->stack_ptr;
Ndb_cond *prev_cond= context->cond_ptr;
Ndb_cond *curr_cond= context->cond_ptr= new Ndb_cond();
if (!ndb_stack->ndb_cond)
ndb_stack->ndb_cond= curr_cond;
curr_cond->prev= prev_cond;
if (prev_cond) prev_cond->next= curr_cond;
// Check if we are rewriting some unsupported function call
if (context->rewrite_stack)
{
Ndb_rewrite_context *rewrite_context= context->rewrite_stack;
const Item_func *func_item= rewrite_context->func_item;
switch (func_item->functype()) {
case Item_func::BETWEEN:
{
/*
Rewrite
<field>|<const> BETWEEN <const1>|<field1> AND <const2>|<field2>
to <field>|<const> > <const1>|<field1> AND
<field>|<const> < <const2>|<field2>
or actually in prefix format
BEGIN(AND) GT(<field>|<const>, <const1>|<field1>),
LT(<field>|<const>, <const2>|<field2>), END()
*/
if (rewrite_context->count == 2)
{
// Lower limit of BETWEEN
DBUG_PRINT("info", ("GE_FUNC"));
curr_cond->ndb_item= new Ndb_item(Item_func::GE_FUNC, 2);
}
else if (rewrite_context->count == 3)
{
// Upper limit of BETWEEN
DBUG_PRINT("info", ("LE_FUNC"));
curr_cond->ndb_item= new Ndb_item(Item_func::LE_FUNC, 2);
}
else
{
// Illegal BETWEEN expression
DBUG_PRINT("info", ("Illegal BETWEEN expression"));
context->supported= FALSE;
DBUG_VOID_RETURN;
}
break;
}
case Item_func::IN_FUNC:
{
/*
Rewrite <field>|<const> IN(<const1>|<field1>, <const2>|<field2>,..)
to <field>|<const> = <const1>|<field1> OR
<field> = <const2>|<field2> ...
or actually in prefix format
BEGIN(OR) EQ(<field>|<const>, <const1><field1>),
EQ(<field>|<const>, <const2>|<field2>), ... END()
Each part of the disjunction is added for each call
to ndb_serialize_cond and end of rewrite statement
is wrapped in end of ndb_serialize_cond
*/
DBUG_PRINT("info", ("EQ_FUNC"));
curr_cond->ndb_item= new Ndb_item(Item_func::EQ_FUNC, 2);
break;
}
default:
context->supported= FALSE;
}
// Handle left hand <field>|<const>
context->rewrite_stack= NULL; // Disable rewrite mode
context->expect_only(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
context->expect(Item::INT_ITEM);
context->expect(Item::STRING_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FUNC_ITEM);
ndb_serialize_cond(rewrite_context->left_hand_item, arg);
context->skip= 0; // Any FUNC_ITEM expression has already been parsed
context->rewrite_stack= rewrite_context; // Enable rewrite mode
if (!context->supported)
DBUG_VOID_RETURN;
prev_cond= context->cond_ptr;
curr_cond= context->cond_ptr= new Ndb_cond();
prev_cond->next= curr_cond;
}
// Check for end of AND/OR expression
if (!item)
{
// End marker for condition group
DBUG_PRINT("info", ("End of condition group"));
curr_cond->ndb_item= new Ndb_item(NDB_END_COND);
}
else
{
switch (item->type()) {
case Item::FIELD_ITEM:
{
Item_field *field_item= (Item_field *) item;
Field *field= field_item->field;
enum_field_types type= field->type();
/*
Check that the field is part of the table of the handler
instance and that we expect a field with of this result type.
*/
if (context->table->s == field->table->s)
{
const NDBTAB *tab= (const NDBTAB *) context->ndb_table;
DBUG_PRINT("info", ("FIELD_ITEM"));
DBUG_PRINT("info", ("table %s", tab->getName()));
DBUG_PRINT("info", ("column %s", field->field_name));
DBUG_PRINT("info", ("type %d", field->type()));
DBUG_PRINT("info", ("result type %d", field->result_type()));
// Check that we are expecting a field and with the correct
// result type
if (context->expecting(Item::FIELD_ITEM) &&
context->expecting_field_type(field->type()) &&
(context->expecting_field_result(field->result_type()) ||
// Date and year can be written as string or int
((type == MYSQL_TYPE_TIME ||
type == MYSQL_TYPE_DATE ||
type == MYSQL_TYPE_YEAR ||
type == MYSQL_TYPE_DATETIME)
? (context->expecting_field_result(STRING_RESULT) ||
context->expecting_field_result(INT_RESULT))
: TRUE)) &&
// Bit fields no yet supported in scan filter
type != MYSQL_TYPE_BIT &&
// No BLOB support in scan filter
type != MYSQL_TYPE_TINY_BLOB &&
type != MYSQL_TYPE_MEDIUM_BLOB &&
type != MYSQL_TYPE_LONG_BLOB &&
type != MYSQL_TYPE_BLOB)
{
const NDBCOL *col= tab->getColumn(field->field_name);
DBUG_ASSERT(col);
curr_cond->ndb_item= new Ndb_item(field, col->getColumnNo());
context->dont_expect(Item::FIELD_ITEM);
context->expect_no_field_result();
if (! context->expecting_nothing())
{
// We have not seen second argument yet
if (type == MYSQL_TYPE_TIME ||
type == MYSQL_TYPE_DATE ||
type == MYSQL_TYPE_YEAR ||
type == MYSQL_TYPE_DATETIME)
{
context->expect_only(Item::STRING_ITEM);
context->expect(Item::INT_ITEM);
}
else
switch (field->result_type()) {
case STRING_RESULT:
// Expect char string or binary string
context->expect_only(Item::STRING_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect_collation(field_item->collation.collation);
break;
case REAL_RESULT:
context->expect_only(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::INT_ITEM);
break;
case INT_RESULT:
context->expect_only(Item::INT_ITEM);
context->expect(Item::VARBIN_ITEM);
break;
case DECIMAL_RESULT:
context->expect_only(Item::DECIMAL_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::INT_ITEM);
break;
default:
break;
}
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
// Check that field and string constant collations are the same
if ((field->result_type() == STRING_RESULT) &&
!context->expecting_collation(item->collation.collation)
&& type != MYSQL_TYPE_TIME
&& type != MYSQL_TYPE_DATE
&& type != MYSQL_TYPE_YEAR
&& type != MYSQL_TYPE_DATETIME)
{
DBUG_PRINT("info", ("Found non-matching collation %s",
item->collation.collation->name));
context->supported= FALSE;
}
}
break;
}
else
{
DBUG_PRINT("info", ("Was not expecting field of type %u(%u)",
field->result_type(), type));
context->supported= FALSE;
}
}
else
{
DBUG_PRINT("info", ("Was not expecting field from table %s (%s)",
context->table->s->table_name.str,
field->table->s->table_name.str));
context->supported= FALSE;
}
break;
}
case Item::FUNC_ITEM:
{
Item_func *func_item= (Item_func *) item;
// Check that we expect a function or functional expression here
if (context->expecting(Item::FUNC_ITEM) ||
func_item->functype() == Item_func::UNKNOWN_FUNC)
context->expect_nothing();
else
{
// Did not expect function here
context->supported= FALSE;
break;
}
switch (func_item->functype()) {
case Item_func::EQ_FUNC:
{
DBUG_PRINT("info", ("EQ_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::NE_FUNC:
{
DBUG_PRINT("info", ("NE_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::LT_FUNC:
{
DBUG_PRINT("info", ("LT_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::LE_FUNC:
{
DBUG_PRINT("info", ("LE_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::GE_FUNC:
{
DBUG_PRINT("info", ("GE_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::GT_FUNC:
{
DBUG_PRINT("info", ("GT_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::REAL_ITEM);
context->expect(Item::DECIMAL_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::LIKE_FUNC:
{
DBUG_PRINT("info", ("LIKE_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::STRING_ITEM);
context->expect(Item::FIELD_ITEM);
context->expect_only_field_type(MYSQL_TYPE_STRING);
context->expect_field_type(MYSQL_TYPE_VAR_STRING);
context->expect_field_type(MYSQL_TYPE_VARCHAR);
context->expect_field_result(STRING_RESULT);
context->expect(Item::FUNC_ITEM);
break;
}
case Item_func::ISNULL_FUNC:
{
DBUG_PRINT("info", ("ISNULL_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::ISNOTNULL_FUNC:
{
DBUG_PRINT("info", ("ISNOTNULL_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::FIELD_ITEM);
context->expect_field_result(STRING_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(INT_RESULT);
context->expect_field_result(DECIMAL_RESULT);
break;
}
case Item_func::NOT_FUNC:
{
DBUG_PRINT("info", ("NOT_FUNC"));
curr_cond->ndb_item= new Ndb_item(func_item->functype(),
func_item);
context->expect(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
break;
}
case Item_func::BETWEEN:
{
DBUG_PRINT("info", ("BETWEEN, rewriting using AND"));
Item_func_between *between_func= (Item_func_between *) func_item;
Ndb_rewrite_context *rewrite_context=
new Ndb_rewrite_context(func_item);
rewrite_context->next= context->rewrite_stack;
context->rewrite_stack= rewrite_context;
if (between_func->negated)
{
DBUG_PRINT("info", ("NOT_FUNC"));
curr_cond->ndb_item= new Ndb_item(Item_func::NOT_FUNC, 1);
prev_cond= curr_cond;
curr_cond= context->cond_ptr= new Ndb_cond();
curr_cond->prev= prev_cond;
prev_cond->next= curr_cond;
}
DBUG_PRINT("info", ("COND_AND_FUNC"));
curr_cond->ndb_item=
new Ndb_item(Item_func::COND_AND_FUNC,
func_item->argument_count() - 1);
context->expect_only(Item::FIELD_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::STRING_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FUNC_ITEM);
break;
}
case Item_func::IN_FUNC:
{
DBUG_PRINT("info", ("IN_FUNC, rewriting using OR"));
Item_func_in *in_func= (Item_func_in *) func_item;
Ndb_rewrite_context *rewrite_context=
new Ndb_rewrite_context(func_item);
rewrite_context->next= context->rewrite_stack;
context->rewrite_stack= rewrite_context;
if (in_func->negated)
{
DBUG_PRINT("info", ("NOT_FUNC"));
curr_cond->ndb_item= new Ndb_item(Item_func::NOT_FUNC, 1);
prev_cond= curr_cond;
curr_cond= context->cond_ptr= new Ndb_cond();
curr_cond->prev= prev_cond;
prev_cond->next= curr_cond;
}
DBUG_PRINT("info", ("COND_OR_FUNC"));
curr_cond->ndb_item= new Ndb_item(Item_func::COND_OR_FUNC,
func_item->argument_count() - 1);
context->expect_only(Item::FIELD_ITEM);
context->expect(Item::INT_ITEM);
context->expect(Item::STRING_ITEM);
context->expect(Item::VARBIN_ITEM);
context->expect(Item::FUNC_ITEM);
break;
}
case Item_func::UNKNOWN_FUNC:
{
DBUG_PRINT("info", ("UNKNOWN_FUNC %s",
func_item->const_item()?"const":""));
DBUG_PRINT("info", ("result type %d", func_item->result_type()));
if (func_item->const_item())
{
switch (func_item->result_type()) {
case STRING_RESULT:
{
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::STRING_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(STRING_RESULT);
context->expect_collation(func_item->collation.collation);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
// Check that string result have correct collation
if (!context->expecting_collation(item->collation.collation))
{
DBUG_PRINT("info", ("Found non-matching collation %s",
item->collation.collation->name));
context->supported= FALSE;
}
}
// Skip any arguments since we will evaluate function instead
DBUG_PRINT("info", ("Skip until end of arguments marker"));
context->skip= func_item->argument_count();
break;
}
case REAL_RESULT:
{
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::REAL_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(REAL_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
// Skip any arguments since we will evaluate function instead
DBUG_PRINT("info", ("Skip until end of arguments marker"));
context->skip= func_item->argument_count();
break;
}
case INT_RESULT:
{
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::INT_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(INT_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
// Skip any arguments since we will evaluate function instead
DBUG_PRINT("info", ("Skip until end of arguments marker"));
context->skip= func_item->argument_count();
break;
}
case DECIMAL_RESULT:
{
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::DECIMAL_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(DECIMAL_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
// Skip any arguments since we will evaluate function instead
DBUG_PRINT("info", ("Skip until end of arguments marker"));
context->skip= func_item->argument_count();
break;
}
default:
break;
}
}
else
// Function does not return constant expression
context->supported= FALSE;
break;
}
default:
{
DBUG_PRINT("info", ("Found func_item of type %d",
func_item->functype()));
context->supported= FALSE;
}
}
break;
}
case Item::STRING_ITEM:
DBUG_PRINT("info", ("STRING_ITEM"));
if (context->expecting(Item::STRING_ITEM))
{
#ifndef DBUG_OFF
char buff[256];
String str(buff,(uint32) sizeof(buff), system_charset_info);
str.length(0);
Item_string *string_item= (Item_string *) item;
DBUG_PRINT("info", ("value \"%s\"",
string_item->val_str(&str)->ptr()));
#endif
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::STRING_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(STRING_RESULT);
context->expect_collation(item->collation.collation);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
// Check that we are comparing with a field with same collation
if (!context->expecting_collation(item->collation.collation))
{
DBUG_PRINT("info", ("Found non-matching collation %s",
item->collation.collation->name));
context->supported= FALSE;
}
}
}
else
context->supported= FALSE;
break;
case Item::INT_ITEM:
DBUG_PRINT("info", ("INT_ITEM"));
if (context->expecting(Item::INT_ITEM))
{
DBUG_PRINT("info", ("value %ld",
(long) ((Item_int*) item)->value));
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::INT_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(INT_RESULT);
context->expect_field_result(REAL_RESULT);
context->expect_field_result(DECIMAL_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
}
else
context->supported= FALSE;
break;
case Item::REAL_ITEM:
DBUG_PRINT("info", ("REAL_ITEM"));
if (context->expecting(Item::REAL_ITEM))
{
DBUG_PRINT("info", ("value %f", ((Item_float*) item)->value));
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::REAL_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(REAL_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
}
else
context->supported= FALSE;
break;
case Item::VARBIN_ITEM:
DBUG_PRINT("info", ("VARBIN_ITEM"));
if (context->expecting(Item::VARBIN_ITEM))
{
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::VARBIN_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(STRING_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
}
else
context->supported= FALSE;
break;
case Item::DECIMAL_ITEM:
DBUG_PRINT("info", ("DECIMAL_ITEM"));
if (context->expecting(Item::DECIMAL_ITEM))
{
DBUG_PRINT("info", ("value %f",
((Item_decimal*) item)->val_real()));
NDB_ITEM_QUALIFICATION q;
q.value_type= Item::DECIMAL_ITEM;
curr_cond->ndb_item= new Ndb_item(NDB_VALUE, q, item);
if (! context->expecting_no_field_result())
{
// We have not seen the field argument yet
context->expect_only(Item::FIELD_ITEM);
context->expect_only_field_result(REAL_RESULT);
context->expect_field_result(DECIMAL_RESULT);
}
else
{
// Expect another logical expression
context->expect_only(Item::FUNC_ITEM);
context->expect(Item::COND_ITEM);
}
}
else
context->supported= FALSE;
break;
case Item::COND_ITEM:
{
Item_cond *cond_item= (Item_cond *) item;
if (context->expecting(Item::COND_ITEM))
{
switch (cond_item->functype()) {
case Item_func::COND_AND_FUNC:
DBUG_PRINT("info", ("COND_AND_FUNC"));
curr_cond->ndb_item= new Ndb_item(cond_item->functype(),
cond_item);
break;
case Item_func::COND_OR_FUNC:
DBUG_PRINT("info", ("COND_OR_FUNC"));
curr_cond->ndb_item= new Ndb_item(cond_item->functype(),
cond_item);
break;
default:
DBUG_PRINT("info", ("COND_ITEM %d", cond_item->functype()));
context->supported= FALSE;
break;
}
}
else
{
/* Did not expect condition */
context->supported= FALSE;
}
break;
}
default:
{
DBUG_PRINT("info", ("Found item of type %d", item->type()));
context->supported= FALSE;
}
}
}
if (context->supported && context->rewrite_stack)
{
Ndb_rewrite_context *rewrite_context= context->rewrite_stack;
if (rewrite_context->count ==
rewrite_context->func_item->argument_count())
{
// Rewrite is done, wrap an END() at the en
DBUG_PRINT("info", ("End of condition group"));
prev_cond= curr_cond;
curr_cond= context->cond_ptr= new Ndb_cond();
curr_cond->prev= prev_cond;
prev_cond->next= curr_cond;
curr_cond->ndb_item= new Ndb_item(NDB_END_COND);
// Pop rewrite stack
context->rewrite_stack= rewrite_context->next;
rewrite_context->next= NULL;
delete(rewrite_context);
}
}
}
}
DBUG_VOID_RETURN;
}
bool
ha_ndbcluster::serialize_cond(const COND *cond, Ndb_cond_stack *ndb_cond)
{
DBUG_ENTER("serialize_cond");
Item *item= (Item *) cond;
Ndb_cond_traverse_context context(table, (void *)m_table, ndb_cond);
// Expect a logical expression
context.expect(Item::FUNC_ITEM);
context.expect(Item::COND_ITEM);
item->traverse_cond(&ndb_serialize_cond, (void *) &context, Item::PREFIX);
DBUG_PRINT("info", ("The pushed condition is %ssupported", (context.supported)?"":"not "));
DBUG_RETURN(context.supported);
}
int
ha_ndbcluster::build_scan_filter_predicate(Ndb_cond * &cond,
NdbScanFilter *filter,
bool negated)
{
DBUG_ENTER("build_scan_filter_predicate");
switch (cond->ndb_item->type) {
case NDB_FUNCTION:
{
if (!cond->next)
break;
Ndb_item *a= cond->next->ndb_item;
Ndb_item *b, *field, *value= NULL;
switch (cond->ndb_item->argument_count()) {
case 1:
field= (a->type == NDB_FIELD)? a : NULL;
break;
case 2:
if (!cond->next->next)
{
field= NULL;
break;
}
b= cond->next->next->ndb_item;
value= ((a->type == NDB_VALUE) ? a :
(b->type == NDB_VALUE) ? b :
NULL);
field= ((a->type == NDB_FIELD) ? a :
(b->type == NDB_FIELD) ? b :
NULL);
break;
default:
field= NULL; //Keep compiler happy
DBUG_ASSERT(0);
break;
}
switch ((negated) ?
Ndb_item::negate(cond->ndb_item->qualification.function_type)
: cond->ndb_item->qualification.function_type) {
case NDB_EQ_FUNC:
{
if (!value || !field) break;
// Save value in right format for the field type
value->save_in_field(field);
DBUG_PRINT("info", ("Generating EQ filter"));
if (filter->cmp(NdbScanFilter::COND_EQ,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_NE_FUNC:
{
if (!value || !field) break;
// Save value in right format for the field type
value->save_in_field(field);
DBUG_PRINT("info", ("Generating NE filter"));
if (filter->cmp(NdbScanFilter::COND_NE,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_LT_FUNC:
{
if (!value || !field) break;
// Save value in right format for the field type
value->save_in_field(field);
if (a == field)
{
DBUG_PRINT("info", ("Generating LT filter"));
if (filter->cmp(NdbScanFilter::COND_LT,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
else
{
DBUG_PRINT("info", ("Generating GT filter"));
if (filter->cmp(NdbScanFilter::COND_GT,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_LE_FUNC:
{
if (!value || !field) break;
// Save value in right format for the field type
value->save_in_field(field);
if (a == field)
{
DBUG_PRINT("info", ("Generating LE filter"));
if (filter->cmp(NdbScanFilter::COND_LE,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
else
{
DBUG_PRINT("info", ("Generating GE filter"));
if (filter->cmp(NdbScanFilter::COND_GE,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_GE_FUNC:
{
if (!value || !field) break;
// Save value in right format for the field type
value->save_in_field(field);
if (a == field)
{
DBUG_PRINT("info", ("Generating GE filter"));
if (filter->cmp(NdbScanFilter::COND_GE,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
else
{
DBUG_PRINT("info", ("Generating LE filter"));
if (filter->cmp(NdbScanFilter::COND_LE,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_GT_FUNC:
{
if (!value || !field) break;
// Save value in right format for the field type
value->save_in_field(field);
if (a == field)
{
DBUG_PRINT("info", ("Generating GT filter"));
if (filter->cmp(NdbScanFilter::COND_GT,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
else
{
DBUG_PRINT("info", ("Generating LT filter"));
if (filter->cmp(NdbScanFilter::COND_LT,
field->get_field_no(),
field->get_val(),
field->pack_length()) == -1)
DBUG_RETURN(1);
}
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_LIKE_FUNC:
{
if (!value || !field) break;
if ((value->qualification.value_type != Item::STRING_ITEM) &&
(value->qualification.value_type != Item::VARBIN_ITEM))
break;
// Save value in right format for the field type
value->save_in_field(field);
DBUG_PRINT("info", ("Generating LIKE filter: like(%d,%s,%d)",
field->get_field_no(), value->get_val(),
value->pack_length()));
if (filter->cmp(NdbScanFilter::COND_LIKE,
field->get_field_no(),
value->get_val(),
value->pack_length()) == -1)
DBUG_RETURN(1);
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_NOTLIKE_FUNC:
{
if (!value || !field) break;
if ((value->qualification.value_type != Item::STRING_ITEM) &&
(value->qualification.value_type != Item::VARBIN_ITEM))
break;
// Save value in right format for the field type
value->save_in_field(field);
DBUG_PRINT("info", ("Generating NOTLIKE filter: notlike(%d,%s,%d)",
field->get_field_no(), value->get_val(),
value->pack_length()));
if (filter->cmp(NdbScanFilter::COND_NOT_LIKE,
field->get_field_no(),
value->get_val(),
value->pack_length()) == -1)
DBUG_RETURN(1);
cond= cond->next->next->next;
DBUG_RETURN(0);
}
case NDB_ISNULL_FUNC:
if (!field)
break;
DBUG_PRINT("info", ("Generating ISNULL filter"));
if (filter->isnull(field->get_field_no()) == -1)
DBUG_RETURN(1);
cond= cond->next->next;
DBUG_RETURN(0);
case NDB_ISNOTNULL_FUNC:
{
if (!field)
break;
DBUG_PRINT("info", ("Generating ISNOTNULL filter"));
if (filter->isnotnull(field->get_field_no()) == -1)
DBUG_RETURN(1);
cond= cond->next->next;
DBUG_RETURN(0);
}
default:
break;
}
break;
}
default:
break;
}
DBUG_PRINT("info", ("Found illegal condition"));
DBUG_RETURN(1);
}
int
ha_ndbcluster::build_scan_filter_group(Ndb_cond* &cond, NdbScanFilter *filter)
{
uint level=0;
bool negated= FALSE;
DBUG_ENTER("build_scan_filter_group");
do
{
if (!cond)
DBUG_RETURN(1);
switch (cond->ndb_item->type) {
case NDB_FUNCTION:
{
switch (cond->ndb_item->qualification.function_type) {
case NDB_COND_AND_FUNC:
{
level++;
DBUG_PRINT("info", ("Generating %s group %u", (negated)?"NAND":"AND",
level));
if ((negated) ? filter->begin(NdbScanFilter::NAND)
: filter->begin(NdbScanFilter::AND) == -1)
DBUG_RETURN(1);
negated= FALSE;
cond= cond->next;
break;
}
case NDB_COND_OR_FUNC:
{
level++;
DBUG_PRINT("info", ("Generating %s group %u", (negated)?"NOR":"OR",
level));
if ((negated) ? filter->begin(NdbScanFilter::NOR)
: filter->begin(NdbScanFilter::OR) == -1)
DBUG_RETURN(1);
negated= FALSE;
cond= cond->next;
break;
}
case NDB_NOT_FUNC:
{
DBUG_PRINT("info", ("Generating negated query"));
cond= cond->next;
negated= TRUE;
break;
}
default:
if (build_scan_filter_predicate(cond, filter, negated))
DBUG_RETURN(1);
negated= FALSE;
break;
}
break;
}
case NDB_END_COND:
DBUG_PRINT("info", ("End of group %u", level));
level--;
if (cond) cond= cond->next;
if (filter->end() == -1)
DBUG_RETURN(1);
if (!negated)
break;
// else fall through (NOT END is an illegal condition)
default:
{
DBUG_PRINT("info", ("Illegal scan filter"));
}
}
} while (level > 0 || negated);
DBUG_RETURN(0);
}
int
ha_ndbcluster::build_scan_filter(Ndb_cond * &cond, NdbScanFilter *filter)
{
bool simple_cond= TRUE;
DBUG_ENTER("build_scan_filter");
switch (cond->ndb_item->type) {
case NDB_FUNCTION:
switch (cond->ndb_item->qualification.function_type) {
case NDB_COND_AND_FUNC:
case NDB_COND_OR_FUNC:
simple_cond= FALSE;
break;
default:
break;
}
break;
default:
break;
}
if (simple_cond && filter->begin() == -1)
DBUG_RETURN(1);
if (build_scan_filter_group(cond, filter))
DBUG_RETURN(1);
if (simple_cond && filter->end() == -1)
DBUG_RETURN(1);
DBUG_RETURN(0);
}
int
ha_ndbcluster::generate_scan_filter(Ndb_cond_stack *ndb_cond_stack,
NdbScanOperation *op)
{
DBUG_ENTER("generate_scan_filter");
if (ndb_cond_stack)
{
NdbScanFilter filter(op);
DBUG_RETURN(generate_scan_filter_from_cond(ndb_cond_stack, filter));
}
else
{
DBUG_PRINT("info", ("Empty stack"));
}
DBUG_RETURN(0);
}
int
ha_ndbcluster::generate_scan_filter_from_cond(Ndb_cond_stack *ndb_cond_stack,
NdbScanFilter& filter)
{
bool multiple_cond= FALSE;
DBUG_ENTER("generate_scan_filter_from_cond");
// Wrap an AND group around multiple conditions
if (ndb_cond_stack->next)
{
multiple_cond= TRUE;
if (filter.begin() == -1)
DBUG_RETURN(1);
}
for (Ndb_cond_stack *stack= ndb_cond_stack;
(stack);
stack= stack->next)
{
Ndb_cond *cond= stack->ndb_cond;
if (build_scan_filter(cond, &filter))
{
DBUG_PRINT("info", ("build_scan_filter failed"));
DBUG_RETURN(1);
}
}
if (multiple_cond && filter.end() == -1)
DBUG_RETURN(1);
DBUG_RETURN(0);
}
int ha_ndbcluster::generate_scan_filter_from_key(NdbScanOperation *op,
const KEY* key_info,
const byte *key,
uint key_len,
byte *buf)
{
KEY_PART_INFO* key_part= key_info->key_part;
KEY_PART_INFO* end= key_part+key_info->key_parts;
NdbScanFilter filter(op);
int res;
DBUG_ENTER("generate_scan_filter_from_key");
filter.begin(NdbScanFilter::AND);
for (; key_part != end; key_part++)
{
Field* field= key_part->field;
uint32 pack_len= field->pack_length();
const byte* ptr= key;
DBUG_PRINT("info", ("Filtering value for %s", field->field_name));
DBUG_DUMP("key", (char*)ptr, pack_len);
if (key_part->null_bit)
{
DBUG_PRINT("info", ("Generating ISNULL filter"));
if (filter.isnull(key_part->fieldnr-1) == -1)
DBUG_RETURN(1);
}
else
{
DBUG_PRINT("info", ("Generating EQ filter"));
if (filter.cmp(NdbScanFilter::COND_EQ,
key_part->fieldnr-1,
ptr,
pack_len) == -1)
DBUG_RETURN(1);
}
key += key_part->store_length;
}
// Add any pushed condition
if (m_cond_stack &&
(res= generate_scan_filter_from_cond(m_cond_stack, filter)))
DBUG_RETURN(res);
if (filter.end() == -1)
DBUG_RETURN(1);
DBUG_RETURN(0);
} }
......
...@@ -37,11 +37,11 @@ class NdbOperation; // Forward declaration ...@@ -37,11 +37,11 @@ class NdbOperation; // Forward declaration
class NdbTransaction; // Forward declaration class NdbTransaction; // Forward declaration
class NdbRecAttr; // Forward declaration class NdbRecAttr; // Forward declaration
class NdbScanOperation; class NdbScanOperation;
class NdbScanFilter;
class NdbIndexScanOperation; class NdbIndexScanOperation;
class NdbBlob; class NdbBlob;
class NdbIndexStat; class NdbIndexStat;
class NdbEventOperation; class NdbEventOperation;
class ha_ndbcluster_cond;
// connectstring to cluster if given by mysqld // connectstring to cluster if given by mysqld
extern const char *ndbcluster_connectstring; extern const char *ndbcluster_connectstring;
...@@ -161,424 +161,6 @@ struct Ndb_tuple_id_range_guard { ...@@ -161,424 +161,6 @@ struct Ndb_tuple_id_range_guard {
#define NSF_NO_BINLOG 4 /* table should not be binlogged */ #define NSF_NO_BINLOG 4 /* table should not be binlogged */
#endif #endif
typedef enum ndb_item_type {
NDB_VALUE = 0, // Qualified more with Item::Type
NDB_FIELD = 1, // Qualified from table definition
NDB_FUNCTION = 2,// Qualified from Item_func::Functype
NDB_END_COND = 3 // End marker for condition group
} NDB_ITEM_TYPE;
typedef enum ndb_func_type {
NDB_EQ_FUNC = 0,
NDB_NE_FUNC = 1,
NDB_LT_FUNC = 2,
NDB_LE_FUNC = 3,
NDB_GT_FUNC = 4,
NDB_GE_FUNC = 5,
NDB_ISNULL_FUNC = 6,
NDB_ISNOTNULL_FUNC = 7,
NDB_LIKE_FUNC = 8,
NDB_NOTLIKE_FUNC = 9,
NDB_NOT_FUNC = 10,
NDB_UNKNOWN_FUNC = 11,
NDB_COND_AND_FUNC = 12,
NDB_COND_OR_FUNC = 13,
NDB_UNSUPPORTED_FUNC = 14
} NDB_FUNC_TYPE;
typedef union ndb_item_qualification {
Item::Type value_type;
enum_field_types field_type; // Instead of Item::FIELD_ITEM
NDB_FUNC_TYPE function_type; // Instead of Item::FUNC_ITEM
} NDB_ITEM_QUALIFICATION;
typedef struct ndb_item_field_value {
Field* field;
int column_no;
} NDB_ITEM_FIELD_VALUE;
typedef union ndb_item_value {
const Item *item;
NDB_ITEM_FIELD_VALUE *field_value;
uint arg_count;
} NDB_ITEM_VALUE;
struct negated_function_mapping
{
NDB_FUNC_TYPE pos_fun;
NDB_FUNC_TYPE neg_fun;
};
/*
Define what functions can be negated in condition pushdown.
Note, these HAVE to be in the same order as in definition enum
*/
static const negated_function_mapping neg_map[]=
{
{NDB_EQ_FUNC, NDB_NE_FUNC},
{NDB_NE_FUNC, NDB_EQ_FUNC},
{NDB_LT_FUNC, NDB_GE_FUNC},
{NDB_LE_FUNC, NDB_GT_FUNC},
{NDB_GT_FUNC, NDB_LE_FUNC},
{NDB_GE_FUNC, NDB_LT_FUNC},
{NDB_ISNULL_FUNC, NDB_ISNOTNULL_FUNC},
{NDB_ISNOTNULL_FUNC, NDB_ISNULL_FUNC},
{NDB_LIKE_FUNC, NDB_NOTLIKE_FUNC},
{NDB_NOTLIKE_FUNC, NDB_LIKE_FUNC},
{NDB_NOT_FUNC, NDB_UNSUPPORTED_FUNC},
{NDB_UNKNOWN_FUNC, NDB_UNSUPPORTED_FUNC},
{NDB_COND_AND_FUNC, NDB_UNSUPPORTED_FUNC},
{NDB_COND_OR_FUNC, NDB_UNSUPPORTED_FUNC},
{NDB_UNSUPPORTED_FUNC, NDB_UNSUPPORTED_FUNC}
};
/*
This class is the construction element for serialization of Item tree
in condition pushdown.
An instance of Ndb_Item represents a constant, table field reference,
unary or binary comparison predicate, and start/end of AND/OR.
Instances of Ndb_Item are stored in a linked list implemented by Ndb_cond
class.
The order of elements produced by Ndb_cond::next corresponds to
breadth-first traversal of the Item (i.e. expression) tree in prefix order.
AND and OR have arbitrary arity, so the end of AND/OR group is marked with
Ndb_item with type == NDB_END_COND.
NOT items represent negated conditions and generate NAND/NOR groups.
*/
class Ndb_item {
public:
Ndb_item(NDB_ITEM_TYPE item_type) : type(item_type) {};
Ndb_item(NDB_ITEM_TYPE item_type,
NDB_ITEM_QUALIFICATION item_qualification,
const Item *item_value)
: type(item_type), qualification(item_qualification)
{
switch(item_type) {
case(NDB_VALUE):
value.item= item_value;
break;
case(NDB_FIELD): {
NDB_ITEM_FIELD_VALUE *field_value= new NDB_ITEM_FIELD_VALUE();
Item_field *field_item= (Item_field *) item_value;
field_value->field= field_item->field;
field_value->column_no= -1; // Will be fetched at scan filter generation
value.field_value= field_value;
break;
}
case(NDB_FUNCTION):
value.item= item_value;
value.arg_count= ((Item_func *) item_value)->argument_count();
break;
case(NDB_END_COND):
break;
}
};
Ndb_item(Field *field, int column_no) : type(NDB_FIELD)
{
NDB_ITEM_FIELD_VALUE *field_value= new NDB_ITEM_FIELD_VALUE();
qualification.field_type= field->type();
field_value->field= field;
field_value->column_no= column_no;
value.field_value= field_value;
};
Ndb_item(Item_func::Functype func_type, const Item *item_value)
: type(NDB_FUNCTION)
{
qualification.function_type= item_func_to_ndb_func(func_type);
value.item= item_value;
value.arg_count= ((Item_func *) item_value)->argument_count();
};
Ndb_item(Item_func::Functype func_type, uint no_args)
: type(NDB_FUNCTION)
{
qualification.function_type= item_func_to_ndb_func(func_type);
value.arg_count= no_args;
};
~Ndb_item()
{
if (type == NDB_FIELD)
{
delete value.field_value;
value.field_value= NULL;
}
};
uint32 pack_length()
{
switch(type) {
case(NDB_VALUE):
if(qualification.value_type == Item::STRING_ITEM)
return value.item->str_value.length();
break;
case(NDB_FIELD):
return value.field_value->field->pack_length();
default:
break;
}
return 0;
};
Field * get_field() { return value.field_value->field; };
int get_field_no() { return value.field_value->column_no; };
int argument_count()
{
return value.arg_count;
};
const char* get_val()
{
switch(type) {
case(NDB_VALUE):
if(qualification.value_type == Item::STRING_ITEM)
return value.item->str_value.ptr();
break;
case(NDB_FIELD):
return value.field_value->field->ptr;
default:
break;
}
return NULL;
};
void save_in_field(Ndb_item *field_item)
{
Field *field = field_item->value.field_value->field;
const Item *item= value.item;
if (item && field)
{
my_bitmap_map *old_map=
dbug_tmp_use_all_columns(field->table, field->table->write_set);
((Item *)item)->save_in_field(field, FALSE);
dbug_tmp_restore_column_map(field->table->write_set, old_map);
}
};
static NDB_FUNC_TYPE item_func_to_ndb_func(Item_func::Functype fun)
{
switch (fun) {
case (Item_func::EQ_FUNC): { return NDB_EQ_FUNC; }
case (Item_func::NE_FUNC): { return NDB_NE_FUNC; }
case (Item_func::LT_FUNC): { return NDB_LT_FUNC; }
case (Item_func::LE_FUNC): { return NDB_LE_FUNC; }
case (Item_func::GT_FUNC): { return NDB_GT_FUNC; }
case (Item_func::GE_FUNC): { return NDB_GE_FUNC; }
case (Item_func::ISNULL_FUNC): { return NDB_ISNULL_FUNC; }
case (Item_func::ISNOTNULL_FUNC): { return NDB_ISNOTNULL_FUNC; }
case (Item_func::LIKE_FUNC): { return NDB_LIKE_FUNC; }
case (Item_func::NOT_FUNC): { return NDB_NOT_FUNC; }
case (Item_func::UNKNOWN_FUNC): { return NDB_UNKNOWN_FUNC; }
case (Item_func::COND_AND_FUNC): { return NDB_COND_AND_FUNC; }
case (Item_func::COND_OR_FUNC): { return NDB_COND_OR_FUNC; }
default: { return NDB_UNSUPPORTED_FUNC; }
}
};
static NDB_FUNC_TYPE negate(NDB_FUNC_TYPE fun)
{
uint i= (uint) fun;
DBUG_ASSERT(fun == neg_map[i].pos_fun);
return neg_map[i].neg_fun;
};
NDB_ITEM_TYPE type;
NDB_ITEM_QUALIFICATION qualification;
private:
NDB_ITEM_VALUE value;
};
/*
This class implements a linked list used for storing a
serialization of the Item tree for condition pushdown.
*/
class Ndb_cond
{
public:
Ndb_cond() : ndb_item(NULL), next(NULL), prev(NULL) {};
~Ndb_cond()
{
if (ndb_item) delete ndb_item;
ndb_item= NULL;
if (next) delete next;
next= prev= NULL;
};
Ndb_item *ndb_item;
Ndb_cond *next;
Ndb_cond *prev;
};
/*
This class implements a stack for storing several conditions
for pushdown (represented as serialized Item trees using Ndb_cond).
The current implementation only pushes one condition, but is
prepared for handling several (C1 AND C2 ...) if the logic for
pushing conditions is extended in sql_select.
*/
class Ndb_cond_stack
{
public:
Ndb_cond_stack() : ndb_cond(NULL), next(NULL) {};
~Ndb_cond_stack()
{
if (ndb_cond) delete ndb_cond;
ndb_cond= NULL;
if (next) delete next;
next= NULL;
};
Ndb_cond *ndb_cond;
Ndb_cond_stack *next;
};
class Ndb_rewrite_context
{
public:
Ndb_rewrite_context(Item_func *func)
: func_item(func), left_hand_item(NULL), count(0) {};
~Ndb_rewrite_context()
{
if (next) delete next;
}
const Item_func *func_item;
const Item *left_hand_item;
uint count;
Ndb_rewrite_context *next;
};
/*
This class is used for storing the context when traversing
the Item tree. It stores a reference to the table the condition
is defined on, the serialized representation being generated,
if the condition found is supported, and information what is
expected next in the tree inorder for the condition to be supported.
*/
class Ndb_cond_traverse_context
{
public:
Ndb_cond_traverse_context(TABLE *tab, void* ndb_tab, Ndb_cond_stack* stack)
: table(tab), ndb_table(ndb_tab),
supported(TRUE), stack_ptr(stack), cond_ptr(NULL),
skip(0), collation(NULL), rewrite_stack(NULL)
{
// Allocate type checking bitmaps
bitmap_init(&expect_mask, 0, 512, FALSE);
bitmap_init(&expect_field_type_mask, 0, 512, FALSE);
bitmap_init(&expect_field_result_mask, 0, 512, FALSE);
if (stack)
cond_ptr= stack->ndb_cond;
};
~Ndb_cond_traverse_context()
{
bitmap_free(&expect_mask);
bitmap_free(&expect_field_type_mask);
bitmap_free(&expect_field_result_mask);
if (rewrite_stack) delete rewrite_stack;
}
void expect(Item::Type type)
{
bitmap_set_bit(&expect_mask, (uint) type);
if (type == Item::FIELD_ITEM) expect_all_field_types();
};
void dont_expect(Item::Type type)
{
bitmap_clear_bit(&expect_mask, (uint) type);
};
bool expecting(Item::Type type)
{
return bitmap_is_set(&expect_mask, (uint) type);
};
void expect_nothing()
{
bitmap_clear_all(&expect_mask);
};
bool expecting_nothing()
{
return bitmap_is_clear_all(&expect_mask);
}
void expect_only(Item::Type type)
{
expect_nothing();
expect(type);
};
void expect_field_type(enum_field_types type)
{
bitmap_set_bit(&expect_field_type_mask, (uint) type);
};
void expect_all_field_types()
{
bitmap_set_all(&expect_field_type_mask);
};
bool expecting_field_type(enum_field_types type)
{
return bitmap_is_set(&expect_field_type_mask, (uint) type);
};
void expect_no_field_type()
{
bitmap_clear_all(&expect_field_type_mask);
};
bool expecting_no_field_type()
{
return bitmap_is_clear_all(&expect_field_type_mask);
}
void expect_only_field_type(enum_field_types result)
{
expect_no_field_type();
expect_field_type(result);
};
void expect_field_result(Item_result result)
{
bitmap_set_bit(&expect_field_result_mask, (uint) result);
};
bool expecting_field_result(Item_result result)
{
return bitmap_is_set(&expect_field_result_mask, (uint) result);
};
void expect_no_field_result()
{
bitmap_clear_all(&expect_field_result_mask);
};
bool expecting_no_field_result()
{
return bitmap_is_clear_all(&expect_field_result_mask);
}
void expect_only_field_result(Item_result result)
{
expect_no_field_result();
expect_field_result(result);
};
void expect_collation(CHARSET_INFO* col)
{
collation= col;
};
bool expecting_collation(CHARSET_INFO* col)
{
bool matching= (!collation) ? true : (collation == col);
collation= NULL;
return matching;
};
TABLE* table;
void* ndb_table;
bool supported;
Ndb_cond_stack* stack_ptr;
Ndb_cond* cond_ptr;
MY_BITMAP expect_mask;
MY_BITMAP expect_field_type_mask;
MY_BITMAP expect_field_result_mask;
uint skip;
CHARSET_INFO* collation;
Ndb_rewrite_context *rewrite_stack;
};
typedef enum ndb_query_state_bits { typedef enum ndb_query_state_bits {
NDB_QUERY_NORMAL = 0, NDB_QUERY_NORMAL = 0,
NDB_QUERY_MULTI_READ_RANGE = 1 NDB_QUERY_MULTI_READ_RANGE = 1
...@@ -905,27 +487,6 @@ private: ...@@ -905,27 +487,6 @@ private:
void release_completed_operations(NdbTransaction*, bool); void release_completed_operations(NdbTransaction*, bool);
/*
Condition pushdown
*/
void cond_clear();
bool serialize_cond(const COND *cond, Ndb_cond_stack *ndb_cond);
int build_scan_filter_predicate(Ndb_cond* &cond,
NdbScanFilter* filter,
bool negated= false);
int build_scan_filter_group(Ndb_cond* &cond,
NdbScanFilter* filter);
int build_scan_filter(Ndb_cond* &cond, NdbScanFilter* filter);
int generate_scan_filter(Ndb_cond_stack* cond_stack,
NdbScanOperation* op);
int generate_scan_filter_from_cond(Ndb_cond_stack* cond_stack,
NdbScanFilter& filter);
int generate_scan_filter_from_key(NdbScanOperation* op,
const KEY* key_info,
const byte *key,
uint key_len,
byte *buf);
friend int execute_commit(ha_ndbcluster*, NdbTransaction*); friend int execute_commit(ha_ndbcluster*, NdbTransaction*);
friend int execute_no_commit_ignore_no_key(ha_ndbcluster*, NdbTransaction*); friend int execute_no_commit_ignore_no_key(ha_ndbcluster*, NdbTransaction*);
friend int execute_no_commit(ha_ndbcluster*, NdbTransaction*, bool); friend int execute_no_commit(ha_ndbcluster*, NdbTransaction*, bool);
...@@ -980,7 +541,7 @@ private: ...@@ -980,7 +541,7 @@ private:
ha_rows m_autoincrement_prefetch; ha_rows m_autoincrement_prefetch;
bool m_transaction_on; bool m_transaction_on;
Ndb_cond_stack *m_cond_stack; ha_ndbcluster_cond *m_cond;
bool m_disable_multi_read; bool m_disable_multi_read;
byte *m_multi_range_result_ptr; byte *m_multi_range_result_ptr;
KEY_MULTI_RANGE *m_multi_ranges; KEY_MULTI_RANGE *m_multi_ranges;
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
#include "mysql_priv.h" #include "mysql_priv.h"
#ifdef HAVE_NDBCLUSTER_DB #ifdef WITH_NDBCLUSTER_STORAGE_ENGINE
#include <ndbapi/NdbApi.hpp> #include <ndbapi/NdbApi.hpp>
#include "ha_ndbcluster_cond.h" #include "ha_ndbcluster_cond.h"
...@@ -351,8 +351,8 @@ void ndb_serialize_cond(const Item *item, void *arg) ...@@ -351,8 +351,8 @@ void ndb_serialize_cond(const Item *item, void *arg)
else else
{ {
DBUG_PRINT("info", ("Was not expecting field from table %s (%s)", DBUG_PRINT("info", ("Was not expecting field from table %s (%s)",
context->table->s->table_name, context->table->s->table_name.str,
field->table->s->table_name)); field->table->s->table_name.str));
context->supported= FALSE; context->supported= FALSE;
} }
break; break;
......
...@@ -212,7 +212,12 @@ public: ...@@ -212,7 +212,12 @@ public:
const Item *item= value.item; const Item *item= value.item;
if (item && field) if (item && field)
((Item *)item)->save_in_field(field, false); {
my_bitmap_map *old_map=
dbug_tmp_use_all_columns(field->table, field->table->write_set);
((Item *)item)->save_in_field(field, FALSE);
dbug_tmp_restore_column_map(field->table->write_set, old_map);
}
}; };
static NDB_FUNC_TYPE item_func_to_ndb_func(Item_func::Functype fun) static NDB_FUNC_TYPE item_func_to_ndb_func(Item_func::Functype fun)
......
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