bool open_table()

in sql/sql_base.cc [2873:3478]


bool open_table(THD *thd, TABLE_LIST *table_list, Open_table_context *ot_ctx)
{
  reg1	TABLE *table;
  const char *key;
  uint key_length;
  char	*alias= table_list->alias;
  uint flags= ot_ctx->get_flags();
  MDL_ticket *mdl_ticket;
  int error;
  TABLE_SHARE *share;
  my_hash_value_type hash_value;
  ulonglong timer = my_timer_now();

  DBUG_ENTER("open_table");

  /*
    The table must not be opened already. The table can be pre-opened for
    some statements if it is a temporary table.

    open_temporary_table() must be used to open temporary tables.
  */
  DBUG_ASSERT(!table_list->table);

  /* an open table operation needs a lot of the stack space */
  if (check_stack_overrun(thd, STACK_MIN_SIZE_FOR_OPEN, (uchar *)&alias))
    DBUG_RETURN(TRUE);

  if (!(flags & MYSQL_OPEN_IGNORE_KILLED) && thd->killed)
    DBUG_RETURN(TRUE);

  /*
    Check if we're trying to take a write lock in a read only transaction.

    Note that we allow write locks on log tables as otherwise logging
    to general/slow log would be disabled in read only transactions.
  */
  if (table_list->mdl_request.type >= MDL_SHARED_WRITE &&
      thd->tx_read_only &&
      !(flags & (MYSQL_LOCK_LOG_TABLE | MYSQL_OPEN_HAS_MDL_LOCK)))
  {
    my_error(ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTION, MYF(0));
    DBUG_RETURN(true);
  }

  key_length= get_table_def_key(table_list, &key);

  /*
    If we're in pre-locked or LOCK TABLES mode, let's try to find the
    requested table in the list of pre-opened and locked tables. If the
    table is not there, return an error - we can't open not pre-opened
    tables in pre-locked/LOCK TABLES mode.
    TODO: move this block into a separate function.
  */
  if (thd->locked_tables_mode &&
      ! (flags & MYSQL_OPEN_GET_NEW_TABLE))
  {						// Using table locks
    TABLE *best_table= 0;
    int best_distance= INT_MIN;
    for (table=thd->open_tables; table ; table=table->next)
    {
      if (table->s->table_cache_key.length == key_length &&
	  !memcmp(table->s->table_cache_key.str, key, key_length))
      {
        if (!my_strcasecmp(system_charset_info, table->alias, alias) &&
            table->query_id != thd->query_id && /* skip tables already used */
            (thd->locked_tables_mode == LTM_LOCK_TABLES ||
             table->query_id == 0))
        {
          int distance= ((int) table->reginfo.lock_type -
                         (int) table_list->lock_type);

          /*
            Find a table that either has the exact lock type requested,
            or has the best suitable lock. In case there is no locked
            table that has an equal or higher lock than requested,
            we us the closest matching lock to be able to produce an error
            message about wrong lock mode on the table. The best_table
            is changed if bd < 0 <= d or bd < d < 0 or 0 <= d < bd.

            distance <  0 - No suitable lock found
            distance >  0 - we have lock mode higher then we require
            distance == 0 - we have lock mode exactly which we need
          */
          if ((best_distance < 0 && distance > best_distance) ||
              (distance >= 0 && distance < best_distance))
          {
            best_distance= distance;
            best_table= table;
            if (best_distance == 0)
            {
              /*
                We have found a perfect match and can finish iterating
                through open tables list. Check for table use conflict
                between calling statement and SP/trigger is done in
                lock_tables().
              */
              break;
            }
          }
        }
      }
    }
    if (best_table)
    {
      table= best_table;
      table->query_id= thd->query_id;
      DBUG_PRINT("info",("Using locked table"));
      goto reset;
    }
    /*
      Is this table a view and not a base table?
      (it is work around to allow to open view with locked tables,
      real fix will be made after definition cache will be made)

      Since opening of view which was not explicitly locked by LOCK
      TABLES breaks metadata locking protocol (potentially can lead
      to deadlocks) it should be disallowed.
    */
    if (thd->mdl_context.is_lock_owner(MDL_key::TABLE,
                                       table_list->db,
                                       table_list->table_name,
                                       MDL_SHARED))
    {
      char path[FN_REFLEN + 1];
      enum legacy_db_type not_used;
      build_table_filename(path, sizeof(path) - 1,
                           table_list->db, table_list->table_name, reg_ext, 0);
      /*
        Note that we can't be 100% sure that it is a view since it's
        possible that we either simply have not found unused TABLE
        instance in THD::open_tables list or were unable to open table
        during prelocking process (in this case in theory we still
        should hold shared metadata lock on it).
      */
      if (dd_frm_type(thd, path, &not_used) == FRMTYPE_VIEW)
      {
        /*
          If parent_l of the table_list is non null then a merge table
          has this view as child table, which is not supported.
        */
        if (table_list->parent_l)
        {
          my_error(ER_WRONG_MRG_TABLE, MYF(0));
          DBUG_RETURN(true);
        }

        if (!tdc_open_view(thd, table_list, alias, key, key_length,
                           CHECK_METADATA_VERSION))
        {
          DBUG_ASSERT(table_list->view != 0);
          DBUG_RETURN(FALSE); // VIEW
        }
      }
    }
    /*
      No table in the locked tables list. In case of explicit LOCK TABLES
      this can happen if a user did not include the table into the list.
      In case of pre-locked mode locked tables list is generated automatically,
      so we may only end up here if the table did not exist when
      locked tables list was created.
    */
    if (thd->locked_tables_mode == LTM_PRELOCKED)
      my_error(ER_NO_SUCH_TABLE, MYF(0), table_list->db, table_list->alias);
    else
      my_error(ER_TABLE_NOT_LOCKED, MYF(0), alias);
    DBUG_RETURN(TRUE);
  }

  /* Non pre-locked/LOCK TABLES mode. This is the normal use case. */

  if (! (flags & MYSQL_OPEN_HAS_MDL_LOCK))
  {
    /*
      We are not under LOCK TABLES and going to acquire write-lock/
      modify the base table. We need to acquire protection against
      global read lock until end of this statement in order to have
      this statement blocked by active FLUSH TABLES WITH READ LOCK.

      We don't block acquire this protection under LOCK TABLES as
      such protection already acquired at LOCK TABLES time and
      not released until UNLOCK TABLES.

      We don't block statements which modify only temporary tables
      as these tables are not preserved by backup by any form of
      backup which uses FLUSH TABLES WITH READ LOCK.

      TODO: The fact that we sometimes acquire protection against
            GRL only when we encounter table to be write-locked
            slightly increases probability of deadlock.
            This problem will be solved once Alik pushes his
            temporary table refactoring patch and we can start
            pre-acquiring metadata locks at the beggining of
            open_tables() call.
    */
    if (table_list->mdl_request.type >= MDL_SHARED_WRITE &&
        ! (flags & (MYSQL_OPEN_IGNORE_GLOBAL_READ_LOCK |
                    MYSQL_OPEN_FORCE_SHARED_MDL |
                    MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL |
                    MYSQL_OPEN_SKIP_SCOPED_MDL_LOCK)) &&
        ! ot_ctx->has_protection_against_grl())
    {
      MDL_request protection_request;
      MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);

      if (thd->global_read_lock.can_acquire_protection())
        DBUG_RETURN(TRUE);

      protection_request.init(MDL_key::GLOBAL, "", "", MDL_INTENTION_EXCLUSIVE,
                              MDL_STATEMENT);

      /*
        Install error handler which if possible will convert deadlock error
        into request to back-off and restart process of opening tables.
      */
      thd->push_internal_handler(&mdl_deadlock_handler);
      bool result= thd->mdl_context.acquire_lock_nsec(&protection_request,
                                                 ot_ctx->get_timeout_nsec());
      thd->pop_internal_handler();

      if (result)
        DBUG_RETURN(TRUE);

      ot_ctx->set_has_protection_against_grl();
    }

    if (open_table_get_mdl_lock(thd, ot_ctx, &table_list->mdl_request,
                                flags, &mdl_ticket) ||
        mdl_ticket == NULL)
    {
      DEBUG_SYNC(thd, "before_open_table_wait_refresh");
      DBUG_RETURN(TRUE);
    }
    DEBUG_SYNC(thd, "after_open_table_mdl_shared");
  }
  else
  {
    /*
      Grab reference to the MDL lock ticket that was acquired
      by the caller.
    */
    mdl_ticket= table_list->mdl_request.ticket;
  }

  DBUG_EXECUTE_IF("sql_opening_table",
    {
      if (thd->slave_thread) {
        const char act[]=
          "now signal opening wait_for slave_killed";
          DBUG_ASSERT(!debug_sync_set_action(current_thd,
            STRING_WITH_LEN(act)));
      }
    }
  );

  hash_value= my_calc_hash(&table_def_cache, (uchar*) key, key_length);

  if (table_list->open_strategy == TABLE_LIST::OPEN_IF_EXISTS ||
      table_list->open_strategy == TABLE_LIST::OPEN_FOR_CREATE)
  {
    bool exists;

    if (check_if_table_exists(thd, table_list, &exists))
      DBUG_RETURN(TRUE);

    if (!exists)
    {
      if (table_list->open_strategy == TABLE_LIST::OPEN_FOR_CREATE &&
          ! (flags & (MYSQL_OPEN_FORCE_SHARED_MDL |
                      MYSQL_OPEN_FORCE_SHARED_HIGH_PRIO_MDL)))
      {
        MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);

        thd->push_internal_handler(&mdl_deadlock_handler);

        DEBUG_SYNC(thd, "before_upgrading_lock_from_S_to_X_for_create_table");
        bool wait_result= thd->mdl_context.upgrade_shared_lock_nsec(
                                 table_list->mdl_request.ticket,
                                 MDL_EXCLUSIVE,
                                 thd->variables.lock_wait_timeout_nsec);

        thd->pop_internal_handler();
        DEBUG_SYNC(thd, "after_upgrading_lock_from_S_to_X_for_create_table");

        /* Deadlock or timeout occurred while upgrading the lock. */
        if (wait_result)
          DBUG_RETURN(TRUE);
      }

      DBUG_RETURN(FALSE);
    }

    /* Table exists. Let us try to open it. */
  }
  else if (table_list->open_strategy == TABLE_LIST::OPEN_STUB)
    DBUG_RETURN(FALSE);

