/********************************************************************//**
Returns TRUEif the user-defined column in a secondary index record
is alphabetically the same as the corresponding BLOB column in the clustered
index record.
NOTE: the comparison is NOT done as a binary comparison, but character
fields are compared with collation!
@return whether the columns are equal */ static bool
row_sel_sec_rec_is_for_blob( /*========================*/
ulint mtype, /*!< in: main type */
ulint prtype, /*!< in: precise type */
ulint mbminlen, /*!< in: minimum length of
a character, in bytes */
ulint mbmaxlen, /*!< in: maximum length of
a character, in bytes */ const byte* clust_field, /*!< in: the locally stored part of theclusteredindexcolumn,including theBLOBpointer;theclustered indexrecordmustbecoveredby alockorapagelatchtoprotectit
against deletion (rollback or purge) */
ulint clust_len, /*!< in: length of clust_field */ const byte* sec_field, /*!< in: column in secondary index */
ulint sec_len, /*!< in: length of sec_field */
ulint prefix_len, /*!< in: index column prefix length
in bytes, or 0 for full column */
dict_table_t* table) /*!< in: table */
{
ulint len;
byte buf[REC_VERSION_56_MAX_INDEX_COL_LEN + 1];
/* This function should never be invoked on tables in ROW_FORMAT=REDUNDANTorROW_FORMAT=COMPACT,becausethey
should always contain enough prefix in the clustered index record. */
ut_ad(dict_table_has_atomic_blobs(table));
ut_a(clust_len >= BTR_EXTERN_FIELD_REF_SIZE);
ut_ad(!prefix_len || prefix_len >= sec_len);
ut_a(prefix_len <= sizeof buf);
if (!memcmp(clust_field + clust_len - BTR_EXTERN_FIELD_REF_SIZE,
field_ref_zero, BTR_EXTERN_FIELD_REF_SIZE)) { /* The externally stored field was not written yet. Thisrecordshouldonlybeseenby trx_rollback_recovered()orany
TRX_ISO_READ_UNCOMMITTED transactions. */ returnfalse;
}
if (len == 0) { /* The BLOB was being deleted as the server crashed. Thereshouldnotbeanysecondaryindexrecords referringtothisclusteredindexrecord,because btr_free_externally_stored_field()iscalledafterall
secondary index entries of the row have been purged. */ returnfalse;
}
if (prefix_len) {
len = dtype_get_at_most_n_mbchars(prtype, mbminlen, mbmaxlen,
prefix_len, len, reinterpret_cast<constchar*>
(buf));
} elseif (len >= sizeof buf) {
ut_ad("too long column" == 0); returnfalse;
}
/* For externally stored field, we need to get full
geo data to generate the MBR for comparing. */ if (rec_offs_nth_extern(clust_offs, clust_pos))
{
clust_field= btr_copy_externally_stored_field(
&clust_len, clust_field, sec_index->table->space->zip_size(),
clust_len, heap); if (clust_field == NULL)
{
ut_ad("corrupted geometry blob" == 0); goto err_exit;
}
}
/** Returns TRUE if the user-defined column values in a secondary index record arealphabeticallythesameasthecorrespondingcolumnsintheclustered indexrecord. NOTE:thecomparisonisNOTdoneasabinarycomparison,butcharacter fieldsarecomparedwithcollation! @param[in]sec_recsecondaryindexrecord @param[in]sec_indexsecondaryindex @param[in]clust_recclusteredindexrecord; mustbeprotectedbyapages-latch @param[in]clust_indexclusteredindex @param[in]thrquerythread @retvalDB_COMPUTE_VALUE_FAILEDincaseofvirtualcolumnvaluecomputation failure. @retvalDB_SUCCESS_LOCKED_RECifthesecondaryrecordisequaltothe correspondingfieldsintheclusteredrecord,whencomparedwith collation; @retvalDB_SUCCESSifnotequaloriftheclusteredrecordhasbeenmarked
for deletion */ static
dberr_t
row_sel_sec_rec_is_for_clust_rec( const rec_t* sec_rec,
dict_index_t* sec_index, const rec_t* clust_rec,
dict_index_t* clust_index,
que_thr_t* thr)
{ if (rec_get_deleted_flag(clust_rec,
dict_table_is_comp(clust_index->table))) { /* In delete-marked records, DB_TRX_ID must
always refer to an existing undo log record. */
ut_ad(rec_get_trx_id(clust_rec, clust_index));
/* The clustered index record is delete-marked; itisnotvisibleinthereadview.Besides, ifthereareanyexternallystoredcolumns,
some of them may have already been purged. */ return DB_SUCCESS;
}
if (dict_index_is_spatial(sec_index)) { return row_sel_spatial_sec_rec_is_for_clust_rec(
sec_rec, sec_index, clust_rec,
clust_index);
}
n = dict_index_get_n_ordering_defined_by_user(sec_index);
for (i = 0; i < n; i++) { const dict_field_t* ifield; const dict_col_t* col;
ulint clust_pos = 0;
ulint clust_len = 0;
ulint len;
ifield = dict_index_get_nth_field(sec_index, i);
col = dict_field_get_col(ifield);
sec_field = rec_get_nth_field(sec_rec, sec_offs, i, &sec_len);
constbool is_virtual = col->is_virtual();
/* For virtual column, its value will need to be
reconstructed from base column in cluster index */ if (is_virtual) { const dict_v_col_t* v_col;
dfield_t* vfield;
row_ext_t* ext;
/*********************************************************************//**
Frees the memory private to a select node when a query graph is freed,
does not free the heap where the node was originally created. */ void
sel_node_free_private( /*==================*/
sel_node_t* node) /*!< in: select node struct */
{
ulint i;
plan_t* plan;
if (node->plans != NULL) { for (i = 0; i < node->n_tables; i++) {
plan = sel_node_get_nth_plan(node, i);
if (plan->old_vers_heap) {
mem_heap_free(plan->old_vers_heap);
}
}
}
}
/*********************************************************************//**
Evaluates the values in a select list. If there are aggregate functions,
their argument value is added to the aggregate total. */
UNIV_INLINE void
sel_eval_select_list( /*=================*/
sel_node_t* node) /*!< in: select node */
{
que_node_t* exp;
exp = node->select_list;
while (exp) {
eval_exp(exp);
exp = que_node_get_next(exp);
}
}
/*********************************************************************//**
Assigns the values in the select list to the possible into-variables in
SELECT ... INTO ... */
UNIV_INLINE void
sel_assign_into_var_values( /*=======================*/
sym_node_t* var, /*!< in: first variable in a list of
variables */
sel_node_t* node) /*!< in: select node */
{
que_node_t* exp;
if (var == NULL) {
return;
}
for (exp = node->select_list;
var != 0;
var = static_cast<sym_node_t*>(que_node_get_next(var))) {
ut_ad(exp);
eval_node_copy_val(var->alias, exp);
exp = que_node_get_next(exp);
}
}
/*********************************************************************//**
Resets the aggregate value totals in the select list of an aggregate type
query. */
UNIV_INLINE void
sel_reset_aggregate_vals( /*=====================*/
sel_node_t* node) /*!< in: select node */
{
func_node_t* func_node;
/*********************************************************************//**
Copies the input variable values when an explicit cursor is opened. */
UNIV_INLINE void
row_sel_copy_input_variable_vals( /*=============================*/
sel_node_t* node) /*!< in: select node */
{
sym_node_t* var;
var = UT_LIST_GET_FIRST(node->copy_variables);
while (var) {
eval_node_copy_val(var, var->alias);
var->indirection = NULL;
var = UT_LIST_GET_NEXT(col_var_list, var);
}
}
/*********************************************************************//**
Fetches the column values from a record. */ static void
row_sel_fetch_columns( /*==================*/
dict_index_t* index, /*!< in: record index */ const rec_t* rec, /*!< in: record in a clustered or non-clustered
index; must be protected by a page latch */ const rec_offs* offsets,/*!< in: rec_get_offsets(rec, index) */
sym_node_t* column) /*!< in: first column in a column list, or
NULL */
{
dfield_t* val;
ulint index_type;
ulint field_no; const byte* data;
ulint len;
while (column) {
mem_heap_t* heap = NULL;
ibool needs_copy;
field_no = column->field_nos[index_type];
if (field_no != ULINT_UNDEFINED) {
if (UNIV_UNLIKELY(rec_offs_nth_extern(
offsets, field_no) != 0)) {
/* Copy an externally stored field to the
temporary heap, if possible. */
heap = mem_heap_create(1);
data = btr_rec_copy_externally_stored_field(
rec, offsets,
index->table->space->zip_size(),
field_no, &len, heap);
/* data == NULL means that the externallystoredfieldwasnot writtenyet.Thisrecord shouldonlybeseenby trx_rollback_recovered()orany TRX_ISO_READ_UNCOMMITTED transactions.TheInnoDBSQLparser (thesolecallerofthisfunction) doesnotimplementREADUNCOMMITTED,
and it is not involved during rollback. */
ut_a(data);
ut_a(len != UNIV_SQL_NULL);
/*********************************************************************//**
Allocates a prefetch buffer for a column when prefetch is first time done. */ static void
sel_col_prefetch_buf_alloc( /*=======================*/
sym_node_t* column) /*!< in: symbol table node for a column */
{
sel_buf_t* sel_buf;
ulint i;
/*********************************************************************//**
Frees a prefetch buffer for a column, including the dynamically allocated
memory for data stored there. */ void
sel_col_prefetch_buf_free( /*======================*/
sel_buf_t* prefetch_buf) /*!< in, own: prefetch buffer */
{
sel_buf_t* sel_buf;
ulint i;
for (i = 0; i < SEL_MAX_N_PREFETCH; i++) {
sel_buf = prefetch_buf + i;
if (sel_buf->val_buf_size > 0) {
ut_free(sel_buf->data);
}
}
ut_free(prefetch_buf);
}
/*********************************************************************//**
Pops the column values for a prefetched, cached row from the column prefetch
buffers and places them to the val fields in the column nodes. */ static void
sel_dequeue_prefetched_row( /*=======================*/
plan_t* plan) /*!< in: plan node for a table */
{
sym_node_t* column;
sel_buf_t* sel_buf;
dfield_t* val;
byte* data;
ulint len;
ulint val_buf_size;
ut_ad(plan->n_rows_prefetched > 0);
column = UT_LIST_GET_FIRST(plan->columns);
while (column) {
val = que_node_get_val(column);
if (!column->copy_val) { /* We did not really push any value for the
column */
/*********************************************************************//**
Pushes the column values for a prefetched, cached row to the column prefetch
buffers from the val fields in the column nodes. */
UNIV_INLINE void
sel_enqueue_prefetched_row( /*=======================*/
plan_t* plan) /*!< in: plan node for a table */
{
sym_node_t* column;
sel_buf_t* sel_buf;
dfield_t* val;
byte* data;
ulint len;
ulint pos;
ulint val_buf_size;
/*********************************************************************//**
Builds a previous version of a clustered index record for a consistent read
@return DB_SUCCESS or error code */ static MY_ATTRIBUTE((nonnull, warn_unused_result))
dberr_t
row_sel_build_prev_vers( /*====================*/
ReadView* read_view, /*!< in: read view */
dict_index_t* index, /*!< in: plan node for table */
rec_t* rec, /*!< in: record in a clustered index */
rec_offs** offsets, /*!< in/out: offsets returned by
rec_get_offsets(rec, plan->index) */
mem_heap_t** offset_heap, /*!< in/out: memory heap from which
the offsets are allocated */
mem_heap_t** old_vers_heap, /*!< out: old version heap to use */
rec_t** old_vers, /*!< out: old version, or NULL if the recorddoesnotexistintheview: i.e.,itwasfreshlyinserted
afterwards */
mtr_t* mtr) /*!< in: mtr */
{
dberr_t err;
/*********************************************************************//**
Builds the last committed version of a clustered index record for a
semi-consistent read. */ static void
row_sel_build_committed_vers_for_mysql( /*===================================*/
dict_index_t* clust_index, /*!< in: clustered index */
row_prebuilt_t* prebuilt, /*!< in: prebuilt struct */ const rec_t* rec, /*!< in: record in a clustered index */
rec_offs** offsets, /*!< in/out: offsets returned by
rec_get_offsets(rec, clust_index) */
mem_heap_t** offset_heap, /*!< in/out: memory heap from which
the offsets are allocated */ const rec_t** old_vers, /*!< out: old version, or NULL if the recorddoesnotexistintheview: i.e.,itwasfreshlyinserted
afterwards */
dtuple_t** vrow, /*!< out: to be filled with old virtual
column version if any */
mtr_t* mtr) /*!< in: mtr */
{ if (prebuilt->trx->snapshot_isolation) {
ut_ad(prebuilt->trx->isolation_level
== TRX_ISO_READ_UNCOMMITTED);
*old_vers = rec; return;
}
/*********************************************************************//**
Tests the conditions which determine when the index segment we are searching
through has been exhausted.
@returnTRUEif row passed the tests */
UNIV_INLINE
ibool
row_sel_test_end_conds( /*===================*/
plan_t* plan) /*!< in: plan for the table; the column values must alreadyhavebeenretrievedandtherightsidesof
comparisons evaluated */
{
func_node_t* cond;
/* All conditions in end_conds are comparisons of a column to an
expression */
/* Evaluate the left side of the comparison, i.e., get the
column value if there is an indirection */
eval_sym(static_cast<sym_node_t*>(cond->args));
/* Do the comparison */
if (!eval_cmp(cond)) {
return(FALSE);
}
}
return(TRUE);
}
/*********************************************************************//**
Tests the other conditions.
@returnTRUEif row passed the tests */
UNIV_INLINE
ibool
row_sel_test_other_conds( /*=====================*/
plan_t* plan) /*!< in: plan for the table; the column values must
already have been retrieved */
{
func_node_t* cond;
cond = UT_LIST_GET_FIRST(plan->other_conds);
while (cond) {
eval_exp(cond);
if (!eval_node_get_ibool_val(cond)) {
return(FALSE);
}
cond = UT_LIST_GET_NEXT(cond_list, cond);
}
return(TRUE);
}
/** Check that a clustered index record is visible in a consistent read view. @paramrecclusteredindexrecord(inleafpage,orinmemory) @paramindexclusteredindex @paramoffsetsrec_get_offsets(rec,index) @paramviewconsistentreadview @retvalDB_SUCCESSifrecisvisibleinview @retvalDB_SUCCESS_LOCKED_RECifrecisnotvisibleinview
@retval DB_CORRUPTION if the DB_TRX_ID is corrupted */ static dberr_t row_sel_clust_sees(const rec_t *rec, const dict_index_t &index, const rec_offs *offsets, const ReadView &view)
{
ut_ad(index.is_primary());
ut_ad(page_rec_is_user_rec(rec));
ut_ad(rec_offs_validate(rec, &index, offsets));
ut_ad(!rec_is_metadata(rec, index));
ut_ad(!index.table->is_temporary());
if (view.changes_visible(id)) return DB_SUCCESS; if (UNIV_LIKELY(id < view.low_limit_id() || id < trx_sys.get_max_trx_id())) return DB_SUCCESS_LOCKED_REC;
ib::warn() << "A transaction id in a record of table " << index.table->name
<< " is newer than the system-wide maximum."; return DB_CORRUPTION;
}
/*********************************************************************//**
Retrieves the clustered index record corresponding to a record in a
non-clustered index. Does the necessary locking.
@return DB_SUCCESS or error code */ static MY_ATTRIBUTE((nonnull, warn_unused_result))
dberr_t
row_sel_get_clust_rec( /*==================*/
sel_node_t* node, /*!< in: select_node */
plan_t* plan, /*!< in: plan node for table */
rec_t* rec, /*!< in: record in a non-clustered index */
que_thr_t* thr, /*!< in: query thread */
rec_t** out_rec,/*!< out: clustered record or an old version of it,NULLiftheoldversiondidnotexist inthereadview,i.e.,itwasafresh
inserted version */
mtr_t* mtr) /*!< in: mtr used to get access to the non-clusteredrecord;thesamemtrisusedto
access the clustered index */
{
dict_index_t* index;
rec_t* clust_rec;
rec_t* old_vers;
mem_heap_t* heap = NULL;
rec_offs offsets_[REC_OFFS_NORMAL_SIZE];
rec_offs* offsets = offsets_;
rec_offs_init(offsets_);
/* Note: only if the search ends up on a non-infimum record is the
low_match value the real match to the search tuple */
if (!page_rec_is_user_rec(clust_rec)
|| btr_pcur_get_low_match(&(plan->clust_pcur))
< dict_index_get_n_unique(index)) {
if (!node->read_view ||
!rec_get_deleted_flag(rec, plan->table->not_redundant())) {
err = DB_CORRUPTION;
}
/* In a rare case it is possible that no clust rec is found foradelete-markedsecondaryindexrecord:ifinrow0umod.cc inrow_undo_mod_remove_clust_low()wehavealreadyremoved theclustrec,whilepurgeisstillcleaningandremoving secondaryindexrecordsassociatedwithearlierversionsof theclusteredindexrecord.Inthatcaseweknowthatthe clusteredindexrecorddidnotexistinthereadviewof
trx. */
if (!node->read_view) { /* Try to place a lock on the index record */
trx_t* trx = thr_get_trx(thr);
/* At READ UNCOMMITTED or READ COMMITTED isolation level welockonlytherecord,i.e.,next-keylockingis
not used. */
err = lock_clust_rec_read_check_and_lock( 0, btr_pcur_get_block(&plan->clust_pcur),
clust_rec, index, offsets,
node->row_lock_mode,
trx->isolation_level <= TRX_ISO_READ_COMMITTED
? LOCK_REC_NOT_GAP : LOCK_ORDINARY,
thr);
switch (err) { case DB_SUCCESS: case DB_SUCCESS_LOCKED_REC: /* Declare the variable uninitialized.
It should be set to DB_SUCCESS at func_exit. */
MEM_UNDEFINED(&err, sizeof err); break; default: goto err_exit;
}
} else { /* This is a non-locking consistent read: if necessary, fetch
a previous version of the record */
/* If we had to go to an earlier version of row or the secondaryindexrecordisdeletemarked,thenitmaybethat thesecondaryindexrecordcorrespondingtoclust_rec (orold_vers)isnotrec;inthatcasewemustignore suchrowbecauseinoursnapshotrecwouldnothaveexisted. Rememberthatfromrecwecannotseedirectlywhichtransaction idcorrespondstoit:wehavetogototheclusteredindex record.Aquerywherewewanttofetchallrowswhere thesecondaryindexvalueisinsomeintervalwouldreturn awrongresultifwewouldnotdroprowswhichwecometo visitthroughsecondaryindexrecordsthatwouldnotreally
exist in our snapshot. */
/* Fetch the columns needed in test conditions. The clustered indexrecordisprotectedbyapagelatchthatwasacquired whenplan->clust_pcurwaspositioned.Thelatchwillnotbe
released until mtr->commit(). */
if (!page_is_leaf(buf_block_get_frame(cur_block))) { /* Page got splitted and promoted (only for rootpageitispossible).Releasethe
page and ask for a re-search */
mtr->commit();
mtr->start();
err = DB_RECORD_NOT_FOUND; goto func_end;
}
/*********************************************************************//**
Restores a stored pcur position to a table index.
@returnTRUEif the cursor should be moved to the next record after we return from this function (moved to the previous, in the case of a
descending cursor) without processing again the current cursor
record */ static
ibool
row_sel_restore_pcur_pos( /*=====================*/
plan_t* plan, /*!< in: table plan */
mtr_t* mtr) /*!< in: mtr */
{
ibool equal_position;
ulint relative_position;
/* If the cursor is traveling upwards, and relative_position is
(1)BTR_PCUR_BEFORE:thisisnotallowed,aswedidnothavealock yetonthesuccessorofthepageinfimum; (2)BTR_PCUR_AFTER:btr_pcur_restore_positionplacedthecursoronthe firstrecordGREATERthanthepredecessorofapagesupremum;wehave notyetprocessedthecursorrecord:noneedtomovethecursortothe nextrecord; (3)BTR_PCUR_ON:btr_pcur_restore_positionplacedthecursoronthe lastrecordLESSorEQUALtotheoldstoreduserrecord;(a)if equal_positionisFALSE,thismeansthatthecursorisnowonarecord lessthantheolduserrecord,andwemustmovetothenextrecord; (b)ifequal_positionisTRUE,thenif plan->stored_cursor_rec_processedisTRUE,wemustmovetothenext
record, else there is no need to move the cursor. */
if (plan->asc) { if (relative_position == BTR_PCUR_ON) {
/* If the cursor is traveling downwards, and relative_position is
(1)BTR_PCUR_BEFORE:btr_pcur_restore_positionplacedthecursoron thelastrecordLESSthanthesuccessorofapageinfimum;wehavenot processedthecursorrecord:noneedtomovethecursor; (2)BTR_PCUR_AFTER:btr_pcur_restore_positionplacedthecursoronthe firstrecordGREATERthanthepredecessorofapagesupremum;wehave processedthecursorrecord:weshouldmovethecursortotheprevious record; (3)BTR_PCUR_ON:btr_pcur_restore_positionplacedthecursoronthe lastrecordLESSorEQUALtotheoldstoreduserrecord;(a)if equal_positionisFALSE,thismeansthatthecursorisnowonarecord lessthantheolduserrecord,andweneednotmovetotheprevious record;(b)ifequal_positionisTRUE,thenif plan->stored_cursor_rec_processedisTRUE,wemustmovetotheprevious
record, else there is no need to move the cursor. */
if (relative_position == BTR_PCUR_BEFORE
|| relative_position == BTR_PCUR_BEFORE_FIRST_IN_TREE) {
/*********************************************************************//**
Resets a plan cursor to a closed state. */
UNIV_INLINE void
plan_reset_cursor( /*==============*/
plan_t* plan) /*!< in: plan */
{
plan->pcur_is_open = FALSE;
plan->cursor_at_end = FALSE;
plan->n_rows_fetched = 0;
plan->n_rows_prefetched = 0;
}
#ifdef BTR_CUR_HASH_ADAPT /*********************************************************************//**
Tries to do a shortcut to fetch a clustered index record with a unique key, using the hash index if possible (not always).
@return SEL_FOUND, SEL_EXHAUSTED, SEL_RETRY */ static
ulint
row_sel_try_search_shortcut( /*========================*/
sel_node_t* node, /*!< in: select node for a consistent read */
plan_t* plan, /*!< in: plan for a unique search in clustered
index */
mtr_t* mtr) /*!< in: mtr */
{
dict_index_t* index = plan->index;
if (!page_rec_is_user_rec(rec) || rec_is_metadata(rec, *index)) { return SEL_RETRY;
}
ut_ad(plan->mode == PAGE_CUR_GE);
/* As the cursor is now placed on a user record after a search with themodePAGE_CUR_GE,theup_matchfieldinthecursortellshowmany
fields in the user record matched to the search tuple */
if (btr_pcur_get_up_match(&(plan->pcur)) < plan->n_exact_match) { return SEL_EXHAUSTED;
}
if (trx_id_t bulk_trx_id = index->table->bulk_trx_id) { /* See row_search_mvcc() for a comment on bulk_trx_id */ if (!node->read_view->changes_visible(bulk_trx_id)) { return SEL_EXHAUSTED;
}
}
/* This is a non-locking consistent read: if necessary, fetch
a previous version of the record */
if (dict_index_is_clust(index)) { if (row_sel_clust_sees(rec, *index, offsets, *node->read_view)
!= DB_SUCCESS) { return SEL_RETRY;
}
} elseif (!srv_read_only_mode) {
trx_id_t trx_id =
page_get_max_trx_id(btr_pcur_get_page(&plan->pcur));
ut_ad(trx_id); if (!node->read_view->sees(trx_id)) { return SEL_RETRY;
}
}
if (rec_get_deleted_flag(rec, dict_table_is_comp(plan->table))) { return SEL_EXHAUSTED;
}
/* Fetch the columns needed in test conditions. The index recordisprotectedbyapagelatchthatwasacquiredwhen plan->pcurwaspositioned.Thelatchwillnotbereleased
until mtr->commit(). */
/* The following flag becomes TRUE when we are doing a consistentreadfromanon-clusteredindexandwemustlook attheclusteredindextofindoutthepreviousdeletemark
state of the non-clustered record: */
ibool cons_read_requires_clust_rec = FALSE;
ulint cost_counter = 0;
ibool cursor_just_opened;
ibool must_go_to_next;
ibool mtr_has_extra_clust_latch = FALSE; /* TRUE if the search was made using anon-clusteredindex,andwehadto accesstheclusteredrecord:now&mtr containsaclusteredindexlatch,and &mtrmustbecommittedbeforewemove
to the next non-clustered record */
dberr_t err;
mem_heap_t* heap = NULL;
rec_offs offsets_[REC_OFFS_NORMAL_SIZE];
rec_offs* offsets = offsets_;
rec_offs_init(offsets_); const trx_t* trx = thr_get_trx(thr);
table_loop: /* TABLE LOOP ---------- Thisistheoutermajorloopincalculatingajoin.Wecomeherewhen node->fetch_tablechanges,andafteraddingarowtoaggregatetotals
and, of course, when this function is called. */
ut_ad(mtr_has_extra_clust_latch == FALSE);
plan = sel_node_get_nth_plan(node, node->fetch_table);
index = plan->index;
if (plan->n_rows_prefetched > 0) {
sel_dequeue_prefetched_row(plan);
goto next_table_no_mtr;
}
if (plan->cursor_at_end) { /* The cursor has already reached the result set end: no more rowstoprocessforthistablecursor,asalsotheprefetch
stack was empty */
ut_ad(plan->pcur_is_open);
goto table_exhausted_no_mtr;
}
/* Open a cursor to index, or restore an open cursor position */
mtr.start();
#ifdef BTR_CUR_HASH_ADAPT if (node->read_view && plan->unique_search && !plan->pcur_is_open
&& !plan->must_get_clust) { switch (row_sel_try_search_shortcut(node, plan, &mtr)) { case SEL_FOUND: goto next_table; case SEL_EXHAUSTED: goto table_exhausted; default:
ut_ad(0); /* fall through */ case SEL_RETRY: break;
}
if (must_go_to_next) { /* We have already processed the cursor record: move
to the next */
goto next_rec;
}
}
if (!node->read_view
|| trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {
} elseif (trx_id_t bulk_trx_id = index->table->bulk_trx_id) { /* See row_search_mvcc() for a comment on bulk_trx_id */ if (!trx->read_view.changes_visible(bulk_trx_id)) { goto table_exhausted;
}
}
NOTEthatifcursor_just_openedisTRUEhere,itmeansthatwecame
to this point right after row_sel_open_pcur. */
ut_ad(mtr_has_extra_clust_latch == FALSE);
rec = btr_pcur_get_rec(&(plan->pcur));
/* PHASE 1: Set a lock if specified */
if (!node->asc && cursor_just_opened
&& !page_rec_is_supremum(rec)) {
/* Do not support "descending search" for Spatial index */
ut_ad(!dict_index_is_spatial(index));
/* When we open a cursor for a descending search, we must set anext-keylockonthesuccessorrecord:otherwiseitwould bepossibletoinsertnewrecordsnexttothecursorposition, anditmightbethatthesenewrecordsshouldappearinthe
search result set, resulting in the phantom problem. */
if (!node->read_view) { const rec_t* next_rec = page_rec_get_next_const(rec); if (UNIV_UNLIKELY(!next_rec)) {
err = DB_CORRUPTION; goto lock_wait_or_error;
} unsigned lock_type;
/* At READ UNCOMMITTED or READ COMMITTED isolationlevel,welockonlytherecord,
i.e., next-key locking is not used. */ if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { if (page_rec_is_supremum(next_rec)) { goto skip_lock;
}
switch (err) { case DB_SUCCESS_LOCKED_REC:
err = DB_SUCCESS; /* fall through */ case DB_SUCCESS: break; default: /* Note that in this case we will store in pcur thePREDECESSORoftherecordwearewaiting
the lock for */ goto lock_wait_or_error;
}
}
}
skip_lock: if (page_rec_is_infimum(rec)) {
/* The infimum record on a page cannot be in the result set, andneithercanarecordlockbeplacedonit:weskipsuch arecord.Wealsoincrementthecostcounteraswemayhave
processed yet another page of index. */
cost_counter++;
goto next_rec;
}
if (rec_is_metadata(rec, *index)) { /* Skip the metadata pseudo-record. */
cost_counter++; goto next_rec;
}
if (!node->read_view) { /* Try to place a lock on the index record */ unsigned lock_type;
/* At READ UNCOMMITTED or READ COMMITTED isolation level, welockonlytherecord,i.e.,next-keylockingis
not used. */ if (trx->isolation_level <= TRX_ISO_READ_COMMITTED
|| dict_index_is_spatial(index)) {
switch (err) { case DB_SUCCESS_LOCKED_REC:
err = DB_SUCCESS; /* fall through */ case DB_SUCCESS: break; default: goto lock_wait_or_error;
}
}
if (page_rec_is_supremum(rec)) {
/* A page supremum record cannot be in the result set: skip
it now when we have placed a possible lock on it */
goto next_rec;
}
ut_ad(page_rec_is_user_rec(rec));
if (cost_counter > SEL_COST_LIMIT) {
/* Now that we have placed the necessary locks, we can stop forawhileandstorethecursorposition;NOTEthatifwe wouldstorethecursorpositionBEFOREplacingarecordlock, itmighthappenthatthecursorwouldjumpoversomerecords thatanothertransactioncouldmeanwhileinsertadjacentto
the cursor: this would result in the phantom problem. */
goto stop_for_a_while;
}
/* PHASE 2: Check a mixed index mix id if needed */
if (plan->unique_search && cursor_just_opened) {
ut_ad(plan->mode == PAGE_CUR_GE);
/* As the cursor is now placed on a user record after a search withthemodePAGE_CUR_GE,theup_matchfieldinthecursor tellshowmanyfieldsintheuserrecordmatchedtothesearch
tuple */
if (btr_pcur_get_up_match(&(plan->pcur))
< plan->n_exact_match) { goto table_exhausted;
}
/* Ok, no need to test end_conds or mix id */
}
/* We are ready to look at a possible new index entry in the result
set: the cursor is now placed on a user record */
/* PHASE 3: Get previous version in a consistent read */
if (node->read_view) { /* This is a non-locking consistent read: if necessary, fetch
a previous version of the record */
if (dict_index_is_clust(index)) { const trx_id_t id = row_get_rec_trx_id(
rec, index, offsets);
if (!node->read_view->changes_visible(id)) { if (id >= node->read_view->low_limit_id()
&& id >= trx_sys.get_max_trx_id()) {
err = DB_CORRUPTION; goto lock_wait_or_error;
}
if (old_vers == NULL) { /* The record does not exist inourreadview.Skipit,but firstattempttodetermine whethertheindexsegmentwe aresearchingthroughhasbeen
exhausted. */
/* Fetch the columns needed in testconditions.Theclustered indexrecordisprotectedbya pagelatchthatwasacquired byrow_sel_open_pcur()or row_sel_restore_pcur_pos(). Thelatchwillnotbereleased
until mtr.commit(). */
/* PHASE 4: Test search end conditions and deleted flag */
/* Fetch the columns needed in test conditions. The record is protectedbyapagelatchthatwasacquiredby row_sel_open_pcur()orrow_sel_restore_pcur_pos().Thelatch
will not be released until mtr.commit(). */
/* Retrieving the clustered record required a search:
increment the cost counter */
cost_counter++;
if (clust_rec == NULL) { /* The record did not exist in the read view */
ut_ad(node->read_view);
goto next_rec;
}
if (rec_get_deleted_flag(clust_rec,
dict_table_is_comp(plan->table))) { /* In delete-marked records, DB_TRX_ID must
always refer to an existing update_undo log record. */
ut_ad(rec_get_trx_id(clust_rec,
dict_table_get_first_index(
plan->table)));
/* PHASE 7: We found a new qualifying row for the current table; push
the row if prefetch is on, or move to the next table in the join */
plan->n_rows_fetched++;
ut_ad(plan->pcur.latch_mode == BTR_SEARCH_LEAF);
if ((plan->n_rows_fetched <= SEL_PREFETCH_LIMIT)
|| plan->unique_search || plan->no_prefetch) {
/* No prefetch in operation: go to the next table */
goto next_table;
}
sel_enqueue_prefetched_row(plan);
if (plan->n_rows_prefetched == SEL_MAX_N_PREFETCH) {
/* The prefetch buffer is now full */
sel_dequeue_prefetched_row(plan);
goto next_table;
}
next_rec: if (mtr_has_extra_clust_latch) {
/* We must commit &mtr if we are moving to the next non-clusteredindexrecord,becausewecouldbreakthe latchingorderifwewouldaccessadifferentclustered
index page right away without releasing the previous. */
goto commit_mtr_for_a_while;
}
if (node->asc) {
moved = btr_pcur_move_to_next(&(plan->pcur), &mtr);
} else {
moved = btr_pcur_move_to_prev(&(plan->pcur), &mtr);
}
if (!moved) {
goto table_exhausted;
}
cursor_just_opened = FALSE;
/* END OF RECORD LOOP
------------------ */ goto rec_loop;
next_table: /* We found a record which satisfies the conditions: we can move to
the next table or return a row in the result set */
ut_ad(btr_pcur_is_on_user_rec(&plan->pcur));
if (plan->unique_search && !node->can_get_updated) {
next_table_no_mtr: /* If we use 'goto' to this label, it means that the row was popped
from the prefetched rows stack, and &mtr is already committed */
table_exhausted: /* The table cursor pcur reached the result set end: backtrack to the
previous table in the join if we do not have cached prefetched rows */
plan->cursor_at_end = TRUE;
mtr.commit();
mtr_has_extra_clust_latch = FALSE;
if (plan->n_rows_prefetched > 0) { /* The table became exhausted during a prefetch */
sel_dequeue_prefetched_row(plan);
goto next_table_no_mtr;
}
table_exhausted_no_mtr: if (node->fetch_table == 0) {
err = DB_SUCCESS;
if (node->is_aggregate && !node->aggregate_already_fetched) {
stop_for_a_while: /* Return control for a while to que_run_threads, so that runaway queriescanbecanceled.NOTEthatwhenwecomehere,wemust,ina lockingread,haveplacedthenecessary(possiblywaitingrequest) recordlockonthecursorrecordoritssuccessor:whenwereposition thecursor,thisrecordlockguaranteesthatnobodycanmeanwhilehave insertednewrecordswhichshouldhaveappearedintheresultset,
which would result in the phantom problem. */
commit_mtr_for_a_while: /* Stores the cursor position and commits &mtr; this is used if &mtrmaycontainlatcheswhichwouldbreakthelatchingorderif
&mtr would not be committed and the latches released. */
plan->stored_cursor_rec_processed = TRUE;
btr_pcur_store_position(&(plan->pcur), &mtr);
mtr.commit();
mtr_has_extra_clust_latch = FALSE;
goto table_loop;
lock_wait_or_error: /* See the note at stop_for_a_while: the same holds for this case */
func_exit: if (heap != NULL) {
mem_heap_free(heap);
} return(err);
}
/**********************************************************************//**
Performs a select step. This is a high-level function used in SQL execution
graphs.
@return query thread to run next or NULL */
que_thr_t*
row_sel_step( /*=========*/
que_thr_t* thr) /*!< in: query thread */
{
sel_node_t* node;
/* If this is a new time this node is executed (or when execution resumesafterwaitforatableintentionlock),setintentionlocks
on the tables, or assign a read view */
if (node->into_list && (thr->prev_node == que_node_get_parent(node))) {
node->state = SEL_NODE_OPEN;
}
if (node->state == SEL_NODE_OPEN) {
/* It may be that the current session has not yet started
its transaction, or it has been committed: */
/* If this is an explicit cursor, copy stored procedure variablevalues,sothatthevaluescannotchangebetween fetches(currently,wecopythemalsofornon-explicit
cursors) */
if (node->explicit_cursor
&& UT_LIST_GET_FIRST(node->copy_variables)) {
if (node->is_aggregate) { /* Reset the aggregate total values */
sel_reset_aggregate_vals(node);
}
}
dberr_t err = row_sel(node, thr);
/* NOTE! if queries are parallelized, the following assignment may haveproblems;theassignmentshouldbemadeonlyifthristhe
only top-level thr in the graph: */
thr->graph->last_sel_node = node;
if (err != DB_SUCCESS) {
thr_get_trx(thr)->error_state = err;
return(NULL);
}
return(thr);
}
/**********************************************************************//**
Performs a fetch for a cursor.
@return query thread to run next or NULL */
que_thr_t*
fetch_step( /*=======*/
que_thr_t* thr) /*!< in: query thread */
{
sel_node_t* sel_node;
fetch_node_t* node;
if (thr->prev_node != que_node_get_parent(node)) {
if (sel_node->state != SEL_NODE_NO_MORE_ROWS) {
if (node->into_list) {
sel_assign_into_var_values(node->into_list,
sel_node);
} else {
ibool ret = (*node->func->func)(
sel_node, node->func->arg);
if (!ret) {
sel_node->state
= SEL_NODE_NO_MORE_ROWS;
}
}
}
thr->run_node = que_node_get_parent(node);
return(thr);
}
/* Make the fetch node the parent of the cursor definition for thetimeofthefetch,sothatexecutionknowstoreturntothis fetchnodeafterarowhasbeenselectedorweknowthatthereis
no row left */
sel_node->common.parent = node;
if (sel_node->state == SEL_NODE_CLOSED) {
ib::error() << "fetch called on a closed cursor";
thr_get_trx(thr)->error_state = DB_ERROR;
return(NULL);
}
thr->run_node = sel_node;
return(thr);
}
/***********************************************************//**
Prints a row in a select result.
@return query thread to run next or NULL */
que_thr_t*
row_printf_step( /*============*/
que_thr_t* thr) /*!< in: query thread */
{
row_printf_node_t* node;
sel_node_t* sel_node;
que_node_t* arg;
if (thr->prev_node == que_node_get_parent(node)) {
/* Reset the cursor */
sel_node->state = SEL_NODE_OPEN;
/* Fetch next row to print */
thr->run_node = sel_node;
return(thr);
}
if (sel_node->state != SEL_NODE_FETCH) {
ut_ad(sel_node->state == SEL_NODE_NO_MORE_ROWS);
/* No more rows to print */
thr->run_node = que_node_get_parent(node);
return(thr);
}
arg = sel_node->select_list;
while (arg) {
dfield_print_also_hex(que_node_get_val(arg));
fputs(" ::: ", stderr);
arg = que_node_get_next(arg);
}
putc('\n', stderr);
/* Fetch next row to print */
thr->run_node = sel_node;
return(thr);
}
/****************************************************************//**
Converts a key value stored in MySQL format to an Innobase dtuple. The last
field of the key value may be just a prefix of a fixed length field: hence
the parameter key_len. But currently we donot allow search keys where the
last field is only a prefix of the full key field len and print a warning if
such appears. A counterpart of this function is
ha_innobase::store_key_val_for_row() in ha_innodb.cc. */ void
row_sel_convert_mysql_key_to_innobase( /*==================================*/
dtuple_t* tuple, /*!< in/out: tuple where to build; NOTE:weassumethatthetypeinfo inthetupleisalreadyaccording
to index! */
byte* buf, /*!< in: buffer to use in field conversions;NOTEthatdtuple->data mayenduppointinginsidebufso donotdiscardthatbufferwhile thetupleisbeingused.See row_mysql_store_col_in_innobase_format()
in the case of DATA_INT */
ulint buf_len, /*!< in: buffer length */
dict_index_t* index, /*!< in: index of the key value */ const byte* key_ptr, /*!< in: MySQL key value */
ulint key_len) /*!< in: MySQL key value length */
{
byte* original_buf = buf; const byte* original_key_ptr = key_ptr;
dict_field_t* field;
dfield_t* dfield;
ulint data_offset;
ulint data_len;
ulint data_field_len;
ibool is_null; const byte* key_end;
ulint n_fields = 0;
/* For documentation of the key value storage format in MySQL, see
ha_innobase::store_key_val_for_row() in ha_innodb.cc. */
key_end = key_ptr + key_len;
/* Permit us to access any field in the tuple: */
ut_d(dtuple_set_n_fields(tuple, uint16_t(~0)));
dfield = dtuple_get_nth_field(tuple, 0);
field = dict_index_get_nth_field(index, 0);
if (UNIV_UNLIKELY(dfield_get_type(dfield)->mtype == DATA_SYS)) { /* A special case: we are looking for a position in the generatedclusteredindexwhichInnoDBautomaticallyadded toatablewithnoprimarykey:thefirstandtheonly orderingcolumnisROW_IDwhichInnoDBstoredtothekey_ptr
buffer. */
ulint type = dfield_get_type(dfield)->mtype;
ut_a(field->col->mtype == type);
data_offset = 0;
is_null = FALSE;
if (!(dfield_get_type(dfield)->prtype & DATA_NOT_NULL)) { /* The first byte in the field tells if this is
an SQL NULL value */
data_offset = 1;
if (*key_ptr != 0) {
dfield_set_null(dfield);
is_null = TRUE;
}
}
/* Calculate data length and data field total length */ if (DATA_LARGE_MTYPE(type) || DATA_GEOMETRY_MTYPE(type)) {
/* For R-tree index, data length should be the
total size of the wkb data.*/ if (dict_index_is_spatial(index)) {
ut_ad(DATA_GEOMETRY_MTYPE(type));
data_len = key_len;
data_field_len = data_offset + data_len;
} else { /* The key field is a column prefix of a BLOB
or TEXT. */
ut_a(field->prefix_len > 0);
/* MySQL stores the actual data length to the first2bytesaftertheoptionalSQLNULL markerbyte.Thestorageformatis little-endian,thatis,themostsignificant byteatahigheraddress.InUTF-8,MySQL seemstoreservefield->prefix_lenbytesfor storingthisfieldinthekeyvaluebuffer, eventhoughtheactualvalueonlytakesdata
len bytes from the start. */
/* Now that we know the length, we store the columnvaluelikeitwouldbeafixedchar
field */
}
} elseif (field->prefix_len > 0) { /* Looks like MySQL pads unused end bytes in the prefixwithspace.Therefore,alsoinUTF-8,itisok tocomparewithaprefixcontainingfullprefix_len bytes,andnoneedtotakeatmostprefix_len/3 UTF-8charactersfromthestart. IftheprefixisusedastheupperendofaLIKE 'abc%'query,thenMySQLpadstheendwithchars 0xff.TODO:inthatcasedoesitanyharmtocompare withthefullprefix_lenbytes.Howdocharacters
0xff in UTF-8 behave? */
if ((dtype_get_mysql_type(dfield_get_type(dfield))
== DATA_MYSQL_TRUE_VARCHAR)
&& (type != DATA_INT)) { /* In a MySQL key value format, a true VARCHAR is alwaysprecededby2bytesofalengthfield. dfield_get_type(dfield)->lenreturnsthemaximum 'payload'leninbytes.Thatdoesnotincludethe 2bytesthattelltheactualdatalength.
Weaddedthecheck!=DATA_INTtomakesurewedo
not treat MySQL ENUM or SET as a true VARCHAR! */
data_len += 2;
data_field_len += 2;
}
/* Storing may use at most data_len bytes of buf */
if (UNIV_LIKELY(!is_null)) {
buf = row_mysql_store_col_in_innobase_format(
dfield, buf, FALSE, /* MySQL key value format col */
key_ptr + data_offset, data_len,
dict_table_is_comp(index->table));
ut_a(buf <= original_buf + buf_len);
}
key_ptr += data_field_len;
if (UNIV_UNLIKELY(key_ptr > key_end)) { /* The last field in key was not a complete key field butaprefixofit.
Printawarningaboutthis!HA_READ_PREFIX_LASTdoes notcurrentlyworkinInnoDBwithpartial-fieldkey valueprefixes.SinceMySQLcurrentlyusesapadding tricktocalculateLIKE'abc%'typequeriesthere
should never be partial-field prefixes in searches. */
ib::warn() << "Using a partial-field key prefix in" " search, index " << index->name
<< " of table " << index->table->name
<< ". Last data field length "
<< data_field_len << " bytes, key ptr now" " exceeds key end by " << (key_ptr - key_end)
<< " bytes. Key value in the MariaDB format:";
if (!is_null) {
ulint len = dfield_get_len(dfield);
dfield_set_len(dfield, len
- (ulint) (key_ptr - key_end));
}
ut_ad(0);
}
n_fields++;
field++;
dfield++;
}
ut_a(buf <= original_buf + buf_len);
/* We set the length of tuple to n_fields: we assume that the memory
area allocated for it is big enough (usually bigger than n_fields). */
dtuple_set_n_fields(tuple, uint16_t(n_fields));
}
/**************************************************************//**
Stores a non-SQL-NULL field in the MySQL format. The counterpart of this
function is row_mysql_store_col_in_innobase_format() in row0mysql.cc. */ void
row_sel_field_store_in_mysql_format_func(
byte* dest, const mysql_row_templ_t* templ, #ifdef UNIV_DEBUG const dict_index_t* index,
ulint field_no, #endif/* UNIV_DEBUG */ const byte* data,
ulint len)
{ #ifdef UNIV_DEBUG const dict_field_t* field
= templ->is_virtual
? NULL : dict_index_get_nth_field(index, field_no); #endif/* UNIV_DEBUG */
switch (templ->type) { const byte* field_end; case DATA_VARCHAR: case DATA_VARMYSQL: case DATA_BINARY:
field_end = dest + templ->mysql_col_len;
if (templ->mysql_type == DATA_MYSQL_TRUE_VARCHAR) { /* This is a >= 5.0.3 type true VARCHAR. Store the lengthofthedatatothefirstbyteorthefirst
two bytes of dest. */
dest = row_mysql_store_true_var_len(
dest, len, templ->mysql_length_bytes); /* Copy the actual data. Leave the rest of the
buffer uninitialized. */
memcpy(dest, data, len); break;
}
/* Copy the actual data */
memcpy(dest, data, len);
/* We treat some Unicode charset strings specially. */ switch (templ->mbminlen) { case4: /* InnoDB should never have stripped partial
UTF-32 characters. */
ut_a(!(len & 3)); break; case2: /* A space char is two bytes,
0x0020 in UCS2 and UTF-16 */
if (UNIV_UNLIKELY(len & 1)) { /* A 0x20 has been stripped from the column.
Pad it back. */
case DATA_GEOMETRY: /* We store all geometry data as BLOB data at server layer. */
row_mysql_store_geometry(dest, templ->mysql_col_len, data, len); break;
if (templ->mbminlen == 1 && templ->mbmaxlen != 1) { /* Pad with spaces. This undoes the stripping doneinrow0mysql.cc,function
row_mysql_store_col_in_innobase_format(). */
default: #ifdef UNIV_DEBUG case DATA_SYS_CHILD: case DATA_SYS: /* These column types should never be shipped to MySQL. */
ut_ad(0); /* fall through */
case DATA_CHAR: case DATA_FIXBINARY: case DATA_FLOAT: case DATA_DOUBLE: case DATA_DECIMAL: #endif/* UNIV_DEBUG */
ut_ad((templ->is_virtual && !field)
|| (field && field->prefix_len
? field->prefix_len == len
: templ->mysql_col_len == len));
memcpy(dest, data, len); break;
case DATA_INT: /* Convert InnoDB big-endian integer to little-endian
format, sign bit restored to 2's complement form */
DBUG_ASSERT(templ->mysql_col_len == len);
byte* ptr = pad; do *--ptr = *data++; while (ptr != dest); if (!templ->is_unsigned) {
pad[-1] ^= 0x80;
}
}
}
/** Convert a field in the Innobase format to a field in the MySQL format. @param[out]mysql_recrecordintheMySQLformat @param[in,out]prebuiltprebuiltstruct @param[in]recInnoDBrecord;mustbeprotected byapagelatch @param[in]indexindexofrec @param[in]offsetsarrayreturnedbyrec_get_offsets() @param[in]field_notempl->rec_field_noor templ->clust_rec_field_no ortempl->icp_rec_field_no @param[in]templrowtemplate
*/ static MY_ATTRIBUTE((warn_unused_result))
ibool
row_sel_store_mysql_field(
byte* mysql_rec,
row_prebuilt_t* prebuilt, const rec_t* rec, const dict_index_t* index, const rec_offs* offsets,
ulint field_no, const mysql_row_templ_t*templ)
{
DBUG_ENTER("row_sel_store_mysql_field_func");
/* NOTE: if we are retrieving a big BLOB, we may alreadyrunoutofmemoryinthenextcall,which
causes an assert */
data = btr_rec_copy_externally_stored_field(
rec, offsets, prebuilt->table->space->zip_size(),
field_no, &len, heap);
if (UNIV_UNLIKELY(!data)) { /* The externally stored field was not written yet.Thisrecordshouldonlybeseenby trx_rollback_recovered()orany
TRX_ISO_READ_UNCOMMITTED transactions. */
if (heap != prebuilt->blob_heap) {
mem_heap_free(heap);
}
if (heap != prebuilt->blob_heap) {
mem_heap_free(heap);
}
} else { /* The field is stored in the index record, or
in the metadata for instant ADD COLUMN. */
data = rec_get_nth_cfield(rec, index, offsets, field_no, &len);
if (len == UNIV_SQL_NULL) { /* MySQL assumes that the field for an SQL
NULL value is set to the default value. */
ut_ad(templ->mysql_null_bit_mask);
if (DATA_LARGE_MTYPE(templ->type)
|| DATA_GEOMETRY_MTYPE(templ->type)) {
/* It is a BLOB field locally stored in the InnoDBrecord:weMUSTcopyitscontentsto prebuilt->blob_heapherebecause row_sel_field_store_in_mysql_format()storesa pointertothedata,andthedatapassedtous willbeinvalidassoonasthe mini-transactioniscommittedandthepage latchontheclusteredindexpageis
released. */
if (templ->mysql_null_bit_mask) { /* It is a nullable column with a non-NULL
value */
mysql_rec[templ->mysql_null_byte_offset]
&= static_cast<byte>(~templ->mysql_null_bit_mask);
}
DBUG_RETURN(TRUE);
}
/** Convert a row in the Innobase format to a row in the MySQL format. Notethatthetemplateinprebuiltmayadviseustocopyonlyafew columnstomysql_rec,othercolumnsareleftblank.Allcolumnsmaynot beneededinthequery. @param[out]mysql_recrowintheMySQLformat @param[in]prebuiltcursor @param[in]recInnobaserecordintheindex whichwasdescribedinprebuilt's template,orintheclusteredindex; mustbeprotectedbyapagelatch @param[in]vrowvirtualcolumns @param[in]rec_clustwhetherindexmustbetheclusteredindex @param[in]indexindexofrec @param[in]offsetsarrayreturnedbyrec_get_offsets(rec) @retvaltrueonsuccess
@retval false if not all columns could be retrieved */
MY_ATTRIBUTE((warn_unused_result)) staticbool row_sel_store_mysql_rec(
byte* mysql_rec,
row_prebuilt_t* prebuilt, const rec_t* rec, const dtuple_t* vrow, bool rec_clust, const dict_index_t* index, const rec_offs* offsets)
{
DBUG_ENTER("row_sel_store_mysql_rec");
ut_ad(rec_clust || index == prebuilt->index);
ut_ad(!rec_clust || dict_index_is_clust(index));
/* Secondary index reads populate only templated columns. SeedtheNULLbitmapfromdefault_recsouncoveredcolumns
read as SQL NULL in ha_partition::swap_blobs(). */ if (!dict_index_is_clust(index) &&
prebuilt->m_mysql_table &&
prebuilt->m_mysql_table->s->blob_fields &&
prebuilt->null_bitmap_len) {
ut_ad(prebuilt->default_rec);
memcpy(mysql_rec, prebuilt->default_rec,
prebuilt->null_bitmap_len);
}
if (UNIV_LIKELY_NULL(prebuilt->blob_heap)) {
row_mysql_prebuilt_free_blob_heap(prebuilt);
}
for (ulint i = 0; i < prebuilt->n_template; i++) { const mysql_row_templ_t*templ = &prebuilt->mysql_template[i];
if (templ->is_virtual && dict_index_is_clust(index)) { /* Skip virtual columns if it is not a covered
search or virtual key read is not requested. */ if (!rec_clust
|| !prebuilt->index->has_virtual()
|| !prebuilt->read_just_key) { /* Initialize the NULL bit. */ if (templ->mysql_null_bit_mask) { #ifdefined __GNUC__ && !defined __clang__ && __GNUC__ < 6 # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wconversion"/* GCC 5 may need this here */ #endif
mysql_rec[templ->mysql_null_byte_offset]
|= (byte) templ->mysql_null_bit_mask; #ifdefined __GNUC__ && !defined __clang__ && __GNUC__ < 6 # pragma GCC diagnostic pop #endif
} continue;
}
dict_v_col_t* col;
col = dict_table_get_nth_v_col(
index->table, templ->clust_rec_field_no);
const ulint field_no
= rec_clust
? templ->clust_rec_field_no
: templ->rec_field_no; /* We should never deliver column prefixes to the SQL layer, exceptforevaluatinghandler_index_cond_check()
or handler_rowid_filter_check(). */ /* ...actually, we do want to do this in order to supporttheprefixqueryoptimization.
if (!row_sel_store_mysql_field(mysql_rec, prebuilt,
rec, index, offsets,
field_no, templ)) {
DBUG_RETURN(false);
}
}
/* FIXME: We only need to read the doc_id if an FTS indexed columnisbeingupdated. NOTE,therecordcanbeclusterorsecondaryindexrecord. ifsecondaryindexisusedthenFTS_DOC_IDcolumnshouldbepart
of this index. */ if (dict_table_has_fts_index(prebuilt->table)) { if (dict_index_is_clust(index)
|| prebuilt->fts_doc_id_in_read_set) {
prebuilt->fts_doc_id = fts_get_doc_id_from_rec(
rec, index, offsets);
}
}
/*********************************************************************//**
Builds a previous version of a clustered index record for a consistent read
@return DB_SUCCESS or error code */ static MY_ATTRIBUTE((warn_unused_result))
dberr_t
row_sel_build_prev_vers_for_mysql( /*==============================*/
row_prebuilt_t* prebuilt, /*!< in/out: prebuilt struct */
dict_index_t* clust_index, /*!< in: clustered index */ const rec_t* rec, /*!< in: record in a clustered index */
rec_offs** offsets, /*!< in/out: offsets returned by
rec_get_offsets(rec, clust_index) */
mem_heap_t** offset_heap, /*!< in/out: memory heap from which
the offsets are allocated */
rec_t** old_vers, /*!< out: old version, or NULL if the recorddoesnotexistintheview: i.e.,itwasfreshlyinserted
afterwards */
dtuple_t** vrow, /*!< out: dtuple to hold old virtual
column data */
mtr_t* mtr) /*!< in: mtr */
{
row_sel_reset_old_vers_heap(prebuilt);
/*********************************************************************//**
Retrieves the clustered index record corresponding to a record in a
non-clustered index. Does the necessary locking. Used in the MySQL
interface.
@return DB_SUCCESS, DB_SUCCESS_LOCKED_REC, or error code */
dberr_t
Row_sel_get_clust_rec_for_mysql::operator()( /*============================*/
row_prebuilt_t* prebuilt,/*!< in: prebuilt struct */
dict_index_t* sec_index,/*!< in: secondary index where rec resides */ const rec_t* rec, /*!< in: record in a non-clustered index; if thisisalockingread,thenrecisnot allowedtobedelete-marked,andthatwould
not make sense either */
que_thr_t* thr, /*!< in: query thread */ const rec_t** out_rec,/*!< out: clustered record or an old version of it,NULLiftheoldversiondidnotexist inthereadview,i.e.,itwasafresh
inserted version */
rec_offs** offsets,/*!< in: offsets returned by rec_get_offsets(rec,sec_index); out:offsetsreturnedby
rec_get_offsets(out_rec, clust_index) */
mem_heap_t** offset_heap,/*!< in/out: memory heap from which
the offsets are allocated */
dtuple_t** vrow, /*!< out: virtual column to fill */
mtr_t* mtr) /*!< in: mtr used to get access to the non-clusteredrecord;thesamemtrisusedto
access the clustered index */
{ /* Extract values from prebuilt */
dtuple_t* clust_ref= prebuilt->clust_ref;
btr_pcur_t* clust_pcur= prebuilt->clust_pcur;
lock_mode select_lock_type= prebuilt->select_lock_type;
trx_t* trx= prebuilt->trx;
/* If this is a spatial index scan, and we are reading fromashadowbuffer,therecordcouldbealready deleted(duetorollbacketc.).Sogettheoriginal
page and verify that */ if (dict_index_is_spatial(sec_index)
&& btr_cur->rtr_info->matches
&& (!(ulint(rec
- btr_cur->rtr_info->matches->block->page.frame)
>> srv_page_size_shift)
|| rec != btr_pcur_get_rec(prebuilt->pcur))) { #ifdef UNIV_DEBUG
rtr_info_t* rtr_info = btr_cur->rtr_info;
mysql_mutex_lock(&rtr_info->matches->rtr_match_mutex); /* The page could be deallocated (by rollback etc.) */ if (!rtr_info->matches->valid) {
mysql_mutex_unlock(&rtr_info->matches->rtr_match_mutex);
clust_rec = NULL; goto func_exit;
}
mysql_mutex_unlock(&rtr_info->matches->rtr_match_mutex);
if (rec_get_deleted_flag(rec,
dict_table_is_comp(sec_index->table))
&& select_lock_type == LOCK_NONE) {
/* FIXME: Why is this block not the sameasbtr_pcur_get_block(prebuilt->pcur),
and is it not unsafe to use RW_NO_LATCH here? */
buf_block_t* block = buf_page_get_gen(
btr_pcur_get_block(prebuilt->pcur)->page.id(),
btr_pcur_get_block(prebuilt->pcur)->zip_size(),
RW_NO_LATCH, NULL, BUF_GET, mtr, &err);
ut_ad(block); // FIXME: avoid crash
mem_heap_t* heap = mem_heap_create(256);
dtuple_t* tuple = dict_index_build_data_tuple(
rec, sec_index, true,
sec_index->n_fields, heap);
page_cur_t page_cursor;
page_cursor.block = block;
page_cursor.index = sec_index;
uint16_t up_match = 0, low_match = 0;
ut_ad(!page_cur_search_with_match(tuple, PAGE_CUR_LE,
&up_match,
&low_match,
&page_cursor,
nullptr));
ut_ad(low_match < dtuple_get_n_fields_cmp(tuple));
mem_heap_free(heap);
err = DB_SUCCESS; #endif/* UNIV_DEBUG */
} elseif (!rec_get_deleted_flag(rec,
dict_table_is_comp(sec_index->table))
|| select_lock_type != LOCK_NONE) { /* In a rare case it is possible that no clust recisfoundforadelete-markedsecondaryindex record:ifrow_undo_mod_clust()hasalreadyremoved theclustrec,whilepurgeisstillcleaningand removingsecondaryindexrecordsassociatedwith earlierversionsoftheclusteredindexrecord. Inthatcaseweknowthattheclusteredindex
record did not exist in the read view of trx. */
ib::error() << "Clustered record for sec rec not found" " index " << sec_index->name
<< " of table " << sec_index->table->name;
fputs("InnoDB: sec index record ", stderr);
rec_print(stderr, rec, sec_index);
fputs("\n" "InnoDB: clust index record ", stderr);
rec_print(stderr, clust_rec, clust_index);
err = DB_CORRUPTION;
}
if (select_lock_type != LOCK_NONE) { /* Try to place a lock on the index record; we are searching theclustrecwithauniquecondition,hence
we set a LOCK_REC_NOT_GAP type lock */
switch (err) { case DB_SUCCESS: case DB_SUCCESS_LOCKED_REC: break; default: return err;
}
} else { /* This is a non-locking consistent read: if necessary, fetch
a previous version of the record */
old_vers = NULL;
if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED
|| clust_index->table->is_temporary()) {
} else { /* If the isolation level allows reading of uncommitteddata,thenweneverlookforan
earlier version */
err = row_sel_clust_sees(clust_rec, *clust_index,
*offsets, trx->read_view);
}
switch (err) { default: return err; case DB_SUCCESS: break; case DB_SUCCESS_LOCKED_REC: const buf_page_t& bpage = btr_pcur_get_block(
clust_pcur)->page;
/* If we had to go to an earlier version of row or the secondaryindexrecordisdeletemarked,thenitmaybethat thesecondaryindexrecordcorrespondingtoclust_rec (orold_vers)isnotrec;inthatcasewemustignore suchrowbecauseinoursnapshotrecwouldnothaveexisted. Rememberthatfromrecwecannotseedirectlywhichtransaction idcorrespondstoit:wehavetogototheclusteredindex record.Aquerywherewewanttofetchallrowswhere thesecondaryindexvalueisinsomeintervalwouldreturn awrongresultifwewouldnotdroprowswhichwecometo visitthroughsecondaryindexrecordsthatwouldnotreally
exist in our snapshot. */
/* And for spatial index, since the rec is from shadow buffer,
so we need to check if it's exactly match the clust_rec. */ if (clust_rec
&& (old_vers
|| trx->isolation_level <= TRX_ISO_READ_UNCOMMITTED
|| dict_index_is_spatial(sec_index)
|| rec_get_deleted_flag(rec, dict_table_is_comp(
sec_index->table)))) {
err = row_sel_sec_rec_is_for_clust_rec(rec, sec_index,
clust_rec, clust_index, thr); switch (err) { case DB_SUCCESS:
clust_rec = NULL; break; case DB_SUCCESS_LOCKED_REC: break; default: return err;
}
}
err = DB_SUCCESS;
}
func_exit:
*out_rec = clust_rec;
if (select_lock_type != LOCK_NONE) { /* We may use the cursor in update or in unlock_row():
store its position */
btr_pcur_store_position(clust_pcur, mtr);
}
return err;
}
/** Restores cursor position after it has been stored. We have to take into accountthattherecordcursorwaspositionedonmayhavebeendeleted. Thenwemayhavetomovethecursoronestepupordown. @param[out]same_user_rectrueifwewereabletorestorethecursoronauser recordwiththesameorderingprefixinintheB-treeindex @param[in]latch_modelatchmodewishedinrestoration @param[in]pcurcursorwhosepositionhasbeenstored @param[in]moves_uptrueifthecursormovesupintheindex @param[in,out]mtrmtr;CAUTION:maycommitmtrtemporarily! @returntrueifwemayneedtoprocesstherecordthecursorisnow
positioned on (i.e. we should not go to the next record yet) */ staticbool sel_restore_position_for_mysql(bool *same_user_rec,
btr_latch_mode latch_mode,
btr_pcur_t *pcur, bool moves_up, mtr_t *mtr)
{ auto status = pcur->restore_position(latch_mode, mtr);
/* The position may need be adjusted for rel_pos and moves_up. */
switch (pcur->rel_pos) { case BTR_PCUR_ON: if (!*same_user_rec && moves_up) { if (status == btr_pcur_t::SAME_UNIQ) returntrue;
next: if (btr_pcur_move_to_next(pcur, mtr)
&& rec_is_metadata(btr_pcur_get_rec(pcur),
*pcur->index())) {
btr_pcur_move_to_next(pcur, mtr);
}
returntrue;
} return(!*same_user_rec); case BTR_PCUR_AFTER_LAST_IN_TREE: case BTR_PCUR_BEFORE_FIRST_IN_TREE: returntrue; case BTR_PCUR_AFTER: /* positioned to record after pcur->old_rec. */
pcur->pos_state = BTR_PCUR_IS_POSITIONED;
prev: if (btr_pcur_is_on_user_rec(pcur) && !moves_up
&& !rec_is_metadata(btr_pcur_get_rec(pcur),
*pcur->index())) { if (!btr_pcur_move_to_prev(pcur, mtr)) { returntrue;
}
} returntrue; case BTR_PCUR_BEFORE: /* For non optimistic restoration: Thepositionisnowsettotherecordbeforepcur->old_rec.
if (templ->mysql_type == DATA_MYSQL_TRUE_VARCHAR
&& (templ->type != DATA_INT)) { /* Check for != DATA_INT to make sure we do nottreatMySQLENUMorSETasatrueVARCHAR!
Find the actual length of the true VARCHAR field. */
row_mysql_read_true_varchar(
&len, cache, templ->mysql_length_bytes);
len += templ->mysql_length_bytes;
MEM_UNDEFINED(buf, templ->mysql_col_len);
} else {
len = templ->mysql_col_len;
}
memcpy(buf, cache, len);
}
/** Copy used fields from cached row. Copycacherecordfieldbyfield,don'ttouchfieldsthat arenotcoveredbycurrentkey. @param[out]bufWheretocopytheMySQLrow. @param[in]cached_recWhattocopy(inMySQLrowformat).
@param[in] prebuilt prebuilt struct. */ void
row_sel_copy_cached_fields_for_mysql(
byte* buf, const byte* cached_rec,
row_prebuilt_t* prebuilt)
{ const mysql_row_templ_t*templ;
ulint i; for (i = 0; i < prebuilt->n_template; i++) {
templ = prebuilt->mysql_template + i;
/* Skip virtual columns */ if (templ->is_virtual) { continue;
}
row_sel_copy_cached_field_for_mysql(
buf, cached_rec, templ); /* Copy NULL bit of the current field from cached_rec
to buf */ if (templ->mysql_null_bit_mask) { #ifdefined __GNUC__ && !defined __clang__ && __GNUC__ < 6 # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wconversion"/* GCC 5 may need this here */ #endif
buf[templ->mysql_null_byte_offset]
^= (buf[templ->mysql_null_byte_offset]
^ cached_rec[templ->mysql_null_byte_offset])
& (byte) templ->mysql_null_bit_mask; #ifdefined __GNUC__ && !defined __clang__ && __GNUC__ < 6 # pragma GCC diagnostic pop #endif
}
}
}
/********************************************************************//**
Pops a cached row for MySQL from the fetch cache. */
UNIV_INLINE void
row_sel_dequeue_cached_row_for_mysql( /*=================================*/
byte* buf, /*!< in/out: buffer where to copy the
row */
row_prebuilt_t* prebuilt) /*!< in: prebuilt struct */
{
ulint i; const mysql_row_templ_t*templ; const byte* cached_rec;
ut_ad(prebuilt->n_fetch_cached > 0);
ut_ad(prebuilt->mysql_prefix_len <= prebuilt->mysql_row_len);
if (UNIV_UNLIKELY(prebuilt->keep_other_fields_on_keyread)) {
row_sel_copy_cached_fields_for_mysql(buf, cached_rec, prebuilt);
} elseif (prebuilt->mysql_prefix_len > 63) { /* The record is long. Copy it field by field, in case therearesomelongVARCHARcolumnofwhichonlya
small length is being used. */
MEM_UNDEFINED(buf, prebuilt->mysql_prefix_len);
/* First copy the NULL bits. */
memcpy(buf, cached_rec, prebuilt->null_bitmap_len); /* Then copy the requested fields. */
for (i = 0; i < prebuilt->n_template; i++) {
templ = prebuilt->mysql_template + i;
/********************************************************************//**
Pushes a row for MySQL to the fetch cache. */
UNIV_INLINE void
row_sel_enqueue_cache_row_for_mysql( /*================================*/
byte* mysql_rec, /*!< in/out: MySQL record */
row_prebuilt_t* prebuilt) /*!< in/out: prebuilt struct */
{ /* For non ICP code path the row should already exist in the
next fetch cache slot. */
if (prebuilt->pk_filter || prebuilt->idx_cond) {
memcpy(row_sel_fetch_last_buf(prebuilt), mysql_rec,
prebuilt->mysql_row_len);
}
++prebuilt->n_fetch_cached;
}
#ifdef BTR_CUR_HASH_ADAPT /*********************************************************************//**
Tries to do a shortcut to fetch a clustered index record with a unique key, using the hash index if possible (not always). We assume that the search
mode is PAGE_CUR_GE, it is a consistent read, there is a read view in trx,
btr search latch has been locked in S-mode if AHI is enabled.
@return SEL_FOUND, SEL_EXHAUSTED, SEL_RETRY */ static
ulint
row_sel_try_search_shortcut_for_mysql( /*==================================*/ const rec_t** out_rec,/*!< out: record if found */
row_prebuilt_t* prebuilt,/*!< in: prebuilt struct */
rec_offs** offsets,/*!< in/out: for rec_get_offsets(*out_rec) */
mem_heap_t** heap, /*!< in/out: heap for rec_get_offsets() */
mtr_t* mtr) /*!< in: started mtr */
{
dict_index_t* index = prebuilt->index; const dtuple_t* search_tuple = prebuilt->search_tuple;
btr_pcur_t* pcur = prebuilt->pcur;
trx_t* trx = prebuilt->trx; const rec_t* rec;
if (!page_rec_is_user_rec(rec) || rec_is_metadata(rec, *index)) { return SEL_RETRY;
}
/* As the cursor is now placed on a user record after a search with themodePAGE_CUR_GE,theup_matchfieldinthecursortellshowmany
fields in the user record matched to the search tuple */
if (btr_pcur_get_up_match(pcur) < dtuple_get_n_fields(search_tuple)) { return SEL_EXHAUSTED;
}
if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED) {
} elseif (trx_id_t bulk_trx_id = index->table->bulk_trx_id) { /* See row_search_mvcc() for a comment on bulk_trx_id */ if (!trx->read_view.changes_visible(bulk_trx_id)) { return SEL_EXHAUSTED;
}
}
/* This is a non-locking consistent read: if necessary, fetch
a previous version of the record */
if (rec_get_deleted_flag(rec, dict_table_is_comp(index->table))) { /* In delete-marked records, DB_TRX_ID must
always refer to an existing undo log record. */
ut_ad(row_get_rec_trx_id(rec, index, *offsets)); return SEL_EXHAUSTED;
}
/* We assume that the index conditions on case-insensitivecolumnsarecase-insensitive.The caseofsuchcolumnsmaybewronginasecondary index,ifthecaseofthecolumnhasbeenupdatedin thepast,orarecordhasbeendeletedandarecord
inserted in a different case. */
check_result_t result = prebuilt->idx_cond
? handler_index_cond_check(prebuilt->idx_cond)
: CHECK_POS;
switch (result) { case CHECK_POS: if (prebuilt->pk_filter &&
handler_rowid_filter_is_active(prebuilt->pk_filter)) {
ut_ad(!prebuilt->index->is_primary()); if (prebuilt->clust_index_was_generated) {
ulint len;
dict_index_t* index = prebuilt->index; const byte* data = rec_get_nth_field(
rec, offsets, index->n_fields - 1,
&len);
ut_ad(dict_index_get_nth_col(index,
index->n_fields - 1)
->prtype == (DATA_ROW_ID | DATA_NOT_NULL));
ut_ad(len == DATA_ROW_ID_LEN);
memcpy(prebuilt->row_id, data, DATA_ROW_ID_LEN);
}
result = handler_rowid_filter_check(prebuilt->pk_filter); switch (result) { case CHECK_NEG:
MONITOR_INC(MONITOR_ICP_NO_MATCH); return(result); case CHECK_OUT_OF_RANGE:
MONITOR_INC(MONITOR_ICP_OUT_OF_RANGE); return(result); case CHECK_POS: break; default: return(result);
}
} /* Convert the remaining fields to MySQL format. Ifthisisasecondaryindexrecord,wemustdefer
this until we have fetched the clustered index record. */ if (!prebuilt->need_to_access_clustered
|| dict_index_is_clust(prebuilt->index)) { if (!row_sel_store_mysql_rec(
mysql_rec, prebuilt, rec, NULL, false,
prebuilt->index, offsets)) {
ut_ad(dict_index_is_clust(prebuilt->index)); return(CHECK_NEG);
}
}
MONITOR_INC(MONITOR_ICP_MATCH); return(result); case CHECK_NEG:
MONITOR_INC(MONITOR_ICP_NO_MATCH); return(result); case CHECK_OUT_OF_RANGE:
MONITOR_INC(MONITOR_ICP_OUT_OF_RANGE); return(result); case CHECK_ERROR: case CHECK_ABORTED_BY_USER: return(result);
}
ut_error; return(result);
}
/** Extract virtual column data from a virtual index record and fill a dtuple @param[in]recthevirtual(secondary)indexrecord @param[in]indexthevirtualindex @param[in,out]vrowthedtuplewheredataextractto @param[in]heapmemoryheaptoallocatememory
*/ static void
row_sel_fill_vrow( const rec_t* rec,
dict_index_t* index,
dtuple_t** vrow,
mem_heap_t* heap)
{
rec_offs offsets_[REC_OFFS_NORMAL_SIZE];
rec_offs* offsets = offsets_;
rec_offs_init(offsets_);
/** Avoid the clustered index lookup if all the following conditions aretrue: 1)allcolumnsareinsecondaryindex 2)allvaluesforcolumnsthatareprefix-onlyindexesareshorter thantheprefixsize.ThisoptimizationcanavoidmanyIOsforcertainschemas.
@return true, to avoid clustered index lookup. */ static bool row_search_with_covering_prefix(
row_prebuilt_t* prebuilt, const rec_t* rec, const rec_offs* offsets)
{ const dict_index_t* index = prebuilt->index;
ut_ad(!dict_index_is_clust(index));
/* In ha_innobase::build_template() we choose to access the wholerowwhenusingexclusiverowlocksorIncaseoffts
query, we need to read from clustered index */ if (prebuilt->select_lock_type == LOCK_X || prebuilt->in_fts_query
|| !index->is_btree()) { returnfalse;
}
/** Optimization only applicable if there the number of secondary index
fields are greater than or equal to number of clustered index fields. */ if (prebuilt->n_template > index->n_fields) { returnfalse;
}
/* We can avoid a clustered index lookup if allofthefollowinghold: (1)allcolumnsareinthesecondaryindex (2)allvaluesforcolumnsthatareprefix-only indexesareshorterthantheprefixsize
This optimization can avoid many IOs for certain schemas. */ for (ulint i = 0; i < prebuilt->n_template; i++) {
mysql_row_templ_t* templ = prebuilt->mysql_template + i;
ulint j = templ->rec_prefix_field_no;
ut_ad(!templ->mbminlen == !templ->mbmaxlen);
/** Condition (1) : is the field in the index. */ if (j == ULINT_UNDEFINED) { returnfalse;
}
/** Condition (2): If this is a prefix index then
row's value size shorter than prefix length. */
if (!templ->rec_field_is_prefix
|| rec_offs_nth_sql_null(offsets, j)) { continue;
}
const dict_field_t* field = dict_index_get_nth_field(index, j);
if (rec_size >= field->prefix_len) { /* Shortest representation string by the bytelengthoftherecordislongerthanthe
maximum possible index prefix. */ returnfalse;
}
if (templ->mbminlen != templ->mbmaxlen
&& rec_field_len_in_chars(field->col, j, rec, offsets)
>= field->prefix_len / templ->mbmaxlen) { /* No of chars to store the record exceeds
the index prefix character length. */ returnfalse;
}
}
/* If prefix index optimization condition satisfied then forallcolumnsabove,userec_prefix_field_noinsteadof
rec_field_no, and skip the clustered lookup below. */ for (ulint i = 0; i < prebuilt->n_template; i++) {
mysql_row_templ_t* templ = prebuilt->mysql_template + i;
templ->rec_field_no = templ->rec_prefix_field_no;
ut_a(templ->rec_field_no != ULINT_UNDEFINED);
}
returntrue;
}
/** Templated policy for row_search_mvcc behavior. @tparamNeedsConversiontrueifrecordsneedMySQLformatconversion
@tparam ManagesMtr true if row_search_mvcc manages its own mtr */ template<bool NeedsConversion, bool ManagesMtr> struct InnoDBPolicy
{ static constexpr bool needs_conversion= NeedsConversion; static constexpr bool manages_mtr= ManagesMtr;
/** Convert a record to MySQL format (onlywhenNeedsConversion=true). @param[out]mysql_bufMySQLrowbuffer @param[in]prebuiltprebuiltstructure @param[in]recrecordtoconvert @param[in]vrowvirtualcolumnvalues(canbeNULL) @param[in]rec_clusttrueifrecisfromclusteredindex @param[in]indexindexoftherecord @param[in]offsetsrecordoffsets @returnDB_SUCCESSonsuccess
@retval DB_ERROR if conversion fails */ static dberr_t output_record(byte* mysql_buf,
row_prebuilt_t* prebuilt, const rec_t* rec, const dtuple_t* vrow, bool rec_clust,
dict_index_t* index,
rec_offs* offsets) noexcept
{
static_assert(NeedsConversion, "MySQL format conversion only"); if (!row_sel_store_mysql_rec(mysql_buf, prebuilt, rec, vrow,
rec_clust, index, offsets)) return DB_ERROR; return DB_SUCCESS;
}
/** Process a record via RecordCallback interface @param[in]callbackRecordCallbackinstance(canbeNULL) @param[in]prebuiltprebuiltstructurewithsearchtuple @param[in]recrecordtoprocess @param[in]indexindexoftherecord @param[in]offsetsrecordoffsets @returnDB_SUCCESSonsuccess @retvalDB_SUCCESS_LOCKED_RECtostopiterationafterthisrecord @retvalDB_RECORD_NOT_FOUNDtoskipthisrecord
@retval error code on failure */ static dberr_t output_record(RecordCallback* callback,
row_prebuilt_t* prebuilt, const rec_t* rec,
dict_index_t* index,
rec_offs* offsets) noexcept
{
static_assert(!NeedsConversion, "InnoDB callback only"); if (!callback) return DB_SUCCESS;
/** Searches for rows in the database using cursor. Functionismainlyusedfortablesthataresharedacrossconnectionsand soitemploystechniquethatcanhelpre-constructtherowsthat transactionissupposetosee. Italsohasoptimizationsuchaspre-cachingtherows,usingAHI,etc.
/* We don't support FTS queries from the HANDLER interfaces, because weimplementedFTSasreversedinvertedindexwithauxiliarytables. Soanythingrelatedtotraditionalindexquerywouldnotapplyto
it. */ if (prebuilt->index->type & DICT_FTS) {
DBUG_RETURN(DB_END_OF_INDEX);
}
/* We need to get the virtual column values stored in secondary indexkey,ifthisiscoveredindexscanorvirtualkeyreadis
requested. */ bool need_vrow = prebuilt->read_just_key
&& prebuilt->index->has_virtual();
/* Reset the new record lock info if READ UNCOMMITTED or READCOMMITEDisolationlevelisused.Then weareabletoremovetherecordlockssethereonanindividual
row. */
prebuilt->new_rec_locks = 0;
/*-------------------------------------------------------------*/ /* PHASE 1: Try to pop the row from the prefetch cache */
if (UNIV_UNLIKELY(direction == 0)) {
trx->op_info = "starting index read";
if (prebuilt->fetch_cache_first > 0
&& prebuilt->fetch_cache_first < MYSQL_FETCH_CACHE_SIZE) {
early_not_found: /* The previous returned row was popped from the fetch cache,butthecachewasnotfullatthetimeofthe
popping: no more rows can exist in the result set */
trx->op_info = "";
DBUG_RETURN(DB_RECORD_NOT_FOUND);
}
#if SIZEOF_SIZE_T < 8 if (UNIV_LIKELY(~prebuilt->n_rows_fetched)) #endif
prebuilt->n_rows_fetched++;
mode = pcur->search_mode;
}
/* In a search where at most one record in the index may match, we canuseaLOCK_REC_NOT_GAPtyperecordlockwhenlockinga non-delete-markedmatchingrecord.
Notethatinauniquesecondaryindextheremaybedifferent delete-markedversionsofarecordwhereonlytheprimarykey valuesdiffer:thusinasecondaryindexwemustusenext-key
locks when locking delete-marked records. */
/* Note above that a UNIQUE secondary index can contain many rowswiththesamekeyvalueifoneofthecolumnsistheSQL null.AclusteredindexunderMySQLcannevercontainnull columnsbecausewedemandthatallthecolumnsinprimarykey
are non-null. */
unique_search = TRUE;
/* Even if the condition is unique, MySQL seems to try to retrievealsoasecondrowifaprimarykeycontainsmorethan 1column.ReturnimmediatelyifthisisnotaHANDLER
command. */
/* We don't support sequential scan for Rtree index because it
is pointless. */ if (dict_index_is_spatial(index) && !RTREE_SEARCH_MODE(mode)) {
trx->op_info = "";
DBUG_RETURN(DB_END_OF_INDEX);
}
/* if the query is a plain locking SELECT, and the isolation level
is <= TRX_ISO_READ_COMMITTED, then this is set to FALSE */ bool did_semi_consistent_read = false;
/* Conditionally manage mtr based on policy */
mtr_t local_mtr{trx};
mtr_t* mtr;
if constexpr (Callback::manages_mtr) { /* row_search_mvcc manages its own mtr */
mtr = &local_mtr;
mtr->start();
} else { /* Use external mtr from prebuilt (internal parser) */
mtr = prebuilt->mtr;
ut_ad(mtr);
ut_ad(mtr->is_active());
}
#ifdef BTR_CUR_HASH_ADAPT /*-------------------------------------------------------------*/ /* PHASE 2: Try fast adaptive hash index search if possible */
/* Next test if this is the special case where we can use the fast adaptivehashindextotrythesearch.Sincewemustreleasethe searchsystemlatchwhenweretrieveanexternallystoredfield,we cannotusetheadaptivehashindexinasearchinthecasetherow
may be long and there may be externally stored fields */
/* This is a SELECT query done as a consistent read, andthereadviewhasalreadybeenallocated: letustryasearchshortcutthroughthehash
index. */
dberr_t err = DB_SUCCESS; switch (row_sel_try_search_shortcut_for_mysql(
&rec, prebuilt, &offsets, &heap,
mtr)) { case SEL_FOUND: /* At this point, rec is protected by apagelatchthatwasacquiredby row_sel_try_search_shortcut_for_mysql(). Thelatchwillnotbereleaseduntil
mtr.commit(). */
ut_ad(!rec_get_deleted_flag(rec, comp));
if (prebuilt->pk_filter || prebuilt->idx_cond) { switch (row_search_idx_cond_check(
buf, prebuilt,
rec, offsets)) { case CHECK_ABORTED_BY_USER: goto aborted; case CHECK_NEG: case CHECK_OUT_OF_RANGE: case CHECK_ERROR:
err = DB_RECORD_NOT_FOUND; goto shortcut_done; case CHECK_POS: goto shortcut_done;
}
if (!row_sel_store_mysql_rec(
buf, prebuilt,
rec, NULL, false, index,
offsets)) { /* Only fresh inserts may contain incompleteexternallystored columns.Pretendthatsuch recordsdonotexist.Such recordsmayonlybeaccessed attheREADUNCOMMITTED isolationlevelorwhen rollingbackarecovered transaction.Rollbackhappens
at a lower level, not here. */
/* Proceed as in case SEL_RETRY. */ break;
}
goto shortcut_done;
case SEL_EXHAUSTED:
err = DB_RECORD_NOT_FOUND;
shortcut_done: if constexpr (Callback::manages_mtr) {
mtr->commit();
}
/* NOTE that we do NOT store the cursor
position */
trx->op_info = "";
ut_ad(!did_semi_consistent_read); if (UNIV_LIKELY_NULL(heap)) {
mem_heap_free(heap);
}
DBUG_RETURN(err);
#ifdef UNIV_DEBUG /* The state of a running trx can only be changed by the threadthatiscurrentlyservingthetransaction.Becausewe arethatthread,wecanreadtrx->statewithoutholdingany
mutex. */ switch (trx->state) { case TRX_STATE_ACTIVE: break; case TRX_STATE_NOT_STARTED:
ut_ad(prebuilt->sql_stat_start
|| prebuilt->table->no_rollback()); break; default:
ut_ad("invalid trx->state" == 0);
} #endif
if (prebuilt->table->no_rollback()) { /* NO_ROLLBACK tables do not support MVCC or locking. */
prebuilt->select_lock_type = LOCK_NONE;
prebuilt->sql_stat_start = FALSE;
} elseif (!prebuilt->sql_stat_start) { /* No need to set an intention lock or assign a read view */
ut_a(prebuilt->select_lock_type != LOCK_NONE
|| srv_read_only_mode || trx->read_view.is_open());
} else {
prebuilt->sql_stat_start = FALSE;
trx_start_if_not_started(trx, false);
if (UNIV_UNLIKELY(need_to_process)) { if (UNIV_UNLIKELY(!btr_pcur_get_rec(pcur))) { if constexpr (Callback::manages_mtr) {
mtr->commit();
}
trx->op_info = ""; if (UNIV_LIKELY_NULL(heap)) {
mem_heap_free(heap);
} return DB_CORRUPTION;
}
if (UNIV_UNLIKELY(prebuilt->row_read_type
== ROW_READ_DID_SEMI_CONSISTENT)) { /* We did a semi-consistent read, buttherecordwasremovedin
the meantime. */
prebuilt->row_read_type
= ROW_READ_TRY_SEMI_CONSISTENT;
}
} elseif (UNIV_LIKELY(prebuilt->row_read_type
!= ROW_READ_DID_SEMI_CONSISTENT)) {
/* The cursor was positioned on the record thatwereturnedpreviously.Ifweneed torepeatasemi-consistentreadasa pessimisticlockingread,therecord
cannot be skipped. */
if (!moves_up
&& set_also_gap_locks
&& !page_rec_is_supremum(rec)
&& !dict_index_is_spatial(index)) {
/* Try to place a gap lock on the next index record
to prevent phantoms in ORDER BY ... DESC queries */ const rec_t* next_rec = page_rec_get_next_const(rec); if (UNIV_UNLIKELY(!next_rec)) {
err = DB_CORRUPTION; goto page_corrupted;
}
/* The infimum record on a page cannot be in the result set, andneithercanarecordlockbeplacedonit:weskipsuch
a record. */
goto next_rec;
}
if (page_rec_is_supremum(rec)) {
if (set_also_gap_locks
&& !dict_index_is_spatial(index)) {
/* Try to place a lock on the index record */
/* If the transaction isolation level is READUNCOMMITTEDorREADCOMMITTED, wedonotlockgaps.Supremumrecordisreally
a gap and therefore we do not set locks there. */
ib::error() << "Index corruption: rec offs "
<< rec - btr_pcur_get_page(pcur)
<< " next offs "
<< next_offs
<< btr_pcur_get_block(pcur)->page.id()
<< ", index " << index->name
<< " of table " << index->table->name
<< ". Run CHECK TABLE. You may need to" " restore from a backup, or dump + drop +" " reimport the table.";
ut_ad(0);
err = DB_CORRUPTION;
goto page_read_error;
} else { /* The user may be dumping a corrupt table. Jump
over the corruption to recover as much as possible. */
ib::info() << "Index corruption: rec offs "
<< rec - btr_pcur_get_page(pcur)
<< " next offs "
<< next_offs
<< btr_pcur_get_block(pcur)->page.id()
<< ", index " << index->name
<< " of table " << index->table->name
<< ". We try to skip the rest of the page.";
if (UNIV_UNLIKELY(srv_force_recovery > 0)) { if (!rec_validate(rec, offsets)
|| !btr_index_rec_validate(pcur->btr_cur.page_cur,
index, FALSE)) {
ib::error() << "Index corruption: rec offs "
<< rec - btr_pcur_get_page(pcur)
<< " next offs "
<< next_offs
<< btr_pcur_get_block(pcur)->page.id()
<< ", index " << index->name
<< " of table " << index->table->name
<< ". We try to skip the record.";
goto next_rec;
}
}
/* Note that we cannot trust the up_match value in the cursor at this placebecausewecanarrivehereaftermovingthecursor!Thus wehavetorecomparerecandsearch_tupletodetermineifthey
match enough. */
if (match_mode == ROW_SEL_EXACT) { /* Test if the index record matches completely to search_tuple
in prebuilt: if not, then we return with DB_RECORD_NOT_FOUND */
/* fputs("Comparing rec and search tuple\n", stderr); */
if (cmp_dtuple_rec(search_tuple, rec, index, offsets)) {
switch (err) { case DB_SUCCESS_LOCKED_REC: case DB_SUCCESS: break; default: goto lock_wait_or_error;
}
}
if constexpr (Callback::manages_mtr) {
btr_pcur_store_position(pcur, mtr);
/* The found record was not a match, butmaybeusedasNEXTrecord(index_next). SettherelativepositiontoBTR_PCUR_BEFORE, toreflectthatthepositionofthepersistent cursorisbeforethefound/storedrow
(pcur->old_rec). */
ut_ad(pcur->rel_pos == BTR_PCUR_ON);
pcur->rel_pos = BTR_PCUR_BEFORE;
}
err = DB_RECORD_NOT_FOUND; goto normal_return;
}
} elseif (match_mode == ROW_SEL_EXACT_PREFIX) {
if (!cmp_dtuple_is_prefix_of_rec(search_tuple, rec,
index, offsets)) {
switch (err) { case DB_SUCCESS_LOCKED_REC: case DB_SUCCESS: break; default: goto lock_wait_or_error;
}
}
if constexpr (Callback::manages_mtr) {
btr_pcur_store_position(pcur, mtr); /* The found record was not a match, butmaybeusedasNEXTrecord(index_next). SettherelativepositiontoBTR_PCUR_BEFORE, toreflectthatthepositionofthepersistent cursorisbeforethefound/storedrow
(pcur->old_rec). */
ut_ad(pcur->rel_pos == BTR_PCUR_ON);
pcur->rel_pos = BTR_PCUR_BEFORE;
}
/* We are ready to look at a possible new index entry in the result
set: the cursor is now placed on a user record */
if (prebuilt->select_lock_type != LOCK_NONE) { /* Try to place a lock on the index record; note that delete markedrecordsareaspecialcaseinauniquesearch.Ifthere isanon-deletemarkedrecord,thenitisenoughtolockits
existence with LOCK_REC_NOT_GAP. */
unsigned lock_type;
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { /* At READ COMMITTED or READ UNCOMMITTED isolationlevels,donotlockcommitted
delete-marked records. */ if (!rec_get_deleted_flag(rec, comp)) { goto no_gap_lock;
}
/* At most one transaction can be active
for temporary table. */ if (clust_index->table->is_temporary()) { goto no_gap_lock;
}
if (index == clust_index) {
trx_id_t trx_id = row_get_rec_trx_id(
rec, index, offsets); /* In delete-marked records, DB_TRX_ID must
always refer to an existing undo log record. */
ut_ad(trx_id); if (!trx_sys.is_registered(trx, trx_id)) { /* The clustered index record wasdelete-markedinacommitted
transaction. Ignore the record. */ goto locks_ok_del_marked;
}
} elseif (trx_t* t = row_vers_impl_x_locked(
trx, rec, index, offsets)) { /* The record belongs to an active
transaction. We must acquire a lock. */
t->release_reference();
} else { /* The secondary index record does not pointtoadelete-markedclusteredindex recordthatbelongstoanactivetransaction. Ignorethesecondaryindexrecord,because
it is not locked. */ goto next_rec;
}
/* Set next-key lock both for delete- and non-delete-marked recordsforuniquesearch,becausenon-delete-markedrecordcan
be marked as deleted while transaction suspends. */ if (index->is_spatial()) { goto no_gap_lock;
}
/* If we are doing a 'greater or equal than a primary key value'searchfromaclusteredindex,andwefindarecord thathasthatexactprimarykeyvalue,thenthereisnoneed tolockthegapbeforetherecord,becausenoinsertinthe gapcanbeinoursearchrange.Thatis,nophantomrowcan appearthatway.
Anexample:ifcol1istheprimarykey,thesearchisWHERE col1>=100,andwefindarecordwherecol1=100,thenno
need to lock the gap before that record. */
/* Test hook (debug builds only): allows a BF abort to mark this transactionawsrepvictimaftertheinterruptcheckatthetopof rec_loopbutbeforeitenqueuesawaitinglockrequestbelow—the windowinwhichtheorphaned-waiterbugforms.Inertinrelease
builds (DEBUG_SYNC_C compiles to nothing without ENABLED_DEBUG_SYNC). */
DEBUG_SYNC_C("row_search_before_rec_lock");
switch (err) { const rec_t* old_vers; case DB_SUCCESS_LOCKED_REC: if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { /* Note that a record of
prebuilt->index was locked. */
prebuilt->new_rec_locks = 1;
}
err = DB_SUCCESS; /* fall through */ case DB_SUCCESS: break; case DB_LOCK_WAIT: /* Lock wait for R-tree should already
be handled in sel_set_rtr_rec_lock() */
ut_ad(!dict_index_is_spatial(index)); /* Never unlock rows that were part of a conflict. */
prebuilt->new_rec_locks = 0;
if (UNIV_LIKELY(prebuilt->row_read_type
!= ROW_READ_TRY_SEMI_CONSISTENT)
|| unique_search
|| index != clust_index /* If read view was opened, sel_set_rec_lock() wouldreturnDB_RECORD_CHANGED,andwewouldnotbe here.Asreadviewwasn'topened,dolockingread insteadofsemi-consistentoneforREADCOMMITTED. ForREADUNCOMMITTED row_sel_build_committed_vers_for_mysql()mustread uncommittedversionoftherecord.ForREPEATABLE READandSERIALIZABLEprebuilt->row_read_type mustbenotequaltoROW_READ_TRY_SEMI_CONSISTENT, sotherewillbelockingreadforthoseisolation
levels. */
|| (trx->snapshot_isolation && trx->isolation_level
== TRX_ISO_READ_COMMITTED )) { if (!prebuilt->skip_locked) { goto lock_wait_or_error;
}
} else { /* The following call returns 'offsets'
associated with 'old_vers' */
row_sel_build_committed_vers_for_mysql(
clust_index, prebuilt, rec,
&offsets, &heap, &old_vers,
need_vrow ? &vrow : NULL, mtr);
}
/* Check whether it was a deadlock or not, if not adeadlockandthetransactionhadtowaitthen
release the lock it is waiting on. */
err = lock_trx_handle_wait(trx);
switch (err) { case DB_SUCCESS:
ut_ad(
!trx->lock.was_chosen_as_deadlock_victim); /* The lock was granted while we were searchingforthelastcommittedversion.
Do a normal locking read. */
offsets = rec_get_offsets(
rec, index, offsets,
index->n_core_fields,
ULINT_UNDEFINED, &heap); goto locks_ok; case DB_DEADLOCK: goto lock_wait_or_error; case DB_LOCK_WAIT:
ut_ad(!dict_index_is_spatial(index));
err = DB_SUCCESS; if (prebuilt->skip_locked) { goto next_rec;
} break; case DB_LOCK_WAIT_TIMEOUT: if (prebuilt->skip_locked) {
err = DB_SUCCESS; goto next_rec;
} /* fall through */ default:
ut_error;
}
if (old_vers == NULL) { /* The row was not yet committed */
goto next_rec;
}
did_semi_consistent_read = true;
rec = old_vers; break; case DB_RECORD_NOT_FOUND: if (dict_index_is_spatial(index)) { goto next_rec;
} else { goto lock_wait_or_error;
} break; case DB_LOCK_WAIT_TIMEOUT: if (prebuilt->skip_locked) {
err = DB_SUCCESS; goto next_rec;
} /* fall through */ default:
goto lock_wait_or_error;
}
} else { /* This is a non-locking consistent read: if necessary, fetch
a previous version of the record */
if (trx->isolation_level == TRX_ISO_READ_UNCOMMITTED
|| prebuilt->table->is_temporary()
|| prebuilt->table->no_rollback()) {
/* Do nothing: we let a non-locking SELECT read the
latest version of the record */
} elseif (index == clust_index) {
/* Fetch a previous version of the row if the current oneisnotvisibleinthesnapshot;ifwehaveavery highforcerecoverylevelset,wetrytoavoidcrashes
by skipping this lookup */
switch (err) { default: goto lock_wait_or_error; case DB_SUCCESS: break; case DB_SUCCESS_LOCKED_REC:
ut_ad(srv_force_recovery
< SRV_FORCE_NO_UNDO_LOG_SCAN);
rec_t* old_vers; /* The following call returns 'offsets'
associated with 'old_vers' */
err = row_sel_build_prev_vers_for_mysql(
prebuilt, clust_index,
rec, &offsets, &heap,
&old_vers,
need_vrow ? &vrow : nullptr, mtr);
if (err != DB_SUCCESS) {
goto lock_wait_or_error;
}
if (old_vers == NULL) { /* The row did not exist yet in
the read view */
goto next_rec;
}
rec = old_vers;
}
} else { /* We are looking into a non-clustered index, andtogettherightversionoftherecordwe havetolookalsointotheclusteredindex:this isnecessary,becausewecanonlygettheundo
information via the clustered index record. */
ut_ad(!dict_index_is_clust(index));
if (!srv_read_only_mode) {
trx_id_t trx_id = page_get_max_trx_id(
btr_pcur_get_page(pcur));
ut_ad(trx_id); if (trx->read_view.sees(trx_id)) { goto locks_ok;
} /* We should look at the clustered index. However,asthisisanon-lockingread, wecanskiptheclusteredindexlookupif theconditiondoesnotmatchthesecondary
index entry. */ switch (row_search_idx_cond_check(
buf, prebuilt, rec, offsets)) { case CHECK_NEG: goto next_rec; case CHECK_ABORTED_BY_USER:
err = DB_INTERRUPTED; goto idx_cond_failed; case CHECK_OUT_OF_RANGE: case CHECK_ERROR:
err = DB_RECORD_NOT_FOUND; goto idx_cond_failed; case CHECK_POS: goto requires_clust_rec;
}
ut_error;
}
}
}
locks_ok: /* NOTE that at this point rec can be an old version of a clustered indexrecordbuiltforaconsistentread.Wecannotassumeafterthis pointthatrecisonabufferpoolpage.Functionslike
page_rec_is_comp() cannot be used! */
if (rec_get_deleted_flag(rec, comp)) {
locks_ok_del_marked: /* In delete-marked records, DB_TRX_ID must
always refer to an existing undo log record. */
ut_ad(index != clust_index
|| row_get_rec_trx_id(rec, index, offsets));
/* The record is delete-marked: we can skip it */
/* This is an optimization to skip setting the next key lock ontherecordthatfollowsthisdelete-markedrecord.This optimizationworksbecauseoftheuniquesearchcriteria whichprecludesthepresenceofarangelockbetweenthis deletemarkedrecordandtherecordfollowingit.
Fornowthisisapplicableonlytoclusteredindexeswhile doingauniquesearchexceptforHANDLERqueriesbecause HANDLERallowsNEXTandPREVeveninuniquesearchon clusteredindex.Thereisscopeforfurtheroptimization applicabletouniquesecondaryindexes.Currentbehaviouris towidenthescopeofalockonanalreadydeletemarkedrecord
if the same record is deleted twice by the same transaction */ if (index == clust_index && unique_search
&& !prebuilt->used_in_HANDLER) {
err = DB_RECORD_NOT_FOUND;
goto normal_return;
}
goto next_rec;
}
/* Check if the record matches the index condition. */ switch (row_search_idx_cond_check(buf, prebuilt, rec, offsets)) { case CHECK_NEG: if (did_semi_consistent_read) {
row_unlock_for_mysql(prebuilt, TRUE);
} goto next_rec; case CHECK_ABORTED_BY_USER:
err = DB_INTERRUPTED; goto idx_cond_failed; case CHECK_OUT_OF_RANGE: case CHECK_ERROR:
err = DB_RECORD_NOT_FOUND; goto idx_cond_failed; case CHECK_POS: break;
}
if (index != clust_index && prebuilt->need_to_access_clustered) { if (row_search_with_covering_prefix(prebuilt, rec, offsets)) { goto use_covering_index;
}
requires_clust_rec:
ut_ad(index != clust_index); /* We use a 'goto' to the preceding label if a consistent readofasecondaryindexrecordrequiresustolookupold
versions of the associated clustered index record. */
ut_ad(rec_offs_validate(rec, index, offsets));
/* It was a non-clustered index and we must fetch also the
clustered index record */
mtr_extra_clust_savepoint = mtr->get_savepoint();
ut_ad(!vrow); /* The following call returns 'offsets' associated with 'clust_rec'.Notethat'clust_rec'canbeanoldversion
built for a consistent read. */
if (err == DB_LOCK_WAIT && prebuilt->skip_locked) {
err = lock_trx_handle_wait(trx);
} switch (err) { case DB_SUCCESS: if (clust_rec == NULL) { /* The record did not exist in the read view */
ut_ad(prebuilt->select_lock_type == LOCK_NONE
|| dict_index_is_spatial(index)); goto next_rec;
} break; case DB_SUCCESS_LOCKED_REC:
ut_a(clust_rec != NULL); if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { /* Note that the clustered index record
was locked. */
prebuilt->new_rec_locks = 2;
}
err = DB_SUCCESS; break; case DB_LOCK_WAIT_TIMEOUT: case DB_LOCK_WAIT: if (prebuilt->skip_locked) {
err = DB_SUCCESS; goto next_rec;
} /* fall through */ default:
vrow = NULL; goto lock_wait_or_error;
}
if (rec_get_deleted_flag(clust_rec, comp)) {
/* The record is delete marked: we can skip it */
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED
&& prebuilt->select_lock_type != LOCK_NONE) {
/* No need to keep a lock on a delete-marked recordifwedonotwanttousenext-key
locking. */
row_unlock_for_mysql(prebuilt, TRUE);
}
goto next_rec;
}
if (need_vrow && !vrow) { if (!heap) {
heap = mem_heap_create(100);
}
row_sel_fill_vrow(rec, index, &vrow, heap);
}
/* pk_filter and idx_cond are only ever set by the handler layer(ICP/rowidfilter),whichalwaysdrivesthesearch withMySQLRowCallback.InnoDBcallbackpathbuildsits
own prebuilt and leaves both NULL */ if constexpr (Callback::needs_conversion) { if (prebuilt->pk_filter || prebuilt->idx_cond) { /* Convert the record to MySQL format. We were unabletodothisinrow_search_idx_cond_check(), becausetheconditionisonthesecondaryindex andtherequestedcolumnisintheclusteredindex. Weconvertallfields,includingthosethat mayhavebeenusedinICP,becausethe secondaryindexmaycontainacolumnprefix ratherthanthefullcolumn.Also,asnoted inBug#56680,thecolumninthesecondary indexmaybeinthewrongcase,andthe authoritativecaseisinresult_rec,the
appropriate version of the clustered index record. */
err= MySQLRowCallback::output_record(
buf, prebuilt, result_rec, vrow, true, clust_index, offsets);
/* Decide whether to prefetch extra rows. Atthispoint,theclusteredindexrecordisprotected byapagelatchthatwasacquiredwhenpcurwaspositioned.
The latch will not be released until mtr.commit(). */
if ((match_mode == ROW_SEL_EXACT
|| prebuilt->n_rows_fetched >= MYSQL_FETCH_CACHE_THRESHOLD)
&& prebuilt->select_lock_type == LOCK_NONE
&& !prebuilt->templ_contains_blob
&& !prebuilt->clust_index_was_generated
&& !prebuilt->used_in_HANDLER
&& !prebuilt->in_fts_query) { /* Inside an update, for example, we do not cache rows, sincewemayusethecursorpositiontodotheactual update,thatiswhywerequire...lock_type==LOCK_NONE. SincewekeepspaceinprebuiltonlyfortheBLOBsof asinglerow,wecannotcacherowsinthecasethere areBLOBsinthefieldstobefetched.InHANDLERwedo notcacherowsbecausetherethecursorisascrollable
cursor. */
/* We only convert from InnoDB row format to MySQL row
format when ICP is disabled. */
if constexpr (Callback::needs_conversion) { /* MySQL format path with prefetch */ if (!prebuilt->pk_filter && !prebuilt->idx_cond) { /* We use next_buf to track the allocation of buffers wherewestoreandenqueuethebuffersforour pre-fetchoptimisation.
if (prebuilt->n_fetch_cached < MYSQL_FETCH_CACHE_SIZE) { goto next_rec;
}
} else { if constexpr (Callback::needs_conversion) { /* MySQL format path */ if (!prebuilt->pk_filter && !prebuilt->idx_cond) { /* The record was not yet converted to MySQL format. */
err= MySQLRowCallback::output_record(
buf, prebuilt, result_rec, vrow,
result_rec != rec,
result_rec != rec ? clust_index : index,
offsets);
if (err != DB_SUCCESS) { /* Only fresh inserts may contain incompleteexternallystored columns.Pretendthatsuchrecordsdo notexist.Suchrecordsmayonlybe accessedattheREADUNCOMMITTED isolationlevelorwhenrollingbacka recoveredtransaction.Rollback
happens at a lower level, not here. */ goto next_rec;
}
}
/* We have an optimization to save CPU time: if this is a consistent readonauniqueconditionontheclusteredindex,thenwedonot storethepcurposition,becauseanyfetchnextorprevwillanyway return'endoffile'.ExceptionsarelockingreadsandtheMySQL HANDLERcommandwheretheusercanmovethecursorwithPREVorNEXT
even after a unique search. */
err = DB_SUCCESS;
idx_cond_failed: if (!unique_search
|| !dict_index_is_clust(index)
|| direction != 0
|| prebuilt->select_lock_type != LOCK_NONE
|| prebuilt->used_in_HANDLER) {
/* Inside an update always store the cursor position. Exception:forexternalmtr(DMLoperations),keepthe
page latched for the subsequent update operation. */
if constexpr (Callback::manages_mtr) { if (!spatial_search) {
btr_pcur_store_position(pcur, mtr);
}
} /* else: external mtr, keep page latched */
}
goto normal_return;
next_rec: /* Reset the old and new "did semi-consistent read" flags. */ if (UNIV_UNLIKELY(prebuilt->row_read_type
== ROW_READ_DID_SEMI_CONSISTENT)) {
prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;
}
next_rec_after_check:
did_semi_consistent_read = false;
prebuilt->new_rec_locks = 0;
vrow = NULL;
/*-------------------------------------------------------------*/ /* PHASE 5: Move the cursor to the next index record */
/* NOTE: For moves_up==FALSE, the mini-transaction will be committedandrestartedeverytimewhenswitchingb-tree pages.Formoves_up==TRUEinindexconditionpushdown,wecan scananentiresecondaryindextreewithinasingle mini-transaction.Aslongastheprebuilt->idx_conddoesnot match,wedonotneedtoconsulttheclusteredindexor returnrecordstoMySQL,andthuswecanavoidrepositioning thecursor.Whatpreventsusfrombuffer-fixingallleafpages withinthemini-transactionisthebtr_leaf_page_release() callinbtr_pcur_move_to_next_page().Onlytheleafpagewhere thecursorispositionedwillremainbuffer-fixed. ForR-treespatialsearch,wealsocommitthemini-transaction
each time */
if (spatial_search) { /* No need to do store restore for R-tree */
mtr->rollback_to_savepoint(0);
} elseif (mtr_extra_clust_savepoint) { /* We must release any clustered index latches ifwearemovingtothenextnon-clustered indexrecord,becausewecouldbreakthelatching orderifwewouldaccessadifferentclustered
index page right away without releasing the previous. */
mtr->rollback_to_savepoint(mtr_extra_clust_savepoint);
}
mtr_extra_clust_savepoint = 0;
if (moves_up) { if (UNIV_UNLIKELY(spatial_search)) { if (rtr_pcur_move_to_next(
search_tuple, mode, pcur, 0, mtr)) { goto rec_loop;
}
} else { /* This is based on btr_pcur_move_to_next() */
ut_ad(pcur->pos_state == BTR_PCUR_IS_POSITIONED);
ut_ad(pcur->latch_mode != BTR_NO_LATCHES);
pcur->old_rec = nullptr; if (btr_pcur_is_after_last_on_page(pcur)) { if (btr_pcur_is_after_last_in_tree(pcur)) { goto not_moved;
}
err = btr_pcur_move_to_next_page(pcur, mtr); if (err != DB_SUCCESS) { goto lock_wait_or_error;
}
} elseif (!btr_pcur_move_to_next_on_page(pcur)) { goto corrupted;
}
if (row_mysql_handle_errors(&err, trx, thr, nullptr)) { /* It was a lock wait, and it ended */
thr->lock_state = QUE_THR_LOCK_NOLOCK; if constexpr (Callback::manages_mtr) {
mtr->start();
}
/* Table lock waited, go try to obtain table lock
again */ if (table_lock_waited) {
table_lock_waited = FALSE;
goto wait_table_again;
}
if (!dict_index_is_spatial(index)) {
sel_restore_position_for_mysql(
&same_user_rec, BTR_SEARCH_LEAF, pcur,
moves_up, mtr);
}
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED
&& !same_user_rec) {
/* Since we were not able to restore the cursor onthesameuserrecord,wecannotuse row_unlock_for_mysql()tounlockanyrecords,and wemustthusresetthenewreclockinfo.Since inlock0lock.ccwehaveblockedtheinheritingofgap X-locks,weactuallydonothaveanynewrecordlocks setinthiscase.
Notethatifwewereabletorestoreonthe'same' userrecord,itisstillpossiblethatwewereactually waitingonadelete-markedrecord,andmeanwhile itwasremovedbypurgeandinsertedagainbysome otheruser.Butthatisnoproblem,becausein rec_loopwewillagaintrytosetalock,and
new_rec_lock_info in trx will be right at the end. */
if (prebuilt->pk_filter || prebuilt->idx_cond) { /* When ICP is active we don't write to the MySQL buffer directly,onlytobuffersthatareenqueuedinthepre-fetch queue.Weneedtodequeuethefirstbufferandcopythecontents
to the record buffer that was passed in by MySQL. */
func_exit:
trx->op_info = ""; if (UNIV_LIKELY_NULL(heap)) {
mem_heap_free(heap);
}
/* Set or reset the "did semi-consistent read" flag on return. Theflagdid_semi_consistent_readissetifandonlyif
the record being returned was fetched with a semi-consistent read. */
ut_ad(prebuilt->row_read_type != ROW_READ_WITH_LOCKS
|| !did_semi_consistent_read);
if (prebuilt->row_read_type != ROW_READ_WITH_LOCKS) { if (did_semi_consistent_read) {
prebuilt->row_read_type = ROW_READ_DID_SEMI_CONSISTENT;
} else {
prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;
}
}
DEBUG_SYNC_C("innodb_row_search_for_mysql_exit");
DBUG_RETURN(err);
}
/********************************************************************//**
Count rows in a R-Tree leaf level.
@return DB_SUCCESS if successful */
dberr_t
row_count_rtree_recs( /*=================*/
row_prebuilt_t* prebuilt, /*!< in: prebuilt struct for the tablehandle;thiscontainstheinfo ofsearch_tuple,index;ifsearch tuplecontains0fieldsthenwe positionthecursoratthestartor theendoftheindex,dependingon
'mode' */
ulint* n_rows) /*!< out: number of entries
seen in the consistent read */
{
dict_index_t* index = prebuilt->index;
dberr_t ret = DB_SUCCESS;
mtr_t mtr{prebuilt->trx};
mem_heap_t* heap;
dtuple_t* entry;
dtuple_t* search_entry = prebuilt->search_tuple;
ulint i;
byte* buf;
ut_a(dict_index_is_spatial(index));
*n_rows = 0;
heap = mem_heap_create(256);
/* Build a search tuple. */
entry = dtuple_create(heap, index->n_fields);
for (i = 0; i < index->n_fields; i++) { const dict_field_t* ind_field
= dict_index_get_nth_field(index, i); const dict_col_t* col
= ind_field->col;
dfield_t* dfield
= dtuple_get_nth_field(entry, i);
if (i == 0) { double* mbr; double tmp_mbr[SPDIMS * 2];
loop:
ret = row_search_mvcc(buf, PAGE_CUR_WITHIN, prebuilt, 0, direction);
direction = ROW_SEL_NEXT;
switch (ret) { case DB_SUCCESS: break; case DB_DEADLOCK: case DB_LOCK_TABLE_FULL: case DB_LOCK_WAIT_TIMEOUT: case DB_INTERRUPTED: goto func_exit; default: /* fall through (this error is ignored by CHECK TABLE) */ case DB_END_OF_INDEX:
ret = DB_SUCCESS;
func_exit:
prebuilt->search_tuple = search_entry;
ut_free(buf);
mem_heap_free(heap);
return(ret);
}
++*n_rows; goto loop;
}
/** Check if a version of a clustered index record and a secondary indexrecordmatch.
@returnerrorcode
@retval DB_SUCCESS if no error was found */
dberr_t row_check_index(row_prebuilt_t *prebuilt, ulint *n_rows)
{
rec_offs offsets_[REC_OFFS_NORMAL_SIZE];
rec_offs_init(offsets_);
page_loop: if (&view == &check_table_extended_view) /* In CHECK TABLE...EXTENDED, we make a copy of purge_sys.end_view whileholdingasharedlatchontheindexleafpage. Shouldacurrentlyactivepurgebatchdesiretoremoveanyfurther recordsfromthispage,itwouldbeblockedbyourpagelatch.
/* Note: only if the search ends up on a non-infimum record is the
low_match value the real match to the search tuple */
if (!page_rec_is_user_rec(clust_rec) ||
btr_pcur_get_low_match(prebuilt->clust_pcur) < clust_index->n_uniq)
{ if (!rec_deleted)
{
not_found: /* MDEV-29823 FIXME: There is a race condition between rollback,purge,andpossiblyotherSQLconnectionsthat arecreatingandreleasingreadviews.Atthetime row_undo_mod_del_mark_or_remove_sec_low()isexecuting rollbackonasecondaryindexrecord,purge_sys.view maynotallowittodeletetherecord,anditwillbe delete-marked.Eventuallypurge_sys.viewwouldadvance, butthedelete-markedrecordcouldneverberemoved, becausenoundologrecordwaseveraddedto thepurgequeuebytrx_purge_add_undo_to_history().
Fornow,wewillnotflaganerroraboutorphansecondaryindex
records that are delete-marked; we will only warn about them. */
if (!rec_deleted || prebuilt->autoinc_error == DB_SUCCESS)
{
ib::error_or_warn w(!rec_deleted);
w << "Clustered index record not found for index "
<< index->name << " of table " << index->table->name
<< ": " << rec_offsets_print(rec, offsets);
push_warning_printf(trx->mysql_thd, Sql_condition::WARN_LEVEL_WARN,
ER_NOT_KEYFILE, "InnoDB: %s",
w.m_oss.str().c_str());
}
if (clust_rec[trx_id_offset + DATA_TRX_ID_LEN] & 0x80)
{ if (UNIV_UNLIKELY
(rec_get_deleted_flag(clust_rec,
prebuilt->table->not_redundant())))
{
err= DB_CORRUPTION; goto func_exit;
}
/* This is the oldest available record version (fresh insert). */ if (!view.changes_visible(rec_trx_id))
{ if (rec_trx_id >= view.low_limit_id() &&
UNIV_UNLIKELY(rec_trx_id >= trx_sys.get_max_trx_id())) goto invalid_rec_trx_id; if (got_extended_match) goto check_latest_version; goto did_not_find;
}
}
} else
{
clust_offsets= rec_get_offsets(clust_rec, clust_index, nullptr,
clust_index->n_core_fields,
ULINT_UNDEFINED, &heap);
ulint trx_id_pos= clust_index->n_uniq ? clust_index->n_uniq : 1;
ulint len;
trx_id_offset= rec_get_nth_field_offs(clust_offsets, trx_id_pos, &len);
ut_ad(len == DATA_TRX_ID_LEN); goto read_trx_id;
}
if (got_extended_match)
{
check_latest_version: /* In CHECK TABLE...EXTENDED, always check if the secondary indexrecordmatchesthelatestclusteredindexrecord version,nomatterifitisvisibleinourownreadview.
Ifthelatestclusteredindexversionisdelete-markedand purgeable,itisnotsafetofetchanyBLOBsforcolumnprefix
indexes because they may already have been freed. */ if (rec_trx_id &&
rec_get_deleted_flag(clust_rec,
prebuilt->table->not_redundant()) &&
purge_sys.is_purgeable(rec_trx_id)) goto did_not_find;
if (!trx->read_view.changes_visible(rec_trx_id)) /* While CHECK TABLE ... EXTENDED checks for a matching clusteredindexrecordversionforeachsecondaryindex record,itmustcountonlythoserecordsthatbelongtoits ownreadview.
Ifthelatestversionofclust_recmatchesrecbutisnot inourreadview,theremaystillbeanolderversionof clust_recthatnotonlymatchesrecbutisinourview. Wemustevaluateoldversionsbeforedecidingwhetherrec
should be counted. */ goto check_old_vers;
/* Remember that this is the visible clust_rec for rec,
and whether it matches rec. */
visible_trx_id= rec_trx_id;
found_in_view= got_extended_match &&
!rec_get_deleted_flag(clust_rec,
prebuilt->table->not_redundant());
if (!clust_offsets)
clust_offsets= rec_get_offsets(clust_rec, clust_index, nullptr,
clust_index->n_core_fields,
ULINT_UNDEFINED, &heap);
row_sel_reset_old_vers_heap(prebuilt); /* The following is adapted from row_vers_build_for_consistent_read() becausewhenusingcheck_table_extended_view,wemust
consider every available version of the clustered index record. */
mem_heap_t *vers_heap= nullptr;
for (;;)
{
mem_heap_t *prev_heap= vers_heap;
vers_heap= mem_heap_create(1024);
err= trx_undo_prev_version_build(clust_rec,
clust_index, clust_offsets,
vers_heap, &old_vers,
&mtr, 0, nullptr, nullptr); if (prev_heap)
mem_heap_free(prev_heap); if (err != DB_SUCCESS)
{
old_vers_err:
mem_heap_free(vers_heap); if (err == DB_MISSING_HISTORY)
{
err= DB_SUCCESS; if (got_extended_match) goto did_not_find; goto not_found;
} goto func_exit;
}
if (UNIV_UNLIKELY(!old_vers))
{
mem_heap_free(vers_heap); /* We did not find a matching clustered index record version forthesecondaryindexrecord.NormalCHECKTABLEwillsimply notcountthesecondaryindexrecord;CHECKTABLE...EXTENDED willflagsuchorphanrecordsifappropriate.
if (&view != &trx->read_view)
{ /* It is not safe to fetch BLOBs of committed delete-marked
records that may have been freed in purge. */
err= clust_rec_deleted && rec_trx_id &&
purge_sys.is_purgeable(rec_trx_id)
? DB_SUCCESS_LOCKED_REC
: row_check_index_match(prebuilt,
clust_rec, clust_index, clust_offsets,
rec, index, offsets);
switch (err) { default: goto old_vers_err; case DB_SUCCESS_LOCKED_REC: if (rec_visible && !~visible_trx_id)
visible_trx_id= rec_trx_id; continue; case DB_SUCCESS:
got_extended_match= true; if (!rec_visible) continue; if (!~visible_trx_id)
{
visible_trx_id= rec_trx_id;
found_in_view= !clust_rec_deleted;
}
mem_heap_free(vers_heap); if (!found_in_view) goto did_not_find; goto found_match;
}
} elseif (rec_visible)
{ if (!clust_rec_deleted)
{
clust_rec= rec_copy(mem_heap_alloc(heap,
rec_offs_size(clust_offsets)),
clust_rec, clust_offsets);
rec_offs_make_valid(clust_rec, clust_index, true, clust_offsets);
}
mem_heap_free(vers_heap); if (clust_rec_deleted) goto did_not_find; goto check_match;
}
}
} elseif (rec_get_deleted_flag(clust_rec,
prebuilt->table->not_redundant())) goto did_not_find;
/* If we had to go to an earlier version of row or the secondary indexrecordisdeletemarked,thenitmaybethatthesecondary indexrecordcorrespondingtoclust_rec(orold_vers)isnot rec;inthatcasewemustignoresuchrowbecauseinour snapshotrecwouldnothaveexisted.Rememberthatfromrecwe cannotseedirectlywhichtransactionidcorrespondstoit:we havetogototheclusteredindexrecord.Aquerywherewewant tofetchallrowswherethesecondaryindexvalueisinsome intervalwouldreturnawrongresultifwewouldnotdroprows whichwecometovisitthroughsecondaryindexrecordsthat
would not really exist in our snapshot. */
if (rec_deleted)
{ if (!clust_offsets)
clust_offsets= rec_get_offsets(clust_rec, clust_index, nullptr,
clust_index->n_core_fields,
ULINT_UNDEFINED, &heap);
check_match: /* This clustered index record version exists in trx->read_viewandisnotdelete-marked. Bydesign,anyBLOBsinitarenotallowedtobe
freed in the purge of committed transaction history. */
err= row_check_index_match(prebuilt, clust_rec, clust_index,
clust_offsets, rec, index, offsets); switch (err) { case DB_SUCCESS: break; case DB_SUCCESS_LOCKED_REC:
err= DB_SUCCESS; goto did_not_find; default: goto func_exit;
}
}
mtr.rollback_to_savepoint(savepoint);
}
} else
{
invalid_PAGE_MAX_TRX_ID: if (UNIV_LIKELY(srv_force_recovery < SRV_FORCE_NO_UNDO_LOG_SCAN))
{
push_warning_printf(trx->mysql_thd,
Sql_condition::WARN_LEVEL_WARN, ER_NOT_KEYFILE, "InnoDB: Invalid PAGE_MAX_TRX_ID=%" PRIu64 " in index '%-.200s'",
page_trx_id, index->name());
prebuilt->autoinc_error= DB_INDEX_CORRUPT;
} goto next_rec;
}
count_row:
++*n_rows;
if (prev_entry)
{
uint16_t matched= 0; int cmp= cmp_dtuple_rec_with_match(prev_entry, rec, index, offsets,
&matched); constchar* msg;
if (UNIV_LIKELY(cmp < 0)); elseif (cmp > 0)
{
prebuilt->autoinc_error= DB_INDEX_CORRUPT;
msg= "index records in a wrong order in ";
not_ok:
ib::error() << msg << index->name << " of table " << index->table->name
<< ": " << *prev_entry << ", "
<< rec_offsets_print(rec, offsets);
} elseif (index->is_unique() && matched >=
dict_index_get_n_ordering_defined_by_user(index))
{ /* NULL values in unique indexes are considered not to be duplicates */ for (ulint i= 0; i < dict_index_get_n_ordering_defined_by_user(index);
i++) if (dfield_is_null(dtuple_get_nth_field(prev_entry, i))) goto next_rec;
if (prebuilt->autoinc_error == DB_SUCCESS)
prebuilt->autoinc_error= DB_DUPLICATE_KEY;
msg= "duplicate key in "; goto not_ok;
}
}
next_rec:
ut_ad(err == DB_SUCCESS);
{
mem_heap_t *tmp_heap= nullptr;
/* Empty the heap on each round. But preserve offsets[] fortherow_rec_to_index_entry()call,bycopyingthem
into a separate memory heap when needed. */ if (UNIV_UNLIKELY(offsets != offsets_))
{
ulint size= rec_offs_get_n_alloc(offsets) * sizeof *offsets;
tmp_heap= mem_heap_create(size);
offsets= static_cast<rec_offs*>(mem_heap_dup(tmp_heap, offsets, size));
}
¤ Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.0.256Bemerkung:
(vorverarbeitet am 2026-10-08)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.