/* Copyright (c) 2009, 2019, 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, version 2.0, as published by the Free Software Foundation. This program is also distributed with certain software (including but not limited to OpenSSL) that is licensed under separate terms, as designated in a particular file or component or in included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the separately licensed software that they have included with MySQL. 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, version 2.0, 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 "storage/ndb/plugin/ha_ndbinfo.h" #include #include "my_compiler.h" #include "my_dbug.h" #include "sql/current_thd.h" #include "sql/derror.h" // ER_THD #include "sql/field.h" #include "sql/sql_class.h" #include "sql/sql_table.h" // build_table_filename #include "sql/table.h" #include "storage/ndb/plugin/ndb_dummy_ts.h" #include "storage/ndb/plugin/ndb_log.h" #include "storage/ndb/plugin/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 = const_cast("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 = const_cast("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 | PLUGIN_VAR_NOPERSIST, "Compile version for ndbinfo", NULL, /* check func. */ NULL, /* update func. */ 0, /* default */ 0, /* min */ 0, /* max */ 0 /* block */ ); static bool opt_ndbinfo_offline; static void offline_update(THD *, SYS_VAR *, void *, const void *save) { DBUG_TRACE; const bool new_offline = (*(static_cast(save)) != 0); if (new_offline == opt_ndbinfo_offline) { // No change 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(); } 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, bool, 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_TRACE; DBUG_PRINT("enter", ("error: %d", error)); assert(error != 0); switch (error) { case NdbInfo::ERR_ClusterFailure: return HA_ERR_NO_CONNECTION; break; case NdbInfo::ERR_OutOfMemory: return HA_ERR_OUT_OF_MEM; break; default: break; } { char errbuf[MYSQL_ERRMSG_SIZE]; push_warning_printf(current_thd, Sql_condition::SL_WARNING, ER_GET_ERRNO, ER_THD(current_thd, ER_GET_ERRNO), error, my_strerror(errbuf, MYSQL_ERRMSG_SIZE, error)); } return HA_ERR_INTERNAL_ERROR; } bool ha_ndbinfo::get_error_message(int error, String *buf) { DBUG_TRACE; DBUG_PRINT("enter", ("error: %d", error)); const char *message = find_error_message(error); if (!message) return false; buf->set(message, (uint32)strlen(message), &my_charset_bin); DBUG_PRINT("exit", ("message: %s", buf->ptr())); 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, ...) MY_ATTRIBUTE((format(printf, 3, 4))); static void warn_incompatible(const NdbInfo::Table *ndb_tab, bool fatal, const char *format, ...) { BaseString msg; DBUG_TRACE; 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); 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()); } int ha_ndbinfo::create(const char *, TABLE *, HA_CREATE_INFO *, dd::Table *) { DBUG_TRACE; 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, const dd::Table *) { DBUG_TRACE; 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")); 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; 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) return HA_ERR_NO_SUCH_TABLE; 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; 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; 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)); return 0; } int ha_ndbinfo::close(void) { DBUG_TRACE; if (is_offline()) return 0; assert(is_open()); if (m_impl.m_table) { g_ndbinfo->closeTable(m_impl.m_table); m_impl.m_table = NULL; } return 0; } int ha_ndbinfo::rnd_init(bool scan) { DBUG_TRACE; 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"); 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")); 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) return err2mysql(err); if ((err = scan_op->readTuples()) != 0) { // Release the scan operation g_ndbinfo->releaseScanOperation(scan_op); 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); return err2mysql(err); } m_impl.m_scan_op = scan_op; return 0; } int ha_ndbinfo::rnd_end() { DBUG_TRACE; if (is_offline()) 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(); return 0; } int ha_ndbinfo::rnd_next(uchar *buf) { int err; DBUG_TRACE; if (is_offline()) 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. */ return HA_ERR_INTERNAL_ERROR; } if ((err = m_impl.m_scan_op->nextResult()) == 0) return HA_ERR_END_OF_FILE; if (err != 1) return err2mysql(err); unpack_record(buf); return 0; } int ha_ndbinfo::rnd_pos(uchar *buf, uchar *pos) { DBUG_TRACE; 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(); return 0; } void ha_ndbinfo::position(const uchar *record) { DBUG_TRACE; assert(is_open()); assert(m_impl.m_scan_op); /* Save away the whole row in "ref" */ memcpy(ref, record, ref_length); } int ha_ndbinfo::info(uint) { DBUG_TRACE; return 0; } void ha_ndbinfo::unpack_record(uchar *dst_row) { DBUG_TRACE; 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: ndb_log_error("Found unexpected field type %u", field->type()); break; } field->move_field_offset(-dst_offset); } else { field->set_null(); } } } static int ndbinfo_find_files(handlerton *, THD *thd, const char *db, const char *, const char *, bool dir, List *files) { DBUG_TRACE; DBUG_PRINT("enter", ("db: '%s', dir: %d", db, dir)); const bool show_hidden = THDVAR(thd, show_hidden); if (show_hidden) return 0; // Don't filter out anything if (dir) { if (!ndbcluster_is_disabled()) 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 database '%s'", dir_name->str)); it.remove(); } return 0; } DBUG_ASSERT(db); if (strcmp(db, opt_ndbinfo_dbname)) 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(); } } return 0; } static int ndbinfo_init(void *plugin) { DBUG_TRACE; handlerton *hton = (handlerton *)plugin; hton->create = create_handler; hton->flags = HTON_TEMPORARY_NOT_SUPPORTED | HTON_ALTER_NOT_SUPPORTED; hton->find_files = ndbinfo_find_files; { // Install dummy callbacks to avoid writing _.SDI files // in the data directory, those are just cumbersome having to delete // and or rename on the other MySQL servers hton->sdi_create = ndb_dummy_ts::sdi_create; hton->sdi_drop = ndb_dummy_ts::sdi_drop; hton->sdi_get_keys = ndb_dummy_ts::sdi_get_keys; hton->sdi_get = ndb_dummy_ts::sdi_get; hton->sdi_set = ndb_dummy_ts::sdi_set; hton->sdi_delete = ndb_dummy_ts::sdi_delete; } if (ndbcluster_is_disabled()) { // Starting in limited mode since ndbcluster is disabled 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 (std::nothrow) NdbInfo(g_ndb_cluster_connection, prefix, opt_ndbinfo_dbname, opt_ndbinfo_table_prefix); if (!g_ndbinfo) { ndb_log_error("Failed to create NdbInfo"); return 1; } if (!g_ndbinfo->init()) { ndb_log_error("Failed to init NdbInfo"); delete g_ndbinfo; g_ndbinfo = NULL; return 1; } return 0; } static int ndbinfo_deinit(void *) { DBUG_TRACE; if (g_ndbinfo) { delete g_ndbinfo; g_ndbinfo = NULL; } return 0; } 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 */ NULL, /* plugin uninstall check */ ndbinfo_deinit, /* plugin deinit */ 0x0001, /* plugin version */ NULL, /* status variables */ ndbinfo_system_variables, /* system variables */ NULL, /* config options */ 0}; template class Vector;