retry_share:
  {
    Table_cache *tc= table_cache_manager.get_cache(thd);

    tc->lock();

    /*
      Try to get unused TABLE object or at least pointer to
      TABLE_SHARE from the table cache.
    */
    table= tc->get_table(thd, hash_value, key, key_length, &share);

    if (table)
    {
      /* We have found an unused TABLE object. */

      if (!(flags & MYSQL_OPEN_IGNORE_FLUSH))
      {
        /*
          TABLE_SHARE::version can only be initialised while holding the
          LOCK_open and in this case no one has a reference to the share
          object, if a reference exists to the share object it is necessary
          to lock both LOCK_open AND all table caches in order to update
          TABLE_SHARE::version. The same locks are required to increment
          refresh_version global variable.

          As result it is safe to compare TABLE_SHARE::version and
          refresh_version values while having only lock on the table
          cache for this thread.

          Table cache should not contain any unused TABLE objects with
          old versions.
        */
        DBUG_ASSERT(!share->has_old_version());

        /*
          Still some of already opened might become outdated (e.g. due to
          concurrent table flush). So we need to compare version of opened
          tables with version of TABLE object we just have got.
        */
        if (thd->open_tables &&
            thd->open_tables->s->version != share->version)
        {
          tc->release_table(thd, table);
          tc->unlock();
          (void)ot_ctx->request_backoff_action(
                          Open_table_context::OT_REOPEN_TABLES,
                          NULL);
          DBUG_RETURN(TRUE);
        }
      }
      tc->unlock();

      /* Call rebind_psi outside of the critical section. */
      DBUG_ASSERT(table->file != NULL);
      table->file->rebind_psi();

      thd->status_var.table_open_cache_hits++;
      goto table_found;
    }
    else if (share)
    {
      /*
        We weren't able to get an unused TABLE object. Still we have
        found TABLE_SHARE for it. So let us try to create new TABLE
        for it. We start by incrementing share's reference count and
        checking its version.
      */
      mysql_mutex_lock(&LOCK_open);
      tc->unlock();
      share->ref_count++;
      goto share_found;
    }
    else
    {
      /*
        We have not found neither TABLE nor TABLE_SHARE object in
        table cache (this means that there are no TABLE objects for
        it in it).
        Let us try to get TABLE_SHARE from table definition cache or
        from disk and then to create TABLE object for it.
      */
      tc->unlock();
    }
  }

  mysql_mutex_lock(&LOCK_open);

  if (!(share= get_table_share_with_discover(thd, table_list, key,
                                             key_length, OPEN_VIEW,
                                             &error,
                                             hash_value)))
  {
    mysql_mutex_unlock(&LOCK_open);
    /*
      If thd->is_error() is not set, we either need discover
      (error == 7), or the error was silenced by the prelocking
      handler (error == 0), in which case we should skip this
      table.
    */
    if (error == 7 && !thd->is_error())
    {
      (void) ot_ctx->request_backoff_action(Open_table_context::OT_DISCOVER,
                                            table_list);
    }
    DBUG_RETURN(TRUE);
  }

  /*
    Check if this TABLE_SHARE-object corresponds to a view. Note, that there is
    no need to call TABLE_SHARE::has_old_version() as we do for regular tables,
    because view shares are always up to date.
  */
  if (share->is_view)
  {
    /*
      If parent_l of the table_list is non null then a merge table
      has this view as child table, which is not supported.
    */
    if (table_list->parent_l)
    {
      my_error(ER_WRONG_MRG_TABLE, MYF(0));
      goto err_unlock;
    }

    /*
      This table is a view. Validate its metadata version: in particular,
      that it was a view when the statement was prepared.
    */
    if (check_and_update_table_version(thd, table_list, share))
      goto err_unlock;
    if (table_list->i_s_requested_object & OPEN_TABLE_ONLY)
    {
      my_error(ER_NO_SUCH_TABLE, MYF(0), table_list->db,
               table_list->table_name);
      goto err_unlock;
    }

    /* Open view */
    if (mysql_make_view(thd, share, table_list, false))
      goto err_unlock;

    /* TODO: Don't free this */
    release_table_share(share);

    DBUG_ASSERT(table_list->view);

    mysql_mutex_unlock(&LOCK_open);
    DBUG_RETURN(FALSE);
  }

  /*
    Note that situation when we are trying to open a table for what
    was a view during previous execution of PS will be handled in by
    the caller. Here we should simply open our table even if
    TABLE_LIST::view is true.
  */

  if (table_list->i_s_requested_object &  OPEN_VIEW_ONLY)
  {
    my_error(ER_NO_SUCH_TABLE, MYF(0), table_list->db,
             table_list->table_name);
    goto err_unlock;
  }

