/* Copyright (c) 2009, 2015, Oracle and/or its affiliates. All rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "ha_ndbcluster_glue.h" #include "ha_ndbinfo.h" #include "ndb_tdc.h" #include "../storage/ndb/src/ndbapi/NdbInfo.hpp" static MYSQL_THDVAR_UINT( max_rows, /* name */ PLUGIN_VAR_RQCMDARG, "Specify max number of rows to fetch per roundtrip to cluster", NULL, /* check func. */ NULL, /* update func. */ 10, /* default */ 1, /* min */ 256, /* max */ 0 /* block */ ); static MYSQL_THDVAR_UINT( max_bytes, /* name */ PLUGIN_VAR_RQCMDARG, "Specify approx. max number of bytes to fetch per roundtrip to cluster", NULL, /* check func. */ NULL, /* update func. */ 0, /* default */ 0, /* min */ 65535, /* max */ 0 /* block */ ); static MYSQL_THDVAR_BOOL( show_hidden, /* name */ PLUGIN_VAR_RQCMDARG, "Control if tables should be visible or not", NULL, /* check func. */ NULL, /* update func. */ FALSE /* default */ ); static char* opt_ndbinfo_dbname = (char*)"ndbinfo"; static MYSQL_SYSVAR_STR( database, /* name */ opt_ndbinfo_dbname, /* var */ PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, "Name of the database used by ndbinfo", NULL, /* check func. */ NULL, /* update func. */ NULL /* default */ ); static char* opt_ndbinfo_table_prefix = (char*)"ndb$"; static MYSQL_SYSVAR_STR( table_prefix, /* name */ opt_ndbinfo_table_prefix, /* var */ PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_READONLY, "Prefix to use for all virtual tables loaded from NDB", NULL, /* check func. */ NULL, /* update func. */ NULL /* default */ ); static Uint32 opt_ndbinfo_version = NDB_VERSION_D; static MYSQL_SYSVAR_UINT( version, /* name */ opt_ndbinfo_version, /* var */ PLUGIN_VAR_NOCMDOPT | PLUGIN_VAR_READONLY, "Compile version for ndbinfo", NULL, /* check func. */ NULL, /* update func. */ 0, /* default */ 0, /* min */ 0, /* max */ 0 /* block */ ); static my_bool opt_ndbinfo_offline; static void offline_update(THD* thd, struct st_mysql_sys_var* var, void* var_ptr, const void* save) { DBUG_ENTER("offline_update"); const my_bool new_offline = (*(static_cast(save)) != 0); if (new_offline == opt_ndbinfo_offline) { // No change DBUG_VOID_RETURN; } // Set offline mode, any tables opened from here on will // be opened in the new mode opt_ndbinfo_offline = new_offline; // Close any open tables which may be in the old mode (void)ndb_tdc_close_cached_tables(); DBUG_VOID_RETURN; } static MYSQL_SYSVAR_BOOL( offline, /* name */ opt_ndbinfo_offline, /* var */ PLUGIN_VAR_NOCMDOPT, "Set ndbinfo in offline mode, tables and views can " "be opened even if they don't exist or have different " "definition in NDB. No rows will be returned.", NULL, /* check func. */ offline_update, /* update func. */ 0 /* default */ ); static NdbInfo* g_ndbinfo; extern Ndb_cluster_connection* g_ndb_cluster_connection; static bool ndbcluster_is_disabled(void) { /* ndbinfo uses the same connection as ndbcluster to avoid using up another nodeid, this also means that if ndbcluster is not enabled, ndbinfo won't start */ if (g_ndb_cluster_connection) return false; assert(g_ndbinfo == NULL); return true; } static handler* create_handler(handlerton *hton, TABLE_SHARE *table, MEM_ROOT *mem_root) { return new (mem_root) ha_ndbinfo(hton, table); } struct ha_ndbinfo_impl { const NdbInfo::Table* m_table; NdbInfoScanOperation* m_scan_op; Vector m_columns; bool m_first_use; // Indicates if table has been opened in offline mode // can only be reset by closing the table bool m_offline; ha_ndbinfo_impl() : m_table(NULL), m_scan_op(NULL), m_first_use(true), m_offline(false) { } }; ha_ndbinfo::ha_ndbinfo(handlerton *hton, TABLE_SHARE *table_arg) : handler(hton, table_arg), m_impl(*new ha_ndbinfo_impl) { } ha_ndbinfo::~ha_ndbinfo() { delete &m_impl; } enum ndbinfo_error_codes { ERR_INCOMPAT_TABLE_DEF = 40001 }; static struct error_message { int error; const char* message; } error_messages[] = { { ERR_INCOMPAT_TABLE_DEF, "Incompatible table definitions" }, { HA_ERR_NO_CONNECTION, "Connection to NDB failed" }, { 0, 0 } }; static const char* find_error_message(int error) { struct error_message* err = error_messages; while (err->error && err->message) { if (err->error == error) { assert(err->message); return err->message; } err++; } return NULL; } static int err2mysql(int error) { DBUG_ENTER("err2mysql"); DBUG_PRINT("enter", ("error: %d", error)); assert(error != 0); switch(error) { case NdbInfo::ERR_ClusterFailure: DBUG_RETURN(HA_ERR_NO_CONNECTION); break; case NdbInfo::ERR_OutOfMemory: DBUG_RETURN(HA_ERR_OUT_OF_MEM); break; default: break; } push_warning_printf(current_thd, Sql_condition::SL_WARNING, ER_GET_ERRNO, ER(ER_GET_ERRNO), error); DBUG_RETURN(HA_ERR_INTERNAL_ERROR); } bool ha_ndbinfo::get_error_message(int error, String *buf) { DBUG_ENTER("ha_ndbinfo::get_error_message"); DBUG_PRINT("enter", ("error: %d", error)); const char* message = find_error_message(error); if (!message) DBUG_RETURN(false); buf->set(message, (uint32)strlen(message), &my_charset_bin); DBUG_PRINT("exit", ("message: %s", buf->ptr())); DBUG_RETURN(false); } static void generate_sql(const NdbInfo::Table* ndb_tab, BaseString& sql) { sql.appfmt("'CREATE TABLE `%s`.`%s%s` (", opt_ndbinfo_dbname, opt_ndbinfo_table_prefix, ndb_tab->getName()); const char* separator = ""; for (unsigned i = 0; i < ndb_tab->columns(); i++) { const NdbInfo::Column* col = ndb_tab->getColumn(i); sql.appfmt("%s", separator); separator = ", "; sql.appfmt("`%s` ", col->m_name.c_str()); switch(col->m_type) { case NdbInfo::Column::Number: sql.appfmt("INT UNSIGNED"); break; case NdbInfo::Column::Number64: sql.appfmt("BIGINT UNSIGNED"); break; case NdbInfo::Column::String: sql.appfmt("VARCHAR(512)"); break; default: sql.appfmt("UNKNOWN"); assert(false); break; } } sql.appfmt(") ENGINE=NDBINFO'"); } /* Push a warning with explanation of the problem as well as the proper SQL so the user can regenerate the table definition */ static void warn_incompatible(const NdbInfo::Table* ndb_tab, bool fatal, const char* format, ...) { BaseString msg; DBUG_ENTER("warn_incompatible"); DBUG_PRINT("enter",("table_name: %s, fatal: %d", ndb_tab->getName(), fatal)); DBUG_ASSERT(format != NULL); va_list args; char explanation[128]; va_start(args,format); my_vsnprintf(explanation, sizeof(explanation), format, args); va_end(args); msg.assfmt("Table '%s%s' is defined differently in NDB, %s. The " "SQL to regenerate is: ", opt_ndbinfo_table_prefix, ndb_tab->getName(), explanation); generate_sql(ndb_tab, msg); const Sql_condition::enum_severity_level level = (fatal ? Sql_condition::SL_WARNING : Sql_condition::SL_NOTE); push_warning(current_thd, level, ERR_INCOMPAT_TABLE_DEF, msg.c_str()); DBUG_VOID_RETURN; } int ha_ndbinfo::create(const char *name, TABLE *form, HA_CREATE_INFO *create_info) { DBUG_ENTER("ha_ndbinfo::create"); DBUG_PRINT("enter", ("name: %s", name)); DBUG_RETURN(0); } bool ha_ndbinfo::is_open(void) const { return m_impl.m_table != NULL; } bool ha_ndbinfo::is_offline(void) const { return m_impl.m_offline; } int ha_ndbinfo::open(const char *name, int mode, uint test_if_locked) { DBUG_ENTER("ha_ndbinfo::open"); DBUG_PRINT("enter", ("name: %s, mode: %d", name, mode)); assert(is_closed()); assert(!is_offline()); // Closed table can not be offline if (mode == O_RDWR) { if (table->db_stat & HA_TRY_READ_ONLY) { DBUG_PRINT("info", ("Telling server to use readonly mode")); DBUG_RETURN(EROFS); // Read only fs } // Find any commands that does not allow open readonly DBUG_ASSERT(false); } if (opt_ndbinfo_offline || ndbcluster_is_disabled()) { // Mark table as being offline and allow it to be opened m_impl.m_offline = true; DBUG_RETURN(0); } int err = g_ndbinfo->openTable(name, &m_impl.m_table); if (err) { assert(m_impl.m_table == 0); if (err == NdbInfo::ERR_NoSuchTable) DBUG_RETURN(HA_ERR_NO_SUCH_TABLE); DBUG_RETURN(err2mysql(err)); } /* Check table def. to detect incompatible differences which should return an error. Differences which only generate a warning is checked on first use */ DBUG_PRINT("info", ("Comparing MySQL's table def against NDB")); const NdbInfo::Table* ndb_tab = m_impl.m_table; for (uint i = 0; i < table->s->fields; i++) { const Field* field = table->field[i]; // Check if field is NULLable if (const_cast(field)->real_maybe_null() == false) { // Only NULLable fields supported warn_incompatible(ndb_tab, true, "column '%s' is NOT NULL", field->field_name); delete m_impl.m_table; m_impl.m_table= 0; DBUG_RETURN(ERR_INCOMPAT_TABLE_DEF); } // Check if column exist in NDB const NdbInfo::Column* col = ndb_tab->getColumn(field->field_name); if (!col) { // The column didn't exist continue; } // Check compatible field and column type bool compatible = false; switch(col->m_type) { case NdbInfo::Column::Number: if (field->type() == MYSQL_TYPE_LONG) compatible = true; break; case NdbInfo::Column::Number64: if (field->type() == MYSQL_TYPE_LONGLONG) compatible = true; break; case NdbInfo::Column::String: if (field->type() == MYSQL_TYPE_VARCHAR) compatible = true; break; default: assert(false); break; } if (!compatible) { // The column type is not compatible warn_incompatible(ndb_tab, true, "column '%s' is not compatible", field->field_name); delete m_impl.m_table; m_impl.m_table= 0; DBUG_RETURN(ERR_INCOMPAT_TABLE_DEF); } } /* Increase "ref_length" to allow a whole row to be stored in "ref" */ ref_length = 0; for (uint i = 0; i < table->s->fields; i++) ref_length += table->field[i]->pack_length(); DBUG_PRINT("info", ("ref_length: %u", ref_length)); DBUG_RETURN(0); } int ha_ndbinfo::close(void) { DBUG_ENTER("ha_ndbinfo::close"); if (is_offline()) DBUG_RETURN(0); assert(is_open()); if (m_impl.m_table) { g_ndbinfo->closeTable(m_impl.m_table); m_impl.m_table = NULL; } DBUG_RETURN(0); } int ha_ndbinfo::rnd_init(bool scan) { DBUG_ENTER("ha_ndbinfo::rnd_init"); DBUG_PRINT("info", ("scan: %d", scan)); if (is_offline()) { push_warning(current_thd, Sql_condition::SL_NOTE, 1, "'NDBINFO' has been started in offline mode " "since the 'NDBCLUSTER' engine is disabled " "or @@global.ndbinfo_offline is turned on " "- no rows can be returned"); DBUG_RETURN(0); } assert(is_open()); if (m_impl.m_scan_op) { /* It should be impossible to come here with an already open scan, assumption is that rnd_end() would be called to indicate that the previous scan should be closed or perhaps like it says in decsription of rnd_init() that it "may be called two times". Once to open the cursor and once to position te cursor at first row. Unfortunately the assumption and description of rnd_init() is not correct. The rnd_init function is used on an open scan to reposition it back to first row. For ha_ndbinfo this means closing the scan and letting it be reopened. */ assert(scan); // "only makes sense if scan=1" (from rnd_init() description) DBUG_PRINT("info", ("Closing scan to position it back to first row")); // Release the scan operation g_ndbinfo->releaseScanOperation(m_impl.m_scan_op); m_impl.m_scan_op = NULL; // Release pointers to the columns m_impl.m_columns.clear(); } assert(m_impl.m_scan_op == NULL); // No scan already ongoing if (m_impl.m_first_use) { m_impl.m_first_use = false; /* Check table def. and generate warnings for incompatibilites which is allowed but should generate a warning. (Done this late due to different code paths in MySQL Server for prepared statement protocol, where warnings from 'handler::open' are lost). */ uint fields_found_in_ndb = 0; const NdbInfo::Table* ndb_tab = m_impl.m_table; for (uint i = 0; i < table->s->fields; i++) { const Field* field = table->field[i]; const NdbInfo::Column* col = ndb_tab->getColumn(field->field_name); if (!col) { // The column didn't exist warn_incompatible(ndb_tab, true, "column '%s' does not exist", field->field_name); continue; } fields_found_in_ndb++; } if (fields_found_in_ndb < ndb_tab->columns()) { // There are more columns available in NDB warn_incompatible(ndb_tab, false, "there are more columns available"); } } if (!scan) { // Just an init to read using 'rnd_pos' DBUG_PRINT("info", ("not scan")); DBUG_RETURN(0); } THD* thd = current_thd; int err; NdbInfoScanOperation* scan_op = NULL; if ((err = g_ndbinfo->createScanOperation(m_impl.m_table, &scan_op, THDVAR(thd, max_rows), THDVAR(thd, max_bytes))) != 0) DBUG_RETURN(err2mysql(err)); if ((err = scan_op->readTuples()) != 0) { // Release the scan operation g_ndbinfo->releaseScanOperation(scan_op); DBUG_RETURN(err2mysql(err)); } /* Read all columns specified in read_set */ for (uint i = 0; i < table->s->fields; i++) { Field *field = table->field[i]; if (bitmap_is_set(table->read_set, i)) m_impl.m_columns.push_back(scan_op->getValue(field->field_name)); else m_impl.m_columns.push_back(NULL); } if ((err = scan_op->execute()) != 0) { // Release pointers to the columns m_impl.m_columns.clear(); // Release the scan operation g_ndbinfo->releaseScanOperation(scan_op); DBUG_RETURN(err2mysql(err)); } m_impl.m_scan_op = scan_op; DBUG_RETURN(0); } int ha_ndbinfo::rnd_end() { DBUG_ENTER("ha_ndbinfo::rnd_end"); if (is_offline()) DBUG_RETURN(0); assert(is_open()); if (m_impl.m_scan_op) { g_ndbinfo->releaseScanOperation(m_impl.m_scan_op); m_impl.m_scan_op = NULL; } m_impl.m_columns.clear(); DBUG_RETURN(0); } int ha_ndbinfo::rnd_next(uchar *buf) { int err; DBUG_ENTER("ha_ndbinfo::rnd_next"); if (is_offline()) DBUG_RETURN(HA_ERR_END_OF_FILE); assert(is_open()); if (!m_impl.m_scan_op) { /* It should be impossible to come here without a scan operation. But apparently it's not safe to assume that rnd_next() isn't called even though rnd_init() returned an error. Thus double check that the scan operation exists and bail out in case it doesn't. */ DBUG_RETURN(HA_ERR_INTERNAL_ERROR); } if ((err = m_impl.m_scan_op->nextResult()) == 0) DBUG_RETURN(HA_ERR_END_OF_FILE); if (err != 1) DBUG_RETURN(err2mysql(err)); unpack_record(buf); DBUG_RETURN(0); } int ha_ndbinfo::rnd_pos(uchar *buf, uchar *pos) { DBUG_ENTER("ha_ndbinfo::rnd_pos"); assert(is_open()); assert(m_impl.m_scan_op == NULL); // No scan started /* Copy the saved row into "buf" and set all fields to not null */ memcpy(buf, pos, ref_length); for (uint i = 0; i < table->s->fields; i++) table->field[i]->set_notnull(); DBUG_RETURN(0); } void ha_ndbinfo::position(const uchar *record) { DBUG_ENTER("ha_ndbinfo::position"); assert(is_open()); assert(m_impl.m_scan_op); /* Save away the whole row in "ref" */ memcpy(ref, record, ref_length); DBUG_VOID_RETURN; } int ha_ndbinfo::info(uint flag) { DBUG_ENTER("ha_ndbinfo::info"); DBUG_PRINT("enter", ("flag: %d", flag)); DBUG_RETURN(0); } void ha_ndbinfo::unpack_record(uchar *dst_row) { DBUG_ENTER("ha_ndbinfo::unpack_record"); my_ptrdiff_t dst_offset = dst_row - table->record[0]; for (uint i = 0; i < table->s->fields; i++) { Field *field = table->field[i]; const NdbInfoRecAttr* record = m_impl.m_columns[i]; if (record && !record->isNULL()) { field->set_notnull(); field->move_field_offset(dst_offset); switch (field->type()) { case (MYSQL_TYPE_VARCHAR): { DBUG_PRINT("info", ("str: %s", record->c_str())); Field_varstring* vfield = (Field_varstring *) field; /* Field_bit in DBUG requires the bit set in write_set for store(). */ my_bitmap_map *old_map = dbug_tmp_use_all_columns(table, table->write_set); (void)vfield->store(record->c_str(), MIN(record->length(), field->field_length)-1, field->charset()); dbug_tmp_restore_column_map(table->write_set, old_map); break; } case (MYSQL_TYPE_LONG): { memcpy(field->ptr, record->ptr(), sizeof(Uint32)); break; } case (MYSQL_TYPE_LONGLONG): { memcpy(field->ptr, record->ptr(), sizeof(Uint64)); break; } default: sql_print_error("Found unexpected field type %u", field->type()); break; } field->move_field_offset(-dst_offset); } else { field->set_null(); } } DBUG_VOID_RETURN; } static int ndbinfo_find_files(handlerton *hton, THD *thd, const char *db, const char *path, const char *wild, bool dir, List *files) { DBUG_ENTER("ndbinfo_find_files"); DBUG_PRINT("enter", ("db: '%s', dir: %d, path: '%s'", db, dir, path)); const bool show_hidden = THDVAR(thd, show_hidden); if(show_hidden) DBUG_RETURN(0); // Don't filter out anything if (dir) { if (!ndbcluster_is_disabled()) DBUG_RETURN(0); // Hide our database when ndbcluster is disabled LEX_STRING *dir_name; List_iterator it(*files); while ((dir_name=it++)) { if (strcmp(dir_name->str, opt_ndbinfo_dbname)) continue; DBUG_PRINT("info", ("Hiding own databse '%s'", dir_name->str)); it.remove(); } DBUG_RETURN(0); } DBUG_ASSERT(db); if (strcmp(db, opt_ndbinfo_dbname)) DBUG_RETURN(0); // Only hide files in "our" db /* Hide all files that start with "our" prefix */ LEX_STRING *file_name; List_iterator it(*files); while ((file_name=it++)) { if (is_prefix(file_name->str, opt_ndbinfo_table_prefix)) { DBUG_PRINT("info", ("Hiding '%s'", file_name->str)); it.remove(); } } DBUG_RETURN(0); } handlerton* ndbinfo_hton; static int ndbinfo_init(void *plugin) { DBUG_ENTER("ndbinfo_init"); handlerton *hton = (handlerton *) plugin; hton->create = create_handler; hton->flags = HTON_TEMPORARY_NOT_SUPPORTED | HTON_ALTER_NOT_SUPPORTED; hton->find_files = ndbinfo_find_files; ndbinfo_hton = hton; if (ndbcluster_is_disabled()) { // Starting in limited mode since ndbcluster is disabled DBUG_RETURN(0); } char prefix[FN_REFLEN]; build_table_filename(prefix, sizeof(prefix) - 1, opt_ndbinfo_dbname, opt_ndbinfo_table_prefix, "", 0); DBUG_PRINT("info", ("prefix: '%s'", prefix)); assert(g_ndb_cluster_connection); g_ndbinfo = new NdbInfo(g_ndb_cluster_connection, prefix, opt_ndbinfo_dbname, opt_ndbinfo_table_prefix); if (!g_ndbinfo) { sql_print_error("Failed to create NdbInfo"); DBUG_RETURN(1); } if (!g_ndbinfo->init()) { sql_print_error("Failed to init NdbInfo"); delete g_ndbinfo; g_ndbinfo = NULL; DBUG_RETURN(1); } DBUG_RETURN(0); } static int ndbinfo_deinit(void *plugin) { DBUG_ENTER("ndbinfo_deinit"); if (g_ndbinfo) { delete g_ndbinfo; g_ndbinfo = NULL; } DBUG_RETURN(0); } struct st_mysql_sys_var* ndbinfo_system_variables[]= { MYSQL_SYSVAR(max_rows), MYSQL_SYSVAR(max_bytes), MYSQL_SYSVAR(show_hidden), MYSQL_SYSVAR(database), MYSQL_SYSVAR(table_prefix), MYSQL_SYSVAR(version), MYSQL_SYSVAR(offline), NULL }; struct st_mysql_storage_engine ndbinfo_storage_engine= { MYSQL_HANDLERTON_INTERFACE_VERSION }; struct st_mysql_plugin ndbinfo_plugin = { MYSQL_STORAGE_ENGINE_PLUGIN, &ndbinfo_storage_engine, "ndbinfo", "Sun Microsystems Inc.", "MySQL Cluster system information storage engine", PLUGIN_LICENSE_GPL, ndbinfo_init, /* plugin init */ ndbinfo_deinit, /* plugin deinit */ 0x0001, /* plugin version */ NULL, /* status variables */ ndbinfo_system_variables, /* system variables */ NULL, /* config options */ 0 }; template class Vector;