share_found:
  if (!(flags & MYSQL_OPEN_IGNORE_FLUSH))
  {
    if (share->has_old_version())
    {
      /*
        We already have an MDL lock. But we have encountered an old
        version of table in the table definition cache which is possible
        when someone changes the table version directly in the cache
        without acquiring a metadata lock (e.g. this can happen during
        "rolling" FLUSH TABLE(S)).
        Release our reference to share, wait until old version of
        share goes away and then try to get new version of table share.
      */
      release_table_share(share);
      mysql_mutex_unlock(&LOCK_open);

      MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);
      bool wait_result;

      thd->push_internal_handler(&mdl_deadlock_handler);
      wait_result= tdc_wait_for_old_version_nsec(thd, table_list->db,
                                            table_list->table_name,
                                            ot_ctx->get_timeout_nsec(),
                                            mdl_ticket->get_deadlock_weight());
      thd->pop_internal_handler();

      if (wait_result)
        DBUG_RETURN(TRUE);

      goto retry_share;
    }

    if (thd->open_tables && thd->open_tables->s->version != share->version)
    {
      /*
        If the version changes while we're opening the tables,
        we have to back off, close all the tables opened-so-far,
        and try to reopen them. Note: refresh_version is currently
        changed only during FLUSH TABLES.
      */
      release_table_share(share);
      mysql_mutex_unlock(&LOCK_open);
      (void)ot_ctx->request_backoff_action(Open_table_context::OT_REOPEN_TABLES,
                                           NULL);
      DBUG_RETURN(TRUE);
    }
  }

  mysql_mutex_unlock(&LOCK_open);

  /* make a new table */
  if (!(table= (TABLE*) my_malloc(sizeof(*table), MYF(MY_WME))))
    goto err_lock;

  error= open_table_from_share(thd, share, alias,
                               (uint) (HA_OPEN_KEYFILE |
                                       HA_OPEN_RNDFILE |
                                       HA_GET_INDEX |
                                       HA_TRY_READ_ONLY),
                               (READ_KEYINFO | COMPUTE_TYPES |
                                EXTRA_RECORD),
                               thd->open_options, table, FALSE);

  if (error)
  {
    my_free(table);

    if (error == 7)
      (void) ot_ctx->request_backoff_action(Open_table_context::OT_DISCOVER,
                                            table_list);
    else if (share->crashed)
      (void) ot_ctx->request_backoff_action(Open_table_context::OT_REPAIR,
                                            table_list);
    goto err_lock;
  }
  if (open_table_entry_fini(thd, share, table))
  {
    closefrm(table, 0);
    my_free(table);
    goto err_lock;
  }
  {
    /* Add new TABLE object to table cache for this connection. */
    Table_cache *tc= table_cache_manager.get_cache(thd);

    tc->lock();

    if (tc->add_used_table(thd, table))
    {
      tc->unlock();
      goto err_lock;
    }
    tc->unlock();
  }
  thd->status_var.open_table_time += my_timer_since(timer);
  thd->status_var.table_open_cache_misses++;

table_found:
  table->mdl_ticket= mdl_ticket;

  table->next= thd->open_tables;		/* Link into simple list */
  thd->set_open_tables(table);
  table->count_comment_bytes= thd->count_comment_bytes;    /* Assigning the
                                   comment bytes count to the relevant table */

  table->reginfo.lock_type=TL_READ;		/* Assume read */

 reset:
  table->set_created();
  /*
    Check that there is no reference to a condition from an earlier query
    (cf. Bug#58553).
  */
  DBUG_ASSERT(table->file->pushed_cond == NULL);
  table_list->updatable= 1; // It is not derived table nor non-updatable VIEW
  table_list->table= table;

#ifdef WITH_PARTITION_STORAGE_ENGINE
  if (table->part_info)
  {
    /* Set all [named] partitions as used. */
    if (table->part_info->set_partition_bitmaps(table_list))
      DBUG_RETURN(true);
  }
  else if (table_list->partition_names)
  {
    /* Don't allow PARTITION () clause on a nonpartitioned table */
    my_error(ER_PARTITION_CLAUSE_ON_NONPARTITIONED, MYF(0));
    DBUG_RETURN(true);
  }
#endif

  table->init(thd, table_list);

  DBUG_RETURN(FALSE);

err_lock:
  mysql_mutex_lock(&LOCK_open);
err_unlock:
  release_table_share(share);
  mysql_mutex_unlock(&LOCK_open);

  DBUG_RETURN(TRUE);
}